// ── Code Repair Agent ─────────────────────────────────────────────
// Phase 2: Auto-fix TypeScript build errors using qwen2.5-coder:1.5b-base
//
// Usage:
//   node scripts/code-repair-agent.mjs           # one-shot: fix build errors
//   node scripts/code-repair-agent.mjs --watch    # watch mode: re-fix on file changes
//   node scripts/code-repair-agent.mjs --ci       # CI mode: fix + commit
//
// Requires: Ollama running at OLLAMA_HOST (default http://localhost:11434)
// Model: qwen2.5-coder:1.5b-base
//
// ── Architecture ──────────────────────────────────────────────────
// 1. Capture build output from `npx tsc --noEmit`
// 2. Parse error locations (file, line, column, message)
// 3. Read the failing source file + surrounding context (~20 lines)
// 4. Send to qwen2.5-coder via Ollama API for fix suggestion
// 5. Apply fix if confidence > threshold (0.7)
// 6. Re-run build to verify fix
// 7. If breaking change detected (wide-reaching import changes), escalate
// 8. In --ci mode: auto-commit with [bot] prefix

import { execSync } from "node:child_process"
import { readFileSync, writeFileSync, existsSync, appendFileSync } from "node:fs"
import { resolve, dirname } from "node:path"
import { fileURLToPath } from "node:url"
import { createRequire } from "node:module"

const SELF = dirname(fileURLToPath(import.meta.url))
const _require = createRequire(import.meta.url)
const ROOT = resolve(SELF, "..")
const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434"
const MODEL = process.env.REPAIR_MODEL || "qwen2.5-coder:1.5b-base"
const MAX_ITERATIONS = 5
const CONFIDENCE_THRESHOLD = 0.7
const ESCALATION_TIMEOUT_MS = 30 * 60 * 1000 // 30 min

// ── Helpers ────────────────────────────────────────────────────────

function log(msg, level = "info") {
  const prefix = level === "error" ? "✗" : level === "warn" ? "⚠" : "✓"
  console.log(`  ${prefix} [repair] ${msg}`)
}

function runTsc() {
  try {
    const out = execSync("npx tsc --noEmit", { stdio: "pipe", timeout: 60000, cwd: ROOT })
    return { ok: true, output: out.toString() }
  } catch (e) {
    return { ok: false, output: e.stdout?.toString() || e.message || "" }
  }
}

function parseErrors(output) {
  const errors = []
  // Match: src/file.ts(line,col): error TSxxxx: message
  const lineRegex = /(src\/[^:]+)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)/g
  let m
  while ((m = lineRegex.exec(output)) !== null) {
    errors.push({
      file: resolve(ROOT, m[1]),
      line: parseInt(m[2], 10),
      column: parseInt(m[3], 10),
      message: m[4].trim(),
    })
  }
  return errors
}

function readContext(filePath, errorLine, contextLines = 20) {
  try {
    const lines = readFileSync(filePath, "utf-8").split("\n")
    const start = Math.max(0, errorLine - 1 - contextLines)
    const end = Math.min(lines.length, errorLine - 1 + contextLines)
    return {
      content: lines.slice(start, end).join("\n"),
      contextLine: errorLine - 1 - start, // 0-indexed line of the error in the snippet
      totalLines: lines.length,
    }
  } catch {
    return null
  }
}

// ── Ollama Integration ─────────────────────────────────────────────

async function queryOllama(prompt) {
  const response = await fetch(`${OLLAMA_HOST}/api/generate`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: MODEL,
      prompt,
      stream: false,
      options: { temperature: 0.3, num_predict: 2048 },
    }),
  })
  if (!response.ok) throw new Error(`Ollama returned ${response.status}`)
  const data = await response.json()
  return data.response || ""
}

