import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { createInterface } from "node:readline/promises";
import { z } from "zod";
import { exportBundle } from "../../../core/src/bundle/export.js";
import { BundleRejection, importBundle } from "../../../core/src/bundle/import.js";
import { loadDecisionStore, readDecisionIndex, writeDecisionIndex } from "../../../core/src/decisions/store.js";
import { parseDecisionBundle } from "../../../schema/src/decisions/bundle.js";
import { decisionScopeSchema } from "../../../schema/src/decisions/scope.js";
import { loadConfig } from "../config.js";
import type { DecisionsExportOptions, DecisionsImportOptions, DecisionsRevokeOptions } from "../parse-args.js";
import type { IoContext } from "./verify.js";

const reportSchema = z.object({
  staticReview: z.object({
    projectIdentity: z.string().min(1),
    configHash: z.string().min(1),
    report: z.object({ id: z.string().min(1), revision: z.string().min(1), findingSetHash: z.string().min(1) }).strict(),
  }).strict(),
}).loose();

const reviewDraftSchema = z.object({
  schemaVersion: z.literal("review-draft/v1"),
  decisions: z.array(z.object({
    findingId: z.string().min(1),
    evidenceFingerprint: z.string().min(1),
    verdict: z.enum(["confirmed_defect", "not_defect"]),
    scope: decisionScopeSchema,
    reason: z.string().optional(),
    decidedAt: z.string().min(1).optional(),
  }).strict()),
}).loose();

type RetainedReport = z.infer<typeof reportSchema>["staticReview"];

function decisionStoreRoot(configPath: string | undefined, io: IoContext): { ok: true; root: string } | { ok: false; message: string } {
  const configResult = loadConfig(configPath, io.env);
  if (!configResult.ok) return { ok: false, message: `${configResult.error.message}\nrecovery: ${configResult.error.recoveryInstructions}` };
  const storePath = configResult.config.acceptedFindings?.store;
  if (storePath === undefined) return { ok: false, message: "config error: acceptedFindings.store is required for decision commands" };
  return { ok: true, root: dirname(resolve(io.cwd, storePath)) };
}

async function retainedReport(root: string, reportId: string): Promise<RetainedReport> {
  const path = resolve(root, "reports", reportId, "data", "report.json");
  let parsed: unknown;
  try {
    parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
  } catch {
    throw new Error(`retained report ${reportId} is unavailable or invalid`);
  }
  const result = reportSchema.safeParse(parsed);
  if (!result.success || result.data.staticReview.report.id !== reportId) {
    throw new Error(`retained report ${reportId} lacks valid static review metadata`);
  }
  return result.data.staticReview;
}

async function reportDraft(root: string, reportId: string): Promise<z.infer<typeof reviewDraftSchema>> {
  const path = resolve(root, "reports", reportId, ".invariantum", "review-draft.json");
  let parsed: unknown;
  try {
    parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") {
      return { schemaVersion: "review-draft/v1", decisions: [] };
    }
    throw new Error(`review draft for retained report ${reportId} is unavailable or invalid`);
  }
  const result = reviewDraftSchema.safeParse(parsed);
  if (!result.success) throw new Error(`review draft for retained report ${reportId} failed schema validation`);
  return result.data;
}

function emit(value: unknown, io: IoContext): void {
  io.stdout.write(`${JSON.stringify(value)}\n`);
}

function fail(message: string, io: IoContext): 2 {
  io.stderr.write(`decision error: ${message}\n`);
  return 2;
}

export async function runDecisionsExport(options: DecisionsExportOptions, io: IoContext): Promise<0 | 2> {
  const store = decisionStoreRoot(options.configPath, io);
  if (!store.ok) return fail(store.message, io);
  try {
    const [report, draft] = await Promise.all([retainedReport(store.root, options.reportId), reportDraft(store.root, options.reportId)]);
    const bundle = exportBundle({
      projectIdentity: report.projectIdentity,
      configHash: report.configHash,
      report: report.report,
      nonce: randomUUID(),
      expiresAt: new Date(Date.now() + 86_400_000).toISOString(),
      decisions: draft.decisions.map(({ findingId, evidenceFingerprint, verdict, scope, reason }) => ({ findingId, evidenceFingerprint, verdict, scope, ...(reason === undefined ? {} : { reason }) })),
    });
    emit(bundle, io);
    return 0;
  } catch (error) {
    return fail(error instanceof Error ? error.message : String(error), io);
  }
}

