import { verifyActionReceiptV04 } from "../browser";
import { canonicalize, sha256Bytes, sha256Json, utf8 } from "../crypto";
import type { JsonObject, JsonValue } from "../types";
import type { ActionReceiptV04 } from "../receipt";
import {
  COUNTERPARTY_ID,
  EFFECT_ARGUMENTS,
  EFFECT_CLASS,
  EFFECT_LOCK_PROFILE,
  EFFECT_TARGET,
  REDEMPTION_PROFILE,
  RECEIVER_ID,
  SEMANTIC_EPOCH,
  declaredRuntimeGraph,
  VERIFIER_DEFINITION,
  EVIDENCE_POLICY,
} from "./constants";
import type {
  AcceptedEffectLockContext,
  RedemptionWire,
  VerifiedRedemption,
} from "./types";

const HASH = /^sha256:[0-9a-f]{64}$/u;
const TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u;

export class EffectLockInputError extends Error {
  constructor(readonly code: string, message: string) {
    super(message);
  }
}

function object(value: unknown, name: string): JsonObject {
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
    throw new EffectLockInputError("INVALID_REDEMPTION", `${name} must be an object`);
  }
  return value as JsonObject;
}

function exact(value: JsonObject, keys: readonly string[], name: string): void {
  const actual = Object.keys(value).sort();
  const expected = [...keys].sort();
  if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
    throw new EffectLockInputError("OPEN_SHAPE", `${name} does not have its exact closed shape`);
  }
}

function string(value: JsonValue | undefined, name: string): string {
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new EffectLockInputError("INVALID_REDEMPTION", `${name} must be a non-empty string`);
  }
  return value;
}

function subject(receipt: ActionReceiptV04, name: string): JsonObject {
  return object(receipt.action.subject, `${name}.action.subject`);
}

function requireHash(value: JsonValue | undefined, name: string): string {
  const result = string(value, name);
  if (!HASH.test(result)) throw new EffectLockInputError("INVALID_REDEMPTION", `${name} is not canonical`);
  return result;
}

function requireTime(value: JsonValue | undefined, name: string): string {
  const result = string(value, name);
  if (!TIME.test(result) || !Number.isFinite(Date.parse(result))) {
    throw new EffectLockInputError("INVALID_REDEMPTION", `${name} is not canonical UTC time`);
  }
  return result;
}

async function verifyReceipt(
  receipt: ActionReceiptV04,
  issuer: string,
  actionType: string,
  name: string,
): Promise<JsonObject> {
  const result = await verifyActionReceiptV04(receipt, { expected_issuers: [issuer] });
  if (result.structure !== "VERIFIED") {
    throw new EffectLockInputError("OPEN_SHAPE", `${name} does not have the exact receipt shape`);
  }
  if (
    result.integrity !== "VERIFIED" ||
    result.signatures !== "VERIFIED" ||
    result.occurrence_binding !== "VERIFIED" ||
    result.authority_authenticity !== "VERIFIED" ||
    result.attestation !== receipt.hashes.attestation
  ) {
    throw new EffectLockInputError("RECEIPT_REJECTED", `${name} did not verify under pinned context`);
  }
  if (receipt.action.type !== actionType) {
    throw new EffectLockInputError("RECEIPT_REJECTED", `${name} has the wrong action type`);
  }
  return subject(receipt, name);
}

