/**
 * Deterministic, in-process x402 facilitator and middleware model.
 *
 * It intentionally has no RPC, HTTP server, or wallet dependency. Ed25519 is
 * used for real signing/verification with fixed test seeds. Its optional
 * in-memory store is only a deterministic test double: it makes in-process
 * restart and cross-instance behavior testable, but is not production
 * durability or live settlement.
 */
import {
  createPrivateKey,
  createPublicKey,
  sign as ed25519Sign,
  verify as ed25519Verify,
} from "node:crypto";
import type { RouteRegistryEntry } from "../../registry/schema";
import {
  canonicalX402Payment,
  decodeX402Payment,
  encodeX402Payment,
  type X402Challenge,
  type X402Payment,
  type X402PaymentSigner,
  type X402Receipt,
} from "../../sdk/v3/x402";

const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");

export class DeterministicClock {
  constructor(private value = 1_800_000_000_000) {}
  now = (): number => this.value;
  advance(milliseconds: number): void {
    this.value += milliseconds;
  }
}

export function deterministicSigner(seedByte: number): X402PaymentSigner {
  const seed = Buffer.alloc(32, seedByte);
  const privateKey = createPrivateKey({
    key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
    format: "der",
    type: "pkcs8",
  });
  const publicDer = createPublicKey(privateKey).export({ format: "der", type: "spki" }) as Buffer;
  const payer = `ed25519:${publicDer.subarray(-32).toString("base64url")}`;
  return {
    payer,
    sign(message: string): string {
      return ed25519Sign(null, Buffer.from(message, "utf8"), privateKey).toString("base64url");
    },
  };
}

function verifyPaymentSignature(payment: X402Payment): boolean {
  if (!payment.payer.startsWith("ed25519:")) return false;
  try {
    const rawKey = Buffer.from(payment.payer.slice("ed25519:".length), "base64url");
    if (rawKey.length !== 32) return false;
    const publicKey = createPublicKey({
      key: Buffer.concat([ED25519_SPKI_PREFIX, rawKey]),
      format: "der",
      type: "spki",
    });
    const { signature: _signature, ...unsigned } = payment;
    return ed25519Verify(
      null,
      Buffer.from(canonicalX402Payment(unsigned), "utf8"),
      publicKey,
      Buffer.from(payment.signature, "base64url"),
    );
  } catch {
    return false;
  }
}

export type StoredChallenge = X402Challenge & {
  route: RouteRegistryEntry;
  idempotencyKey?: string;
};

export type StoredResult = {
  data: Record<string, unknown>;
  receipt: X402Receipt;
};

type SettlementClaim =
  | { readonly accepted: true; readonly receipt: X402Receipt }
  | { readonly accepted: false };

/**
 * The small persistence boundary exercised by this sandbox. Its mutating
 * operations are synchronous so an in-memory implementation can model atomic
 * uniqueness only while `handle` does not yield. A real deployment needs a
 * durable store with equivalent transactional/conditional-write guarantees.
 */
export interface X402SandboxStore {
  nextRequestId(): string;
  getChallenge(requestId: string): StoredChallenge | undefined;
  saveChallenge(challenge: StoredChallenge): void;
  getFulfilled(requestId: string): StoredResult | undefined;
  getIdempotencyResult(idempotencyId: string): StoredResult | undefined;
  commitFulfillment(
    requestId: string,
    idempotencyId: string | undefined,
    create: (count: number) => StoredResult,
  ): StoredResult;
  claimSettlement(
    paymentId: string,
    requestId: string,
    create: (sequence: number) => X402Receipt,
  ): SettlementClaim;
  fulfillmentCount(requestId: string): number | undefined;
  settledPayments(): ReadonlyMap<string, X402Receipt>;
}

/** In-process state only; intentionally not a claim of crash/process durability. */
export class InMemoryX402SandboxStore implements X402SandboxStore {
  private nextRequest = 0;
  private readonly challenges = new Map<string, StoredChallenge>();
  private readonly fulfilled = new Map<string, StoredResult>();
  private readonly idempotencyResults = new Map<string, StoredResult>();
  private readonly fulfillmentCounts = new Map<string, number>();
  private readonly settlements = new Map<string, { requestId: string; receipt: X402Receipt }>();

