import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
import type { VaultEntry, VaultPropertyValue } from '../types'
import type { FrontmatterValue } from '../components/Inspector'
import { updateMockFrontmatter, deleteMockFrontmatterProperty } from './mockFrontmatterHelpers'
import { updateMockContent, trackMockChange } from '../mock-tauri'
import { parseFrontmatter } from '../utils/frontmatter'
import { normalizeNoteFormat } from '../utils/noteFormat'
import { canonicalFrontmatterKey, isSystemMetadataKey } from '../utils/systemMetadata'
import { normalizeNoteWidthMode } from '../utils/noteWidth'

type FrontmatterCommand = 'update_frontmatter' | 'delete_frontmatter_property'
type FrontmatterKey = string
type FrontmatterOp = 'update' | 'delete'
type MarkdownContent = string
type ToastMessage = string | null
type VaultPath = string
type WikilinkText = string
type PropertyPatchValue = VaultEntry['properties'][string]

const ENTRY_DELETE_MAP: Record<string, Partial<VaultEntry>> = {
  title: { title: '' },
  type: { isA: null }, status: { status: null }, color: { color: null },
  _icon: { icon: null }, _sidebar_label: { sidebarLabel: null },
  aliases: { aliases: [] }, belongs_to: { belongsTo: [] }, related_to: { relatedTo: [] },
  _archived: { archived: false },
  _order: { order: null },
  template: { template: null }, _sort: { sort: null }, view: { view: null },
  _width: { noteWidth: null }, _display: { display: null }, visible: { visible: null },
  _organized: { organized: false },
  _favorite: { favorite: false }, _favorite_index: { favoriteIndex: null },
  _list_properties_display: { listPropertiesDisplay: [] },
}

/** Check if a string contains a wikilink pattern `[[...]]`. */
function isWikilink(value: WikilinkText): boolean {
  return value.startsWith('[[') && value.includes(']]')
}

/** Extract wikilink strings from a FrontmatterValue. Returns empty array if none. */
function extractWikilinks(value: FrontmatterValue): WikilinkText[] {
  if (typeof value === 'string') return isWikilink(value) ? [value] : []
  if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string' && isWikilink(v))
  return []
}

/**
 * Relationship patch: a partial update to merge into `entry.relationships`.
 * Keys map to their new ref arrays. A `null` value means "remove this key".
 */
export type RelationshipPatch = Record<FrontmatterKey, WikilinkText[] | null>

/** Properties patch: a partial update to merge into `entry.properties`.
 *  Keys map to their new property values. A `null` value means "remove this key". */
export type PropertiesPatch = Record<FrontmatterKey, PropertyPatchValue>

export interface EntryPatchResult {
  patch: Partial<VaultEntry>
  relationshipPatch: RelationshipPatch | null
  propertiesPatch: PropertiesPatch | null
}

interface FrontmatterPatchInput {
  key: FrontmatterKey
  lookupKey: FrontmatterKey
  systemMetadataKey: boolean
  value?: FrontmatterValue
}

function singleEntryRecord<T>({ key, value }: { key: FrontmatterKey; value: T }): Record<FrontmatterKey, T> {
  return Object.fromEntries([[key, value]])
}

function applyRecordPatch<T>(
  existing: Record<string, T>,
  patch: Record<string, T | null>,
): Record<string, T> {
  const merged = { ...existing }
  for (const [key, value] of Object.entries(patch)) {
    if (value === null) Reflect.deleteProperty(merged, key)
    else Reflect.set(merged, key, value)
  }
  return merged
}

function frontmatterString(value: FrontmatterValue | undefined): string | null {
  return value != null ? String(value) : null
}

function frontmatterStringList(value: FrontmatterValue | undefined): string[] {
  return Array.isArray(value) ? value.map(String) : []
}

function frontmatterNumber(value: FrontmatterValue | undefined): number | null {
  return typeof value === 'number' ? value : null
}

function visibleValue(value: FrontmatterValue | undefined): false | null {
  return value === false ? false : null
}

