// SPEC: PJ2026-01060703 GitHub PR merge closeout.
// Responsibility: guarded local/node worktree closeout after a successful PR merge.

import { resolve } from "node:path";
import { repoRoot } from "../config";
import { runCommand, type CommandResult } from "../command";
import type { GitHubOptions, GitHubPullRequest } from "./types";

interface WorktreeEntry {
  path: string;
  branch: string | null;
  head: string | null;
}

export function runPrMergeCloseout(repo: string, pr: GitHubPullRequest, options: GitHubOptions): Record<string, unknown> {
  const headRef = pr.head?.ref ?? null;
  const baseRef = pr.base?.ref ?? null;
  const localEnabled = options.localCloseout;
  return {
    ok: true,
    valuesPrinted: false,
    headRef,
    baseRef,
    localWorktree: localEnabled && options.deleteBranch
      ? cleanupHeadWorktree(headRef, options.dryRun)
      : skipped(localEnabled ? "branch-preserved" : "local-closeout-disabled"),
    mainWorktree: localEnabled ? syncLocalMainWorktree(repo, baseRef, options.dryRun) : skipped("local-closeout-disabled"),
    nodeSyncs: syncRequestedNodes(repo, baseRef, options.syncNodes, options.dryRun),
  };
}

function cleanupHeadWorktree(headRef: string | null, dryRun: boolean): Record<string, unknown> {
  if (headRef === null || headRef.length === 0) return skipped("head-ref-missing");
  const list = git(["worktree", "list", "--porcelain"], 10_000);
  if (list.exitCode !== 0) return failed("worktree-list-failed", list);
  const entries = parseWorktreeList(list.stdout);
  const matches = entries.filter((entry) => entry.branch === `refs/heads/${headRef}` && isRemovableHeadWorktree(entry.path));
  if (matches.length === 0) return skipped("local-head-worktree-not-found", { headRef });
  const removals = matches.map((entry) => removeWorktree(entry.path, dryRun));
  const branchDelete = deleteLocalBranch(headRef, dryRun);
  return {
    ok: removals.every((item) => item.ok === true || item.planned === true) && (branchDelete.ok === true || branchDelete.planned === true || branchDelete.skipped === true),
    headRef,
    worktrees: removals,
    branchDelete,
  };
}

function removeWorktree(path: string, dryRun: boolean): Record<string, unknown> {
  const status = git(["-C", path, "status", "--porcelain"], 10_000);
  if (status.exitCode !== 0) return failed("worktree-status-failed", status, { path });
  if (status.stdout.trim().length > 0) return skipped("worktree-dirty", { path, porcelainLines: status.stdout.trim().split(/\r?\n/u).slice(0, 20) });
  if (dryRun) return { planned: true, action: "git-worktree-remove", path };
  const removed = git(["worktree", "remove", path], 30_000);
  return removed.exitCode === 0 ? { ok: true, action: "git-worktree-remove", path } : failed("worktree-remove-failed", removed, { path });
}

function deleteLocalBranch(headRef: string, dryRun: boolean): Record<string, unknown> {
  const list = git(["branch", "--list", headRef], 10_000);
  if (list.exitCode !== 0) return failed("branch-list-failed", list, { headRef });
  if (list.stdout.trim().length === 0) return skipped("local-branch-not-found", { headRef });
  if (dryRun) return { planned: true, action: "git-branch-delete", branch: headRef };
  const deleted = git(["branch", "-d", headRef], 20_000);
  return deleted.exitCode === 0 ? { ok: true, action: "git-branch-delete", branch: headRef } : failed("branch-delete-failed", deleted, { branch: headRef });
}

