/**
 * Address-index commitment.
 *
 * The Merkle log itself records mutation events (`address → bundle_hash`
 * at time T), but doesn't natively answer "what's the *current* state for
 * `address`?" or "does `address` exist?".
 *
 * The address index is a **lexicographically sorted snapshot** of the
 * current `(address, latest_leaf_index)` mapping. Its commitment hash —
 * `index_root` — is part of every Signed Tree Head.
 *
 * Inclusion proof:  the entry exists at sorted index `i`, prove it via
 *                   audit path (same Merkle construction as the main log).
 *
 * Absence proof:    the address would sort between two adjacent existing
 *                   entries; prove inclusion of those two adjacent entries
 *                   and that the queried address sorts strictly between them.
 *
 * V1 representation: a flat sorted array. We re-hash the whole index per
 * STH (cheap up to ~1M entries). V2 will move to a sparse Merkle tree if
 * the dataset grows enough that flat re-hash becomes a bottleneck.
 */

import { leafHash, emptyRootHash } from './hashes.js';
import { sha256Sync } from './sha256.js';
import { constantTimeEqual } from './util.js';
import { mth, auditPath, recomputeRootFromAuditPath } from './log.js';

export interface AddressIndexEntry {
  /** The address (UTF-8 string). */
  address: string;
  /** Most recent log leaf index that mutated this address. */
  latestLeafIndex: number;
  /** Most recent bundle hash committed for this address. */
  bundleHash: Uint8Array;
  /** Whether the latest event was a delete (tombstone). */
  deleted: boolean;
}

/** Encode an index entry into the bytes that go into a Merkle leaf. */
export function encodeIndexEntry(entry: AddressIndexEntry): Uint8Array {
  const addrBytes = new TextEncoder().encode(entry.address);
  if (addrBytes.length > 0xffff) throw new Error('address too long');
  if (entry.bundleHash.length > 0xffff) throw new Error('bundleHash too long');
  const len = 2 + addrBytes.length + 4 + 1 + 2 + entry.bundleHash.length;
  const out = new Uint8Array(len);
  const view = new DataView(out.buffer);
  let off = 0;
  view.setUint16(off, addrBytes.length);
  off += 2;
  out.set(addrBytes, off);
  off += addrBytes.length;
  view.setUint32(off, entry.latestLeafIndex >>> 0);
  off += 4;
  out[off++] = entry.deleted ? 1 : 0;
  view.setUint16(off, entry.bundleHash.length);
  off += 2;
  out.set(entry.bundleHash, off);
  return out;
}

/** Compute index_root over a sorted entry list. */
export function computeIndexRoot(sortedEntries: AddressIndexEntry[]): Uint8Array {
  if (sortedEntries.length === 0) return emptyRootHash();
  const leaves = sortedEntries.map((e) => leafHash(encodeIndexEntry(e)));
  return mth(leaves, 0, leaves.length);
}

/** Compare two addresses lexicographically (by UTF-8 byte order). */
export function compareAddresses(a: string, b: string): number {
  const ab = new TextEncoder().encode(a);
  const bb = new TextEncoder().encode(b);
  const len = Math.min(ab.length, bb.length);
  for (let i = 0; i < len; i++) {
    if (ab[i]! !== bb[i]!) return ab[i]! - bb[i]!;
  }
  return ab.length - bb.length;
}

/**
 * In-memory address index. Maintains the canonical sorted ordering; on
 * mutate, the operator re-computes index_root for the next STH.
 */
export class AddressIndex {
  private entries: AddressIndexEntry[] = [];
  private positionByAddress = new Map<string, number>();

  get size(): number {
    return this.entries.length;
  }

