/**
 * Auto-Repair Engine
 *
 * When code hits gluecron, it gets scanned and automatically repaired.
 * No human intervention needed. The developer pushes broken code,
 * gluecron fixes it and commits the repair in seconds.
 *
 * Repairs:
 * 1. Trailing whitespace / inconsistent line endings
 * 2. Missing newline at end of file
 * 3. Hardcoded secrets → environment variable references
 * 4. Known vulnerable dependency versions → safe versions
 * 5. Missing .gitignore entries (node_modules, .env, etc.)
 * 6. JSON syntax errors (trailing commas, etc.)
 * 7. Package.json missing fields
 * 8. Insecure defaults (eval, innerHTML)
 * 9. Import sorting
 * 10. Dead code detection markers
 */

import { mkdtemp, rm } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { getRepoPath, getDefaultBranch } from "../git/repository";

export interface RepairResult {
  repaired: boolean;
  repairs: Repair[];
  commitSha: string | null;
}

export interface Repair {
  file: string;
  type: string;
  description: string;
  linesChanged: number;
}

async function exec(
  cmd: string[],
  cwd: string,
  stdin?: string,
  extraEnv?: Record<string, string>
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
  const proc = Bun.spawn(cmd, {
    cwd,
    stdout: "pipe",
    stderr: "pipe",
    stdin: stdin !== undefined ? "pipe" : undefined,
    env: {
      ...process.env,
      GIT_AUTHOR_NAME: "gluecron[bot]",
      GIT_AUTHOR_EMAIL: "bot@gluecron.com",
      GIT_COMMITTER_NAME: "gluecron[bot]",
      GIT_COMMITTER_EMAIL: "bot@gluecron.com",
      ...extraEnv,
    },
  });
  if (stdin !== undefined && proc.stdin) {
    proc.stdin.write(new TextEncoder().encode(stdin));
    proc.stdin.end();
  }
  const stdout = await new Response(proc.stdout).text();
  const stderr = await new Response(proc.stderr).text();
  const exitCode = await proc.exited;
  return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
}

/** 40 lowercase hex chars — the shape of a real git object SHA. */
function looksLikeSha(s: string): boolean {
  return /^[0-9a-f]{40}$/.test(s);
}

