/**
 * Payer-attribution persistence layer.
 *
 * Records every paid x402 call to a small ring buffer (LIST + TRIM) in
 * Upstash Redis when configured, so we can answer "who hit which endpoint
 * recently" without trawling Vercel function logs.
 *
 * DESIGN PROMISES:
 *   1. ADDITIVE ONLY — never blocks the request. All persistence is
 *      fire-and-forget. If Upstash is down or env is missing, the call
 *      becomes a no-op and the request proceeds normally.
 *   2. NO PII — only public on-chain addresses, paths, prices, timestamps.
 *      Same data already in Vercel function logs.
 *   3. NO LEAKED KEYS — credentials read from env only:
 *        KV_REST_API_URL          (Vercel KV — primary)
 *        KV_REST_API_TOKEN        (Vercel KV — primary)
 *        UPSTASH_REDIS_REST_URL   (Upstash direct — fallback for local dev)
 *        UPSTASH_REDIS_REST_TOKEN (Upstash direct — fallback for local dev)
 *      If neither pair is present we silently skip (this is also the test path).
 *      Vercel KV is just a managed Upstash instance, so the REST API is identical.
 *
 * Storage shape:
 *   LIST `vaultfire:payers:recent`     last 1000 paid calls (LPUSH + LTRIM)
 *   HASH `vaultfire:payers:by-payer`   payer-address → JSON of last entry
 *   HASH `vaultfire:payers:by-path`    path → count of paid hits
 *
 * Read it back via GET /api/admin/recent-payers (token-gated).
 */

const RECENT_KEY = 'vaultfire:payers:recent';
const RECENT_MAX = 1000;
const BY_PAYER_KEY = 'vaultfire:payers:by-payer';
const BY_PATH_KEY = 'vaultfire:payers:by-path';
const LIFETIME_CALLS_KEY = 'vaultfire:payers:lifetime-calls';
const LIFETIME_USDC_MICRO_KEY = 'vaultfire:payers:lifetime-usdc-micro';
const HEARTBEAT_KEY = 'vaultfire:health:heartbeat';

export interface PayerEntry {
  ts: number;
  path: string;
  payer: string;
  price: string;
  /** Price in micro-USDC (1 USDC = 1_000_000). Used for accurate lifetime totals. */
  priceMicro: number;
  chain?: string;
  status: number;
  ms: number;
  source: 'action' | 'multinet';
}

/**
 * Convert a price string like '$0.01' or '0.05 USDC' to micro-USDC.
 * Returns 0 on parse failure (so a malformed price never breaks the recorder).
 */
export function priceToMicro(price: string): number {
  if (!price) return 0;
  const cleaned = price.replace(/[$,\s]/g, '').replace(/USDC/i, '');
  const usd = parseFloat(cleaned);
  if (!Number.isFinite(usd) || usd < 0) return 0;
  return Math.round(usd * 1_000_000);
}

function getCreds(): { url: string; token: string } | null {
  // Prefer Vercel KV env vars (auto-injected when KV is connected to the project).
  // Fall back to direct Upstash env vars (useful for local dev or BYO Redis).
  const url = process.env.KV_REST_API_URL || process.env.UPSTASH_REDIS_REST_URL;
  const token = process.env.KV_REST_API_TOKEN || process.env.UPSTASH_REDIS_REST_TOKEN;
  if (!url || !token) return null;
  return { url: url.replace(/\/$/, ''), token };
}

/**
 * Run a Redis command via Upstash REST API. Returns null on any error or
 * missing creds — never throws.
 *
 * Pipeline format: array of arrays, e.g. [['LPUSH','k','v'], ['LTRIM','k','0','999']]
 */
async function pipeline(commands: Array<Array<string>>): Promise<unknown[] | null> {
  const creds = getCreds();
  if (!creds) return null;
  try {
    const res = await fetch(`${creds.url}/pipeline`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${creds.token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(commands),
      // Cap latency so a slow Upstash never stalls the request.
      signal: AbortSignal.timeout(1500),
    });
    if (!res.ok) return null;
    const data = await res.json();
    return Array.isArray(data) ? data : null;
  } catch {
    return null;
  }
}

/**
 * Record one paid call. Fire-and-forget — caller does not await.
 *
 * Safe to call from anywhere; if KV isn't configured this is a no-op.
 *
 * Self-heartbeat: every paid call also bumps the heartbeat key. This means
 * the /live freshness badge stays green as long as paid traffic is flowing,
 * without depending on Vercel cron (which is daily-only on the Hobby plan).
 * If the badge goes stale, that's an honest signal — either the recorder
 * broke OR there's been no paid traffic for >2 min. Both worth surfacing.
 */