export async function effectLockContext(input: {
  receiverIssuer: string;
  authorizationIssuer: string;
  boundaryIssuer: string;
}): Promise<AcceptedEffectLockContext> {
  const issuers = [input.receiverIssuer, input.authorizationIssuer, input.boundaryIssuer];
  if (new Set(issuers).size !== issuers.length) {
    throw new Error("receiver, authorization, and boundary roles require distinct keys");
  }
  const roles: JsonObject = {
    receiver: input.receiverIssuer,
    authorization: input.authorizationIssuer,
    boundary: input.boundaryIssuer,
  };
  const roleSetHash = await sha256Json({
    profile: "bulla.effect-lock-role-set/0.1-experimental",
    roles,
  });
  const runtimeGraphRoot = await sha256Json(declaredRuntimeGraph());
  return {
    profile: EFFECT_LOCK_PROFILE,
    receiver_issuer: input.receiverIssuer,
    authorization_issuer: input.authorizationIssuer,
    boundary_issuer: input.boundaryIssuer,
    evidence_policy_hash: await sha256Json(EVIDENCE_POLICY),
    verifier_hash: await sha256Json({
      definition: VERIFIER_DEFINITION,
      runtime_graph_root: runtimeGraphRoot,
    }),
    runtime_graph_root: runtimeGraphRoot,
    role_set_hash: roleSetHash,
    semantic_epoch: SEMANTIC_EPOCH,
    authority_epoch: await sha256Json({
      profile: "bulla.effect-lock-authority-epoch/1",
      role_set_hash: roleSetHash,
    }),
  };
}

export async function evidenceBundleRoot(input: {
  grant: ActionReceiptV04;
  mandate: ActionReceiptV04;
  boundaryDecision: ActionReceiptV04;
  authzenRequest: JsonObject;
  authzenResponse: JsonObject;
  effectArguments: JsonObject;
  receiverNonce: string;
}): Promise<string> {
  return sha256Json({
    profile: "bulla.effect-evidence-bundle/0.1-experimental",
    grant_receipt_sha256: await sha256Bytes(utf8(`${canonicalize(input.grant as unknown as JsonValue)}\n`)),
    mandate_receipt_sha256: await sha256Bytes(utf8(`${canonicalize(input.mandate as unknown as JsonValue)}\n`)),
    boundary_decision_receipt_sha256: await sha256Bytes(
      utf8(`${canonicalize(input.boundaryDecision as unknown as JsonValue)}\n`),
    ),
    authzen_request_sha256: await sha256Json(input.authzenRequest),
    authzen_response_sha256: await sha256Json(input.authzenResponse),
    effect_arguments_hash: await sha256Json(input.effectArguments),
    receiver_nonce: input.receiverNonce,
  });
}