  /** Idempotently set an entry; re-sorts only when a new address is added. */
  upsert(entry: AddressIndexEntry): void {
    const existingPos = this.positionByAddress.get(entry.address);
    if (existingPos !== undefined) {
      this.entries[existingPos] = { ...entry };
      return;
    }
    // Insert keeping sort order
    let lo = 0;
    let hi = this.entries.length;
    while (lo < hi) {
      const mid = (lo + hi) >>> 1;
      if (compareAddresses(this.entries[mid]!.address, entry.address) < 0) lo = mid + 1;
      else hi = mid;
    }
    this.entries.splice(lo, 0, { ...entry });
    // Rebuild position map (positions shift after insert)
    this.positionByAddress.clear();
    for (let i = 0; i < this.entries.length; i++) {
      this.positionByAddress.set(this.entries[i]!.address, i);
    }
  }

  /** Mark an address tombstoned. Keeps the entry in sorted order. */
  tombstone(address: string, latestLeafIndex: number): void {
    const pos = this.positionByAddress.get(address);
    if (pos === undefined) return;
    const e = this.entries[pos]!;
    this.entries[pos] = {
      ...e,
      deleted: true,
      latestLeafIndex,
      bundleHash: new Uint8Array(0),
    };
  }

  /** Snapshot ordered list (defensive copy). */
  snapshot(): AddressIndexEntry[] {
    return this.entries.map((e) => ({ ...e, bundleHash: new Uint8Array(e.bundleHash) }));
  }

  /** Compute the index commitment root over the current sorted list. */
  rootHash(): Uint8Array {
    return computeIndexRoot(this.entries);
  }

  /** Look up an entry. */
  get(address: string): AddressIndexEntry | undefined {
    const pos = this.positionByAddress.get(address);
    if (pos === undefined) return undefined;
    return { ...this.entries[pos]!, bundleHash: new Uint8Array(this.entries[pos]!.bundleHash) };
  }

  /** Build inclusion proof: returns sorted-position + audit path. */
  inclusionProof(address: string): IndexInclusionProof | null {
    const pos = this.positionByAddress.get(address);
    if (pos === undefined) return null;
    const leaves = this.entries.map((e) => leafHash(encodeIndexEntry(e)));
    return {
      kind: 'inclusion',
      position: pos,
      treeSize: this.entries.length,
      entry: { ...this.entries[pos]!, bundleHash: new Uint8Array(this.entries[pos]!.bundleHash) },
      auditPath: auditPath(leaves, pos, leaves.length),
    };
  }

  /**
   * Build absence proof: returns the two adjacent entries that bracket the
   * queried address (or boundary case for first/last).
   */
  absenceProof(address: string): IndexAbsenceProof | null {
    if (this.positionByAddress.has(address)) return null;

    if (this.entries.length === 0) {
      return {
        kind: 'absence',
        treeSize: 0,
        queryAddress: address,
        prev: null,
        next: null,
      };
    }

    // Find insertion position
    let lo = 0;
    let hi = this.entries.length;
    while (lo < hi) {
      const mid = (lo + hi) >>> 1;
      if (compareAddresses(this.entries[mid]!.address, address) < 0) lo = mid + 1;
      else hi = mid;
    }

    const leaves = this.entries.map((e) => leafHash(encodeIndexEntry(e)));
    const prevPos = lo - 1;
    const nextPos = lo;

    const prev =
      prevPos >= 0
        ? {
            position: prevPos,
            entry: {
              ...this.entries[prevPos]!,
              bundleHash: new Uint8Array(this.entries[prevPos]!.bundleHash),
            },
            auditPath: auditPath(leaves, prevPos, leaves.length),
          }
        : null;

    const next =
      nextPos < this.entries.length
        ? {
            position: nextPos,
            entry: {
              ...this.entries[nextPos]!,
              bundleHash: new Uint8Array(this.entries[nextPos]!.bundleHash),
            },
            auditPath: auditPath(leaves, nextPos, leaves.length),
          }
        : null;

    return {
      kind: 'absence',
      treeSize: this.entries.length,
      queryAddress: address,
      prev,
      next,
    };
  }