function propertyValue(value: FrontmatterValue): PropertyPatchValue {
  if (Array.isArray(value)) {
    const values = value.map(String)
    return values.length === 1 ? values[0] ?? '' : values
  }
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value
  return String(value)
}

function knownFrontmatterUpdates(value: FrontmatterValue | undefined): Record<FrontmatterKey, Partial<VaultEntry>> {
  const str = frontmatterString(value)
  const arr = frontmatterStringList(value)
  return {
    title: { title: str ?? '' },
    type: { isA: str }, status: { status: str }, color: { color: str },
    _icon: { icon: str }, _sidebar_label: { sidebarLabel: str },
    aliases: { aliases: arr }, belongs_to: { belongsTo: arr }, related_to: { relatedTo: arr },
    _archived: { archived: Boolean(value) },
    _order: { order: frontmatterNumber(value) },
    template: { template: str },
    _sort: { sort: str },
    view: { view: str },
    _width: { noteWidth: normalizeNoteWidthMode(value) },
    _display: { display: normalizeNoteFormat(value) },
    visible: { visible: visibleValue(value) },
    _organized: { organized: Boolean(value) },
    _favorite: { favorite: Boolean(value) },
    _favorite_index: { favoriteIndex: frontmatterNumber(value) },
    _list_properties_display: { listPropertiesDisplay: arr },
  }
}

function deleteEntryPatch({ key, lookupKey, systemMetadataKey }: FrontmatterPatchInput): EntryPatchResult {
  const relationshipPatch = systemMetadataKey ? null : singleEntryRecord({ key, value: null })
  const hasKnownDelete = Object.hasOwn(ENTRY_DELETE_MAP, lookupKey)
  const propertiesPatch = !systemMetadataKey && !hasKnownDelete ? singleEntryRecord({ key, value: null }) : null
  return {
    patch: (Reflect.get(ENTRY_DELETE_MAP, lookupKey) as Partial<VaultEntry> | undefined) ?? {},
    relationshipPatch,
    propertiesPatch,
  }
}

function relationshipUpdatePatch(
  key: FrontmatterKey,
  systemMetadataKey: boolean,
  value: FrontmatterValue | undefined,
): RelationshipPatch | null {
  const wikilinks = value != null ? extractWikilinks(value) : []
  return !systemMetadataKey && wikilinks.length > 0 ? singleEntryRecord({ key, value: wikilinks }) : null
}

function propertiesUpdatePatch({
  key,
  lookupKey,
  systemMetadataKey,
  value,
}: FrontmatterPatchInput): PropertiesPatch | null {
  const knownUpdates = knownFrontmatterUpdates(value)
  if (systemMetadataKey || Object.hasOwn(knownUpdates, lookupKey) || value == null) return null
  return singleEntryRecord({ key, value: propertyValue(value) })
}

function updateEntryPatch(input: FrontmatterPatchInput): EntryPatchResult {
  const updates = knownFrontmatterUpdates(input.value)
  return {
    patch: (Reflect.get(updates, input.lookupKey) as Partial<VaultEntry> | undefined) ?? {},
    relationshipPatch: relationshipUpdatePatch(input.key, input.systemMetadataKey, input.value),
    propertiesPatch: propertiesUpdatePatch(input),
  }
}

/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */
export function frontmatterToEntryPatch(
  op: FrontmatterOp, key: FrontmatterKey, value?: FrontmatterValue,
): EntryPatchResult {
  const lookupKey = canonicalFrontmatterKey(key)
  const systemMetadataKey = isSystemMetadataKey(key)
  const input = { key, lookupKey, systemMetadataKey, value }
  return op === 'delete' ? deleteEntryPatch(input) : updateEntryPatch(input)
}

/** Parse frontmatter from full content and return a merged VaultEntry patch for all known fields. */
export function contentToEntryPatch(content: MarkdownContent): Partial<VaultEntry> {
  const fm = parseFrontmatter(content)
  const merged: Partial<VaultEntry> = {}
  const customProps: Record<FrontmatterKey, PropertyPatchValue> = {}
  for (const [key, value] of Object.entries(fm)) {
    const { patch, propertiesPatch } = frontmatterToEntryPatch('update', key, value)
    Object.assign(merged, patch)
    if (propertiesPatch) Object.assign(customProps, propertiesPatch)
  }
  if (Object.keys(customProps).length > 0) merged.properties = customProps
  return merged
}