  nextRequestId(): string {
    this.nextRequest += 1;
    return `request-${this.nextRequest.toString().padStart(6, "0")}`;
  }

  getChallenge(requestId: string): StoredChallenge | undefined {
    return this.challenges.get(requestId);
  }

  saveChallenge(challenge: StoredChallenge): void {
    this.challenges.set(challenge.requestId, challenge);
  }

  getFulfilled(requestId: string): StoredResult | undefined {
    return this.fulfilled.get(requestId);
  }

  getIdempotencyResult(idempotencyId: string): StoredResult | undefined {
    return this.idempotencyResults.get(idempotencyId);
  }

  commitFulfillment(
    requestId: string,
    idempotencyId: string | undefined,
    create: (count: number) => StoredResult,
  ): StoredResult {
    const fulfilled = this.fulfilled.get(requestId);
    if (fulfilled) return fulfilled;
    if (idempotencyId) {
      const idempotent = this.idempotencyResults.get(idempotencyId);
      if (idempotent) return idempotent;
    }
    const count = (this.fulfillmentCounts.get(requestId) ?? 0) + 1;
    const result = create(count);
    this.fulfillmentCounts.set(requestId, count);
    this.fulfilled.set(requestId, result);
    if (idempotencyId) this.idempotencyResults.set(idempotencyId, result);
    return result;
  }

  claimSettlement(
    paymentId: string,
    requestId: string,
    create: (sequence: number) => X402Receipt,
  ): SettlementClaim {
    const existing = this.settlements.get(paymentId);
    if (existing) {
      return existing.requestId === requestId
        ? { accepted: true, receipt: existing.receipt }
        : { accepted: false };
    }
    const receipt = create(this.settlements.size + 1);
    this.settlements.set(paymentId, { requestId, receipt });
    return { accepted: true, receipt };
  }

  fulfillmentCount(requestId: string): number | undefined {
    return this.fulfillmentCounts.get(requestId);
  }

  settledPayments(): ReadonlyMap<string, X402Receipt> {
    return new Map([...this.settlements].map(([paymentId, settled]) => [paymentId, settled.receipt]));
  }
}

export class FacilitatorTimeout extends Error {
  constructor() {
    super("facilitator timed out before settlement");
    this.name = "FacilitatorTimeout";
  }
}

export class DeterministicFacilitator {
  settleAttempts = 0;
  timeoutNext = false;
  currentFinality: "soft" | "safe" | "finalized" = "safe";

  constructor(private readonly store: X402SandboxStore = new InMemoryX402SandboxStore()) {}

  get settled(): ReadonlyMap<string, X402Receipt> {
    return this.store.settledPayments();
  }

  settle(payment: X402Payment, challenge: StoredChallenge): X402Receipt {
    this.settleAttempts += 1;
    if (this.timeoutNext) {
      this.timeoutNext = false;
      throw new FacilitatorTimeout();
    }
    if (finalityRank(this.currentFinality) < finalityRank(challenge.finality)) {
      throw new Error("finality_not_reached");
    }
    const settled = this.store.claimSettlement(payment.paymentId, payment.requestId, (sequence) => ({
      receiptId: `receipt-${sequence.toString().padStart(6, "0")}`,
      txHash: `0x${sequence.toString(16).padStart(64, "0")}`,
      blockNumber: 10_000 + sequence,
      chainId: chainIdFor(challenge.network),
      paymentId: payment.paymentId,
      requestId: payment.requestId,
      method: payment.method,
      path: payment.path,
      network: payment.network,
      asset: challenge.asset,
      amount: challenge.amount,
      payer: payment.payer,
      payee: challenge.payee,
      x402Version: challenge.x402Version,
      finality: challenge.finality,
    }));
    if (!settled.accepted) throw new Error("payment_id_reused");
    return settled.receipt;
  }
}

function finalityRank(value: X402Challenge["finality"]): number {
  return ({ soft: 0, safe: 1, finalized: 2 })[value];
}