  /** Hot-load from a sorted entry array (used by persistent stores). */
  static fromEntries(sortedEntries: AddressIndexEntry[]): AddressIndex {
    const idx = new AddressIndex();
    for (const e of sortedEntries) {
      idx.entries.push({ ...e, bundleHash: new Uint8Array(e.bundleHash) });
    }
    for (let i = 0; i < idx.entries.length; i++) {
      idx.positionByAddress.set(idx.entries[i]!.address, i);
    }
    return idx;
  }
}

export interface IndexInclusionProof {
  kind: 'inclusion';
  position: number;
  treeSize: number;
  entry: AddressIndexEntry;
  auditPath: Uint8Array[];
}

export interface IndexAbsenceProof {
  kind: 'absence';
  treeSize: number;
  queryAddress: string;
  /**
   * Largest existing entry less than the query (null if the query would
   * be the first entry).
   */
  prev: { position: number; entry: AddressIndexEntry; auditPath: Uint8Array[] } | null;
  /**
   * Smallest existing entry greater than the query (null if the query
   * would be appended after the last entry).
   */
  next: { position: number; entry: AddressIndexEntry; auditPath: Uint8Array[] } | null;
}

export type IndexProof = IndexInclusionProof | IndexAbsenceProof;

/**
 * Verify an inclusion proof against an `index_root` commitment.
 */
export function verifyInclusionProof(
  proof: IndexInclusionProof,
  indexRoot: Uint8Array,
): boolean {
  const lh = leafHash(encodeIndexEntry(proof.entry));
  let recomputed: Uint8Array;
  try {
    recomputed = recomputeRootFromAuditPath(lh, proof.position, proof.treeSize, proof.auditPath);
  } catch {
    return false;
  }
  return constantTimeEqual(recomputed, indexRoot);
}

/**
 * Verify an absence proof:
 *  - prev exists and address(prev) < query
 *  - next exists and query < address(next)
 *  - prev.position + 1 === next.position (they are adjacent)
 *  - both inclusion sub-proofs verify against indexRoot
 *
 * Boundary cases:
 *  - tree empty (treeSize === 0): valid if both prev and next are null
 *  - query smaller than all entries: prev is null, next.position === 0
 *  - query larger than all entries:  next is null, prev.position === treeSize - 1
 */
export function verifyAbsenceProof(
  proof: IndexAbsenceProof,
  indexRoot: Uint8Array,
): boolean {
  if (proof.treeSize === 0) {
    if (proof.prev !== null || proof.next !== null) return false;
    return constantTimeEqual(emptyRootHash(), indexRoot);
  }

  const queryAddr = proof.queryAddress;

  if (proof.prev) {
    if (compareAddresses(proof.prev.entry.address, queryAddr) >= 0) return false;
    const lh = leafHash(encodeIndexEntry(proof.prev.entry));
    let r: Uint8Array;
    try {
      r = recomputeRootFromAuditPath(lh, proof.prev.position, proof.treeSize, proof.prev.auditPath);
    } catch {
      return false;
    }
    if (!constantTimeEqual(r, indexRoot)) return false;
  }

  if (proof.next) {
    if (compareAddresses(queryAddr, proof.next.entry.address) >= 0) return false;
    const lh = leafHash(encodeIndexEntry(proof.next.entry));
    let r: Uint8Array;
    try {
      r = recomputeRootFromAuditPath(lh, proof.next.position, proof.treeSize, proof.next.auditPath);
    } catch {
      return false;
    }
    if (!constantTimeEqual(r, indexRoot)) return false;
  }

  // Boundary checks
  if (proof.prev === null) {
    if (proof.next === null) return false; // already handled treeSize===0
    if (proof.next.position !== 0) return false;
  } else if (proof.next === null) {
    if (proof.prev.position !== proof.treeSize - 1) return false;
  } else {
    if (proof.prev.position + 1 !== proof.next.position) return false;
  }

  return true;
}

/** sha256 helper export for callers that need the same hash function. */
export { sha256Sync };
