import { sha256Canonical } from "../../../schema/src/canonical.js";
import {
  CONTRACT_VERSION,
  type ContractEnvelope,
} from "../../../schema/src/decisions/contract.js";
import type { ReviewDecision } from "../../../schema/src/decisions/decision.js";
import {
  normalizeDecisionScope,
  type ExactDecisionScope,
} from "../../../schema/src/decisions/scope.js";
import type { Finding } from "../../../schema/src/records/finding.js";

export type DraftContract = ContractEnvelope & {
  status: "draft";
  kind: "confirmed";
};

export class PromotionEvidenceError extends Error {
  readonly reason: string;

  constructor(reason: string) {
    super(`Promotion evidence unavailable: ${reason}`);
    this.name = "PromotionEvidenceError";
    this.reason = reason;
  }
}

export class PromotionScopeError extends Error {
  readonly reason: string;

  constructor(reason: string) {
    super(`Promotion scope widening rejected: ${reason}`);
    this.name = "PromotionScopeError";
    this.reason = reason;
  }
}

function contractIdForDecision(decision: ReviewDecision): string {
  return `contract-for-${decision.id}`;
}

function calibrationEvidenceFromFinding(finding: Finding): {
  relativePath: string;
  contentHash: string;
} {
  const artifact = finding.artifacts[0];
  if (artifact !== undefined) {
    return {
      relativePath: artifact.relativePath,
      contentHash: artifact.contentHash,
    };
  }

  if (finding.evidence === undefined || finding.evidence === null) {
    throw new PromotionEvidenceError("finding retained evidence is unavailable");
  }

  return {
    relativePath: `evidence/${finding.id}.json`,
    contentHash: sha256Canonical(finding.evidence),
  };
}

function exactScopeFromDecision(
  decision: ReviewDecision,
  contractId: string,
  contractVersion: string,
): ExactDecisionScope {
  if (decision.scope.kind !== "exact") {
    throw new PromotionEvidenceError("decision scope must be exact for promotion");
  }

  return normalizeDecisionScope({
    ...decision.scope,
    contractVersion: { id: contractId, version: contractVersion },
  }) as ExactDecisionScope;
}

export function derivePromotion(
  decision: ReviewDecision,
  finding: Finding,
): DraftContract {
  if (decision.scope.kind !== "exact") {
    throw new PromotionEvidenceError("decision scope must be exact for promotion");
  }

  const detectorVersion = decision.scope.detector.version.trim();
  if (detectorVersion.length === 0) {
    throw new PromotionEvidenceError("detector version is unavailable");
  }

  if (finding.evidence === undefined || finding.evidence === null) {
    throw new PromotionEvidenceError("finding retained evidence is unavailable");
  }

  const contractId = contractIdForDecision(decision);
  const contractVersion = "1.0.0";
  const applicableScope = exactScopeFromDecision(
    decision,
    contractId,
    contractVersion,
  );

  const draft: DraftContract = {
    schemaVersion: CONTRACT_VERSION,
    id: contractId,
    kind: "confirmed",
    status: "draft",
    version: contractVersion,
    provenance: {
      sourceDecisionId: decision.id,
      evidenceFingerprint: decision.evidenceFingerprint,
    },
    applicableScope,
    oracleRef: {
      kind: "detector",
      detector: {
        id: decision.scope.detector.id,
        version: detectorVersion,
      },
    },
    calibrationEvidenceRef: calibrationEvidenceFromFinding(finding),
    reviewer: decision.reviewer,
    effectiveAt: decision.reviewedAt,
    supersessionHistory: [],
  };

  rejectWidenedScopeDraft(draft, decision, finding);
  return draft;
}

export function rejectWidenedScopeDraft(
  draft: DraftContract,
  decision: ReviewDecision,
  finding: Finding,
): void {
  if (decision.scope.kind !== "exact") {
    throw new PromotionScopeError("decision scope must be exact");
  }

  const scope = draft.applicableScope;
  if (scope.kind !== "exact") {
    throw new PromotionScopeError("applicable scope must be exact");
  }

  const decisionScope = decision.scope;

  if (scope.detector.id !== decisionScope.detector.id) {
    throw new PromotionScopeError("detector id does not match decision scope");
  }
  if (scope.detector.version !== decisionScope.detector.version) {
    throw new PromotionScopeError("detector version does not match decision scope");
  }
  if (scope.target.canonicalTarget !== decisionScope.target.canonicalTarget) {
    throw new PromotionScopeError("target does not match decision scope");
  }
  if (scope.context.kind !== decisionScope.context.kind) {
    throw new PromotionScopeError("context kind does not match decision scope");
  }

  const decisionDimensions = new Map(
    decisionScope.context.dimensions.map((dimension) => [dimension.key, dimension.value]),
  );
  for (const dimension of scope.context.dimensions) {
    if (decisionDimensions.get(dimension.key) !== dimension.value) {
      throw new PromotionScopeError(
        `context dimension ${dimension.key} does not match decision scope`,
      );
    }
  }
  if (scope.context.dimensions.length !== decisionScope.context.dimensions.length) {
    throw new PromotionScopeError("context dimensions do not match decision scope");
  }

  if (scope.contractVersion?.id !== draft.id || scope.contractVersion.version !== draft.version) {
    throw new PromotionScopeError(
      "applicable scope must reference the draft contract id and version",
    );
  }

  if (finding.detector.id !== decisionScope.detector.id) {
    throw new PromotionScopeError("finding detector does not match decision scope");
  }
  if (finding.detector.version !== decisionScope.detector.version) {
    throw new PromotionScopeError("finding detector version does not match decision scope");
  }
  if (finding.target.canonical !== decisionScope.target.canonicalTarget) {
    throw new PromotionScopeError("finding target does not match decision scope");
  }
}