function hasValidWindow(validAfter: unknown, expiresAt: unknown): validAfter is number {
  return (
    typeof validAfter === "number" &&
    Number.isSafeInteger(validAfter) &&
    typeof expiresAt === "number" &&
    Number.isSafeInteger(expiresAt) &&
    validAfter < expiresAt
  );
}

function chainIdFor(network: string): number {
  const match = /^eip155:([1-9]\d*)$/.exec(network);
  if (!match) throw new Error("unsupported_receipt_network");
  const chainId = Number(match[1]);
  if (!Number.isSafeInteger(chainId)) throw new Error("unsupported_receipt_network");
  return chainId;
}

function response(status: number, body: unknown, headers?: HeadersInit): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json", ...headers },
  });
}

/**
 * Facilitator-backed middleware. `fetch` exposes it through the standard Fetch
 * shape so it can also drive the SDK without a web server.
 */
export class X402MiddlewareSandbox {
  readonly facilitator: DeterministicFacilitator;
  dropNextSuccessfulResponse = false;

  constructor(
    private readonly routes: RouteRegistryEntry[],
    private readonly clock: DeterministicClock,
    private readonly allowedPayers: ReadonlySet<string>,
    readonly store: X402SandboxStore = new InMemoryX402SandboxStore(),
    private readonly options: { validAfterDelayMs?: number } = {},
  ) {
    this.facilitator = new DeterministicFacilitator(store);
  }