async function interactiveTrust(bundle: ReturnType<typeof parseDecisionBundle>, io: IoContext): Promise<{ reviewer: string; bundleHash: string } | undefined> {
  if (io.stdin === undefined) return undefined;
  io.stderr.write(`trusted local import\nproject identity: ${bundle.projectIdentity}\nconfig hash: ${bundle.configHash}\nreport revision: ${bundle.report.revision}\nfinding-set hash: ${bundle.report.findingSetHash}\nnonce: ${bundle.nonce}\nexpiry: ${bundle.expiresAt}\nbundle hash: ${bundle.bundleHash}\n`);
  const prompt = createInterface({ input: io.stdin, output: io.stderr, terminal: false });
  const answers: string[] = [];
  let closed = false;
  let release: (() => void) | undefined;
  prompt.on("line", (line) => {
    answers.push(line);
    release?.();
    release = undefined;
  });
  prompt.on("close", () => {
    closed = true;
    release?.();
    release = undefined;
  });
  const nextAnswer = async (question: string): Promise<string> => {
    io.stderr.write(question);
    while (answers.length === 0 && !closed) {
      await new Promise<void>((resolve) => { release = resolve; });
    }
    return answers.shift() ?? "";
  };
  try {
    const reviewer = (await nextAnswer("local reviewer identity: ")).trim();
    const bundleHash = (await nextAnswer("type exact bundle hash to trust local import: ")).trim();
    return reviewer.length === 0 || bundleHash.length === 0 ? undefined : { reviewer, bundleHash };
  } finally {
    prompt.close();
  }
}

export async function runDecisionsImport(options: DecisionsImportOptions, io: IoContext): Promise<0 | 2> {
  const store = decisionStoreRoot(options.configPath, io);
  if (!store.ok) return fail(store.message, io);
  let bundle: ReturnType<typeof parseDecisionBundle>;
  try {
    bundle = parseDecisionBundle(JSON.parse(await readFile(resolve(io.cwd, options.bundlePath), "utf8")) as unknown);
  } catch {
    return fail("input is not an unsigned decision-bundle/v1", io);
  }
  const trust = options.confirmedBundleHash === undefined
    ? await interactiveTrust(bundle, io)
    : { reviewer: options.reviewer as string, bundleHash: options.confirmedBundleHash };
  if (trust === undefined) return fail("import requires explicit local reviewer identity and exact bundle hash confirmation", io);
  try {
    const report = await retainedReport(store.root, bundle.report.id);
    const result = await importBundle(store.root, bundle, {
      confirmedBundleHash: trust.bundleHash,
      reviewer: trust.reviewer,
      retainedReport: report.report,
      now: new Date().toISOString(),
    });
    emit({ schemaVersion: "decision-import-result/v1", ...result }, io);
    return 0;
  } catch (error) {
    const message = error instanceof BundleRejection ? `${error.reason}: ${error.message}` : error instanceof Error ? error.message : String(error);
    return fail(message, io);
  }
}

export async function runDecisionsRevoke(options: DecisionsRevokeOptions, io: IoContext): Promise<0 | 2> {
  const store = decisionStoreRoot(options.configPath, io);
  if (!store.ok) return fail(store.message, io);
  try {
    const decisions = await loadDecisionStore(store.root);
    if (!decisions.isActive(options.decisionId) || decisions.getDecision(options.decisionId) === undefined) {
      return fail(`decision ${options.decisionId} is not active`, io);
    }
    const index = await readDecisionIndex(store.root);
    await writeDecisionIndex(store.root, { ...index, activeIds: index.activeIds.filter((id) => id !== options.decisionId) });
    emit({ schemaVersion: "decision-revoke-result/v1", decisionId: options.decisionId, status: "revoked" }, io);
    return 0;
  } catch (error) {
    return fail(error instanceof Error ? error.message : String(error), io);
  }
}