function buildRepairPrompt(filePath, errorMessage, codeContext) {
  return `You are a TypeScript repair agent. Fix the following error.

FILE: ${filePath}
ERROR: ${errorMessage}

CODE CONTEXT (line ${codeContext.contextLine + 1} is the error line):
\`\`\`typescript
${codeContext.content}
\`\`\`

Respond with ONLY the fix JSON in this exact format:
{"fix": "the exact replacement code for the error line(s)", "confidence": 0.0-1.0, "explanation": "brief explanation"}

Rules:
- Keep the fix minimal and precise
- Only change what's necessary to fix the error
- If the error is a missing import, suggest the import statement
- If the error is a type mismatch, suggest the corrected code
- confidence < 0.7 will not be auto-applied
- If you cannot determine a fix, set confidence to 0`
}

function parseFixResponse(text) {
  try {
    // Extract JSON from response (it might have markdown fences)
    const jsonMatch = text.match(/\{[\s\S]*"fix"[\s\S]*"confidence"[\s\S]*\}/)
    if (!jsonMatch) return null
    return JSON.parse(jsonMatch[0])
  } catch {
    return null
  }
}

function applyFix(filePath, fixSuggestion) {
  if (!fixSuggestion || !fixSuggestion.fix) return { applied: false, reason: "No fix suggestion" }

  try {
    const original = readFileSync(filePath, "utf-8")
    writeFileSync(filePath, fixSuggestion.fix, "utf-8")
    return { applied: true, backup: original }
  } catch (e) {
    return { applied: false, reason: e.message }
  }
}

// ── Breaking Change Escalation ─────────────────────────────────────

function isBreakingChange(errors, fixes) {
  // Heuristic: if fix touches import structure or exports, it's breaking
  for (const fix of fixes) {
    if (!fix || !fix.fix) continue
    if (fix.fix.includes("export ") || fix.fix.includes("import ") || fix.fix.includes("delete ")) {
      return true
    }
  }
  return false
}

async function escalateBreakingChange(errors, fixes) {
  const logFile = resolve(ROOT, "repair-escalation.log")
  const timestamp = new Date().toISOString()
  const entry = [
    `=== Breaking Change Escalation @ ${timestamp} ===`,
    `Errors: ${errors.map(e => `${e.file}:${e.line}: ${e.message}`).join("\n  ")}`,
    `Fixes: ${fixes.map(f => f?.fix?.slice(0, 200)).join("\n  ")}`,
    `Auto-apply in ${ESCALATION_TIMEOUT_MS / 60000} minutes or cancel with Ctrl+C`,
    "========================================\n",
  ].join("\n")

  appendFileSync(logFile, entry)
  log(`Breaking change escalated — see ${logFile}`, "warn")
  log(`Auto-applying in ${ESCALATION_TIMEOUT_MS / 60000} min (Ctrl+C to cancel)...`, "warn")

  // Wait for timeout or user intervention
  await new Promise(resolve => setTimeout(resolve, ESCALATION_TIMEOUT_MS))
  log("Escalation timeout reached — applying fixes")
  return true
}

// ── Git Auto-Commit ────────────────────────────────────────────────

function gitCommit(message) {
  try {
    execSync("git add -A", { stdio: "pipe", cwd: ROOT })
    execSync(`git commit -m "[bot] ${message}"`, { stdio: "pipe", cwd: ROOT })
    log(`Committed: [bot] ${message}`)
    return true
  } catch (e) {
    log(`Git commit failed: ${e.message}`, "warn")
    return false
  }
}

// ── Main Repair Loop ───────────────────────────────────────────────

