import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { z } from "zod";

import { derivePromotion } from "../../../core/src/contracts/promotion.js";
import type { DraftContract } from "../../../core/src/contracts/promotion.js";
import {
  REVIEW_DECISION_VERSION,
  type ReviewDecision,
} from "../../../schema/src/decisions/decision.js";
import { contractCodec } from "../../../schema/src/decisions/contract.js";
import type { ExactDecisionScope } from "../../../schema/src/decisions/scope.js";
import type { Finding } from "../../../schema/src/records/finding.js";
import type { ReportData } from "../build/types.js";
import { atomicWriteUtf8, canonicalJson } from "./atomic.js";
import { loadReviewDraft, withReviewDraft } from "./draft.js";
import { exactScopeFromFinding } from "./scope.js";
import { stagedDecisionForFinding } from "./decisions.js";
import type { ReviewDraft, StagedDecisionEntry } from "./types.js";

export const PROMOTION_REQUEST_VERSION = "promotion-request/v1" as const;
export const PROMOTION_REQUESTS_RELATIVE_DIR = ".invariantum/promotion-requests" as const;

const promotionOracleRefSchema = z
  .object({
    kind: z.literal("detector"),
    detector: z
      .object({
        id: z.string(),
        version: z.string(),
      })
      .strict(),
  })
  .strict();

const promotionEvidenceRefSchema = z
  .object({
    relativePath: z.string(),
    contentHash: z.string(),
  })
  .strict();

export const promotionPreviewSchema = z
  .object({
    promotable: z.boolean(),
    autoDerived: z.boolean(),
    contractId: z.string().optional(),
    contractVersion: z.string().optional(),
    oracleRef: promotionOracleRefSchema.optional(),
    calibrationEvidenceRef: promotionEvidenceRefSchema.optional(),
    nextRunBlockingEffect: z.string().optional(),
    reasonNotPromotable: z.string().optional(),
  })
  .strict();

export type PromotionPreview = z.infer<typeof promotionPreviewSchema>;

const promotionDecisionRefSchema = z
  .object({
    id: z.string(),
    scope: z.custom<ExactDecisionScope>(),
    reason: z.string().optional(),
    decidedAt: z.string(),
  })
  .strict();

export const promotionRequestSchema = z
  .object({
    schemaVersion: z.literal(PROMOTION_REQUEST_VERSION),
    findingId: z.string(),
    evidenceFingerprint: z.string(),
    stagedAt: z.string(),
    autoDerived: z.literal(true),
    draftContract: contractCodec.schema,
    decisionRef: promotionDecisionRefSchema,
    orchestratorCommand: z.literal("promote-contract"),
  })
  .strict();

export type PromotionRequest = z.infer<typeof promotionRequestSchema>;

export class PromotionError extends Error {
  readonly code: string;

  constructor(code: string, message: string) {
    super(message);
    this.name = "PromotionError";
    this.code = code;
  }
}

function findingById(reportData: ReportData, findingId: string): Finding {
  const finding = reportData.findings.find((entry) => entry.id === findingId);
  if (finding === undefined) {
    throw new PromotionError("unknown-target", `finding not found: ${findingId}`);
  }
  return finding;
}

function stagedDecisionOrThrow(
  draft: ReviewDraft,
  findingId: string,
): StagedDecisionEntry {
  const staged = stagedDecisionForFinding(draft, findingId);
  if (staged === undefined) {
    throw new PromotionError(
      "no-staged-decision",
      `cannot promote finding ${findingId} without a staged decision`,
    );
  }
  if (staged.verdict !== "confirmed_defect") {
    throw new PromotionError(
      "invalid-verdict",
      "only confirmed_defect decisions can be promoted to blocking contracts",
    );
  }
  return staged;
}

function decisionIdForFinding(findingId: string): string {
  return `decision-report-${findingId}`;
}

function stagedDecisionToReviewDecision(
  staged: StagedDecisionEntry,
  findingId: string,
): ReviewDecision {
  return {
    schemaVersion: REVIEW_DECISION_VERSION,
    id: decisionIdForFinding(findingId),
    findingId: staged.findingId,
    evidenceFingerprint: staged.evidenceFingerprint,
    verdict: staged.verdict,
    scope: staged.scope,
    reviewer: "report-reviewer",
    reviewedAt: staged.decidedAt,
    reportId: "report-staged",
    supersedes: [],
    ...(staged.reason === undefined ? {} : { reason: staged.reason }),
  };
}

function deriveDraftContract(
  staged: StagedDecisionEntry,
  finding: Finding,
): DraftContract {
  if (staged.scope.kind !== "exact") {
    throw new PromotionError(
      "policy-scope-not-promotable",
      "reviewed-policy scope cannot be promoted to blocking contracts",
    );
  }

  try {
    return derivePromotion(stagedDecisionToReviewDecision(staged, finding.id), finding);
  } catch (error) {
    if (error instanceof Error && error.name === "PromotionEvidenceError") {
      throw new PromotionError("promotion-evidence-unavailable", error.message);
    }
    throw error;
  }
}

