import type { VaultEntry, ViewDefinition, FilterGroup, FilterNode, FilterCondition } from '../types'
import { toDateFilterTimestamp } from './filterDates'

/** Evaluate a view's filters against a list of entries, returning only matches. */
export function evaluateView(definition: ViewDefinition, entries: VaultEntry[]): VaultEntry[] {
  return entries.filter((e) => !e.archived && evaluateGroup(definition.filters, e))
}

function evaluateGroup(group: FilterGroup, entry: VaultEntry): boolean {
  if ('all' in group) return group.all.every((node) => evaluateNode(node, entry))
  if ('any' in group) return group.any.some((node) => evaluateNode(node, entry))
  return true
}

function isFilterGroup(node: FilterNode): node is FilterGroup {
  return 'all' in node || 'any' in node
}

function evaluateNode(node: FilterNode, entry: VaultEntry): boolean {
  if (isFilterGroup(node)) return evaluateGroup(node, entry)
  return evaluateCondition(node as FilterCondition, entry)
}

function resolveField(entry: VaultEntry, field: string): { scalar?: string | number | boolean | null; array?: string[] } {
  const lower = field.toLowerCase()
  if (lower === 'type' || lower === 'isa') return { scalar: entry.isA }
  if (lower === 'status') return { scalar: entry.status }
  if (lower === 'title') return { scalar: entry.title }
  if (lower === 'filename') return { scalar: entry.filename }
  if (lower === 'archived') return { scalar: entry.archived }
  if (lower === 'favorite') return { scalar: entry.favorite }
  if (lower === 'body') return { scalar: entry.snippet }

  // Check relationships first (returns string[])
  const relKey = Object.keys(entry.relationships).find((k) => k.toLowerCase() === lower)
  if (relKey) return { array: entry.relationships[relKey] }

  // Then properties (returns scalar)
  const propKey = Object.keys(entry.properties).find((k) => k.toLowerCase() === lower)
  if (propKey) return { scalar: entry.properties[propKey] }

  return { scalar: null }
}

function wikilinkStem(raw: string): string {
  let s = raw.trim()
  if (s.startsWith('[[')) s = s.slice(2)
  if (s.endsWith(']]')) s = s.slice(0, -2)
  const pipe = s.indexOf('|')
  if (pipe >= 0) s = s.substring(0, pipe)
  return s.toLowerCase()
}

function relationshipCandidates(raw: string): string[] {
  const trimmed = raw.trim()
  let inner = trimmed
  if (inner.startsWith('[[')) inner = inner.slice(2)
  if (inner.endsWith(']]')) inner = inner.slice(0, -2)
  const pipe = inner.indexOf('|')
  if (pipe >= 0) {
    return [trimmed, inner.slice(0, pipe), inner.slice(pipe + 1)]
  }
  return [trimmed, inner]
}

/** Extract all comparable parts (path and alias) from a wikilink string. */
function wikilinkParts(raw: string): string[] {
  let s = raw.trim()
  if (s.startsWith('[[')) s = s.slice(2)
  if (s.endsWith(']]')) s = s.slice(0, -2)
  const pipe = s.indexOf('|')
  if (pipe >= 0) return [s.substring(0, pipe).toLowerCase(), s.substring(pipe + 1).toLowerCase()]
  return [s.toLowerCase()]
}

/** Check if two wikilink values match by comparing all path/alias combinations. */
function wikilinkEquals(a: string, b: string): boolean {
  const partsA = wikilinkParts(a)
  const partsB = wikilinkParts(b)
  return partsA.some(pa => partsB.some(pb => pa === pb))
}

function toString(v: unknown): string {
  if (v == null) return ''
  if (typeof v === 'string') return v
  return String(v)
}

function compileRegex(cond: FilterCondition, value: string): RegExp | null {
  if (cond.regex !== true) return null
  try {
    return new RegExp(value, 'i')
  } catch {
    return null
  }
}

function usesRegex(cond: FilterCondition): boolean {
  return cond.regex === true
    && (cond.op === 'contains' || cond.op === 'not_contains' || cond.op === 'equals' || cond.op === 'not_equals')
}

function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
  const resolved = resolveField(entry, cond.field)
  const { op, value } = cond

  if (op === 'is_empty') {
    if (resolved.array) return resolved.array.length === 0
    const s = resolved.scalar
    return s == null || s === '' || s === false
  }
  if (op === 'is_not_empty') {
    if (resolved.array) return resolved.array.length > 0
    const s = resolved.scalar
    return s != null && s !== '' && s !== false
  }

  const condVal = toString(value)
  const regex = usesRegex(cond) ? compileRegex(cond, condVal) : null

  if (usesRegex(cond) && !regex) return false

  if (resolved.array) {
    if (regex) {
      const matched = resolved.array.some((item) => relationshipCandidates(item).some((candidate) => regex.test(candidate)))
      if (op === 'contains' || op === 'equals') return matched
      if (op === 'not_contains' || op === 'not_equals') return !matched
    }

    const stem = wikilinkStem(condVal)
    const isWikilink = condVal.trim().startsWith('[[')
    const arrayMatch = (arr: string[]) => arr.some((item) =>
      isWikilink ? wikilinkEquals(item, condVal) : wikilinkStem(item).includes(stem)
    )
    if (op === 'contains') return arrayMatch(resolved.array)
    if (op === 'not_contains') return !arrayMatch(resolved.array)
    if (op === 'any_of' && Array.isArray(value)) {
      return resolved.array.some((item) =>
        (value as string[]).some((v) => wikilinkEquals(item, v))
      )
    }
    if (op === 'none_of' && Array.isArray(value)) {
      return !resolved.array.some((item) =>
        (value as string[]).some((v) => wikilinkEquals(item, v))
      )
    }
    return false
  }

  const fieldRaw = toString(resolved.scalar)
  if (regex) {
    const matched = regex.test(fieldRaw)
    if (op === 'equals' || op === 'contains') return matched
    if (op === 'not_equals' || op === 'not_contains') return !matched
  }

  const fieldStr = fieldRaw.toLowerCase()
  const condStr = condVal.toLowerCase()

  if (op === 'equals') return fieldStr === condStr
  if (op === 'not_equals') return fieldStr !== condStr
  if (op === 'contains') return fieldStr.includes(condStr)
  if (op === 'not_contains') return !fieldStr.includes(condStr)
  if (op === 'any_of' && Array.isArray(value)) return (value as string[]).some((v) => toString(v).toLowerCase() === fieldStr)
  if (op === 'none_of' && Array.isArray(value)) return !(value as string[]).some((v) => toString(v).toLowerCase() === fieldStr)

  // Date comparisons
  if (op === 'before' || op === 'after') {
    let tsMs: number | null = null
    if (typeof resolved.scalar === 'number') {
      tsMs = resolved.scalar * 1000 // Unix timestamp (seconds) → milliseconds
    } else if (typeof resolved.scalar === 'string') {
      tsMs = toDateFilterTimestamp(resolved.scalar)
    }
    if (tsMs == null) return false
    const target = toDateFilterTimestamp(condVal)
    if (target == null) return false
    return op === 'before' ? tsMs < target : tsMs > target
  }

  return false
}