async function repairOnce(doCommit = false) {
  log("Running build check...")
  const build = runTsc()
  if (build.ok) {
    log("No build errors found")
    return { fixed: false, errors: [] }
  }

  const errors = parseErrors(build.output)
  if (errors.length === 0) {
    log("Could not parse build errors from output", "warn")
    log(build.output.slice(0, 1000))
    return { fixed: false, errors: [] }
  }

  log(`Found ${errors.length} error(s):`)
  for (const e of errors.slice(0, 10)) {
    const shortFile = e.file.replace(ROOT, "").replace(/^\//, "")
    log(`${shortFile}:${e.line}:${e.column} — ${e.message}`)
  }

  const fixes = []
  for (const error of errors) {
    const context = readContext(error.file, error.line)
    if (!context) {
      log(`Cannot read ${error.file}`, "error")
      continue
    }

    log(`Asking ${MODEL} to fix ${error.file}:${error.line}...`)
    try {
      const prompt = buildRepairPrompt(error.file, error.message, context)
      const response = await queryOllama(prompt)
      const fix = parseFixResponse(response)

      if (fix && fix.confidence >= CONFIDENCE_THRESHOLD) {
        const result = applyFix(error.file, fix)
        if (result.applied) {
          log(`${fix.explanation || "Applied fix"} (confidence: ${fix.confidence})`)
          fixes.push(fix)
        } else {
          log(`Failed to apply fix: ${result.reason}`, "error")
        }
      } else {
        log(`Low confidence (${fix?.confidence || 0}) or invalid response — skipping`, "warn")
      }
    } catch (e) {
      log(`Ollama error: ${e.message}`, "error")
    }
  }

  if (fixes.length === 0) {
    log("No fixes could be applied", "error")
    return { fixed: false, errors }
  }

  // Verify by re-running build
  log("Verifying fixes...")
  const verify = runTsc()
  if (verify.ok) {
    log("All fixes verified — build passes!")
    if (doCommit) gitCommit(`Auto-fix: ${fixes.length} error(s) resolved`)
    return { fixed: true, errors: [], fixes }
  }

  const remainingErrors = parseErrors(verify.output)
  log(`${remainingErrors.length} error(s) remaining after fix`, "warn")

  // Check for breaking changes
  if (isBreakingChange(remainingErrors, fixes)) {
    log("Breaking change detected — escalating", "warn")
    const approved = await escalateBreakingChange(remainingErrors, fixes)
    if (approved) {
      if (doCommit) gitCommit(`Breaking change applied after escalation`)
      return { fixed: true, errors: remainingErrors, fixes, escalated: true }
    }
  }

  return { fixed: remainingErrors.length === 0, errors: remainingErrors, fixes }
}

async function repairLoop(maxIterations = MAX_ITERATIONS, doCommit = false) {
  for (let i = 0; i < maxIterations; i++) {
    log(`=== Repair iteration ${i + 1}/${maxIterations} ===`)
    const result = await repairOnce(doCommit)
    if (!result.fixed) {
      if (result.errors?.length > 0) {
        log(`${result.errors.length} error(s) remaining — run again for deeper fix`, "warn")
      }
      break
    }
    if (result.errors?.length === 0) break
  }
  log("Repair agent finished")
}

// ── Watch Mode ─────────────────────────────────────────────────────

function watchMode() {
  log("Watch mode active — monitoring src/ for changes...", "info")
  const chokidar = (() => {
    try {
      return _require("chokidar")
    } catch {
      return null
    }
  })()

  if (!chokidar) {
    log("chokidar not available — using simple polling (5s interval)", "warn")
    setInterval(async () => {
      const result = await repairOnce(false)
      if (result.fixed) log("Auto-fix applied")
    }, 5000)
    return
  }

  const watcher = chokidar.watch("src/**/*.{ts,tsx}", { cwd: ROOT, ignoreInitial: true })
  let debounceTimer = null

  watcher.on("change", async (filePath) => {
    clearTimeout(debounceTimer)
    debounceTimer = setTimeout(async () => {
      log(`Change detected: ${filePath}`)
      await repairLoop(3, false)
    }, 1000)
  })

  log("Watching for changes...")
}

// ── Entry ──────────────────────────────────────────────────────────

const args = process.argv.slice(2)
const isWatch = args.includes("--watch") || args.includes("-w")
const isCI = args.includes("--ci") || args.includes("-c")

async function main() {
  if (isWatch) {
    watchMode()
  } else {
    await repairLoop(MAX_ITERATIONS, isCI)
  }
}

main().catch((e) => {
  console.error("Repair agent crashed:", e)
  process.exit(1)
})
