"use strict";

const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");

const {
  PACKAGE_ROOT,
  GLOBAL_CLAUDE_DIR,
  INSTALL_TARGETS,
} = require("./constants");
const { getTheme, log, logHeader } = require("./logging");
const { computeDirChecksums, isBinaryFile, countFiles } = require("./fs-utils");
const { MIGRATIONS, readMigrationState } = require("./migrations");

function compareDirs(srcDir, destDir, label, results) {
  const srcChecksums = computeDirChecksums(srcDir);
  const destChecksums = computeDirChecksums(destDir);

  for (const [relPath, srcHash] of srcChecksums) {
    const destHash = destChecksums.get(relPath);
    if (!destHash) {
      results.removed.push({ path: `${label}/${relPath}`, type: "missing" });
    } else if (srcHash !== destHash) {
      results.modified.push({ path: `${label}/${relPath}`, type: "modified" });
    }
  }

  for (const [relPath] of destChecksums) {
    if (!srcChecksums.has(relPath)) {
      results.added.push({ path: `${label}/${relPath}`, type: "added" });
    }
  }
}

function verifyInstallation(_globalDir = GLOBAL_CLAUDE_DIR) {
  const results = { modified: [], added: [], removed: [] };

  for (const [key, target] of Object.entries(INSTALL_TARGETS.global)) {
    if (target.subDirPrefix) {
      // Skills: each subdir maps with prefix
      if (!fs.existsSync(target.src)) continue;
      const subdirs = fs
        .readdirSync(target.src, { withFileTypes: true })
        .filter((e) => e.isDirectory());
      for (const subdir of subdirs) {
        const srcPath = path.join(target.src, subdir.name);
        const destPath = path.join(
          target.dest,
          target.subDirPrefix + subdir.name,
        );
        compareDirs(
          srcPath,
          destPath,
          `${key}/${target.subDirPrefix}${subdir.name}`,
          results,
        );
      }
    } else {
      compareDirs(target.src, target.dest, key, results);
    }
  }

  const clean = results.modified.length === 0 && results.removed.length === 0;
  const summary = `${results.modified.length} modified, ${results.added.length} added, ${results.removed.length} missing`;

  return {
    clean,
    modified: results.modified,
    added: results.added,
    removed: results.removed,
    summary,
  };
}

function showStatus(globalDir = GLOBAL_CLAUDE_DIR) {
  const pkg = require(path.join(PACKAGE_ROOT, "package.json"));
  const stateFile = path.join(globalDir, "evolv-coder-kit", ".migrations");
  const state = readMigrationState(stateFile);

  // Count installed files per target
  const fileCounts = {};
  for (const [key, target] of Object.entries(INSTALL_TARGETS.global)) {
    if (target.subDirPrefix) {
      let total = 0;
      if (fs.existsSync(target.dest)) {
        const entries = fs
          .readdirSync(target.dest, { withFileTypes: true })
          .filter(
            (e) => e.isDirectory() && e.name.startsWith(target.subDirPrefix),
          );
        for (const entry of entries) {
          total += countFiles(path.join(target.dest, entry.name));
        }
      }
      fileCounts[key] = total;
    } else {
      fileCounts[key] = countFiles(target.dest);
    }
  }

  // Run drift check
  const drift = verifyInstallation(globalDir);

  const t = getTheme();
  logHeader("Installation Status");
  log(`  Package version:  ${t.bold(pkg.version)}`);
  log(`  Last install:     ${state.lastRunAt || t.dim("never")}`);
  log(
    `  Migrations:       ${state.appliedMigrations.length}/${MIGRATIONS.length} applied`,
  );
  log("");
  log(t.bold("Installed files:"));
  for (const [key, count] of Object.entries(fileCounts)) {
    log(`  ${key}: ${count} files`);
  }
  log("");
  log(`Drift: ${drift.clean ? t.success("clean") : t.warning(drift.summary)}`);

  return { version: pkg.version, state, drift };
}

