/**
 * Combined KT proof attached to a bundle response.
 *
 * Wire shape (returned by GET /v1/keys/bundle/:address when KT is on):
 *
 *   {
 *     bundle: { ... },
 *     ktProof: {
 *       sth: STHWire,
 *       inclusion: {
 *         leafIndex: number,
 *         leafTimestampMs: number,
 *         operation: number,
 *         auditPath: string[]   // base64 sibling hashes
 *       },
 *       indexProof: IndexInclusionProof | IndexAbsenceProof   (wire-encoded)
 *     }
 *   }
 *
 * The verifier:
 *   1. Confirms STH signature against pinned log_public_key.
 *   2. Confirms STH timestamp is fresh (<= maxStaleMs old).
 *   3. Re-derives bundle_hash from the bundle and re-builds the leaf.
 *   4. Verifies inclusion against sth.root_hash via auditPath.
 *   5. Verifies index proof (inclusion w/ matching bundle hash, or absence
 *      proof for tombstoned address) against sth.index_root.
 */

import type { CryptoProvider } from '@shade/core';
import { computeBundleHash, encodeLeafData, leafHash, OP_DELETE } from './hashes.js';
import { recomputeRootFromAuditPath } from './log.js';
import {
  type SignedTreeHead,
  type STHWire,
  sthFromWire,
  sthToWire,
  verifySthSignature,
} from './sth.js';
import {
  type AddressIndexEntry,
  type IndexAbsenceProof,
  type IndexInclusionProof,
  verifyInclusionProof,
  verifyAbsenceProof,
} from './index-tree.js';
import {
  KTLogIdMismatchError,
  KTStaleSTHError,
  KTVerificationError,
} from './errors.js';
import { computeLogId } from './sth.js';
import { constantTimeEqual, fromBase64, toBase64 } from './util.js';

/** Exists-style proof: leaf is in the log, address is in the index. */
export interface KTBundleInclusionProof {
  kind: 'inclusion';
  leafIndex: number;
  leafTimestampMs: number;
  operation: number;
  auditPath: Uint8Array[];
  indexProof: IndexInclusionProof;
}

/** Tombstone proof: latest leaf for the address is a delete; index entry is deleted. */
export interface KTBundleTombstoneProof {
  kind: 'tombstone';
  leafIndex: number;
  leafTimestampMs: number;
  /** operation will be OP_DELETE here. */
  operation: number;
  auditPath: Uint8Array[];
  indexProof: IndexInclusionProof;
}

/** Absence proof: address has never been registered. */
export interface KTBundleAbsenceProof {
  kind: 'absence';
  indexProof: IndexAbsenceProof;
}

export type KTProofBody = KTBundleInclusionProof | KTBundleTombstoneProof | KTBundleAbsenceProof;

export interface KTProof {
  sth: SignedTreeHead;
  body: KTProofBody;
}

// ─── Wire encoding ────────────────────────────────────────

interface IndexInclusionWire {
  kind: 'inclusion';
  position: number;
  treeSize: number;
  entry: { address: string; latestLeafIndex: number; bundleHash: string; deleted: boolean };
  auditPath: string[];
}

interface IndexAbsenceWire {
  kind: 'absence';
  treeSize: number;
  queryAddress: string;
  prev: {
    position: number;
    entry: { address: string; latestLeafIndex: number; bundleHash: string; deleted: boolean };
    auditPath: string[];
  } | null;
  next: {
    position: number;
    entry: { address: string; latestLeafIndex: number; bundleHash: string; deleted: boolean };
    auditPath: string[];
  } | null;
}

export type IndexProofWire = IndexInclusionWire | IndexAbsenceWire;

interface BundleInclusionWire {
  kind: 'inclusion' | 'tombstone';
  leafIndex: number;
  leafTimestampMs: number;
  operation: number;
  auditPath: string[];
  indexProof: IndexInclusionWire;
}

interface BundleAbsenceWire {
  kind: 'absence';
  indexProof: IndexAbsenceWire;
}

