import { timingSafeEqual } from "node:crypto";
import { z } from "zod";
import type {
  DeliveryDeploymentRecord,
  DeliveryFeatureReceiptRecord,
} from "./store";

const GitShaSchema = z.string().regex(/^[0-9a-f]{40}$/);
const DigestSchema = z.string().regex(/^[0-9a-f]{64}$/);
const IdSchema = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/);
const Base64UrlSchema = z.string().regex(/^[A-Za-z0-9_-]+$/);

const EvidenceReceiptSchema = z.object({
  receiptId: IdSchema,
  kind: z.enum(["ACCEPTANCE", "SMOKE"]),
  checkId: z.string().min(1),
  result: z.literal("PASSED"),
  observedAt: z.string().datetime(),
  evidenceReference: z.string().min(1),
}).strict();

export const EvidencePayloadSchema = z.object({
  attestationId: IdSchema,
  keyId: IdSchema,
  issuer: z.string().min(1),
  audience: z.string().min(1),
  repository: z.string().min(1),
  workflow: z.string().min(1),
  featureId: IdSchema,
  definitionDigest: DigestSchema,
  targetId: IdSchema,
  candidateSha: GitShaSchema,
  candidateTree: GitShaSchema,
  deployedSha: GitShaSchema,
  deployedTree: GitShaSchema,
  artifactDigest: DigestSchema,
  result: z.literal("PASSED"),
  issuedAt: z.string().datetime(),
  expiresAt: z.string().datetime(),
  deployment: z.object({
    deploymentId: IdSchema,
    status: z.literal("ACTIVE"),
    observedAt: z.string().datetime(),
    evidenceReference: z.string().min(1),
  }).strict(),
  receipts: z.array(EvidenceReceiptSchema).min(1),
}).strict();

export type EvidencePayload = z.infer<typeof EvidencePayloadSchema>;

export const EvidenceTrustSchema = z.object({
  authorityToken: z.string().min(1),
  maxLifetimeSeconds: z.number().int().positive().max(3600).default(300),
  keys: z.array(z.object({
    keyId: IdSchema,
    publicKey: Base64UrlSchema,
    issuer: z.string().min(1),
    audience: z.string().min(1),
    repository: z.string().min(1),
    workflow: z.string().min(1),
  }).strict()).min(1),
}).strict();

export type EvidenceTrust = z.infer<typeof EvidenceTrustSchema>;

export const EvidenceEnvelopeSchema = z.object({
  payload: EvidencePayloadSchema,
  signature: Base64UrlSchema,
}).strict();

function canonical(value: unknown): string {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
  const object = value as Record<string, unknown>;
  return `{${Object.keys(object)
    .sort()
    .map((key) => `${JSON.stringify(key)}:${canonical(object[key])}`)
    .join(",")}}`;
}

export function canonicalEvidenceBytes(payload: EvidencePayload): Uint8Array<ArrayBuffer> {
  return new TextEncoder().encode(canonical(payload));
}

function decodeBase64Url(value: string, expectedBytes: number): Uint8Array<ArrayBuffer> {
  const parsed = Base64UrlSchema.parse(value);
  const decoded = Buffer.from(parsed, "base64url");
  if (
    decoded.length !== expectedBytes
    || decoded.toString("base64url") !== parsed
  ) {
    throw new Error("invalid evidence encoding");
  }
  return Uint8Array.from(decoded);
}

function requireObservedAtWithinWindow(
  observedAt: string,
  issuedAt: number,
  expiresAt: number,
  now: number,
): void {
  const observed = Date.parse(observedAt);
  if (
    !Number.isFinite(observed)
    || observed < issuedAt
    || observed > expiresAt
    || observed > now
  ) {
    throw new Error("invalid evidence observation time");
  }
}

export async function verifyEvidenceEnvelope(
  input: unknown,
  trust: EvidenceTrust,
  now = Date.now(),
): Promise<{
  deployment: DeliveryDeploymentRecord;
  receipts: DeliveryFeatureReceiptRecord[];
  attestationId: string;
}> {
  const envelope = EvidenceEnvelopeSchema.parse(input);
  const payload = envelope.payload;
  const key = trust.keys.find((candidate) => candidate.keyId === payload.keyId);
  if (
    !key
    || key.issuer !== payload.issuer
    || key.audience !== payload.audience
    || key.repository !== payload.repository
    || key.workflow !== payload.workflow
  ) {
    throw new Error("untrusted evidence binding");
  }

  const issuedAt = Date.parse(payload.issuedAt);
  const expiresAt = Date.parse(payload.expiresAt);
  const maxLifetime = trust.maxLifetimeSeconds * 1000;
  if (
    !Number.isFinite(issuedAt)
    || !Number.isFinite(expiresAt)
    || issuedAt > now
    || expiresAt <= now
    || expiresAt <= issuedAt
    || expiresAt - issuedAt > maxLifetime
  ) {
    throw new Error("invalid evidence time window");
  }
  requireObservedAtWithinWindow(
    payload.deployment.observedAt,
    issuedAt,
    expiresAt,
    now,
  );
  for (const receipt of payload.receipts) {
    requireObservedAtWithinWindow(receipt.observedAt, issuedAt, expiresAt, now);
  }

  let verified = false;
  try {
    const publicKey = await crypto.subtle.importKey(
      "raw",
      decodeBase64Url(key.publicKey, 32),
      { name: "Ed25519" },
      false,
      ["verify"],
    );
    verified = await crypto.subtle.verify(
      "Ed25519",
      publicKey,
      decodeBase64Url(envelope.signature, 64),
      canonicalEvidenceBytes(payload),
    );
  } catch {
    throw new Error("invalid evidence signature");
  }
  if (!verified) throw new Error("invalid evidence signature");

  const deployment: DeliveryDeploymentRecord = {
    ...payload.deployment,
    targetId: payload.targetId,
    deployedSha: payload.deployedSha,
    deployedTree: payload.deployedTree,
    artifactDigest: payload.artifactDigest,
  };
  const receipts = payload.receipts.map((receipt) => ({
    ...receipt,
    featureId: payload.featureId,
    definitionDigest: payload.definitionDigest,
    candidateSha: payload.candidateSha,
    candidateTree: payload.candidateTree,
    deploymentId: payload.deployment.deploymentId,
    targetId: payload.targetId,
    deployedSha: payload.deployedSha,
    deployedTree: payload.deployedTree,
    artifactDigest: payload.artifactDigest,
  }));
  return {
    deployment,
    receipts,
    attestationId: payload.attestationId,
  };
}

export function hasEvidenceAuthority(
  value: string | null,
  expected: string,
): boolean {
  const actualBytes = Buffer.from(value ?? "");
  const expectedBytes = Buffer.from(expected);
  return actualBytes.length === expectedBytes.length
    && timingSafeEqual(actualBytes, expectedBytes);
}

export async function signEvidenceForTest(
  payload: EvidencePayload,
  privateKey: CryptoKey,
): Promise<string> {
  const signature = await crypto.subtle.sign(
    "Ed25519",
    privateKey,
    canonicalEvidenceBytes(payload),
  );
  return Buffer.from(signature).toString("base64url");
}