function runDoctor(globalDir = GLOBAL_CLAUDE_DIR) {
  const checks = [];

  // 1. Node.js version
  const [major] = process.versions.node.split(".").map(Number);
  checks.push({
    check: "Node.js version",
    status: major >= 18 ? "ok" : "fail",
    message: `v${process.versions.node}${major >= 18 ? "" : " (requires >= 18)"}`,
  });

  // 2. ~/.claude/ directory
  checks.push({
    check: "~/.claude/ directory",
    status: fs.existsSync(globalDir) ? "ok" : "fail",
    message: fs.existsSync(globalDir) ? "exists" : "not found",
  });

  // 3. Templates installed
  const kitDir = path.join(globalDir, "evolv-coder-kit");
  if (fs.existsSync(kitDir)) {
    const count = countFiles(kitDir);
    checks.push({
      check: "Templates installed",
      status: "ok",
      message: `${count} files in evolv-coder-kit/`,
    });
  } else {
    checks.push({
      check: "Templates installed",
      status: "fail",
      message: "evolv-coder-kit/ not found",
    });
  }

  // 4. settings.json validity
  const settingsPath = path.join(globalDir, "settings.json");
  if (fs.existsSync(settingsPath)) {
    try {
      JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
      checks.push({
        check: "settings.json",
        status: "ok",
        message: "valid JSON",
      });
    } catch (err) {
      checks.push({
        check: "settings.json",
        status: "fail",
        message: `invalid JSON: ${err.message}`,
      });
    }
  } else {
    checks.push({
      check: "settings.json",
      status: "warn",
      message: "not found",
    });
  }

  // 5. Hook permissions
  const hooksDir = path.join(globalDir, "hooks");
  if (fs.existsSync(hooksDir)) {
    const hookFiles = fs
      .readdirSync(hooksDir)
      .filter((f) => f.endsWith(".sh") || f.endsWith(".js"));
    let allExec = true;
    for (const hf of hookFiles) {
      try {
        fs.accessSync(path.join(hooksDir, hf), fs.constants.X_OK);
      } catch {
        allExec = false;
      }
    }
    checks.push({
      check: "Hook permissions",
      status: allExec ? "ok" : "warn",
      message: allExec
        ? `${hookFiles.length} hooks executable`
        : "some hooks not executable",
    });
  } else {
    checks.push({
      check: "Hook permissions",
      status: "warn",
      message: "hooks/ not found",
    });
  }

  // 6. GitHub CLI
  try {
    const ghVersion = execSync("gh --version", {
      encoding: "utf-8",
      stdio: ["pipe", "pipe", "pipe"],
      timeout: 10000,
    })
      .trim()
      .split("\n")[0];
    checks.push({ check: "GitHub CLI (gh)", status: "ok", message: ghVersion });
  } catch {
    checks.push({
      check: "GitHub CLI (gh)",
      status: "warn",
      message: "not installed",
    });
  }

  // 7. jq — required by every scaffold hook; without it the PreToolUse guards
  // exit 2 and block all Edit/Write/Bash. A missing jq is a hard failure.
  try {
    const jqVersion = execSync("jq --version", {
      encoding: "utf-8",
      stdio: ["pipe", "pipe", "pipe"],
      timeout: 10000,
    }).trim();
    checks.push({ check: "jq", status: "ok", message: jqVersion });
  } catch {
    checks.push({
      check: "jq",
      status: "fail",
      message:
        "not found — required by all hooks (install: brew/apt/dnf/winget per OS)",
    });
  }

  // 8. bash — Windows only (ADR-031 §4: assumed present on POSIX base installs).
  if (process.platform === "win32") {
    try {
      execSync("bash --version", {
        stdio: ["pipe", "pipe", "pipe"],
        timeout: 10000,
      });
      checks.push({ check: "bash", status: "ok", message: "present on PATH" });
    } catch {
      checks.push({
        check: "bash",
        status: "fail",
        message: "not found — install Git for Windows (git-scm.com)",
      });
    }
  }

  // 9. Migration state
  const stateFile = path.join(globalDir, "evolv-coder-kit", ".migrations");
  const state = readMigrationState(stateFile);
  const pendingCount = MIGRATIONS.filter(
    (m) => !state.appliedMigrations.includes(m.id),
  ).length;
  checks.push({
    check: "Migration state",
    status: pendingCount === 0 ? "ok" : "warn",
    message:
      pendingCount === 0
        ? `all ${MIGRATIONS.length} migrations applied`
        : `${pendingCount} pending migration${pendingCount > 1 ? "s" : ""}`,
  });

  return checks;
}

function computeDiff(oldText, newText, oldLabel, newLabel) {
  const oldLines = oldText.split("\n");
  const newLines = newText.split("\n");

  // Simple LCS-based diff
  const m = oldLines.length;
  const n = newLines.length;
  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (oldLines[i - 1] === newLines[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }

  const lines = [];
  lines.push(`--- ${oldLabel}`);
  lines.push(`+++ ${newLabel}`);

  let i = m,
    j = n;
  const result = [];
  while (i > 0 || j > 0) {
    if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
      result.push(` ${oldLines[i - 1]}`);
      i--;
      j--;
    } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
      result.push(`+${newLines[j - 1]}`);
      j--;
    } else {
      result.push(`-${oldLines[i - 1]}`);
      i--;
    }
  }

  result.reverse();
  lines.push(...result);
  return lines;
}

