/**
 * Order/Conversion-Webhook für Shops (Server-Side Conversion-Reporting).
 *
 * Wird vom Shop-Backend (z.B. Supabase Edge Function `create-order`) aufgerufen,
 * sobald eine Conversion sicher persistiert ist. Im Gegensatz zum Pixel-Beacon
 * ist dieser Pfad **adblocker-immun und private-mode-resistent** — der einzig
 * verlässliche Weg, harte Conversions (Orders) zu zählen.
 *
 * URL: POST /api/track/order-webhook
 * Auth: HMAC-SHA256(body, MAILFORGE_WEBHOOK_SECRET), Hex, Header `X-MF-Signature`.
 *        Plus `X-MF-Timestamp` (Unix-ms), max 5min Drift, Replay-Schutz.
 *
 * Body:
 *   {
 *     "recipientId": "<MailForge-Recipient-UUID>",
 *     "kind": "order" | "lead" | "form_submit" | "magic_login",
 *     "value"?: number,        // EUR oder Punkte
 *     "orderId"?: string,
 *     "meta"?: object
 *   }
 *
 * Antwort:
 *   200 { ok: true, eventId } bei Erfolg
 *   200 { ok: true, skipped: reason } bei Recipient-Lookup-Failure (Best-Effort)
 *   401 bei fehlender/falscher Signatur oder zu altem Timestamp
 *   400 bei Schema-Verletzung
 */

import { NextRequest, NextResponse } from 'next/server';
import { createHmac, timingSafeEqual } from 'node:crypto';
import { z } from 'zod';
import { recordConversion } from '@/lib/db/campaign-repo';
import { db } from '@/lib/db/client';
import { recipients, campaigns, workspaces } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

const MAX_CLOCK_DRIFT_MS = 5 * 60 * 1000;

const BodySchema = z.object({
  recipientId: z.string().min(1).max(200),
  kind: z.enum(['order', 'lead', 'form_submit', 'magic_login']),
  value: z.number().int().min(0).max(10_000_000).optional(),
  orderId: z.string().max(200).optional(),
  meta: z.record(z.string(), z.unknown()).optional(),
});

function constantTimeEquals(a: string, b: string): boolean {
  // Length-Mismatch ist auch ein Mismatch — wir füllen vor dem timingSafeEqual auf
  // gleiche Länge auf, damit kein early-exit-Timing-Leck entsteht.
  const aBuf = Buffer.from(a, 'utf8');
  const bBuf = Buffer.from(b, 'utf8');
  if (aBuf.length !== bBuf.length) return false;
  return timingSafeEqual(aBuf, bBuf);
}

/**
 * Findet das Webhook-Secret für eine Recipient-ID:
 *  1. recipientId → recipients.campaignId → campaigns.workspaceId → workspaces.webhookSecret
 *  2. Fallback auf process.env.MAILFORGE_WEBHOOK_SECRET
 * Liefert beide Kandidaten zurück damit wir BEIDE prüfen können (Workspace-Secret bevorzugt).
 */
function resolveSecrets(recipientId: string): { workspace: string | null; global: string | null } {
  const brandSecret = (() => {
    try {
      const row = db.select({ secret: workspaces.webhookSecret })
        .from(recipients)
        .innerJoin(campaigns, eq(campaigns.id, recipients.campaignId))
        .innerJoin(workspaces, eq(workspaces.id, campaigns.workspaceId))
        .where(eq(recipients.id, recipientId))
        .get();
      return row?.secret ?? null;
    } catch {
      return null;
    }
  })();
  return {
    workspace: brandSecret && brandSecret.length > 0 ? brandSecret : null,
    global: process.env.MAILFORGE_WEBHOOK_SECRET ?? null,
  };
}

export async function POST(req: NextRequest) {
  const signature = req.headers.get('x-mf-signature');
  const timestamp = req.headers.get('x-mf-timestamp');
  if (!signature || !timestamp) {
    return NextResponse.json({ error: 'Missing signature headers' }, { status: 401 });
  }

  const tsNum = Number(timestamp);
  if (!Number.isFinite(tsNum) || Math.abs(Date.now() - tsNum) > MAX_CLOCK_DRIFT_MS) {
    return NextResponse.json({ error: 'Timestamp out of range' }, { status: 401 });
  }

  // Raw body als String — wir signieren `${timestamp}.${body}` damit Header und
  // Body zusammen verifiziert werden (Stripe-Pattern).
  const rawBody = await req.text();

  let bodyJson: unknown;
  try {
    bodyJson = JSON.parse(rawBody);
  } catch {
    return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
  }

  const parsed = BodySchema.safeParse(bodyJson);
  if (!parsed.success) {
    return NextResponse.json({
      error: 'Invalid payload',
      issues: parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`),
    }, { status: 400 });
  }

  const { recipientId, kind, value, orderId, meta } = parsed.data;

  // Secret-Cascade: Workspace-Secret hat Priorität. Wenn Workspace-Secret gesetzt und
  // matcht → akzeptieren. Sonst global env-var probieren. Beide leer → 500.
  const { workspace: brandSecret, global: globalSecret } = resolveSecrets(recipientId);
  if (!brandSecret && !globalSecret) {
    console.error('[/api/track/order-webhook] No secret configured (neither workspace nor global)');
    return NextResponse.json({ error: 'Webhook not configured' }, { status: 500 });
  }
  const sigOk = (secret: string | null) => {
    if (!secret) return false;
    const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
    return constantTimeEquals(expected, signature);
  };
  // Workspace zuerst — wenn Workspace-Secret gesetzt UND matcht, ok. Wenn Workspace-Secret
  // gesetzt aber NICHT matcht, dann globalen Fallback prüfen (für Übergang
  // während Rotation). Wenn Workspace-Secret leer, gleich global prüfen.
  if (!sigOk(brandSecret) && !sigOk(globalSecret)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  const mergedMeta = {
    ...(meta ?? {}),
    kind,
    ...(orderId ? { orderId } : {}),
  };

  const result = recordConversion({
    recipientId,
    value,
    meta: mergedMeta,
    source: 'webhook',
  });

  if (!result.ok) {
    // Recipient nicht gefunden — Shop hat möglicherweise einen veralteten/gefälschten
    // mf_rcp gesendet. Wir loggen, antworten aber 200 damit der Shop nicht in
    // Retry-Loops läuft. HMAC hat bereits validiert, dass der Shop authorisiert ist.
    console.warn(`[/api/track/order-webhook] skipped: ${result.reason} (r=${recipientId})`);
    return NextResponse.json({ ok: true, skipped: result.reason });
  }

  return NextResponse.json({ ok: true, eventId: result.eventId });
}
