import { readFileSync } from "node:fs";
import { basename, join } from "node:path";
import { z } from "zod";
import type { Adapter } from "../adapter";
import type { AdapterResult, Item, Panel } from "../schema";
import type { GitRunner } from "./golive";

export type { GitRunner };

export interface GatesAdapterOptions {
  id?: string;
  interval?: number;
  /** Absolute path to security-gate prevent/confirmed.json. */
  confirmedPath: string;
  /** Absolute path to security-gate precision_records.json. */
  precisionRecordsPath: string;
  /** Absolute repo roots — each may host .slopgate/baseline.json and .warnignore.pending.json. */
  repos: string[];
  slopgateBaselineRel?: string;
  warnignorePendingRel?: string;
  runGit?: GitRunner;
  readFileImpl?: (path: string) => string;
  now?: () => number;
}

const ConfirmedEntrySchema = z.object({
  class: z.string(),
  file: z.string(),
  symbol: z.string(),
  confirmed: z.string(),
  ref: z.string().optional(),
});
const ConfirmedSchema = z.array(ConfirmedEntrySchema);

const PrecisionRecordSchema = z.object({
  cell_id: z.string(),
  detector: z.string().optional(),
  catches: z.number().int().nonnegative(),
  k: z.number().int().positive(),
  metric: z.string(),
  measured_against: z.string().optional(),
  measured_when: z.string().optional(),
  report_ref: z.string().optional(),
});
const PrecisionRecordsFileSchema = z.object({
  records: z.array(PrecisionRecordSchema),
});

const SlopgateBaselineSchema = z.object({
  version: z.number().optional(),
  generated: z.string().optional(),
  entries: z.record(z.unknown()).default({}),
});

const WarnignorePendingEntrySchema = z.object({
  id: z.string().min(1),
  pattern: z.string().optional(),
  title: z.string().min(1),
  context: z.string().optional(),
  requestedAt: z.string(),
});
const WarnignorePendingSchema = z.array(WarnignorePendingEntrySchema);

const SlopgateRepoSchema = z.object({
  repo: z.string().min(1),
  debt: z.number().int().nonnegative(),
  trend7d: z.number().int(),
});
export type SlopgateRepo = z.infer<typeof SlopgateRepoSchema>;

const GatesPanelDataSchema = z.object({
  preventOpen: z.number().int().nonnegative(),
  slopgateDebtTotal: z.number().int().nonnegative(),
  slopgateDebtTrend7d: z.number().int(),
  warnignorePending: z.number().int().nonnegative(),
  warnignoreAdds7d: z.number().int().nonnegative(),
  slopgateRepos: z.array(SlopgateRepoSchema),
  precisionRecords: z.array(
    z.object({
      cellId: z.string(),
      rate: z.number(),
      k: z.number().int().positive(),
      metric: z.string(),
      measuredWhen: z.string().optional(),
    }),
  ),
});

const DEFAULT_INTERVAL_MS = 5 * 60_000;
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const DEFAULT_SLOPGATE_BASELINE_REL = ".slopgate/baseline.json";
const DEFAULT_WARNIGNORE_PENDING_REL = ".warnignore.pending.json";

async function defaultRunGit(args: string[], cwd: string): Promise<string> {
  const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "pipe" });
  const [stdout, stderr, exitCode] = await Promise.all([
    new Response(proc.stdout).text(),
    new Response(proc.stderr).text(),
    proc.exited,
  ]);
  if (exitCode !== 0) {
    throw new Error(`git ${args.join(" ")} failed (${exitCode}): ${stderr.trim()}`);
  }
  return stdout;
}

function readRequired(
  readFileImpl: (path: string) => string,
  path: string,
  label: string,
): string {
  try {
    return readFileImpl(path);
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    throw new Error(`cannot read ${label} at ${path}: ${message}`);
  }
}

function countBaselineDebt(content: string): number {
  const parsed = SlopgateBaselineSchema.parse(JSON.parse(content));
  return Object.keys(parsed.entries).length;
}

async function baselineDebtAtRef(
  runGit: GitRunner,
  repoPath: string,
  baselineRel: string,
  ref: string,
): Promise<number> {
  const content = await runGit(["show", `${ref}:${baselineRel}`], repoPath);
  return countBaselineDebt(content);
}

async function computeBaselineTrend7d(
  runGit: GitRunner,
  repoPath: string,
  baselineRel: string,
  debtNow: number,
  nowMs: number,
): Promise<number> {
  const cutoffIso = new Date(nowMs - SEVEN_DAYS_MS).toISOString();
  const raw = await runGit(
    ["log", "-1", "--format=%H", `--until=${cutoffIso}`, "--", baselineRel],
    repoPath,
  );
  const hash = raw.trim();
  if (hash === "") return debtNow;
  const debtThen = await baselineDebtAtRef(runGit, repoPath, baselineRel, hash);
  return debtNow - debtThen;
}

function preventItemId(entry: z.infer<typeof ConfirmedEntrySchema>): string {
  return `gates:prevent:${entry.class}:${entry.file}:${entry.symbol}`;
}

function warnignoreItemId(repo: string, entryId: string): string {
  return `gates:warnignore:${basename(repo)}:${entryId}`;
}

function buildPreventItem(
  entry: z.infer<typeof ConfirmedEntrySchema>,
  adapterId: string,
  ts: string,
): Item {
  return {
    id: preventItemId(entry),
    source: adapterId,
    project: "security-gate",
    severity: "act",
    kind: "gate",
    title: `Prevent-band [${entry.class}]: ${entry.file}`,
    detail: `symbol=${entry.symbol} · confirmed ${entry.confirmed}${entry.ref ? ` · ${entry.ref}` : ""}`,
    ts,
    actions: [],
  };
}