function collectDiffs(srcDir, destDir, label, diffs) {
  const srcChecksums = computeDirChecksums(srcDir);
  const destChecksums = computeDirChecksums(destDir);

  for (const [relPath, srcHash] of srcChecksums) {
    const destHash = destChecksums.get(relPath);
    const srcPath = path.join(srcDir, relPath);
    const destPath = path.join(destDir, relPath);

    if (isBinaryFile(srcPath)) {
      if (!destHash || srcHash !== destHash) {
        diffs.push({
          path: `${label}/${relPath}`,
          lines: [`[binary file] ${label}/${relPath}`],
        });
      }
      continue;
    }

    if (!destHash) {
      // New file
      const content = fs.readFileSync(srcPath, "utf-8");
      diffs.push({
        path: `${label}/${relPath}`,
        lines: [
          `(new file) ${label}/${relPath}`,
          ...content.split("\n").map((l) => `+${l}`),
        ],
      });
    } else if (srcHash !== destHash) {
      const oldText = fs.readFileSync(destPath, "utf-8");
      const newText = fs.readFileSync(srcPath, "utf-8");
      const diffLines = computeDiff(
        oldText,
        newText,
        `installed/${label}/${relPath}`,
        `package/${label}/${relPath}`,
      );
      diffs.push({ path: `${label}/${relPath}`, lines: diffLines });
    }
  }
}

function previewDiff(_globalDir = GLOBAL_CLAUDE_DIR) {
  const diffs = [];

  for (const [key, target] of Object.entries(INSTALL_TARGETS.global)) {
    if (target.subDirPrefix) {
      if (!fs.existsSync(target.src)) continue;
      const subdirs = fs
        .readdirSync(target.src, { withFileTypes: true })
        .filter((e) => e.isDirectory());
      for (const subdir of subdirs) {
        const srcPath = path.join(target.src, subdir.name);
        const destPath = path.join(
          target.dest,
          target.subDirPrefix + subdir.name,
        );
        collectDiffs(
          srcPath,
          destPath,
          `${key}/${target.subDirPrefix}${subdir.name}`,
          diffs,
        );
      }
    } else {
      collectDiffs(target.src, target.dest, key, diffs);
    }
  }

  return { diffs, totalFiles: diffs.length };
}

function parseFrontmatter(filePath) {
  const content = fs.readFileSync(filePath, "utf-8");
  const match = content.match(/^---\n([\s\S]*?)\n---/);
  if (!match) return null;

  const result = {};
  const lines = match[1].split("\n");
  for (const line of lines) {
    const kv = line.match(/^(\w[\w_-]*):\s*(.+)$/);
    if (kv) {
      let value = kv[2].trim();
      // Strip quotes
      if (
        (value.startsWith('"') && value.endsWith('"')) ||
        (value.startsWith("'") && value.endsWith("'"))
      ) {
        value = value.slice(1, -1);
      }
      result[kv[1]] = value;
    }
  }
  return Object.keys(result).length > 0 ? result : null;
}

const AGENT_DOMAIN_KEYWORDS = {
  Backend: [
    "backend",
    "api",
    "server",
    "database",
    "db",
    "fastapi",
    "django",
    "spring",
    "express",
  ],
  Frontend: [
    "frontend",
    "ui",
    "react",
    "vue",
    "angular",
    "css",
    "design",
    "ux",
  ],
  "Cross-Cutting": [
    "devops",
    "security",
    "technical-writer",
    "code-review",
    "architect",
    "bot",
  ],
  "Language-Specific": [
    "python",
    "go",
    "java",
    "rust",
    "swift",
    "cpp",
    "typescript",
  ],
};

function classifyAgent(name, description) {
  const text = `${name} ${description || ""}`.toLowerCase();
  for (const [domain, keywords] of Object.entries(AGENT_DOMAIN_KEYWORDS)) {
    if (keywords.some((kw) => text.includes(kw))) return domain;
  }
  return "Other";
}

function generateAgentsMd(agentsDir) {
  if (!fs.existsSync(agentsDir)) return null;

  const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".md"));
  if (files.length === 0) return null;

  const agents = [];
  for (const file of files) {
    const fm = parseFrontmatter(path.join(agentsDir, file));
    if (fm) {
      agents.push({
        name: fm.name || file.replace(".md", ""),
        description: fm.description || "",
        model: fm.model || "",
        tools: fm.tools || "",
        domain: classifyAgent(fm.name || file, fm.description),
      });
    }
  }

  if (agents.length === 0) return null;

  // Group by domain
  const groups = {};
  for (const agent of agents) {
    if (!groups[agent.domain]) groups[agent.domain] = [];
    groups[agent.domain].push(agent);
  }

  let md = "# AGENTS.md\n\n";
  md += `> Auto-generated from ${agents.length} agent definitions.\n\n`;

  for (const [domain, domainAgents] of Object.entries(groups)) {
    md += `## ${domain}\n\n`;
    md += "| Agent | Description | Model | Tools |\n";
    md += "|-------|-------------|-------|-------|\n";
    for (const a of domainAgents) {
      md += `| ${a.name} | ${a.description} | ${a.model} | ${a.tools} |\n`;
    }
    md += "\n";
  }

  return md;
}

module.exports = {
  compareDirs,
  verifyInstallation,
  showStatus,
  runDoctor,
  computeDiff,
  previewDiff,
  parseFrontmatter,
  generateAgentsMd,
};
