/**
 * RailGuard — TypeScript guardrail SDK (Node 18+, Deno, Bun, Workers).
 *
 *   const guardrail = new Guardrail({ apiKey: "rg_...", baseUrl: "https://railguardsecurity.com" });
 *   try {
 *     const payment = await guardrail.spend({
 *       amount: 45, merchant: "OpenAI API", category: "AI & Compute",
 *       reasoning: "I need to process 500 PDF invoices uploaded by the user.",
 *       promptChain: messages, rail: "stripe",
 *     });
 *     use(payment.credential);
 *   } catch (e) {
 *     if (e instanceof SpendBlocked) stopOrTryAlternative(e.reason);
 *   }
 *
 * Flagged spend waits for a human by default; pass wait: false plus webhookUrl to return immediately.
 */

export type Rail = "stripe" | "ramp" | "crypto";

export interface SpendRequest {
  amount: number;
  merchant: string;
  category?: string;
  reasoning?: string;
  promptChain?: { role: string; content: string }[];
  rail?: Rail;
  justification?: string;
  recipientAddress?: string;
  webhookUrl?: string;
  /** Reuse on retries: same key + same body never issues a second card. */
  idempotencyKey?: string;
  wait?: boolean;
  waitTimeoutMs?: number;
  pollIntervalMs?: number;
}

export interface SpendResult {
  status: "approved" | "flagged";
  transaction_id: string;
  status_detail?: string;
  credential?: Record<string, unknown>;
  detail?: string;
}

export class SpendBlocked extends Error {
  constructor(public reason: string, public code: string, public response: unknown) {
    super(`${code}: ${reason}`);
  }
}
export class SpendPending extends Error {}

export class Guardrail {
  private apiKey: string;
  private baseUrl: string;
  constructor(opts: { apiKey: string; baseUrl: string }) {
    this.apiKey = opts.apiKey;
    this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
  }

  private headers() {
    return { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" };
  }

  async spend(req: SpendRequest): Promise<SpendResult> {
    const res = await fetch(`${this.baseUrl}/api/public/transactions`, {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify({
        amount: req.amount,
        merchant: req.merchant,
        category: req.category ?? "General",
        rail: req.rail ?? "stripe",
        reasoning: req.reasoning ?? "",
        justification: req.justification ?? (req.reasoning ?? "").slice(0, 1000),
        prompt_chain: req.promptChain,
        recipient_address: req.recipientAddress,
        webhook_url: req.webhookUrl,
        idempotency_key: req.idempotencyKey,
      }),
    });
    const data = await res.json();
    if (res.status === 409) throw new SpendBlocked(data.instruction ?? data.error, data.error_code ?? "IDEMPOTENCY_MISMATCH", data);
    if (res.status === 429) throw new SpendPending("Rate limited; retry after 60s");
    if ([400, 401, 404, 500].includes(res.status)) throw new Error(data.error ?? `HTTP ${res.status}`);
    return this.resolve(data, req.wait ?? true, req.waitTimeoutMs ?? 3_600_000, req.pollIntervalMs ?? 5_000);
  }

  async poll(transactionId: string): Promise<any> {
    const res = await fetch(`${this.baseUrl}/api/public/transactions/${transactionId}`, { headers: this.headers() });
    return res.json();
  }

  /** After paying, report the final charge so the ledger can verify it matches the approval. */
  async reportReceipt(
    transactionId: string,
    r: { amount: number; merchant?: string; recipient_address?: string; receipt_ref?: string; receipt_url?: string },
  ): Promise<any> {
    const res = await fetch(`${this.baseUrl}/api/public/transactions/${transactionId}/receipt`, {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify(r),
    });
    return res.json();
  }

  /** Log-only mode: record a payment the agent already made. */
  async logPayment(p: { amount: number; merchant: string; category?: string; reasoning?: string; receiptRef?: string; recipientAddress?: string }): Promise<any> {
    const res = await fetch(`${this.baseUrl}/api/public/transactions`, {
      method: "POST",
      headers: this.headers(),
      body: JSON.stringify({
        mode: "report",
        amount: p.amount,
        merchant: p.merchant,
        category: p.category ?? "General",
        reasoning: p.reasoning ?? "",
        recipient_address: p.recipientAddress,
        receipt: { amount: p.amount, merchant: p.merchant, receipt_ref: p.receiptRef, recipient_address: p.recipientAddress },
      }),
    });
    return res.json();
  }

  private async resolve(data: any, wait: boolean, timeoutMs: number, intervalMs: number): Promise<SpendResult> {
    if (data.status === "blocked" || data.status === "rejected")
      throw new SpendBlocked(data.reason ?? "", data.error_code ?? data.status.toUpperCase(), data);
    if (data.status === "flagged" && wait) {
      const deadline = Date.now() + timeoutMs;
      while (Date.now() < deadline) {
        await new Promise((r) => setTimeout(r, intervalMs));
        const next = await this.poll(data.transaction_id);
        if (next.status !== "flagged") return this.resolve(next, false, 0, 0);
      }
      throw new SpendPending(`No human decision in time for ${data.transaction_id}`);
    }
    const payment = data.payment ?? {};
    if (data.status === "approved" && payment.status && payment.status !== "issued")
      throw new Error(`Approved but payment ${payment.status}: ${payment.detail}`);
    return { status: data.status, transaction_id: data.transaction_id, ...payment };
  }

  /** Verify the x-railguard-signature webhook header. Key = hex sha256(apiKey). */
  async verifyWebhook(rawBody: string, timestamp: string, signature: string, toleranceSec = 300): Promise<boolean> {
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) return false;
    const enc = new TextEncoder();
    const keyHex = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", enc.encode(this.apiKey))), (b) =>
      b.toString(16).padStart(2, "0"),
    ).join("");
    const key = await crypto.subtle.importKey("raw", enc.encode(keyHex), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
    const sig = await crypto.subtle.sign("HMAC", key, enc.encode(`${timestamp}.${rawBody}`));
    const expected = Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("");
    if (expected.length !== signature.length) return false;
    let diff = 0;
    for (let i = 0; i < expected.length; i++) diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
    return diff === 0;
  }
}