export function recordPaidCall(entry: PayerEntry): void {
  // Fire and forget — never block the response.
  void (async () => {
    const json = JSON.stringify(entry);
    await pipeline([
      ['LPUSH', RECENT_KEY, json],
      ['LTRIM', RECENT_KEY, '0', String(RECENT_MAX - 1)],
      ['HSET', BY_PAYER_KEY, entry.payer, json],
      ['HINCRBY', BY_PATH_KEY, entry.path, '1'],
      ['INCR', LIFETIME_CALLS_KEY],
      ['INCRBY', LIFETIME_USDC_MICRO_KEY, String(entry.priceMicro || 0)],
      // Self-heartbeat — piggybacks on every paid call so the /live freshness
      // badge stays accurate even without an external cron.
      ['SET', HEARTBEAT_KEY, String(entry.ts || Date.now())],
    ]);
  })();
}

/**
 * Read the most recent N paid calls. Returns [] if Upstash is not configured
 * or the read fails — caller should treat empty as "no data available".
 */
export async function getRecentPaidCalls(limit = 100): Promise<PayerEntry[]> {
  const creds = getCreds();
  if (!creds) return [];
  try {
    const res = await fetch(`${creds.url}/lrange/${RECENT_KEY}/0/${limit - 1}`, {
      method: 'GET',
      headers: { Authorization: `Bearer ${creds.token}` },
      signal: AbortSignal.timeout(2000),
    });
    if (!res.ok) return [];
    const data = await res.json();
    const items: unknown = data?.result;
    if (!Array.isArray(items)) return [];
    const out: PayerEntry[] = [];
    for (const item of items) {
      try {
        if (typeof item === 'string') out.push(JSON.parse(item) as PayerEntry);
      } catch {
        // skip malformed
      }
    }
    return out;
  } catch {
    return [];
  }
}

/**
 * Per-path hit counts (since persistence began).
 */
export async function getPathCounts(): Promise<Record<string, number>> {
  const creds = getCreds();
  if (!creds) return {};
  try {
    const res = await fetch(`${creds.url}/hgetall/${BY_PATH_KEY}`, {
      method: 'GET',
      headers: { Authorization: `Bearer ${creds.token}` },
      signal: AbortSignal.timeout(2000),
    });
    if (!res.ok) return {};
    const data = await res.json();
    const flat = data?.result;
    if (!Array.isArray(flat)) return {};
    const out: Record<string, number> = {};
    for (let i = 0; i < flat.length; i += 2) {
      const k = flat[i];
      const v = flat[i + 1];
      if (typeof k === 'string' && typeof v === 'string') {
        const n = parseInt(v, 10);
        if (Number.isFinite(n)) out[k] = n;
      }
    }
    return out;
  } catch {
    return {};
  }
}

/**
 * Lifetime totals: total paid calls + total USDC settled.
 * Returns zeros if KV is not configured.
 */
export async function getLifetimeTotals(): Promise<{ calls: number; usdcMicro: number }> {
  const creds = getCreds();
  if (!creds) return { calls: 0, usdcMicro: 0 };
  const result = await pipeline([
    ['GET', LIFETIME_CALLS_KEY],
    ['GET', LIFETIME_USDC_MICRO_KEY],
  ]);
  if (!result) return { calls: 0, usdcMicro: 0 };
  // Upstash returns [{result: "123"}, {result: "456789"}]
  const parse = (r: unknown): number => {
    if (r && typeof r === 'object' && 'result' in r) {
      const v = (r as { result: unknown }).result;
      if (typeof v === 'string') {
        const n = parseInt(v, 10);
        return Number.isFinite(n) ? n : 0;
      }
      if (typeof v === 'number') return v;
    }
    return 0;
  };
  return { calls: parse(result[0]), usdcMicro: parse(result[1]) };
}

/**
 * Heartbeat write — records the current epoch ms to a known key.
 * Called by the cron route every minute.
 */
export async function writeHeartbeat(): Promise<boolean> {
  const creds = getCreds();
  if (!creds) return false;
  const result = await pipeline([['SET', HEARTBEAT_KEY, String(Date.now())]]);
  return result !== null;
}

/**
 * Heartbeat read — returns last successful heartbeat write timestamp.
 * Returns null if KV is not configured or never written.
 */
export async function readHeartbeat(): Promise<number | null> {
  const creds = getCreds();
  if (!creds) return null;
  try {
    const res = await fetch(`${creds.url}/get/${HEARTBEAT_KEY}`, {
      method: 'GET',
      headers: { Authorization: `Bearer ${creds.token}` },
      signal: AbortSignal.timeout(2000),
    });
    if (!res.ok) return null;
    const data = await res.json();
    const v = data?.result;
    if (typeof v === 'string') {
      const n = parseInt(v, 10);
      return Number.isFinite(n) ? n : null;
    }
    return null;
  } catch {
    return null;
  }
}

/**
 * True when KV creds are present (informational only — read APIs above
 * already handle the unconfigured case gracefully).
 */
export function isPersistenceConfigured(): boolean {
  return getCreds() !== null;
}