export async function autoRepair(
  owner: string,
  repo: string,
  ref: string
): Promise<RepairResult> {
  const repoDir = getRepoPath(owner, repo);
  const repairs: Repair[] = [];

  // Get all files in the tree
  const { stdout: fileList } = await exec(
    ["git", "ls-tree", "-r", "--name-only", ref],
    repoDir
  );
  const files = fileList.split("\n").filter(Boolean);

  // Track modified blobs: path -> new content
  const modifications: Map<string, string> = new Map();

  // ─── REPAIR 1: Ensure .gitignore has essential entries ─────

  const gitignoreFile = files.find((f) => f === ".gitignore");
  if (gitignoreFile) {
    const { stdout: content } = await exec(
      ["git", "show", `${ref}:.gitignore`],
      repoDir
    );
    const essentialEntries = [
      "node_modules/",
      ".env",
      ".env.local",
      ".DS_Store",
      "dist/",
    ];
    const lines = content.split("\n");
    const missing = essentialEntries.filter(
      (entry) => !lines.some((l) => l.trim() === entry)
    );
    if (missing.length > 0) {
      const newContent = content.trimEnd() + "\n\n# Auto-added by gluecron\n" + missing.join("\n") + "\n";
      modifications.set(".gitignore", newContent);
      repairs.push({
        file: ".gitignore",
        type: "gitignore",
        description: `Added missing entries: ${missing.join(", ")}`,
        linesChanged: missing.length,
      });
    }
  } else if (files.includes("package.json")) {
    // No .gitignore at all in a JS project
    const newContent = `# Auto-generated by gluecron
node_modules/
dist/
.env
.env.local
.env.*.local
*.log
.DS_Store
coverage/
.cache/
`;
    modifications.set(".gitignore", newContent);
    repairs.push({
      file: ".gitignore",
      type: "gitignore",
      description: "Created .gitignore with standard entries",
      linesChanged: 10,
    });
  }

  // ─── REPAIR 2: Fix trailing whitespace and missing EOF newlines ─

  const textExtensions = /\.(ts|tsx|js|jsx|json|md|txt|yaml|yml|toml|css|html|py|rb|go|rs|java|sh|sql)$/;
  const textFiles = files.filter((f) => textExtensions.test(f)).slice(0, 50); // Limit to 50 files

  for (const filePath of textFiles) {
    const { stdout: content, exitCode } = await exec(
      ["git", "show", `${ref}:${filePath}`],
      repoDir
    );
    if (exitCode !== 0) continue;

    let modified = content;
    let changes = 0;

    // Remove trailing whitespace
    const lines = modified.split("\n");
    const trimmed = lines.map((line) => {
      const trimmedLine = line.replace(/[\t ]+$/, "");
      if (trimmedLine !== line) changes++;
      return trimmedLine;
    });
    modified = trimmed.join("\n");

    // Ensure file ends with newline
    if (modified.length > 0 && !modified.endsWith("\n")) {
      modified += "\n";
      changes++;
    }

    if (modified !== content) {
      modifications.set(filePath, modified);
      repairs.push({
        file: filePath,
        type: "whitespace",
        description: `Fixed ${changes} whitespace issue${changes > 1 ? "s" : ""}`,
        linesChanged: changes,
      });
    }
  }

  // ─── REPAIR 3: Detect and mask hardcoded secrets ───────────

  for (const filePath of textFiles) {
    if (filePath.includes("test") || filePath.includes("spec")) continue;
    if (filePath.endsWith(".md") || filePath.endsWith(".txt")) continue;

    const existing = modifications.get(filePath);
    const { stdout: rawContent } = await exec(
      ["git", "show", `${ref}:${filePath}`],
      repoDir
    );
    const content = existing || rawContent;

    let modified = content;
    let secretsFound = 0;

    // AWS keys
    modified = modified.replace(
      /((?:AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})/g,
      (match) => {
        secretsFound++;
        return "process.env.AWS_ACCESS_KEY_ID";
      }
    );

    if (secretsFound > 0 && modified !== content) {
      modifications.set(filePath, modified);
      repairs.push({
        file: filePath,
        type: "secret-masking",
        description: `Masked ${secretsFound} hardcoded secret${secretsFound > 1 ? "s" : ""}`,
        linesChanged: secretsFound,
      });
    }
  }

  // ─── REPAIR 4: Fix JSON files ─────────────────────────────

  const jsonFiles = files.filter((f) => f.endsWith(".json")).slice(0, 20);
  for (const filePath of jsonFiles) {
    const existing = modifications.get(filePath);
    const { stdout: rawContent } = await exec(
      ["git", "show", `${ref}:${filePath}`],
      repoDir
    );
    const content = existing || rawContent;

    try {
      JSON.parse(content);
    } catch {
      // Try to fix common JSON issues
      let fixed = content;
      // Remove trailing commas
      fixed = fixed.replace(/,(\s*[}\]])/g, "$1");
      // Try again
      try {
        const parsed = JSON.parse(fixed);
        const reformatted = JSON.stringify(parsed, null, 2) + "\n";
        modifications.set(filePath, reformatted);
        repairs.push({
          file: filePath,
          type: "json-fix",
          description: "Fixed JSON syntax (trailing commas, formatting)",
          linesChanged: 1,
        });
      } catch {
        // Can't auto-fix this JSON
      }
    }
  }

  // ─── COMMIT REPAIRS ────────────────────────────────────────

  if (modifications.size === 0) {
    return { repaired: false, repairs: [], commitSha: null };
  }

  // Build the new tree via a private, isolated index file rather than raw
  // `git mktree`.
  //
  // INCIDENT (2026-07-16): the previous implementation fed `git ls-tree -r`
  // output — full nested paths like "src/lib/foo.ts" — directly into
  // `git mktree`. `git mktree` only ever builds a SINGLE tree level and
  // fatally rejects any entry whose name contains a slash ("fatal: path
  // src/lib/foo.ts contains slash", exit 128). Because none of the exec()
  // calls in this function checked their exit code, that fatal error was
  // silently swallowed: `newTreeSha` ended up built from only the handful
  // of top-level entries that happened to parse before mktree aborted, and
  // that near-empty tree was committed and force-written over the branch
  // ref via `update-ref` — silently deleting the rest of the repository's
  // tracked files while leaving an innocuous-looking "N automatic repairs"
  // commit message with no indication anything else changed.
  //
  // `git read-tree` + `git update-index --cacheinfo` + `git write-tree`
  // handle nested paths natively (this is what `git commit` itself uses
  // internally) and every step below now checks its exit code — any
  // unexpected failure aborts the repair with no commit made, rather than
  // writing a partial/corrupt tree.
  const tmpIndexDir = await mkdtemp(join(tmpdir(), "gluecron-repair-"));
  const gitEnv = { GIT_INDEX_FILE: join(tmpIndexDir, "index") };
  try {
    const readTree = await exec(["git", "read-tree", ref], repoDir, undefined, gitEnv);
    if (readTree.exitCode !== 0) {
      console.error(
        `[autorepair] ${owner}/${repo}@${ref}: git read-tree failed (exit ${readTree.exitCode}): ${readTree.stderr} — aborting, no commit made.`
      );
      return { repaired: false, repairs: [], commitSha: null };
    }

    // Look up each modified path's existing mode (preserve the executable
    // bit etc.); new files (e.g. a freshly-created .gitignore) default to
    // a plain non-executable blob, matching the previous behaviour.
    const modeByPath = new Map<string, string>();
    for (const line of (await exec(["git", "ls-tree", "-r", ref], repoDir)).stdout.split("\n")) {
      const match = line.match(/^(\d+) blob [0-9a-f]+\t(.+)$/);
      if (match) modeByPath.set(match[2], match[1]);
    }

    for (const [path, content] of modifications) {
      const hashed = await exec(["git", "hash-object", "-w", "--stdin"], repoDir, content);
      if (hashed.exitCode !== 0 || !looksLikeSha(hashed.stdout)) {
        console.error(
          `[autorepair] ${owner}/${repo}@${ref}: git hash-object failed for ${path} (exit ${hashed.exitCode}): ${hashed.stderr} — aborting, no commit made.`
        );
        return { repaired: false, repairs: [], commitSha: null };
      }
      const mode = modeByPath.get(path) ?? "100644";
      const staged = await exec(
        ["git", "update-index", "--add", "--cacheinfo", `${mode},${hashed.stdout},${path}`],
        repoDir,
        undefined,
        gitEnv
      );
      if (staged.exitCode !== 0) {
        console.error(
          `[autorepair] ${owner}/${repo}@${ref}: git update-index failed for ${path} (exit ${staged.exitCode}): ${staged.stderr} — aborting, no commit made.`
        );
        return { repaired: false, repairs: [], commitSha: null };
      }
    }

    const writeTree = await exec(["git", "write-tree"], repoDir, undefined, gitEnv);
    if (writeTree.exitCode !== 0 || !looksLikeSha(writeTree.stdout)) {
      console.error(
        `[autorepair] ${owner}/${repo}@${ref}: git write-tree failed (exit ${writeTree.exitCode}): ${writeTree.stderr} — aborting, no commit made.`
      );
      return { repaired: false, repairs: [], commitSha: null };
    }
    const newTreeSha = writeTree.stdout;

    // Get parent
    const parent = await exec(["git", "rev-parse", ref], repoDir);
    if (parent.exitCode !== 0 || !looksLikeSha(parent.stdout)) {
      console.error(
        `[autorepair] ${owner}/${repo}@${ref}: git rev-parse failed (exit ${parent.exitCode}): ${parent.stderr} — aborting, no commit made.`
      );
      return { repaired: false, repairs: [], commitSha: null };
    }
    const parentSha = parent.stdout;

    // Sanity check: the new tree must not be suspiciously smaller than the
    // original — a last-resort guard against any future variant of this
    // same class of bug silently truncating the repository. `files` was
    // already fetched from the same ref at the top of this function, so
    // this reuses it instead of spending another subprocess round-trip.
    const originalFileCount = files.length;
    const newFileCount = (await exec(["git", "ls-tree", "-r", "--name-only", newTreeSha], repoDir)).stdout
      .split("\n")
      .filter(Boolean).length;
    if (newFileCount < originalFileCount) {
      console.error(
        `[autorepair] ${owner}/${repo}@${ref}: refusing to commit — new tree has ${newFileCount} files, original had ${originalFileCount}. This should never happen; aborting with no commit made.`
      );
      return { repaired: false, repairs: [], commitSha: null };
    }

    // Create commit
    const repairSummary = repairs
      .map((r) => `- ${r.file}: ${r.description}`)
      .join("\n");

    const commitMsg = `fix: auto-repair by gluecron\n\n${repairs.length} automatic repair${repairs.length > 1 ? "s" : ""}:\n${repairSummary}\n\nThis commit was created automatically by gluecron's repair engine.`;

    const commit = await exec(["git", "commit-tree", newTreeSha, "-p", parentSha, "-m", commitMsg], repoDir);
    if (commit.exitCode !== 0 || !looksLikeSha(commit.stdout)) {
      console.error(
        `[autorepair] ${owner}/${repo}@${ref}: git commit-tree failed (exit ${commit.exitCode}): ${commit.stderr} — aborting, no commit made.`
      );
      return { repaired: false, repairs: [], commitSha: null };
    }
    const commitSha = commit.stdout;

    // Update ref — compare-and-swap against the parent we just read, so a
    // concurrent push landing between rev-parse and here is rejected by git
    // rather than silently overwritten.
    const updateRef = await exec(
      ["git", "update-ref", `refs/heads/${ref}`, commitSha, parentSha],
      repoDir
    );
    if (updateRef.exitCode !== 0) {
      console.error(
        `[autorepair] ${owner}/${repo}@${ref}: git update-ref failed (exit ${updateRef.exitCode}): ${updateRef.stderr} — repair commit ${commitSha.slice(0, 7)} was created but NOT applied to the branch.`
      );
      return { repaired: false, repairs: [], commitSha: null };
    }

    console.log(
      `[autorepair] ${owner}/${repo}@${ref}: ${repairs.length} repairs committed as ${commitSha.slice(0, 7)}`
    );

    return { repaired: true, repairs, commitSha };
  } finally {
    await rm(tmpIndexDir, { recursive: true, force: true }).catch(() => {});
  }
}
