/**
 * Failure Tracker
 *
 * Tracks recurring failure patterns during Flow loop execution.
 * When a failure type reaches threshold, generates a self-patch proposal.
 *
 * Persists to .sandcode/failures.json
 */

import { readFile, writeFile, mkdir } from 'fs/promises'
import { join } from 'path'
import { getCwd } from '../../utils/cwd.js'
import { logForDebugging } from '../../utils/debug.js'
import type { FailurePattern, FailureType, FailureTrackingState, SelfPatch } from './types.js'

const FAILURES_FILE = '.sandcode/failures.json'

/**
 * Record a failure during Flow execution.
 *
 * Classifies the failure and increments its occurrence count.
 * If the count reaches threshold, triggers self-patch generation.
 */
export async function recordFailure(
  error: string,
  affectedFiles: string[],
): Promise<{ failure: FailurePattern; patchNeeded: boolean }> {
  const state = await loadFailureState()
  const failureType = classifyFailure(error)

  // Find existing or create new failure pattern
  let failure = state.failures.find(f => f.type === failureType)
  const now = Date.now()

  if (!failure) {
    failure = {
      id: `${failureType}_${now}`,
      type: failureType,
      description: extractFailureDescription(error, failureType),
      count: 0,
      firstOccurrence: now,
      lastOccurrence: now,
      affectedFiles,
      threshold: 3,
      patchProposed: false,
    }
    state.failures.push(failure)
  }

  failure.count++
  failure.lastOccurrence = now
  failure.affectedFiles = [...new Set([...failure.affectedFiles, ...affectedFiles])]

  await saveFailureState(state)

  const patchNeeded = failure.count >= failure.threshold && !failure.patchProposed

  if (patchNeeded) {
    logForDebugging(`Failure "${failureType}" reached threshold (${failure.count}x). Self-patch needed.`)
  }

  return { failure, patchNeeded }
}

/**
 * Generate a self-patch proposal for a recurring failure.
 */
export function generateSelfPatch(failure: FailurePattern): SelfPatch {
  const patch = createPatchForFailureType(failure)

  logForDebugging(`Self-patch generated for "${failure.type}": ${patch.type} - ${patch.description}`)

  return patch
}

/**
 * Mark a patch as proposed (so we don't re-propose).
 */
export async function markPatchProposed(failureId: string): Promise<void> {
  const state = await loadFailureState()
  const failure = state.failures.find(f => f.id === failureId)
  if (failure) {
    failure.patchProposed = true
    await saveFailureState(state)
  }
}

/**
 * Get all tracked failures.
 */
export async function getTrackedFailures(): Promise<FailurePattern[]> {
  const state = await loadFailureState()
  return state.failures
}

/**
 * Get failures that have reached threshold but haven't been patched.
 */
export async function getUnpatchedFailures(): Promise<FailurePattern[]> {
  const state = await loadFailureState()
  return state.failures.filter(f => f.count >= f.threshold && !f.patchProposed)
}

/**
 * Get a formatted summary of failure patterns.
 */
export async function getFailureSummary(): Promise<string> {
  const failures = await getTrackedFailures()

  if (failures.length === 0) {
    return 'No failure patterns tracked yet. Use /flow to start autonomous development.'
  }

  const lines: string[] = ['Failure Patterns:', '']

  for (const f of failures.sort((a, b) => b.count - a.count)) {
    const status = f.patchProposed ? '[PATCHED]' : f.count >= f.threshold ? '[NEEDS PATCH]' : '[MONITORING]'
    lines.push(`  ${status} ${f.type}: ${f.count}x — ${f.description}`)
    lines.push(`       Last: ${new Date(f.lastOccurrence).toLocaleDateString()} | Files: ${f.affectedFiles.slice(0, 3).join(', ')}`)
  }

  return lines.join('\n')
}

// --- Internal helpers ---

/**
 * Classify an error into a failure type.
 */
