// Hotmart Postback webhook receiver.
//
// Returns { status, body } and is wired into index.js' http.createServer
// dispatcher at POST /webhooks/hotmart.
//
// Handles BOTH legacy form-encoded Postback (v1) and modern Postback v2 JSON
// payloads — Hotmart's docs show v2 JSON and ECO-5 lists v2 fields, but
// older webhook configurations still send form-urlencoded so we tolerate
// both for robustness.
//
// Verification: matches `hottok` from payload against HOTMART_HOTTOK env var.
// When the env var is unset (e.g. before Monday's Hotmart credential drop),
// the handler returns 503 so events are NOT silently processed with no
// auth — this is intentional safety, not a bug.
//
// Idempotency: every accepted transaction id is recorded in
// data/hotmart/seen-transactions.json — replays are a 200 OK no-op.
//
// Audit trail: every request (accepted or rejected) is appended to
// data/hotmart/webhook-log.jsonl for a 60-day retention window matching
// Hotmart's own retention policy.
//
// Linear: ECO-5, related: ECO-6, ECO-10, ECO-24

import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
import { join } from 'path';
import { startSilverFromHotmart, pauseSilverFromHotmart } from './silverCourse.js';
import { nowIso } from './clock.js';

// ── Payload normalization ───────────────────────────────────────────────────

// Normalises both Hotmart Postback v1 (form-encoded flat) and v2 (JSON
// with nested data) into a single shape used downstream.
export function normalizePayload(payload) {
  // v2 (JSON nested)
  if (payload && payload.data && payload.event) {
    const d = payload.data;
    const purchase = d.purchase || {};
    const buyer = d.buyer || {};
    const product = d.product || {};
    const subscription = d.subscription || {};
    const offer = (purchase.offer && (purchase.offer.code || purchase.offer.key)) || d.offer?.code || null;
    return {
      version: 'v2',
      event: payload.event,
      hottok: payload.hottok || payload.hottok_token || null,
      transaction: purchase.transaction || null,
      status: purchase.status ? String(purchase.status).toLowerCase() : eventToStatus(payload.event),
      productId: product.id ? String(product.id) : null,
      productName: product.name || null,
      offerId: offer ? String(offer) : null,
      buyerName: buyer.name || null,
      buyerEmail: buyer.email || null,
      buyerPhone: normalizePhone(buyer.checkout_phone || buyer.phone),
      buyerDoc: buyer.document || buyer.doc || null,
      subscriberCode: subscription.subscriber?.code || null,
      subscriptionStatus: subscription.status || null,
      fullPrice: purchase.price?.value || null,
      currency: purchase.price?.currency_value || null,
      raw: payload,
    };
  }
  // v1 (form-encoded flat)
  return {
    version: 'v1',
    event: null,
    hottok: payload.hottok || null,
    transaction: payload.transaction || null,
    status: payload.status ? String(payload.status).toLowerCase() : null,
    productId: payload.prod ? String(payload.prod) : null,
    productName: payload.prod_name || null,
    offerId: payload.off ? String(payload.off) : null,
    buyerName: payload.name || null,
    buyerEmail: payload.email || null,
    buyerPhone: normalizePhone(payload.phone),
    buyerDoc: payload.doc || null,
    subscriberCode: payload.subscriber_code || null,
    subscriptionStatus: payload.subscription_status || null,
    fullPrice: payload.full_price ? Number(payload.full_price) : null,
    currency: payload.currency || null,
    raw: payload,
  };
}

function eventToStatus(event) {
  if (!event) return null;
  const ev = String(event).toUpperCase();
  if (ev.includes('APPROVED')) return 'approved';
  if (ev.includes('REFUNDED')) return 'refunded';
  if (ev.includes('CHARGEBACK')) return 'chargeback';
  if (ev.includes('CANCELED') || ev.includes('CANCELLED')) return 'canceled';
  if (ev.includes('EXPIRED')) return 'expired';
  if (ev.includes('DELAYED')) return 'delayed';
  if (ev.includes('COMPLETED')) return 'completed';
  if (ev.includes('DISPUTE')) return 'dispute';
  if (ev.includes('BILLET')) return 'billet_printed';
  if (ev.includes('BLOCKED')) return 'blocked';
  return null;
}