export async function verifyRedemption(input: {
  value: JsonObject;
  persistedGrant: ActionReceiptV04;
  acceptedContext: AcceptedEffectLockContext;
  now: string;
  priorStateRoot: string;
}): Promise<VerifiedRedemption> {
  const value = input.value;
  exact(
    value,
    [
      "profile",
      "grant",
      "mandate",
      "boundary_decision",
      "authzen_request",
      "authzen_response",
      "effect_arguments",
      "receiver_nonce",
      "evidence_bundle_root",
    ],
    "redemption",
  );
  if (value.profile !== REDEMPTION_PROFILE) {
    throw new EffectLockInputError("INVALID_REDEMPTION", "redemption profile is not accepted");
  }
  const grant = object(value.grant, "grant") as unknown as ActionReceiptV04;
  const mandate = object(value.mandate, "mandate") as unknown as ActionReceiptV04;
  const decision = object(value.boundary_decision, "boundary_decision") as unknown as ActionReceiptV04;
  const request = object(value.authzen_request, "authzen_request");
  const response = object(value.authzen_response, "authzen_response");
  const argumentsValue = object(value.effect_arguments, "effect_arguments");
  exact(argumentsValue, ["value"], "effect_arguments");
  if (canonicalize(argumentsValue) !== canonicalize(EFFECT_ARGUMENTS)) {
    throw new EffectLockInputError("EFFECT_MISMATCH", "effect arguments are not accepted");
  }
  const nonce = string(value.receiver_nonce, "receiver_nonce");
  const bundleRoot = requireHash(value.evidence_bundle_root, "evidence_bundle_root");

  for (const [name, receipt] of [
    ["grant", grant],
    ["mandate", mandate],
    ["boundary_decision", decision],
  ] as const) {
    const shape = await verifyActionReceiptV04(receipt);
    if (shape.structure !== "VERIFIED") {
      throw new EffectLockInputError("OPEN_SHAPE", `${name} does not have the exact receipt shape`);
    }
  }

  if (canonicalize(grant as unknown as JsonValue) !== canonicalize(input.persistedGrant as unknown as JsonValue)) {
    throw new EffectLockInputError("GRANT_NOT_ISSUED", "grant bytes do not match receiver state");
  }

  const grantSubject = await verifyReceipt(
    grant,
    input.acceptedContext.receiver_issuer,
    "effect.grant.issue",
    "grant",
  );
  const mandateSubject = await verifyReceipt(
    mandate,
    input.acceptedContext.authorization_issuer,
    "eval.run.authorize",
    "mandate",
  );
  const decisionSubject = await verifyReceipt(
    decision,
    input.acceptedContext.boundary_issuer,
    "boundary.decision",
    "boundary_decision",
  );

  exact(
    grantSubject,
    [
      "profile", "grant_id", "run_id", "receiver_id", "counterparty_id", "target",
      "effect_class", "arguments_hash", "receiver_nonce", "evidence_policy_hash",
      "verifier_hash", "semantic_epoch", "authority_epoch", "valid_from", "valid_until",
      "runtime_graph_root", "role_set_hash", "use_limit", "issuer_role",
    ],
    "grant subject",
  );
  exact(
    mandateSubject,
    [
      "profile", "mandate_id", "run_id", "receiver_id", "counterparty_id", "target",
      "effect_class", "arguments_hash", "valid_from", "valid_until", "issuer_role",
    ],
    "mandate subject",
  );
  exact(
    decisionSubject,
    [
      "profile", "decision", "run_id", "receiver_id", "counterparty_id", "target",
      "effect_class", "arguments_hash", "receiver_nonce", "request_hash", "response_hash",
      "evidence_policy_hash", "semantic_epoch", "authority_epoch", "issuer_role",
    ],
    "boundary decision subject",
  );

  exact(request, ["subject", "action", "resource", "context"], "authzen_request");
  exact(response, ["decision", "context"], "authzen_response");
  const requestSubject = object(request.subject, "authzen_request.subject");
  const requestAction = object(request.action, "authzen_request.action");
  const requestResource = object(request.resource, "authzen_request.resource");
  const requestContext = object(request.context, "authzen_request.context");
  const responseContext = object(response.context, "authzen_response.context");
  exact(requestSubject, ["id"], "authzen_request.subject");
  exact(requestAction, ["name"], "authzen_request.action");
  exact(requestResource, ["id", "type"], "authzen_request.resource");
  exact(requestContext, ["arguments_hash", "receiver_nonce", "run_id"], "authzen_request.context");
  exact(responseContext, ["authority_epoch", "policy_hash", "request_hash"], "authzen_response.context");

  const argumentsHash = await sha256Json(argumentsValue);
  const requestHash = await sha256Json(request);
  const responseHash = await sha256Json(response);
  const common = {
    run_id: string(grantSubject.run_id, "grant.run_id"),
    receiver_id: RECEIVER_ID,
    counterparty_id: COUNTERPARTY_ID,
    target: EFFECT_TARGET,
    effect_class: EFFECT_CLASS,
    arguments_hash: argumentsHash,
  };
  const requireEqual = (actual: JsonValue | undefined, expected: JsonValue, name: string): void => {
    if (actual !== expected) throw new EffectLockInputError("BINDING_MISMATCH", `${name} is not bound`);
  };
  for (const [key, expected] of Object.entries(common)) {
    requireEqual(grantSubject[key], expected, `grant.${key}`);
    requireEqual(mandateSubject[key], expected, `mandate.${key}`);
    requireEqual(decisionSubject[key], expected, `boundary_decision.${key}`);
  }
  requireEqual(grantSubject.profile, EFFECT_LOCK_PROFILE, "grant.profile");
  requireEqual(mandateSubject.profile, EFFECT_LOCK_PROFILE, "mandate.profile");
  requireEqual(decisionSubject.profile, EFFECT_LOCK_PROFILE, "boundary_decision.profile");
  requireEqual(grantSubject.receiver_nonce, nonce, "grant.receiver_nonce");
  requireEqual(decisionSubject.receiver_nonce, nonce, "boundary_decision.receiver_nonce");
  requireEqual(grantSubject.evidence_policy_hash, input.acceptedContext.evidence_policy_hash, "grant.evidence_policy_hash");
  requireEqual(decisionSubject.evidence_policy_hash, input.acceptedContext.evidence_policy_hash, "boundary_decision.evidence_policy_hash");
  requireEqual(grantSubject.verifier_hash, input.acceptedContext.verifier_hash, "grant.verifier_hash");
  requireEqual(grantSubject.runtime_graph_root, input.acceptedContext.runtime_graph_root, "grant.runtime_graph_root");
  requireEqual(grantSubject.role_set_hash, input.acceptedContext.role_set_hash, "grant.role_set_hash");
  requireEqual(grantSubject.semantic_epoch, input.acceptedContext.semantic_epoch, "grant.semantic_epoch");
  requireEqual(decisionSubject.semantic_epoch, input.acceptedContext.semantic_epoch, "boundary_decision.semantic_epoch");
  requireEqual(grantSubject.authority_epoch, input.acceptedContext.authority_epoch, "grant.authority_epoch");
  requireEqual(decisionSubject.authority_epoch, input.acceptedContext.authority_epoch, "boundary_decision.authority_epoch");
  requireEqual(grantSubject.use_limit, 1, "grant.use_limit");
  requireEqual(grantSubject.issuer_role, "target", "grant.issuer_role");
  requireEqual(mandateSubject.issuer_role, "authorization", "mandate.issuer_role");
  requireEqual(decisionSubject.issuer_role, "boundary", "boundary_decision.issuer_role");
  requireEqual(decisionSubject.decision, "PERMIT", "boundary_decision.decision");
  requireEqual(decisionSubject.request_hash, requestHash, "boundary_decision.request_hash");
  requireEqual(decisionSubject.response_hash, responseHash, "boundary_decision.response_hash");
  requireEqual(requestSubject.id, COUNTERPARTY_ID, "authzen_request.subject.id");
  requireEqual(requestAction.name, EFFECT_CLASS, "authzen_request.action.name");
  requireEqual(requestResource.id, EFFECT_TARGET, "authzen_request.resource.id");
  requireEqual(requestResource.type, "receiver-state", "authzen_request.resource.type");
  requireEqual(requestContext.arguments_hash, argumentsHash, "authzen_request.context.arguments_hash");
  requireEqual(requestContext.receiver_nonce, nonce, "authzen_request.context.receiver_nonce");
  requireEqual(requestContext.run_id, common.run_id, "authzen_request.context.run_id");
  requireEqual(response.decision, true, "authzen_response.decision");
  requireEqual(responseContext.authority_epoch, input.acceptedContext.authority_epoch, "authzen_response.context.authority_epoch");
  requireEqual(responseContext.policy_hash, input.acceptedContext.evidence_policy_hash, "authzen_response.context.policy_hash");
  requireEqual(responseContext.request_hash, requestHash, "authzen_response.context.request_hash");

  if (
    grant.evidence_refs.length !== 0 ||
    mandate.evidence_refs.length !== 0 ||
    decision.evidence_refs.length !== 3 ||
    decision.evidence_refs[0]?.name !== "mandate" ||
    decision.evidence_refs[0]?.hash !== mandate.hashes.attestation ||
    decision.evidence_refs[0]?.grounding !== "counterparty_signed" ||
    decision.evidence_refs[1]?.name !== "authzen-request" ||
    decision.evidence_refs[1]?.hash !== requestHash ||
    decision.evidence_refs[1]?.grounding !== "self_asserted" ||
    decision.evidence_refs[2]?.name !== "authzen-response" ||
    decision.evidence_refs[2]?.hash !== responseHash ||
    decision.evidence_refs[2]?.grounding !== "self_asserted"
  ) {
    throw new EffectLockInputError(
      "EVIDENCE_REFERENCE_MISMATCH",
      "boundary decision does not bind the exact mandate and AuthZEN records",
    );
  }

  const validFrom = requireTime(grantSubject.valid_from, "grant.valid_from");
  const validUntil = requireTime(grantSubject.valid_until, "grant.valid_until");
  requireEqual(mandateSubject.valid_from, validFrom, "mandate.valid_from");
  requireEqual(mandateSubject.valid_until, validUntil, "mandate.valid_until");
  const now = requireTime(input.now, "evaluation_time");
  if (Date.parse(now) < Date.parse(validFrom) || Date.parse(now) >= Date.parse(validUntil)) {
    throw new EffectLockInputError("GRANT_EXPIRED", "grant is not valid at receiver evaluation time");
  }

  const recomputedRoot = await evidenceBundleRoot({
    grant,
    mandate,
    boundaryDecision: decision,
    authzenRequest: request,
    authzenResponse: response,
    effectArguments: argumentsValue,
    receiverNonce: nonce,
  });
  if (bundleRoot !== recomputedRoot) {
    throw new EffectLockInputError("BUNDLE_ROOT_MISMATCH", "evidence bundle root does not recompute");
  }

  return {
    grant,
    redemption: value as RedemptionWire,
    redemption_digest: await sha256Json(value),
    grant_attestation: grant.hashes.attestation,
    effect_arguments_hash: argumentsHash,
    prior_state_root: input.priorStateRoot,
  };
}