type KTProofBodyWire = BundleInclusionWire | BundleAbsenceWire;

export interface KTProofWire {
  sth: STHWire;
  body: KTProofBodyWire;
}

function entryToWire(e: AddressIndexEntry) {
  return {
    address: e.address,
    latestLeafIndex: e.latestLeafIndex,
    bundleHash: toBase64(e.bundleHash),
    deleted: e.deleted,
  };
}

function entryFromWire(w: {
  address: string;
  latestLeafIndex: number;
  bundleHash: string;
  deleted: boolean;
}): AddressIndexEntry {
  return {
    address: w.address,
    latestLeafIndex: w.latestLeafIndex,
    bundleHash: fromBase64(w.bundleHash),
    deleted: w.deleted,
  };
}

function indexInclusionToWire(p: IndexInclusionProof): IndexInclusionWire {
  return {
    kind: 'inclusion',
    position: p.position,
    treeSize: p.treeSize,
    entry: entryToWire(p.entry),
    auditPath: p.auditPath.map(toBase64),
  };
}

function indexInclusionFromWire(w: IndexInclusionWire): IndexInclusionProof {
  return {
    kind: 'inclusion',
    position: w.position,
    treeSize: w.treeSize,
    entry: entryFromWire(w.entry),
    auditPath: w.auditPath.map(fromBase64),
  };
}

function indexAbsenceToWire(p: IndexAbsenceProof): IndexAbsenceWire {
  return {
    kind: 'absence',
    treeSize: p.treeSize,
    queryAddress: p.queryAddress,
    prev: p.prev
      ? {
          position: p.prev.position,
          entry: entryToWire(p.prev.entry),
          auditPath: p.prev.auditPath.map(toBase64),
        }
      : null,
    next: p.next
      ? {
          position: p.next.position,
          entry: entryToWire(p.next.entry),
          auditPath: p.next.auditPath.map(toBase64),
        }
      : null,
  };
}

function indexAbsenceFromWire(w: IndexAbsenceWire): IndexAbsenceProof {
  return {
    kind: 'absence',
    treeSize: w.treeSize,
    queryAddress: w.queryAddress,
    prev: w.prev
      ? {
          position: w.prev.position,
          entry: entryFromWire(w.prev.entry),
          auditPath: w.prev.auditPath.map(fromBase64),
        }
      : null,
    next: w.next
      ? {
          position: w.next.position,
          entry: entryFromWire(w.next.entry),
          auditPath: w.next.auditPath.map(fromBase64),
        }
      : null,
  };
}

export function ktProofToWire(proof: KTProof): KTProofWire {
  const sth = sthToWire(proof.sth, toBase64);
  let body: KTProofBodyWire;
  if (proof.body.kind === 'absence') {
    body = { kind: 'absence', indexProof: indexAbsenceToWire(proof.body.indexProof) };
  } else {
    body = {
      kind: proof.body.kind,
      leafIndex: proof.body.leafIndex,
      leafTimestampMs: proof.body.leafTimestampMs,
      operation: proof.body.operation,
      auditPath: proof.body.auditPath.map(toBase64),
      indexProof: indexInclusionToWire(proof.body.indexProof),
    };
  }
  return { sth, body };
}

export function ktProofFromWire(wire: KTProofWire): KTProof {
  const sth = sthFromWire(wire.sth, fromBase64);
  let body: KTProofBody;
  if (wire.body.kind === 'absence') {
    body = { kind: 'absence', indexProof: indexAbsenceFromWire(wire.body.indexProof) };
  } else if (wire.body.kind === 'tombstone') {
    body = {
      kind: 'tombstone',
      leafIndex: wire.body.leafIndex,
      leafTimestampMs: wire.body.leafTimestampMs,
      operation: wire.body.operation,
      auditPath: wire.body.auditPath.map(fromBase64),
      indexProof: indexInclusionFromWire(wire.body.indexProof),
    };
  } else {
    body = {
      kind: 'inclusion',
      leafIndex: wire.body.leafIndex,
      leafTimestampMs: wire.body.leafTimestampMs,
      operation: wire.body.operation,
      auditPath: wire.body.auditPath.map(fromBase64),
      indexProof: indexInclusionFromWire(wire.body.indexProof),
    };
  }
  return { sth, body };
}