function normalizePhone(raw) {
  if (!raw) return null;
  const digits = String(raw).replace(/\D/g, '');
  return digits.length >= 8 ? digits : null;
}

// ── Body parsing (form-urlencoded vs JSON) ──────────────────────────────────

export function parseBody(rawBody, contentType = '') {
  const ct = String(contentType || '').toLowerCase();
  const text = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : String(rawBody || '');
  if (!text) return {};
  if (ct.includes('application/json') || text.trim().startsWith('{')) {
    try { return JSON.parse(text); } catch { return null; }
  }
  // Default: form-urlencoded (Hotmart Postback v1)
  const out = {};
  for (const part of text.split('&')) {
    if (!part) continue;
    const eq = part.indexOf('=');
    const k = decodeURIComponent((eq === -1 ? part : part.slice(0, eq)).replace(/\+/g, ' '));
    const v = eq === -1 ? '' : decodeURIComponent(part.slice(eq + 1).replace(/\+/g, ' '));
    out[k] = v;
  }
  return out;
}

// ── Idempotency log ─────────────────────────────────────────────────────────

function seenPath(scriptDir) {
  return join(scriptDir, 'data', 'hotmart', 'seen-transactions.json');
}

function logPath(scriptDir) {
  return join(scriptDir, 'data', 'hotmart', 'webhook-log.jsonl');
}

export function loadSeenTransactions(scriptDir) {
  const p = seenPath(scriptDir);
  if (!existsSync(p)) return {};
  try { return JSON.parse(readFileSync(p, 'utf8')); }
  catch { return {}; }
}

function saveSeenTransactions(scriptDir, seen) {
  const p = seenPath(scriptDir);
  mkdirSync(join(scriptDir, 'data', 'hotmart'), { recursive: true });
  writeFileSync(p, JSON.stringify(seen, null, 2));
}

export function isSeen(scriptDir, transactionId, status) {
  if (!transactionId) return false;
  const seen = loadSeenTransactions(scriptDir);
  // We key by transaction+status so the same purchase can flow through
  // approved → refunded → ... without each later event being suppressed.
  return !!seen[`${transactionId}:${status}`];
}

export function markSeen(scriptDir, transactionId, status) {
  if (!transactionId) return;
  const seen = loadSeenTransactions(scriptDir);
  seen[`${transactionId}:${status}`] = nowIso();
  saveSeenTransactions(scriptDir, seen);
}

export function auditLog(scriptDir, entry) {
  mkdirSync(join(scriptDir, 'data', 'hotmart'), { recursive: true });
  appendFileSync(logPath(scriptDir), JSON.stringify({ at: nowIso(), ...entry }) + '\n');
}

// ── Verification ────────────────────────────────────────────────────────────

export function verifyHottok(receivedHottok, expectedHottok) {
  if (!expectedHottok) return false; // no env = reject (safe default)
  if (!receivedHottok) return false;
  // Hotmart sends `hottok` as plain string in the body. Constant-time compare.
  if (receivedHottok.length !== expectedHottok.length) return false;
  let result = 0;
  for (let i = 0; i < receivedHottok.length; i += 1) {
    result |= receivedHottok.charCodeAt(i) ^ expectedHottok.charCodeAt(i);
  }
  return result === 0;
}

// ── Slug derivation ─────────────────────────────────────────────────────────

// Mirrors the slug shape used by the existing report.js / silverStore.js
// pattern: phone-only, prefixed with `silver-` to keep Silver mentees in a
// distinct namespace from Gold leads.
export function phoneToSlug(phone) {
  const digits = normalizePhone(phone);
  if (!digits) return null;
  return `silver-${digits}`;
}

// ── Main handler ────────────────────────────────────────────────────────────

