import { DependencyGraph, type RecomputeState } from './dependencyGraph'
import type { Diagnostic, DiagnosticRepairAction, DiagnosticTreeNode, DocumentSnapshot } from './types'

type RecomputeDiagnosticInput = {
  document: DocumentSnapshot
  generation: number
  affected: string[]
  objectStates: Record<string, RecomputeState>
  errors: Array<{ objectId: string; code: string; message: string }>
}

const cloneAction = (action: DiagnosticRepairAction): DiagnosticRepairAction => ({ ...action })

export const cloneDiagnostic = (diagnostic: Diagnostic): Diagnostic => ({
  ...diagnostic,
  dependencyPath: diagnostic.dependencyPath ? [...diagnostic.dependencyPath] : undefined,
  repairActions: diagnostic.repairActions?.map(cloneAction),
  topologyRepair: diagnostic.topologyRepair ? { ...diagnostic.topologyRepair, candidates: [...diagnostic.topologyRepair.candidates] } : undefined,
})

const rootCausePath = (objectId: string, graph: DependencyGraph, states: Record<string, RecomputeState>) => {
  const path = [objectId]
  const visited = new Set(path)
  let current = objectId
  while (states[current] === 'upstream-failed') {
    const dependency = graph.dependenciesOf(current).find((candidate) => states[candidate] === 'error' || states[candidate] === 'upstream-failed')
    if (!dependency || visited.has(dependency)) break
    path.push(dependency)
    visited.add(dependency)
    current = dependency
  }
  return path
}

const repairActions = (document: DocumentSnapshot, targetObjectId: string, rootCode?: string): DiagnosticRepairAction[] => {
  const target = document.objects.find((object) => object.id === targetObjectId)
  const suppressibleFeature = target?.properties.some((property) => property.name === 'Suppressed' && property.type === 'App::PropertyBool') ?? false
  const canSuppress = suppressibleFeature && rootCode !== 'DEPENDENCY_CYCLE'
  return [
    { id: 'select-object', label: 'Select root object', targetObjectId, enabled: Boolean(target) },
    { id: 'recompute-root', label: 'Recompute affected branch', targetObjectId, enabled: Boolean(target) },
    { id: 'suppress-root', label: 'Suppress failing feature', targetObjectId, enabled: canSuppress, reason: canSuppress ? undefined : rootCode === 'DEPENDENCY_CYCLE' ? 'Dependency cycles must be repaired by changing a reference or expression.' : 'The root object is not a suppressible feature.' },
  ]
}

export const buildRecomputeDiagnostics = ({ document, generation, affected, objectStates, errors }: RecomputeDiagnosticInput): Diagnostic[] => {
  const graph = new DependencyGraph(document.dependencies ?? [], document.objects.map((object) => object.id))
  const errorsByObject = new Map<string, Array<{ code: string; message: string }>>()
  for (const error of errors) {
    const entries = errorsByObject.get(error.objectId) ?? []
    entries.push({ code: error.code, message: error.message })
    errorsByObject.set(error.objectId, entries)
  }

  const diagnostics: Diagnostic[] = []
  for (const objectId of affected) {
    const state = objectStates[objectId]
    if (state !== 'error' && state !== 'upstream-failed') continue
    const path = rootCausePath(objectId, graph, objectStates)
    const rootCauseObjectId = path.at(-1) as string
    const rootCode = errorsByObject.get(rootCauseObjectId)?.[0]?.code
    const objectErrors = errorsByObject.get(objectId)
    const entries = objectErrors?.length ? objectErrors : [{ code: 'UPSTREAM_FAILED', message: `Skipped because ${rootCauseObjectId} failed to recompute.` }]
    for (const [index, error] of entries.entries()) {
      diagnostics.push({
        id: `recompute:${document.id}:${generation}:${objectId}:${error.code}:${index}`,
        source: 'recompute',
        severity: state === 'error' ? 'error' : 'warning',
        code: error.code,
        message: error.message,
        objectId,
        documentId: document.id,
        documentVersion: document.version,
        generation,
        rootCauseObjectId,
        dependencyPath: path,
        repairActions: repairActions(document, rootCauseObjectId, rootCode),
      })
    }
  }
  return diagnostics
}

export const buildDiagnosticTree = (diagnostics: Diagnostic[]): DiagnosticTreeNode[] => {
  const active = diagnostics.filter((diagnostic) => !diagnostic.resolved).map(cloneDiagnostic)
  const childrenByRoot = new Map<string, Diagnostic[]>()
  const roots: Diagnostic[] = []
  const rootKeys = new Set<string>()
  const keyFor = (diagnostic: Diagnostic, objectId = diagnostic.objectId) => `${diagnostic.source}:${diagnostic.documentId ?? ''}:${diagnostic.generation ?? ''}:${objectId ?? diagnostic.id}`
  for (const diagnostic of active.filter((candidate) => !candidate.rootCauseObjectId || candidate.objectId === candidate.rootCauseObjectId)) {
    const key = keyFor(diagnostic)
    if (rootKeys.has(key)) {
      const children = childrenByRoot.get(key) ?? []
      children.push(diagnostic)
      childrenByRoot.set(key, children)
    } else {
      rootKeys.add(key)
      roots.push(diagnostic)
    }
  }
  for (const diagnostic of active.filter((candidate) => candidate.rootCauseObjectId && candidate.objectId !== candidate.rootCauseObjectId)) {
    const key = keyFor(diagnostic, diagnostic.rootCauseObjectId)
    if (!rootKeys.has(key)) {
      roots.push(diagnostic)
      rootKeys.add(keyFor(diagnostic))
      continue
    }
    const children = childrenByRoot.get(key) ?? []
    children.push(diagnostic)
    childrenByRoot.set(key, children)
  }
  return roots.map((diagnostic) => ({
    diagnostic,
    children: childrenByRoot.get(keyFor(diagnostic))?.map((child) => ({ diagnostic: child, children: [] })) ?? [],
  }))
}

export const replaceRecomputeDiagnostics = (existing: Diagnostic[], documentId: string, affected: string[], next: Diagnostic[]) => {
  const affectedIds = new Set(affected)
  return [...existing.filter((diagnostic) => diagnostic.source !== 'recompute' || diagnostic.documentId !== documentId || !diagnostic.objectId || !affectedIds.has(diagnostic.objectId)), ...next].map(cloneDiagnostic)
}
