import { z } from "zod";
import { sha256Canonical } from "../canonical.js";
import {
  defineRecordCodec,
  SchemaError,
  type RecordCodec,
} from "../records/envelope.js";
import {
  decisionScopeSchema,
  normalizeDecisionScope,
  type DecisionScope,
} from "./scope.js";

export const DECISION_BUNDLE_VERSION = "decision-bundle/v1" as const;

const bundleDecisionSchema = z
  .object({
    findingId: z.string(),
    evidenceFingerprint: z.string(),
    verdict: z.enum(["confirmed_defect", "not_defect"]),
    scope: decisionScopeSchema,
    reason: z.string().optional(),
  })
  .strict();

export type BundleDecision = z.infer<typeof bundleDecisionSchema>;

const decisionBundleBodySchema = z
  .object({
    schemaVersion: z.literal(DECISION_BUNDLE_VERSION),
    projectIdentity: z.string(),
    configHash: z.string(),
    report: z
      .object({
        id: z.string(),
        revision: z.string(),
        findingSetHash: z.string(),
      })
      .strict(),
    nonce: z.string(),
    expiresAt: z.string(),
    decisions: z.array(bundleDecisionSchema),
  })
  .strict();

export const decisionBundleSchema = decisionBundleBodySchema
  .extend({
    bundleHash: z.string(),
  })
  .strict()
  .superRefine((value, ctx) => {
    const expectedHash = computeBundleHash(value);
    if (value.bundleHash !== expectedHash) {
      ctx.addIssue({
        code: "custom",
        message: "bundleHash does not match canonical bundle digest",
        path: ["bundleHash"],
      });
    }
  });

export type DecisionBundle = z.infer<typeof decisionBundleSchema>;

export const decisionBundleCodec: RecordCodec<DecisionBundle> = defineRecordCodec({
  schema: decisionBundleSchema,
  currentVersion: DECISION_BUNDLE_VERSION,
});

function stripBundleHash(
  bundle: Omit<DecisionBundle, "bundleHash"> & { bundleHash?: string },
): Omit<DecisionBundle, "bundleHash"> {
  return {
    schemaVersion: bundle.schemaVersion,
    projectIdentity: bundle.projectIdentity,
    configHash: bundle.configHash,
    report: bundle.report,
    nonce: bundle.nonce,
    expiresAt: bundle.expiresAt,
    decisions: bundle.decisions,
  };
}

export function normalizeDecisionBundle(
  bundle: Omit<DecisionBundle, "bundleHash"> & { bundleHash?: string },
): Omit<DecisionBundle, "bundleHash"> {
  const parsed = decisionBundleBodySchema.parse(stripBundleHash(bundle));
  return {
    ...parsed,
    decisions: parsed.decisions.map((decision) => ({
      ...decision,
      scope: normalizeDecisionScope(decision.scope),
    })),
  };
}

export function computeBundleHash(
  bundle: Omit<DecisionBundle, "bundleHash"> & { bundleHash?: string },
): string {
  return sha256Canonical(normalizeDecisionBundle(bundle));
}

export function parseDecisionBundle(value: unknown): DecisionBundle {
  if (typeof value !== "object" || value === null) {
    throw new SchemaError("$", "expected object", "validation");
  }

  const record = value as Record<string, unknown>;
  const schemaVersion = record.schemaVersion;
  if (typeof schemaVersion !== "string") {
    throw new SchemaError("schemaVersion", "required string", "validation");
  }

  if (!decisionBundleCodec.supportedVersions.includes(schemaVersion)) {
    throw new SchemaError(
      "schemaVersion",
      `unsupported schema version: ${schemaVersion}`,
      "unsupported-version",
    );
  }

  const normalized = {
    ...normalizeDecisionBundle(value as DecisionBundle),
    bundleHash: computeBundleHash(value as DecisionBundle),
  };

  const result = decisionBundleSchema.safeParse({
    ...normalized,
    bundleHash:
      typeof record.bundleHash === "string" ? record.bundleHash : normalized.bundleHash,
  });

  if (!result.success) {
    const issue = result.error.issues[0];
    throw new SchemaError(
      issue?.path.join(".") ?? "$",
      issue?.message ?? "decision bundle validation failed",
      "validation",
    );
  }

  return result.data;
}

export type { DecisionScope };