export async function declaredGraphMediation(graph: JsonObject = declaredRuntimeGraph()): Promise<{
  result: "ESTABLISHED_IN_DECLARED_GRAPH";
  protected_node: string;
  incoming: string[];
  graph_root: string;
}> {
  const expected = declaredRuntimeGraph();
  if (canonicalize(graph) !== canonicalize(expected)) {
    throw new Error("declared runtime graph does not have its exact accepted topology");
  }
  const nodes = graph.nodes as JsonValue[];
  const transitions = graph.transitions as JsonValue[];
  if (
    !Array.isArray(nodes) ||
    !Array.isArray(transitions) ||
    new Set(nodes).size !== nodes.length ||
    transitions.some(
      (raw) =>
        !Array.isArray(raw) ||
        raw.length !== 2 ||
        typeof raw[0] !== "string" ||
        typeof raw[1] !== "string" ||
        !nodes.includes(raw[0]) ||
        !nodes.includes(raw[1]),
    )
  ) {
    throw new Error("declared runtime graph is not a closed graph");
  }
  const incoming = transitions
    .filter((raw) => Array.isArray(raw) && raw[1] === graph.protected_node)
    .map((raw) => (raw as JsonValue[])[0] as string);
  if (incoming.length !== 1 || incoming[0] !== "grant_consume") {
    throw new Error("declared runtime graph does not completely mediate the protected append");
  }
  const adjacency = new Map<string, string[]>();
  for (const node of nodes as string[]) adjacency.set(node, []);
  for (const raw of transitions as string[][]) adjacency.get(raw[0] as string)?.push(raw[1] as string);
  const protectedNode = graph.protected_node as string;
  const visit = (node: string, path: string[]): string[][] => {
    if (path.includes(node)) throw new Error("declared runtime graph contains a cycle");
    const nextPath = [...path, node];
    if (node === protectedNode) return [nextPath];
    return (adjacency.get(node) ?? []).flatMap((next) => visit(next, nextPath));
  };
  const mutationEntries = ["grant_issue", "redemption_receive"];
  const paths = mutationEntries.flatMap((entry) => visit(entry, []));
  if (paths.length === 0) throw new Error("declared runtime graph has no path to the protected append");
  for (const path of paths) {
    const verify = path.indexOf("target_verify");
    const consume = path.indexOf("grant_consume");
    const commit = path.indexOf("protected_append_commit");
    if (!(verify >= 0 && verify < consume && consume < commit)) {
      throw new Error("a declared mutation path bypasses target verification or grant consumption");
    }
  }
  return {
    result: "ESTABLISHED_IN_DECLARED_GRAPH",
    protected_node: graph.protected_node as string,
    incoming,
    graph_root: await sha256Json(graph),
  };
}