async function invokeFrontmatter(command: FrontmatterCommand, args: Record<string, unknown>): Promise<MarkdownContent> {
  return invoke<string>(command, args)
}

function seedMockContent(path: VaultPath, content: MarkdownContent): void {
  updateMockContent(path, content)
}

async function loadMockContent(path: VaultPath): Promise<MarkdownContent> {
  try {
    return await mockInvoke<MarkdownContent>('get_note_content', { path })
  } catch {
    return typeof window === 'undefined'
      ? ''
      : (window.__mockContent ? Reflect.get(window.__mockContent, path) as string | undefined : undefined) ?? ''
  }
}

async function persistMockContent(path: VaultPath, content: MarkdownContent): Promise<void> {
  try {
    await mockInvoke('save_note_content', { path, content })
  } finally {
    updateMockContent(path, content)
    trackMockChange(path)
  }
}

function applyMockFrontmatterUpdate(path: VaultPath, key: FrontmatterKey, value: FrontmatterValue): MarkdownContent {
  const content = updateMockFrontmatter(path, key, value)
  return content
}

function applyMockFrontmatterDelete(path: VaultPath, key: FrontmatterKey): MarkdownContent {
  const content = deleteMockFrontmatterProperty(path, key)
  return content
}

async function executeMockFrontmatterOp(
  op: FrontmatterOp,
  path: VaultPath,
  key: FrontmatterKey,
  value?: FrontmatterValue,
): Promise<MarkdownContent> {
  seedMockContent(path, await loadMockContent(path))
  const content = op === 'update'
    ? applyMockFrontmatterUpdate(path, key, value!)
    : applyMockFrontmatterDelete(path, key)
  await persistMockContent(path, content)
  return content
}

async function executeFrontmatterOp(
  op: FrontmatterOp,
  path: VaultPath,
  key: FrontmatterKey,
  value?: FrontmatterValue,
): Promise<MarkdownContent> {
  if (op === 'update') {
    return isTauri()
      ? invokeFrontmatter('update_frontmatter', { path, key, value })
      : executeMockFrontmatterOp(op, path, key, value)
  }
  return isTauri()
    ? invokeFrontmatter('delete_frontmatter_property', { path, key })
    : executeMockFrontmatterOp(op, path, key)
}

export interface FrontmatterOpOptions {
  /** Suppress toast feedback (caller manages its own toast). */
  silent?: boolean
  /** Require this note to still be active before applying UI-side mutation state. */
  requireActivePath?: VaultPath
}

export interface FrontmatterApplyCallbacks {
  updateTab: (path: VaultPath, content: MarkdownContent) => void
  updateEntry: (path: VaultPath, patch: Partial<VaultEntry>) => void
  cacheContent?: (path: VaultPath, content: MarkdownContent) => void
  toast: (message: ToastMessage) => void
  getEntry?: (path: VaultPath) => VaultEntry | undefined
  onMissingNotePath?: (path: VaultPath, error: unknown) => void | Promise<void>
  shouldApply?: (path: VaultPath) => boolean
}

export interface FrontmatterRunRequest {
  op: FrontmatterOp
  path: VaultPath
  key: FrontmatterKey
  value?: FrontmatterValue
  callbacks: FrontmatterApplyCallbacks
  options?: FrontmatterOpOptions
}

/** Apply a properties patch by merging into the existing properties map. */
export function applyPropertiesPatch(
  existing: Record<FrontmatterKey, VaultPropertyValue>, propPatch: PropertiesPatch,
): Record<FrontmatterKey, VaultPropertyValue> {
  return applyRecordPatch(existing, propPatch)
}

