// Provider-agnostic webhook audit log. Every inbound provider webhook (Xendit
// today; others later) gets a row in `webhook_logs` BEFORE we run any auth or
// business logic, so a forensic record survives rejection, malformed bodies,
// and processing exceptions.
//
// Primary fields are parsed as columns (queryable / indexed); the entire body
// stays in `raw_body` JSONB verbatim so we can replay or diff later. Outcome
// columns are stamped by the route after processing.
//
// SECONDARY SINK — when the DB insert itself fails (DB outage, schema drift),
// an optional file-based sink writes one JSONL line per event to a daily
// rolling file. Disabled by default; opt in via env. We deliberately do NOT
// fall back to stdout — operators who want survival pick the file sink.
//
// This module exposes only what the route needs:
//   sanitizeHeaders(headers)               → header dict with secrets redacted
//   insertWebhookLog({...})                → returns inserted row id
//   updateWebhookLog(id, {...})            → stamps outcome columns
//   writeWebhookFallback({...})            → optional file sink on DB failure
//   getWebhookLog(id) [test/CC use]        → row or null

import { appendFile, mkdir } from 'node:fs/promises'
import path from 'node:path'
import { getDb } from '../db/client.js'

const sql = getDb()

// Header values we never persist. We still keep the *names* so callers can see
// which auth scheme the request used — only the secret material is dropped.
const SECRET_HEADER_NAMES = new Set([
  'x-callback-token',
  'authorization',
  'cookie',
  'set-cookie',
  'proxy-authorization',
])

export const sanitizeHeaders = (headers) => {
  const out = {}
  for (const [k, v] of Object.entries(headers ?? {})) {
    const key = k.toLowerCase()
    out[key] = SECRET_HEADER_NAMES.has(key) ? '[REDACTED]' : v
  }
  return out
}

const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const isUuid = (s) => typeof s === 'string' && UUID_RE.test(s)
const str = (v) => (typeof v === 'string' ? v : null)
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null)

const parseTimestamp = (v) => {
  if (typeof v !== 'string') return null
  const d = new Date(v)
  return Number.isFinite(d.getTime()) ? d.toISOString() : null
}

/**
 * Insert the survival row. Throws only if the DB itself is unreachable — the
 * caller wraps this in try/catch and falls through to a structured-log
 * fallback so a misbehaving DB never causes Xendit to retry forever.
 */
export const insertWebhookLog = async ({ provider, headers, rawBody, callbackTokenValid }) => {
  const body = rawBody ?? {}
  const externalId = str(body.external_id)
  const paymentRequestId = isUuid(externalId) ? externalId : null
  const [row] = await sql`
    INSERT INTO webhook_logs (
      provider, headers, raw_body, callback_token_valid,
      xendit_event_id, external_id, payment_request_id,
      status, amount, currency, payment_method, paid_at, metadata_app
    ) VALUES (
      ${provider},
      ${sql.json(headers ?? {})},
      ${sql.json(body)},
      ${callbackTokenValid},
      ${str(body.id)},
      ${externalId},
      ${paymentRequestId},
      ${str(body.status)},
      ${num(body.amount)},
      ${str(body.currency)},
      ${str(body.payment_method)},
      ${parseTimestamp(body.paid_at)},
      ${str(body?.metadata?.app)}
    )
    RETURNING id
  `
  return row.id
}

/**
 * Stamp outcome columns once the handler has decided a response. Wrapped by
 * the caller in its own try/catch so a failed update never replaces the
 * original processing error.
 */
export const updateWebhookLog = async (logId, { httpStatus, processingResult, processingError }) => {
  await sql`
    UPDATE webhook_logs
       SET http_status       = ${httpStatus ?? null},
           processing_result = ${processingResult ?? null},
           processing_error  = ${processingError ?? null},
           processed_at      = NOW()
     WHERE id = ${logId}
  `
}

export const getWebhookLog = async (logId) => {
  const rows = await sql`SELECT * FROM webhook_logs WHERE id = ${logId}`
  return rows[0] ?? null
}

// --- Secondary sink: rolling JSONL file --------------------------------------
//
// Only used when the DB insert above fails. Env-controlled so production can
// opt in (e.g. mount a persistent volume + tail with logrotate) without
// affecting dev/test machines. By default the sink is OFF — the handler will
// silently skip it and rely on the DB as the sole audit trail.
//
// Config (xendit_ prefix groups these with the other XENDIT_* vars):
//   XENDIT_WEBHOOK_FALLBACK_ENABLED  — "true" to enable; anything else = off (default off)
//   XENDIT_WEBHOOK_FALLBACK_DIR      — directory the JSONL files live in (default "./logs")
//   XENDIT_WEBHOOK_FALLBACK_NAME     — basename for rolling files (default "xendit-webhook-fallback")
//
// Rolling pattern: one file per UTC day, named "<NAME>-YYYY-MM-DD.jsonl".
// Old files accumulate — pair with logrotate / GCS sync if you need retention
// limits. We don't size-rotate inside Node because the fallback only fires on
// DB outage, which should be rare enough that daily granularity is plenty.

export const getWebhookFallbackConfig = () => ({
  enabled: process.env.XENDIT_WEBHOOK_FALLBACK_ENABLED === 'true',
  dir: process.env.XENDIT_WEBHOOK_FALLBACK_DIR || './logs',
  name: process.env.XENDIT_WEBHOOK_FALLBACK_NAME || 'xendit-webhook-fallback',
})

export const rollingFallbackFilename = (name, now = new Date()) => {
  const yyyy = now.getUTCFullYear()
  const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
  const dd = String(now.getUTCDate()).padStart(2, '0')
  return `${name}-${yyyy}-${mm}-${dd}.jsonl`
}

/**
 * Append one JSONL line to the rolling fallback file. Returns true if the
 * write happened, false if the sink is disabled. Throws on real I/O errors —
 * the caller decides whether to swallow.
 */
export const writeWebhookFallback = async ({ provider, headers, rawBody, callbackTokenValid, dbErrorMessage }) => {
  const cfg = getWebhookFallbackConfig()
  if (!cfg.enabled) return false

  const filename = rollingFallbackFilename(cfg.name)
  const filepath = path.join(cfg.dir, filename)
  const line = JSON.stringify({
    received_at: new Date().toISOString(),
    provider,
    callback_token_valid: callbackTokenValid,
    headers,
    raw_body: rawBody,
    db_error: dbErrorMessage ?? null,
  }) + '\n'

  await mkdir(cfg.dir, { recursive: true })
  await appendFile(filepath, line, 'utf8')
  return true
}