function buildWarnignoreItem(
  repoPath: string,
  entry: z.infer<typeof WarnignorePendingEntrySchema>,
  adapterId: string,
  ts: string,
): Item {
  const repoName = basename(repoPath);
  return {
    id: warnignoreItemId(repoPath, entry.id),
    source: adapterId,
    project: repoName,
    severity: "warn",
    kind: "gate",
    title: `.warnignore addition requested: ${entry.title}`,
    detail: entry.context ?? "needs benign-justification sign-off",
    ts,
    actions: [],
    decision: {
      question: `Approve adding this pattern to ${repoName}/.warnignore?`,
      options: [
        { label: "Approve suppression (upstream)", recommended: true },
        { label: "Reject — try dep bump first" },
      ],
      freeText: true,
      context: [entry.context, entry.pattern ? `pattern: ${entry.pattern}` : undefined]
        .filter((part) => part !== undefined)
        .join(" · "),
      waitingSince: entry.requestedAt,
    },
  };
}

function loadWarnignorePending(
  repoPath: string,
  pendingRel: string,
  readFileImpl: (path: string) => string,
): z.infer<typeof WarnignorePendingSchema> {
  const path = join(repoPath, pendingRel);
  try {
    return WarnignorePendingSchema.parse(JSON.parse(readFileImpl(path)));
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code;
    if (code === "ENOENT" || (err instanceof Error && err.message.includes("ENOENT"))) {
      return [];
    }
    throw err;
  }
}

async function scoreSlopgateRepo(
  repoPath: string,
  baselineRel: string,
  runGit: GitRunner,
  readFileImpl: (path: string) => string,
  nowMs: number,
): Promise<SlopgateRepo> {
  const baselinePath = join(repoPath, baselineRel);
  let debt: number;
  try {
    debt = countBaselineDebt(readFileImpl(baselinePath));
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    throw new Error(`cannot read slopgate baseline at ${baselinePath}: ${message}`);
  }
  const trend7d = await computeBaselineTrend7d(runGit, repoPath, baselineRel, debt, nowMs);
  return { repo: basename(repoPath), debt, trend7d };
}

function countWarnignoreAdds7d(
  pending: z.infer<typeof WarnignorePendingSchema>,
  nowMs: number,
): number {
  const cutoff = nowMs - SEVEN_DAYS_MS;
  return pending.filter((entry) => Date.parse(entry.requestedAt) >= cutoff).length;
}

/**
 * Security-gate prevent ratchet + slopgate debt adapter. Reads configured ledger files
 * and per-repo baselines; emits gate decisions for open prevent findings and pending
 * .warnignore sign-offs.
 */
export function createGatesAdapter(opts: GatesAdapterOptions): Adapter {
  const id = opts.id ?? "gates";
  const interval = opts.interval ?? DEFAULT_INTERVAL_MS;
  const confirmedPath = opts.confirmedPath;
  const precisionRecordsPath = opts.precisionRecordsPath;
  const repos = opts.repos;
  const baselineRel = opts.slopgateBaselineRel ?? DEFAULT_SLOPGATE_BASELINE_REL;
  const pendingRel = opts.warnignorePendingRel ?? DEFAULT_WARNIGNORE_PENDING_REL;
  const runGit = opts.runGit ?? defaultRunGit;
  const readFileImpl = opts.readFileImpl ?? ((path: string) => readFileSync(path, "utf8"));
  const now = opts.now ?? Date.now;

  async function poll(): Promise<AdapterResult> {
    const nowMs = now();
    const ts = new Date(nowMs).toISOString();

    const confirmed = ConfirmedSchema.parse(
      JSON.parse(readRequired(readFileImpl, confirmedPath, "prevent confirmed.json")),
    );
    const precisionFile = PrecisionRecordsFileSchema.parse(
      JSON.parse(readRequired(readFileImpl, precisionRecordsPath, "precision records")),
    );
    const precisionRecords = precisionFile.records.map((record) => ({
      cellId: record.cell_id,
      rate: record.k > 0 ? record.catches / record.k : 0,
      k: record.k,
      metric: record.metric,
      measuredWhen: record.measured_when,
    }));

    const slopgateRepos: SlopgateRepo[] = [];
    const items: Item[] = [];
    let warnignorePending = 0;
    let warnignoreAdds7d = 0;

    for (const entry of confirmed) {
      items.push(buildPreventItem(entry, id, ts));
    }

    for (const repoPath of repos) {
      const pending = loadWarnignorePending(repoPath, pendingRel, readFileImpl);
      warnignorePending += pending.length;
      warnignoreAdds7d += countWarnignoreAdds7d(pending, nowMs);
      for (const entry of pending) {
        items.push(buildWarnignoreItem(repoPath, entry, id, ts));
      }
      slopgateRepos.push(await scoreSlopgateRepo(repoPath, baselineRel, runGit, readFileImpl, nowMs));
    }

    const slopgateDebtTotal = slopgateRepos.reduce((sum, repo) => sum + repo.debt, 0);
    const slopgateDebtTrend7d = slopgateRepos.reduce((sum, repo) => sum + repo.trend7d, 0);

    const data = GatesPanelDataSchema.parse({
      preventOpen: confirmed.length,
      slopgateDebtTotal,
      slopgateDebtTrend7d,
      warnignorePending,
      warnignoreAdds7d,
      slopgateRepos,
      precisionRecords,
    });

    const panels: Panel[] = [{ id: "gates", ts, data }];
    return { items, panels };
  }

  return { id, interval, poll };
}