// ─── Verifier ────────────────────────────────────────────

export interface KTVerifyOptions {
  crypto: CryptoProvider;
  /** Pinned log signing public key (Ed25519, 32 bytes). */
  logPublicKey: Uint8Array;
  /** Reject STH older than this many milliseconds. Default 24h. */
  maxStaleMs?: number;
  /** `now` for time-checks. Defaults to `Date.now()`. */
  nowMs?: number;
  /** Allow STH timestamps slightly in the future (clock-skew). Default 60 s. */
  futureSkewMs?: number;
}

const DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1000;
const DEFAULT_FUTURE_SKEW_MS = 60_000;

/**
 * Verify an inclusion KT proof for a freshly fetched bundle.
 *
 * Throws on any failure (signature mismatch, stale STH, broken audit
 * path, address-mismatch, bundle-hash mismatch). On success returns the
 * STH so callers can cache it for split-view detection.
 */
export async function verifyBundleInclusion(
  options: KTVerifyOptions,
  address: string,
  bundle: {
    identitySigningKey: Uint8Array;
    identityDHKey: Uint8Array;
    signedPreKey: { keyId: number; publicKey: Uint8Array; signature: Uint8Array };
  },
  proof: KTProof,
): Promise<SignedTreeHead> {
  if (proof.body.kind !== 'inclusion') {
    throw new KTVerificationError(`expected inclusion proof, got ${proof.body.kind}`);
  }

  await verifyStaticSth(options, proof.sth);

  // 1. Re-derive bundle_hash and confirm it matches the index entry.
  const bundleHash = computeBundleHash(bundle);
  const indexEntry = proof.body.indexProof.entry;
  if (indexEntry.address !== address) {
    throw new KTVerificationError(`index entry address mismatch: ${indexEntry.address} != ${address}`);
  }
  if (indexEntry.deleted) {
    throw new KTVerificationError('index entry marked deleted, but inclusion proof claims live');
  }
  if (!constantTimeEqual(indexEntry.bundleHash, bundleHash)) {
    throw new KTVerificationError('bundle hash does not match committed index entry');
  }

  // 2. Verify log inclusion.
  const leafBytes = encodeLeafData(
    proof.body.leafTimestampMs,
    proof.body.operation,
    address,
    bundleHash,
  );
  const expectedLeafHash = leafHash(leafBytes);
  let recomputedRoot: Uint8Array;
  try {
    recomputedRoot = recomputeRootFromAuditPath(
      expectedLeafHash,
      proof.body.leafIndex,
      proof.sth.treeSize,
      proof.body.auditPath,
    );
  } catch (err) {
    throw new KTVerificationError(`audit path malformed: ${(err as Error).message}`);
  }
  if (!constantTimeEqual(recomputedRoot, proof.sth.rootHash)) {
    throw new KTVerificationError('audit path does not yield STH root_hash');
  }

  // 3. Verify the index inclusion proof.
  if (proof.body.indexProof.entry.latestLeafIndex !== proof.body.leafIndex) {
    throw new KTVerificationError('index entry latestLeafIndex mismatch with log leaf');
  }
  if (!verifyInclusionProof(proof.body.indexProof, proof.sth.indexRoot)) {
    throw new KTVerificationError('index inclusion proof failed');
  }

  return proof.sth;
}

/**
 * Verify an absence KT proof — used when the server replies "no such
 * address". The verifier returns `null` to indicate absence (vs. a
 * verified live STH). On any failure it throws.
 */