  async fetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
    const url = typeof input === "string"
      ? new URL(input, "https://sandbox.invalid")
      : input instanceof URL
        ? input
        : new URL(input.url);
    const request = {
      method: (init.method ?? (typeof input === "object" && "method" in input ? input.method : "GET")).toUpperCase(),
      path: url.pathname,
      headers: new Headers(init.headers ?? (typeof input === "object" && "headers" in input ? input.headers : undefined)),
    };
    const result = this.handle(request);
    if (this.dropNextSuccessfulResponse && result.status >= 200 && result.status < 300) {
      this.dropNextSuccessfulResponse = false;
      throw new Error("simulated HTTP delivery timeout after settlement");
    }
    return result;
  }

  handle(request: { method: string; path: string; headers?: Headers }): Response {
    const route = this.findRoute(request.method, request.path);
    if (!route || route.free || !route.price) return response(404, { code: "route_not_priced" });
    const headers = request.headers ?? new Headers();
    const idempotencyKey = headers.get("Idempotency-Key") ?? undefined;
    const idempotencyId = idempotencyKey ? `${route.method}:${request.path}:${idempotencyKey}` : undefined;
    if (idempotencyId) {
      const priorIdempotent = this.store.getIdempotencyResult(idempotencyId);
      if (priorIdempotent) return this.success(priorIdempotent);
    }
    if (route.idempotencyRule === "key-required" && !idempotencyKey) {
      return response(400, { code: "idempotency_key_required" });
    }

    const encoded = headers.get("X-Payment");
    if (!encoded) return this.challenge(route, request.method, request.path, idempotencyKey);

    let payment: X402Payment;
    try {
      payment = decodeX402Payment(encoded);
    } catch {
      return response(400, { code: "malformed_payment" });
    }
    const challenge = this.store.getChallenge(payment.requestId);
    if (!challenge) return response(400, { code: "unknown_or_replayed_request" });
    if (!this.allowedPayers.has(payment.payer)) return response(401, { code: "wrong_signer" });
    if (!verifyPaymentSignature(payment)) return response(401, { code: "invalid_signature" });
    if (!hasValidWindow(challenge.validAfter, challenge.expiresAt)) {
      return response(400, { code: "invalid_payment_window" });
    }
    const expected = challenge;
    if (
      payment.x402Version !== expected.x402Version ||
      payment.method !== request.method ||
      payment.path !== request.path ||
      payment.method !== expected.method ||
      payment.path !== expected.path ||
      payment.amount !== expected.amount ||
      payment.asset !== expected.asset ||
      payment.network !== expected.network ||
      payment.payee !== expected.payee ||
      payment.validAfter !== expected.validAfter ||
      payment.expiresAt !== expected.expiresAt ||
      expected.idempotencyKey !== idempotencyKey
    ) {
      return response(400, { code: "payment_request_binding_mismatch" });
    }
    if (!hasValidWindow(payment.validAfter, payment.expiresAt)) {
      return response(400, { code: "invalid_payment_window" });
    }
    const now = this.clock.now();
    if (now < challenge.validAfter || now < payment.validAfter) {
      return response(400, { code: "payment_not_yet_valid" });
    }
    if (now >= challenge.expiresAt || now >= payment.expiresAt) {
      return response(400, { code: "expired_payment" });
    }
    // A delivery retry is safe only after all current-request bindings have
    // been rechecked. This must come after the check above so a settled payment
    // cannot be replayed onto another path merely because it has a receipt.
    const prior = this.store.getFulfilled(payment.requestId);
    if (prior) return this.success(prior);

    let receipt: X402Receipt;
    try {
      receipt = this.facilitator.settle(payment, challenge);
    } catch (error) {
      if (error instanceof FacilitatorTimeout) return response(503, { code: "facilitator_timeout" });
      if (error instanceof Error && error.message === "finality_not_reached") {
        return response(425, { code: "finality_not_reached" });
      }
      if (error instanceof Error && error.message === "payment_id_reused") {
        return response(409, { code: "payment_id_reused" });
      }
      if (error instanceof Error && error.message === "unsupported_receipt_network") {
        return response(400, { code: "unsupported_receipt_network" });
      }
      throw error;
    }
    const result = this.store.commitFulfillment(expected.requestId, idempotencyId, (count) => ({
      data: { ok: true, routeId: route.id, fulfillments: count },
      receipt,
    }));
    return this.success(result);
  }

  paymentFor(challenge: X402Challenge, signer: X402PaymentSigner, paymentId = "payment-001"): X402Payment {
    const unsigned = {
      x402Version: challenge.x402Version,
      paymentId,
      requestId: challenge.requestId,
      method: challenge.method,
      path: challenge.path,
      amount: challenge.amount,
      asset: challenge.asset,
      network: challenge.network,
      payee: challenge.payee,
      validAfter: challenge.validAfter,
      expiresAt: challenge.expiresAt,
      payer: signer.payer,
    };
    const signature = signer.sign(canonicalX402Payment(unsigned));
    if (typeof signature !== "string") throw new Error("sandbox signer must be synchronous");
    return { ...unsigned, signature };
  }

  challengeFrom(result: Response): Promise<X402Challenge> {
    return result.json().then((body: { x402: X402Challenge }) => body.x402);
  }

  private challenge(
    route: RouteRegistryEntry,
    method: string,
    path: string,
    idempotencyKey?: string,
  ): Response {
    const validAfter = this.clock.now() + (this.options.validAfterDelayMs ?? 0);
    const challenge: StoredChallenge = {
      x402Version: route.price!.x402Version,
      requestId: this.store.nextRequestId(),
      method,
      path,
      amount: route.price!.amount,
      asset: route.price!.asset,
      network: route.price!.network,
      payee: route.price!.payee,
      validAfter,
      expiresAt: this.clock.now() + 60_000,
      finality: route.finality,
      route,
      idempotencyKey,
    };
    this.store.saveChallenge(challenge);
    return response(402, { error: "payment_required", x402: challenge });
  }

  private success(result: StoredResult): Response {
    return response(200, { data: result.data, receipt: result.receipt }, {
      "X-Payment-Receipt": Buffer.from(JSON.stringify(result.receipt)).toString("base64url"),
    });
  }

  private findRoute(method: string, path: string): RouteRegistryEntry | undefined {
    return this.routes.find((route) =>
      route.method === method &&
      new RegExp(`^${route.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[^/]+/g, "[^/]+")}$`).test(path),
    );
  }
}

export function materializePath(path: string): string {
  return path.replace(/:[^/]+/g, "deterministic-value");
}

export function paymentHeader(payment: X402Payment): Headers {
  return new Headers({ "X-Payment": encodeX402Payment(payment) });
}
