import type { FilterGroup, FilterNode, VaultEntry, ViewDefinition, ViewFile, WorkspaceIdentity } from '../types'

type UnknownRecord = Record<string, unknown>

interface EntryNormalizationArgs {
  rawEntry: unknown
  vaultPath: string
  index: number
  workspace?: WorkspaceIdentity
}

interface EntryPathArgs {
  explicitPath: string
  filename: string
  vaultPath: string
}

interface ViewNormalizationArgs {
  rawView: unknown
  index: number
}

interface ViewDefinitionArgs {
  rawDefinition: unknown
  filename: string
  index: number
}

function isRecord(value: unknown): value is UnknownRecord {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function recordFrom(value: unknown): UnknownRecord {
  return isRecord(value) ? value : {}
}

function stringFrom(value: unknown): string {
  return typeof value === 'string' ? value : ''
}

function nullableStringFrom(value: unknown): string | null {
  const text = stringFrom(value).trim()
  return text.length > 0 ? text : null
}

function numberFrom(value: unknown, fallback = 0): number {
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback
}

function nullableNumberFrom(value: unknown): number | null {
  return typeof value === 'number' && Number.isFinite(value) ? value : null
}

function booleanFrom(value: unknown, fallback = false): boolean {
  return typeof value === 'boolean' ? value : fallback
}

function nullableBooleanFrom(value: unknown): boolean | null {
  return typeof value === 'boolean' ? value : null
}

function stringArrayFrom(value: unknown): string[] {
  return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
}

function hasUsablePath(rawEntry: unknown): boolean {
  const source = recordFrom(rawEntry)
  return typeof source.path === 'string' && source.path.trim().length > 0
}

function filenameFromPath(path: string): string {
  const normalizedPath = path.replace(/\\/g, '/')
  return normalizedPath.split('/').filter(Boolean).pop() ?? ''
}

function stripExtension(filename: string): string {
  return filename.replace(/\.[^.]+$/, '')
}

function fallbackEntryFilename(source: UnknownRecord, index: number): string {
  return stringFrom(source.filename) || filenameFromPath(stringFrom(source.path)) || `untitled-${index + 1}.md`
}

function resolveEntryPath({ explicitPath, filename, vaultPath }: EntryPathArgs): string {
  if (explicitPath) return explicitPath
  const root = vaultPath.replace(/\/+$/, '')
  return root ? `${root}/${filename}` : filename
}

function normalizeRelationships(value: unknown): Record<string, string[]> {
  const source = recordFrom(value)
  const result: Record<string, string[]> = {}
  for (const [key, rawRefs] of Object.entries(source)) {
    const refs = stringArrayFrom(rawRefs)
    if (refs.length > 0) Reflect.set(result, key, refs)
  }
  return result
}

function normalizeProperties(value: unknown): VaultEntry['properties'] {
  const source = recordFrom(value)
  const result: VaultEntry['properties'] = {}
  for (const [key, rawValue] of Object.entries(source)) {
    if (Array.isArray(rawValue)) {
      const values = rawValue.filter((item): item is string | number | boolean => (
        typeof item === 'string'
        || typeof item === 'boolean'
        || (typeof item === 'number' && Number.isFinite(item))
      ))
      if (values.length === rawValue.length) {
        Reflect.set(result, key, values.length === 1 ? values[0] : values)
      }
      continue
    }
    if (
      rawValue === null
      || typeof rawValue === 'string'
      || typeof rawValue === 'boolean'
      || (typeof rawValue === 'number' && Number.isFinite(rawValue))
    ) {
      Reflect.set(result, key, rawValue)
    }
  }
  return result
}

function normalizeFileKind(value: unknown): VaultEntry['fileKind'] {
  if (value === 'markdown' || value === 'text' || value === 'binary') return value
  return undefined
}

function normalizeWorkspaceIdentity(value: unknown): WorkspaceIdentity | undefined {
  const source = recordFrom(value)
  const id = stringFrom(source.id).trim()
  const label = stringFrom(source.label).trim()
  const alias = stringFrom(source.alias).trim()
  const path = stringFrom(source.path).trim()
  if (!id || !label || !alias || !path) return undefined
  return {
    id,
    label,
    alias,
    path,
    shortLabel: stringFrom(source.shortLabel).trim() || label.slice(0, 2).toUpperCase(),
    color: nullableStringFrom(source.color),
    icon: nullableStringFrom(source.icon),
    mounted: booleanFrom(source.mounted, true),
    available: booleanFrom(source.available, true),
    defaultForNewNotes: booleanFrom(source.defaultForNewNotes),
  }
}

function normalizeFilterGroup(value: unknown): FilterGroup {
  const source = recordFrom(value)
  if (Array.isArray(source.all)) return { all: source.all as FilterNode[] }
  if (Array.isArray(source.any)) return { any: source.any as FilterNode[] }
  return { all: [] }
}

function fallbackViewName(filename: string, index: number): string {
  const stem = stripExtension(filename).trim()
  return stem && stem !== `view-${index + 1}` ? stem : `View ${index + 1}`
}

function normalizeVaultEntryRecord({ rawEntry, vaultPath, index, workspace }: EntryNormalizationArgs): VaultEntry {
  const source = recordFrom(rawEntry)
  const filename = fallbackEntryFilename(source, index)
  const path = resolveEntryPath({
    explicitPath: stringFrom(source.path),
    filename,
    vaultPath,
  })
  const title = stringFrom(source.title).trim() || stripExtension(filename) || 'Untitled'
  const fileKind = normalizeFileKind(source.fileKind)

  const entry = {
    ...(source as Partial<VaultEntry>),
    path,
    filename,
    title,
    workspace: workspace ?? normalizeWorkspaceIdentity(source.workspace),
    isA: nullableStringFrom(source.isA),
    aliases: stringArrayFrom(source.aliases),
    belongsTo: stringArrayFrom(source.belongsTo),
    relatedTo: stringArrayFrom(source.relatedTo),
    status: nullableStringFrom(source.status),
    archived: booleanFrom(source.archived),
    modifiedAt: nullableNumberFrom(source.modifiedAt),
    createdAt: nullableNumberFrom(source.createdAt),
    fileSize: numberFrom(source.fileSize),
    snippet: stringFrom(source.snippet),
    wordCount: numberFrom(source.wordCount),
    relationships: normalizeRelationships(source.relationships),
    icon: nullableStringFrom(source.icon),
    color: nullableStringFrom(source.color),
    order: nullableNumberFrom(source.order),
    sidebarLabel: nullableStringFrom(source.sidebarLabel),
    template: nullableStringFrom(source.template),
    sort: nullableStringFrom(source.sort),
    view: nullableStringFrom(source.view),
    visible: nullableBooleanFrom(source.visible),
    organized: booleanFrom(source.organized),
    favorite: booleanFrom(source.favorite),
    favoriteIndex: nullableNumberFrom(source.favoriteIndex),
    listPropertiesDisplay: stringArrayFrom(source.listPropertiesDisplay),
    outgoingLinks: stringArrayFrom(source.outgoingLinks),
    properties: normalizeProperties(source.properties),
    hasH1: booleanFrom(source.hasH1),
  } as VaultEntry

  if (fileKind) entry.fileKind = fileKind
  return entry
}

function normalizeViewDefinition({ rawDefinition, filename, index }: ViewDefinitionArgs): ViewDefinition {
  const definition = recordFrom(rawDefinition)
  const name = stringFrom(definition.name).trim() || fallbackViewName(filename, index)

  const normalized = {
    ...(definition as Partial<ViewDefinition>),
    name,
    icon: nullableStringFrom(definition.icon),
    color: nullableStringFrom(definition.color),
    sort: nullableStringFrom(definition.sort),
    filters: normalizeFilterGroup(definition.filters),
  } as ViewDefinition

  if ('order' in definition) normalized.order = nullableNumberFrom(definition.order)
  if ('listPropertiesDisplay' in definition) {
    normalized.listPropertiesDisplay = stringArrayFrom(definition.listPropertiesDisplay)
  }
  return normalized
}

function normalizeViewFile({ rawView, index }: ViewNormalizationArgs): ViewFile {
  const source = recordFrom(rawView)
  const filename = stringFrom(source.filename) || `view-${index + 1}.yml`

  return {
    filename,
    definition: normalizeViewDefinition({
      rawDefinition: source.definition,
      filename,
      index,
    }),
  }
}

export function normalizeVaultEntries(rawEntries: unknown, vaultPath: string, workspace?: WorkspaceIdentity): VaultEntry[] {
  if (!Array.isArray(rawEntries)) return []
  return rawEntries
    .filter(hasUsablePath)
    .map((rawEntry, index) => normalizeVaultEntry(rawEntry, vaultPath, index, workspace))
}

export function normalizeVaultEntry(rawEntry: unknown, vaultPath = '', index = 0, workspace?: WorkspaceIdentity): VaultEntry {
  return normalizeVaultEntryRecord({ rawEntry, vaultPath, index, workspace })
}

export function normalizeViewFiles(rawViews: unknown): ViewFile[] {
  if (!Array.isArray(rawViews)) return []
  return rawViews.map((rawView, index) => normalizeViewFile({ rawView, index }))
}