/** Apply a relationship patch by merging into the existing relationships map. */
export function applyRelationshipPatch(
  existing: Record<FrontmatterKey, WikilinkText[]>, relPatch: RelationshipPatch,
): Record<FrontmatterKey, WikilinkText[]> {
  return applyRecordPatch(existing, relPatch)
}

function patchNeedsExistingEntry(result: EntryPatchResult): boolean {
  return Boolean(result.relationshipPatch || result.propertiesPatch)
}

function buildFullEntryPatch(
  path: VaultPath,
  callbacks: FrontmatterApplyCallbacks,
  result: EntryPatchResult,
): Partial<VaultEntry> {
  const fullPatch = { ...result.patch }
  if (!patchNeedsExistingEntry(result) || !callbacks.getEntry) return fullPatch

  const current = callbacks.getEntry(path)
  if (!current) return fullPatch

  if (result.relationshipPatch) {
    fullPatch.relationships = applyRelationshipPatch(current.relationships, result.relationshipPatch)
  }
  if (result.propertiesPatch) {
    fullPatch.properties = applyPropertiesPatch(current.properties, result.propertiesPatch)
  }
  return fullPatch
}

function applyEntryPatch(path: VaultPath, callbacks: FrontmatterApplyCallbacks, result: EntryPatchResult) {
  const fullPatch = buildFullEntryPatch(path, callbacks, result)
  if (Object.keys(fullPatch).length > 0) callbacks.updateEntry(path, fullPatch)
}

function successToastMessage(op: FrontmatterOp): ToastMessage {
  return op === 'update' ? 'Property updated' : 'Property deleted'
}

function notifyFrontmatterSuccess(op: FrontmatterOp, callbacks: FrontmatterApplyCallbacks, options?: FrontmatterOpOptions) {
  if (!options?.silent) callbacks.toast(successToastMessage(op))
}

function failureToastMessage(op: FrontmatterOp): ToastMessage {
  return `Failed to ${op} property`
}

function frontmatterErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message
  if (typeof error === 'string') return error
  return String(error)
}

export function isMissingFrontmatterTargetError(error: unknown): boolean {
  return /does not exist|not found|enoent/i.test(frontmatterErrorMessage(error))
}

async function runMissingNotePathCallback(
  path: VaultPath,
  error: unknown,
  callbacks: FrontmatterApplyCallbacks,
): Promise<void> {
  try {
    await callbacks.onMissingNotePath?.(path, error)
  } catch (callbackError) {
    console.warn('Failed to handle missing frontmatter target:', callbackError)
  }
}

interface FrontmatterFailureParams {
  callbacks: FrontmatterApplyCallbacks
  err: unknown
  op: FrontmatterOp
  options?: FrontmatterOpOptions
  path: VaultPath
}

async function handleFrontmatterFailure({
  callbacks,
  err,
  op,
  options,
  path,
}: FrontmatterFailureParams): Promise<undefined> {
  const missingTarget = isMissingFrontmatterTargetError(err)
  if (missingTarget) {
    console.warn(`Skipped ${op} frontmatter for missing note:`, err)
    await runMissingNotePathCallback(path, err, callbacks)
  } else {
    console.error(`Failed to ${op} frontmatter:`, err)
  }
  if (options?.silent) throw err
  if (!missingTarget) callbacks.toast(failureToastMessage(op))
  return undefined
}

/** Run a frontmatter update/delete and apply the result to state.
 *  Returns the new file content on success, or undefined on failure. */
export async function runFrontmatterAndApply(request: FrontmatterRunRequest): Promise<MarkdownContent | undefined> {
  const { op, path, key, value, callbacks, options } = request
  try {
    const newContent = await executeFrontmatterOp(op, path, key, value)
    callbacks.cacheContent?.(path, newContent)
    if (callbacks.shouldApply && !callbacks.shouldApply(path)) return undefined
    callbacks.updateTab(path, newContent)
    applyEntryPatch(path, callbacks, frontmatterToEntryPatch(op, key, value))
    notifyFrontmatterSuccess(op, callbacks, options)
    return newContent
  } catch (err) {
    return handleFrontmatterFailure({ op, path, err, callbacks, options })
  }
}