export function buildPromotionPreviewFromStaged(
  staged: StagedDecisionEntry,
  finding: Finding,
): PromotionPreview {
  if (staged.scope.kind === "reviewed-policy") {
    return {
      promotable: false,
      autoDerived: false,
      reasonNotPromotable:
        "Reviewed-policy scope cannot be promoted. Use exact finding scope for blocking contract promotion.",
    };
  }

  const draft = deriveDraftContract(staged, finding);
  return {
    promotable: true,
    autoDerived: true,
    contractId: draft.id,
    contractVersion: draft.version,
    oracleRef: draft.oracleRef,
    calibrationEvidenceRef: draft.calibrationEvidenceRef,
    nextRunBlockingEffect:
      "On the next comparable run, identical violations matching this exact scope will classify as blocking through the active executable contract.",
  };
}

export async function buildPromotionPreview(
  reportDir: string,
  reportData: ReportData,
  findingId: string,
): Promise<PromotionPreview> {
  const draft = await loadReviewDraft(reportDir);
  const staged = stagedDecisionOrThrow(draft, findingId);
  const finding = findingById(reportData, findingId);
  return buildPromotionPreviewFromStaged(staged, finding);
}

export function promotionRequestPath(reportDir: string, findingId: string): string {
  return join(reportDir, PROMOTION_REQUESTS_RELATIVE_DIR, `${findingId}.json`);
}

export async function loadPromotionRequest(
  reportDir: string,
  findingId: string,
): Promise<PromotionRequest> {
  const path = promotionRequestPath(reportDir, findingId);
  let raw: string;
  try {
    raw = await readFile(path, "utf8");
  } catch {
    throw new PromotionError(
      "missing-promotion-request",
      `promotion request not found for finding ${findingId}`,
    );
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw) as unknown;
  } catch {
    throw new PromotionError("invalid-promotion-request", "promotion request is not valid JSON");
  }

  const result = promotionRequestSchema.safeParse(parsed);
  if (!result.success) {
    throw new PromotionError("invalid-promotion-request", "promotion request failed schema validation");
  }
  return result.data;
}

export async function stagePromotionRequest(
  reportDir: string,
  reportData: ReportData,
  findingId: string,
): Promise<ReviewDraft> {
  const finding = findingById(reportData, findingId);
  const preview = await buildPromotionPreview(reportDir, reportData, findingId);
  if (!preview.promotable || preview.contractId === undefined || preview.contractVersion === undefined) {
    throw new PromotionError(
      "not-promotable",
      preview.reasonNotPromotable ?? "finding cannot be promoted",
    );
  }

  const draft = await loadReviewDraft(reportDir);
  const staged = stagedDecisionOrThrow(draft, findingId);
  const draftContract = deriveDraftContract(staged, finding);
  const stagedAt = new Date().toISOString();
  const request: PromotionRequest = {
    schemaVersion: PROMOTION_REQUEST_VERSION,
    findingId,
    evidenceFingerprint: staged.evidenceFingerprint,
    stagedAt,
    autoDerived: true,
    draftContract,
    decisionRef: {
      id: decisionIdForFinding(findingId),
      scope: staged.scope as ExactDecisionScope,
      ...(staged.reason === undefined ? {} : { reason: staged.reason }),
      decidedAt: staged.decidedAt,
    },
    orchestratorCommand: "promote-contract",
  };

  const requestPath = promotionRequestPath(reportDir, findingId);
  await atomicWriteUtf8(requestPath, `${canonicalJson(promotionRequestSchema.parse(request))}\n`);

  return withReviewDraft(reportDir, (current) => {
    const entry = stagedDecisionForFinding(current, findingId);
    if (entry === undefined) {
      throw new PromotionError(
        "no-staged-decision",
        `cannot promote finding ${findingId} without a staged decision`,
      );
    }

    const relativeRequestPath = join(PROMOTION_REQUESTS_RELATIVE_DIR, `${findingId}.json`);
    const contractId = preview.contractId;
    const contractVersion = preview.contractVersion;
    if (contractId === undefined || contractVersion === undefined) {
      throw new PromotionError("not-promotable", "promotion preview is missing contract identity");
    }
    const decisions = current.decisions.map((item) =>
      item.findingId === findingId
        ? {
            ...item,
            promotion: {
              status: "staged" as const,
              requestPath: relativeRequestPath,
              contractId,
              contractVersion,
              stagedAt,
            },
          }
        : item,
    );

    return {
      ...current,
      decisions,
    };
  });
}

export function buildPromotionPreviewsForDraft(
  reportData: ReportData,
  draft: ReviewDraft,
): Record<string, PromotionPreview> {
  const previews: Record<string, PromotionPreview> = {};
  for (const entry of draft.decisions) {
    if (entry.verdict !== "confirmed_defect") {
      continue;
    }
    const finding = reportData.findings.find((candidate) => candidate.id === entry.findingId);
    if (finding === undefined) {
      continue;
    }
    try {
      previews[entry.findingId] = buildPromotionPreviewFromStaged(entry, finding);
    } catch (error) {
      if (error instanceof PromotionError) {
        previews[entry.findingId] = {
          promotable: false,
          autoDerived: false,
          reasonNotPromotable: error.message,
        };
      } else {
        throw error;
      }
    }
  }
  return previews;
}

export function defaultExactScopeForFinding(finding: Finding): ExactDecisionScope {
  return exactScopeFromFinding(finding);
}