export async function verifyBundleAbsence(
  options: KTVerifyOptions,
  address: string,
  proof: KTProof,
): Promise<SignedTreeHead> {
  if (proof.body.kind !== 'absence') {
    throw new KTVerificationError(`expected absence proof, got ${proof.body.kind}`);
  }
  await verifyStaticSth(options, proof.sth);
  if (proof.body.indexProof.queryAddress !== address) {
    throw new KTVerificationError('absence proof query address mismatch');
  }
  if (!verifyAbsenceProof(proof.body.indexProof, proof.sth.indexRoot)) {
    throw new KTVerificationError('absence proof failed');
  }
  return proof.sth;
}

/**
 * Verify a tombstone proof — the address used to exist but has been
 * deleted. The verifier returns the STH so split-view caching still
 * applies; callers treat the bundle as "not available".
 */
export async function verifyBundleTombstone(
  options: KTVerifyOptions,
  address: string,
  proof: KTProof,
): Promise<SignedTreeHead> {
  if (proof.body.kind !== 'tombstone') {
    throw new KTVerificationError(`expected tombstone proof, got ${proof.body.kind}`);
  }
  await verifyStaticSth(options, proof.sth);
  if (proof.body.operation !== OP_DELETE) {
    throw new KTVerificationError('tombstone proof must reference an OP_DELETE leaf');
  }
  if (proof.body.indexProof.entry.address !== address) {
    throw new KTVerificationError('tombstone index entry address mismatch');
  }
  if (!proof.body.indexProof.entry.deleted) {
    throw new KTVerificationError('tombstone proof index entry is not marked deleted');
  }
  // Deleted entries have empty bundleHash but the leaf data still uses an
  // empty hash too; encode and verify the audit path with that.
  const leafBytes = encodeLeafData(
    proof.body.leafTimestampMs,
    proof.body.operation,
    address,
    proof.body.indexProof.entry.bundleHash,
  );
  const expectedLeafHash = leafHash(leafBytes);
  let recomputedRoot: Uint8Array;
  try {
    recomputedRoot = recomputeRootFromAuditPath(
      expectedLeafHash,
      proof.body.leafIndex,
      proof.sth.treeSize,
      proof.body.auditPath,
    );
  } catch (err) {
    throw new KTVerificationError(`audit path malformed: ${(err as Error).message}`);
  }
  if (!constantTimeEqual(recomputedRoot, proof.sth.rootHash)) {
    throw new KTVerificationError('tombstone audit path does not yield STH root_hash');
  }
  if (!verifyInclusionProof(proof.body.indexProof, proof.sth.indexRoot)) {
    throw new KTVerificationError('tombstone index inclusion proof failed');
  }
  return proof.sth;
}

async function verifyStaticSth(options: KTVerifyOptions, sth: SignedTreeHead): Promise<void> {
  const expectedLogId = computeLogId(options.logPublicKey);
  if (!constantTimeEqual(expectedLogId, sth.logId)) {
    throw new KTLogIdMismatchError('STH log_id does not match pinned log_public_key');
  }
  const sigOk = await verifySthSignature(options.crypto, sth, options.logPublicKey);
  if (!sigOk) {
    throw new KTVerificationError('STH signature did not verify');
  }
  // Encode entry into the in-memory canonical bytes used by the leaf and
  // confirm the leaf actually re-encodes to the right bytes when the entry
  // says it's present (we let the caller do this to keep per-mode flow
  // small).
  const now = options.nowMs ?? Date.now();
  const maxStale = options.maxStaleMs ?? DEFAULT_MAX_STALE_MS;
  const futureSkew = options.futureSkewMs ?? DEFAULT_FUTURE_SKEW_MS;
  if (sth.timestampMs > now + futureSkew) {
    throw new KTStaleSTHError(`STH timestamp in the future: ${sth.timestampMs} > ${now + futureSkew}`);
  }
  if (sth.timestampMs + maxStale < now) {
    throw new KTStaleSTHError(`STH older than maxStale: ${now - sth.timestampMs}ms`);
  }
}