function syncLocalMainWorktree(repo: string, baseRef: string | null, dryRun: boolean): Record<string, unknown> {
  if (baseRef === null || baseRef.length === 0) return skipped("base-ref-missing");
  const remote = git(["remote", "get-url", "origin"], 10_000);
  if (remote.exitCode !== 0) return failed("remote-read-failed", remote);
  if (!remoteMatchesRepo(remote.stdout.trim(), repo)) return skipped("local-repo-mismatch", { repo, origin: redactRemote(remote.stdout.trim()) });
  const current = git(["rev-parse", "--abbrev-ref", "HEAD"], 10_000);
  if (current.exitCode !== 0) return failed("current-branch-read-failed", current);
  const currentBranch = current.stdout.trim();
  if (currentBranch !== baseRef) return skipped("main-worktree-on-different-branch", { currentBranch, baseRef });
  if (dryRun) return { planned: true, action: "git-fetch-merge-ff-only", path: repoRoot, branch: baseRef };

  const status = git(["status", "--porcelain"], 10_000);
  if (status.exitCode !== 0) return failed("main-worktree-status-failed", status);
  const hadDirty = status.stdout.trim().length > 0;
  const stash = hadDirty ? git(["stash", "push", "-u", "-m", `unidesk-gh-pr-merge-closeout ${repo} ${baseRef}`], 30_000) : null;
  if (stash !== null && stash.exitCode !== 0) return failed("main-worktree-stash-failed", stash, { branch: baseRef });

  const fetched = git(["fetch", "origin", baseRef], 60_000);
  const merged = fetched.exitCode === 0 ? git(["merge", "--ff-only", `origin/${baseRef}`], 60_000) : null;
  const applied = stash !== null ? git(["stash", "apply"], 60_000) : null;
  const ok = fetched.exitCode === 0 && merged?.exitCode === 0 && (applied === null || applied.exitCode === 0);
  return {
    ok,
    action: "git-fetch-merge-ff-only",
    path: repoRoot,
    branch: baseRef,
    dirtyPreservedByStash: hadDirty,
    fetch: commandSummary(fetched),
    merge: merged === null ? null : commandSummary(merged),
    stashApply: applied === null ? null : commandSummary(applied),
  };
}

function syncRequestedNodes(repo: string, baseRef: string | null, nodes: string[], dryRun: boolean): Record<string, unknown>[] {
  return nodes.map((node) => syncRequestedNode(repo, baseRef, node, dryRun));
}

function syncRequestedNode(repo: string, baseRef: string | null, node: string, dryRun: boolean): Record<string, unknown> {
  const command = nodeSyncCommand(repo, baseRef, node);
  if (command === null) return skipped("node-sync-unsupported", { repo, baseRef, node });
  if (dryRun) return { planned: true, node, command: command.join(" ") };
  const result = runCommand(command, repoRoot, { timeoutMs: 120_000 });
  return result.exitCode === 0 ? { ok: true, node, command: command.join(" ") } : failed("node-sync-failed", result, { node, command: command.join(" ") });
}

function nodeSyncCommand(repo: string, baseRef: string | null, node: string): string[] | null {
  if (repo.toLowerCase() === "pikastech/hwlab" && baseRef === "v0.3") {
    return ["bun", "scripts/cli.ts", "hwlab", "nodes", "control-plane", "source-workspace", "sync", "--node", node.toUpperCase(), "--lane", "v03", "--confirm"];
  }
  return null;
}

function parseWorktreeList(text: string): WorktreeEntry[] {
  const entries: WorktreeEntry[] = [];
  let current: WorktreeEntry | null = null;
  for (const line of text.split(/\r?\n/u)) {
    if (line.startsWith("worktree ")) {
      if (current !== null) entries.push(current);
      current = { path: line.slice("worktree ".length), branch: null, head: null };
    } else if (line.startsWith("branch ") && current !== null) {
      current.branch = line.slice("branch ".length);
    } else if (line.startsWith("HEAD ") && current !== null) {
      current.head = line.slice("HEAD ".length);
    }
  }
  if (current !== null) entries.push(current);
  return entries;
}

export function isRemovableHeadWorktree(path: string): boolean {
  return resolve(path) !== resolve(repoRoot);
}

function git(args: string[], timeoutMs: number): CommandResult {
  return runCommand(["git", ...args], repoRoot, { timeoutMs });
}

function remoteMatchesRepo(remote: string, repo: string): boolean {
  const normalized = remote
    .replace(/^git@github\.com:/u, "")
    .replace(/^https:\/\/github\.com\//u, "")
    .replace(/\.git$/u, "")
    .toLowerCase();
  return normalized === repo.toLowerCase();
}

function redactRemote(remote: string): string {
  return remote.replace(/:\/\/[^/@]+@/u, "://<redacted>@");
}

function skipped(reason: string, details: Record<string, unknown> = {}): Record<string, unknown> {
  return { ok: true, skipped: true, skippedReason: reason, ...details };
}

function failed(reason: string, result: CommandResult, details: Record<string, unknown> = {}): Record<string, unknown> {
  return { ok: false, degradedReason: reason, ...details, command: result.command.join(" "), exitCode: result.exitCode, timedOut: result.timedOut, stderrTail: tail(result.stderr), stdoutTail: tail(result.stdout) };
}

function commandSummary(result: CommandResult): Record<string, unknown> {
  return { exitCode: result.exitCode, timedOut: result.timedOut, stderrTail: tail(result.stderr), stdoutTail: tail(result.stdout) };
}

function tail(text: string): string {
  return text.trim().split(/\r?\n/u).slice(-8).join("\n").slice(-1000);
}