const PAUSE_STATUSES = new Set(['refunded', 'chargeback', 'canceled', 'expired', 'blocked']);
const ACCEPT_STATUSES = new Set(['approved', 'completed']);
// `delayed`, `billet_printed`, `dispute` are noted but don't change state.

export async function handleHotmartWebhook(scriptDir, { rawBody, headers }) {
  const expected = process.env.HOTMART_HOTTOK;
  const ct = headers['content-type'] || headers['Content-Type'] || '';

  // Hard fail before any env config exists. Avoids accidentally onboarding
  // mentees during pre-launch testing if a curl is fired at the endpoint.
  if (!expected) {
    auditLog(scriptDir, { phase: 'rejected', reason: 'webhook-not-configured' });
    return { status: 503, body: { error: 'webhook not configured' } };
  }

  const payload = parseBody(rawBody, ct);
  if (!payload || typeof payload !== 'object') {
    auditLog(scriptDir, { phase: 'rejected', reason: 'bad-payload', ct });
    return { status: 400, body: { error: 'bad-payload' } };
  }

  const event = normalizePayload(payload);

  if (!verifyHottok(event.hottok, expected)) {
    auditLog(scriptDir, { phase: 'rejected', reason: 'bad-hottok', transaction: event.transaction });
    return { status: 401, body: { error: 'bad-hottok' } };
  }

  if (!event.transaction || !event.status) {
    auditLog(scriptDir, { phase: 'rejected', reason: 'missing-fields', event });
    return { status: 400, body: { error: 'missing-required-fields' } };
  }

  if (isSeen(scriptDir, event.transaction, event.status)) {
    auditLog(scriptDir, { phase: 'duplicate', transaction: event.transaction, status: event.status });
    return { status: 200, body: { ok: true, duplicate: true, transaction: event.transaction } };
  }

  // Status routing
  try {
    if (ACCEPT_STATUSES.has(event.status)) {
      const slug = phoneToSlug(event.buyerPhone);
      if (!slug) {
        auditLog(scriptDir, { phase: 'rejected', reason: 'no-phone', transaction: event.transaction });
        return { status: 400, body: { error: 'no-buyer-phone' } };
      }
      const result = await startSilverFromHotmart(scriptDir, { slug, event });
      markSeen(scriptDir, event.transaction, event.status);
      auditLog(scriptDir, { phase: 'accepted', action: 'start', transaction: event.transaction, slug, result });
      return { status: 200, body: { ok: true, action: 'start', slug } };
    }

    if (PAUSE_STATUSES.has(event.status)) {
      const slug = phoneToSlug(event.buyerPhone);
      if (!slug) {
        // No phone? still mark seen so we don't retry forever; log for triage
        markSeen(scriptDir, event.transaction, event.status);
        auditLog(scriptDir, { phase: 'accepted', action: 'pause-skipped-no-phone', transaction: event.transaction });
        return { status: 200, body: { ok: true, action: 'noop' } };
      }
      const result = await pauseSilverFromHotmart(scriptDir, { slug, event });
      markSeen(scriptDir, event.transaction, event.status);
      auditLog(scriptDir, { phase: 'accepted', action: 'pause', transaction: event.transaction, slug, status: event.status, result });
      return { status: 200, body: { ok: true, action: 'pause', slug, status: event.status } };
    }

    // Informational only (delayed, dispute, billet_printed): log and move on.
    markSeen(scriptDir, event.transaction, event.status);
    auditLog(scriptDir, { phase: 'accepted', action: 'noop', transaction: event.transaction, status: event.status });
    return { status: 200, body: { ok: true, action: 'noop', status: event.status } };
  } catch (err) {
    // DO NOT mark seen — webhook will retry, which is desired for transient
    // failures (e.g. group create timeouts). Hotmart retries up to 5 times.
    auditLog(scriptDir, { phase: 'error', transaction: event.transaction, status: event.status, error: err.message });
    return { status: 500, body: { error: err.message } };
  }
}