function classifyFailure(error: string): FailureType {
  const lower = error.toLowerCase()

  if (/type\s*error|type\s*mismatch|type.*is not assignable/.test(lower)) return 'type_mismatch'
  if (/cannot find (module|name|import)|is not defined/.test(lower)) return 'missing_import'
  if (/no test|test not found|missing.*test/.test(lower)) return 'missing_test'
  if (/unhandled|uncaught|error not (handled|caught)/.test(lower)) return 'missing_error_handling'
  if (/property.*does not exist|type.*has no/.test(lower)) return 'forgot_typescript_types'
  if (/validation|invalid input|required field/.test(lower)) return 'forgot_validation'
  if (/runtime error|referenceerror|typeerror|rangeerror/.test(lower)) return 'runtime_error'
  if (/test.*fail|assert|expected.*received/.test(lower)) return 'test_failure'
  if (/lint|eslint|style|formatting/.test(lower)) return 'lint_failure'
  if (/build fail|compilation error|syntax error/.test(lower)) return 'build_failure'

  return 'unknown'
}

/**
 * Extract a human-readable description from an error.
 */
function extractFailureDescription(error: string, type: FailureType): string {
  // Take the first meaningful line of the error
  const lines = error.split('\n').filter(l => l.trim().length > 0)
  const firstLine = lines[0]?.trim() ?? 'Unknown error'

  // Truncate to reasonable length
  return firstLine.slice(0, 200)
}

/**
 * Create a patch proposal for a given failure type.
 */
function createPatchForFailureType(failure: FailurePattern): SelfPatch {
  switch (failure.type) {
    case 'forgot_typescript_types':
      return {
        type: 'hook',
        description: 'Auto-run TypeScript type-check after file edits',
        reason: `TypeScript type errors occurred ${failure.count} times. Running tsc after edits will catch these immediately.`,
        content: JSON.stringify({
          matcher: 'Edit|Write',
          command: 'npx tsc --noEmit --pretty false 2>&1 | head -20',
          description: 'TypeScript type-check after edits',
        }, null, 2),
        failureId: failure.id,
        proposedAt: Date.now(),
        approved: false,
      }

    case 'missing_import':
      return {
        type: 'instinct_modification',
        description: 'Add "verify imports" step to implementation instincts',
        reason: `Missing imports occurred ${failure.count} times. Adding an import verification step will prevent this.`,
        content: 'Add step: "Verify all imports are correct by running the build/type-check after implementation"',
        failureId: failure.id,
        proposedAt: Date.now(),
        approved: false,
      }

    case 'missing_error_handling':
      return {
        type: 'rule',
        description: 'Add error handling requirement to SANDCODE.md',
        reason: `Missing error handling occurred ${failure.count} times. Adding a persistent rule will enforce it.`,
        content: '- Always add error handling to async functions and API calls\n- Use try/catch with specific error types\n- Never silently catch errors',
        failureId: failure.id,
        proposedAt: Date.now(),
        approved: false,
      }

    case 'lint_failure':
      return {
        type: 'hook',
        description: 'Auto-run linter after file edits',
        reason: `Lint failures occurred ${failure.count} times. Running linter after edits will catch issues immediately.`,
        content: JSON.stringify({
          matcher: 'Edit|Write',
          command: 'npx eslint --fix "$FILE_PATH" 2>&1 | head -20',
          description: 'ESLint check after edits',
        }, null, 2),
        failureId: failure.id,
        proposedAt: Date.now(),
        approved: false,
      }

    default:
      return {
        type: 'rule',
        description: `Add rule to prevent "${failure.type}" failures`,
        reason: `This failure type occurred ${failure.count} times. Adding awareness will help prevent recurrence.`,
        content: `- Watch for "${failure.type}" errors: ${failure.description}`,
        failureId: failure.id,
        proposedAt: Date.now(),
        approved: false,
      }
  }
}

/**
 * Load failure tracking state from disk.
 */
async function loadFailureState(): Promise<FailureTrackingState> {
  try {
    const filepath = join(getCwd(), FAILURES_FILE)
    const content = await readFile(filepath, 'utf-8')
    return JSON.parse(content) as FailureTrackingState
  } catch {
    return { version: 1, failures: [], patches: [] }
  }
}

/**
 * Save failure tracking state to disk.
 */
async function saveFailureState(state: FailureTrackingState): Promise<void> {
  const filepath = join(getCwd(), FAILURES_FILE)
  await mkdir(join(getCwd(), '.sandcode'), { recursive: true })
  await writeFile(filepath, JSON.stringify(state, null, 2), 'utf-8')
}
