/**
 * Trigger-Template-Repo: Transactional-Mailer-Pfad für externe Systeme (Online-Shop, CRM).
 *
 * Architektur (siehe ~/.claude/plans/wir-brauchen-ein-system-dreamy-pizza.md):
 *  - Pro Trigger-Template existiert eine versteckte Container-Campaign mit kind='trigger_container'
 *    und 1 Variant — diese hält den eingefrorenen MJML/HTML-Snapshot.
 *  - Eingehende Trigger-Calls erzeugen einen neuen `recipient` + `email_queue`-Job in der
 *    Container-Campaign. Worker pollt wie immer und sendet via M365.
 *  - Personalisierung läuft über `recipients.customDataJson` → der Worker substituiert
 *    `{{KEY}}`-Tags ohne weitere Anpassung.
 *
 * API-Key-Sicherheit:
 *  - 256-bit Random-Token (`crypto.randomBytes(32)` → base64url) mit Prefix `mf_live_`.
 *  - Persistiert wird HMAC-SHA-256(key, MAILFORGE_API_KEY_PEPPER) als hex.
 *  - Bei Verify: HMAC neu berechnen + `timingSafeEqual`. Kein bcrypt nötig, weil
 *    Token hochentropisch ist (kein Brute-Force in realistischer Zeit möglich).
 */

import { and, desc, eq, gt, sql } from 'drizzle-orm';
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
import { db } from './client';
import { getWorkspaceById, resolveM365Sender } from './repositories';
import {
  campaigns,
  campaignVariants,
  emailQueue,
  recipients,
  triggerLogs,
  triggerTemplates,
  unsubscribes,
  users,
  type TriggerTemplate,
  type TriggerTemplateInsert,
} from './schema';

/**
 * UPPER_SNAKE-Tags, die der Worker selbst füllt — diese werden bei der Variablen-
 * Extraktion gefiltert, damit der User sie nicht als „Pflicht-Variable" angezeigt bekommt.
 * Quelle: src/worker/index.ts SYSTEM_TAGS + Pipeline-Platzhalter.
 */
const WORKER_SYSTEM_TAGS = new Set([
  'RECIPIENT_ID', 'CRM_ID', 'EMAIL', 'FIRST_NAME',
  'CTA_PRIMARY_URL', 'CTA_SECONDARY_URL', 'LOGIN_LINK',
  'HERO_URL', 'UNSUBSCRIBE_URL', 'BROWSER_URL',
  'IMPRINT_URL', 'PRIVACY_URL', 'LOGO_URL',
  'BRAND_NAME', 'BRAND_PRIMARY_COLOR', 'BRAND_SECONDARY_COLOR',
  'TIPTAP_HTML',
]);

export interface TriggerVariable {
  key: string;
  required: boolean;
  defaultValue?: string;
}

export interface CreateTriggerTemplateInput {
  key: string;
  name: string;
  description?: string;
  workspaceId: string;
  subject: string;
  preheader: string;
  mjml: string;
  html: string;
  htmlText: string;
  senderEmail?: string;
  utmDefaults?: Record<string, unknown>;
  rateLimitPerHour?: number;
  /** Override des auto-extrahierten Schemas (z.B. um Variablen optional zu machen). */
  variableSchema?: TriggerVariable[];
}

export interface CreateTriggerTemplateResult {
  template: TriggerTemplate;
  /** API-Key im Klartext — wird NUR EINMAL hier zurückgegeben. Niemals erneut abrufbar. */
  apiKey: string;
}

const KEY_PATTERN = /^[a-z0-9_-]{2,64}$/;

/**
 * Scannt alle `{{...}}`-Platzhalter in MJML/Subject/Preheader, filtert System-Tags
 * und gibt die User-Variablen zurück (deduped, alle als `required: true` per Default).
 */
export function extractVariablesFromMjml(mjml: string, subject: string, preheader: string): TriggerVariable[] {
  const combined = `${subject}\n${preheader}\n${mjml}`;
  const matches = combined.matchAll(/\{\{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*\}\}/g);
  const seen = new Set<string>();
  const out: TriggerVariable[] = [];
  for (const m of matches) {
    const key = m[1];
    if (WORKER_SYSTEM_TAGS.has(key)) continue;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push({ key, required: true });
  }
  return out;
}

function getPepper(): string {
  const pepper = process.env.MAILFORGE_API_KEY_PEPPER;
  if (!pepper || pepper.length < 16) {
    throw new Error('MAILFORGE_API_KEY_PEPPER fehlt oder zu kurz (>=16 Zeichen erforderlich) — in .env.local setzen');
  }
  return pepper;
}

function hashApiKey(key: string): string {
  return createHmac('sha256', getPepper()).update(key).digest('hex');
}

function generateApiKey(): { plaintext: string; prefix: string; hash: string } {
  const random = randomBytes(32).toString('base64url');
  const plaintext = `mf_live_${random}`;
  return { plaintext, prefix: plaintext.slice(0, 10), hash: hashApiKey(plaintext) };
}

/**
 * Verifiziert einen vom Caller übergebenen API-Key gegen den persistierten Hash.
 * Timing-safe-compare verhindert Timing-Side-Channel auf den Hash-Vergleich.
 */
export function verifyApiKey(template: { apiKeyHash: string }, providedKey: string): boolean {
  if (!providedKey || typeof providedKey !== 'string') return false;
  const candidateHash = hashApiKey(providedKey);
  const a = Buffer.from(candidateHash, 'hex');
  const b = Buffer.from(template.apiKeyHash, 'hex');
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

/**
 * Default-User-Resolution: für die Container-Campaign braucht `campaigns.createdById`
 * eine User-ID. Pattern aus src/app/api/campaign/send/route.ts wiederverwendet.
 */
function getOrCreateDefaultUserId(): string {
  const existing = db.select().from(users).limit(1).get()?.id;
  if (existing) return existing;
  const id = crypto.randomUUID();
  db.insert(users).values({
    id,
    email: process.env.M365_AUDIT_BCC ?? 'admin@mailforge.local',
    role: 'admin',
  }).run();
  return id;
}

/**
 * Legt ein Trigger-Template + zugehörige Container-Campaign + Variant atomar an.
 * Gibt den Klartext-API-Key NUR EINMAL zurück.
 */
export function createTriggerTemplate(input: CreateTriggerTemplateInput): CreateTriggerTemplateResult {
  if (!KEY_PATTERN.test(input.key)) {
    throw new Error(`Ungültiger Template-Key '${input.key}' — nur a-z, 0-9, _, - erlaubt (2-64 Zeichen)`);
  }
  const existing = db.select().from(triggerTemplates).where(eq(triggerTemplates.key, input.key)).get();
  if (existing) {
    throw new Error(`Template-Key '${input.key}' bereits vergeben — bitte anderen wählen`);
  }

  const variables = input.variableSchema ?? extractVariablesFromMjml(input.mjml, input.subject, input.preheader);
  const apiKey = generateApiKey();
  const userId = getOrCreateDefaultUserId();
  const templateId = crypto.randomUUID();
  const containerCampaignId = crypto.randomUUID();
  const containerVariantId = crypto.randomUUID();
  const now = new Date();

  // Sender-Resolution beim Freischalten festfrieren — passt zum Pattern in
  // createCampaignWithRecipients (campaign-repo.ts:218-230). getJobContext liest
  // senderEmail/senderName aus strategyJson._generated. Wenn input.senderEmail leer
  // bleibt, fällt der Worker auf workspace.companyId zurück (klassischer Default-Pfad).
  const workspace = getWorkspaceById(input.workspaceId);
  if (!workspace) throw new Error(`Workspace nicht gefunden: ${input.workspaceId}`);
  let resolvedSenderEmail: string | undefined;
  let resolvedSenderName: string | undefined;
  if (input.senderEmail) {
    const sender = resolveM365Sender({
      explicitEmail: input.senderEmail,
      companyId: workspace.companyId,
      workspaceDefaultEmail: workspace.senderEmail,
    });
    if (!sender) {
      throw new Error(`Sender '${input.senderEmail}' nicht im aktiven M365-Pool — bitte in den Settings ergänzen oder aktivieren`);
    }
    resolvedSenderEmail = sender.email;
    resolvedSenderName = sender.name;
  }
  const containerStrategyJson = resolvedSenderEmail
    ? {
        _generated: {
          emailHtml: input.html,
          emailText: input.htmlText,
          mjmlSource: input.mjml,
          senderEmail: resolvedSenderEmail,
          senderName: resolvedSenderName,
        },
      }
    : null;

  const inserted = db.transaction((tx) => {
    // 1. Versteckte Container-Campaign (Status bleibt dauerhaft 'sending'; UI filtert via kind)
    tx.insert(campaigns).values({
      id: containerCampaignId,
      workspaceId: input.workspaceId,
      createdById: userId,
      name: `__trigger:${input.key}`,
      briefingJson: { triggerTemplate: input.key, kind: 'trigger_container' },
      tiptapJson: {},
      strategyJson: containerStrategyJson,
      spamReportJson: null,
      spamScore: 0,
      abEnabled: false,
      abWinnerHours: 4,
      utmDefaultsJson: input.utmDefaults ?? null,
      status: 'sending',
      kind: 'trigger_container',
      startedAt: now,
    }).run();

    // 2. Single Variant mit dem eingefrorenen Snapshot
    tx.insert(campaignVariants).values({
      id: containerVariantId,
      campaignId: containerCampaignId,
      label: 'A',
      subject: input.subject,
      preheader: input.preheader,
      mjml: input.mjml,
      html: input.html,
      htmlText: input.htmlText,
      strategyJson: null,
      spamReportJson: null,
      spamScore: null,
    }).run();

    // 3. Trigger-Template-Row
    tx.insert(triggerTemplates).values({
      id: templateId,
      key: input.key,
      name: input.name,
      description: input.description ?? null,
      workspaceId: input.workspaceId,
      containerCampaignId,
      containerVariantId,
      subject: input.subject,
      preheader: input.preheader,
      mjml: input.mjml,
      html: input.html,
      htmlText: input.htmlText,
      variableSchemaJson: variables,
      senderEmail: input.senderEmail ?? null,
      apiKeyHash: apiKey.hash,
      apiKeyPrefix: apiKey.prefix,
      isActive: true,
      utmDefaultsJson: input.utmDefaults ?? null,
      rateLimitPerHour: input.rateLimitPerHour ?? null,
      triggerCount: 0,
    } satisfies TriggerTemplateInsert).run();

    return tx.select().from(triggerTemplates).where(eq(triggerTemplates.id, templateId)).get()!;
  });

  return { template: inserted, apiKey: apiKey.plaintext };
}

export function getTriggerTemplateByKey(key: string): TriggerTemplate | undefined {
  return db.select().from(triggerTemplates).where(eq(triggerTemplates.key, key)).get();
}

export function getTriggerTemplateById(id: string): TriggerTemplate | undefined {
  return db.select().from(triggerTemplates).where(eq(triggerTemplates.id, id)).get();
}

export function listTriggerTemplates(): TriggerTemplate[] {
  return db.select().from(triggerTemplates).orderBy(desc(triggerTemplates.createdAt)).all();
}

export function setTriggerTemplateActive(id: string, isActive: boolean): void {
  db.update(triggerTemplates)
    .set({ isActive, updatedAt: new Date() })
    .where(eq(triggerTemplates.id, id))
    .run();
}

/**
 * Rotiert den API-Key. Gibt den neuen Klartext einmalig zurück; alter Key wird sofort ungültig.
 */
export function regenerateApiKey(id: string): { apiKey: string; prefix: string } {
  const apiKey = generateApiKey();
  db.update(triggerTemplates)
    .set({ apiKeyHash: apiKey.hash, apiKeyPrefix: apiKey.prefix, updatedAt: new Date() })
    .where(eq(triggerTemplates.id, id))
    .run();
  return { apiKey: apiKey.plaintext, prefix: apiKey.prefix };
}

export function incrementTriggerStats(id: string): void {
  db.update(triggerTemplates)
    .set({
      triggerCount: sql`${triggerTemplates.triggerCount} + 1`,
      lastTriggeredAt: new Date(),
    })
    .where(eq(triggerTemplates.id, id))
    .run();
}

export interface EnqueueTriggerInput {
  template: TriggerTemplate;
  to: { email: string; firstName?: string; lastName?: string; crmId?: string };
  data: Record<string, unknown>;
}

export interface EnqueueTriggerResult {
  recipientId: string;
  queueJobId: string;
}

/**
 * Atomarer Insert von `recipient` + `email_queue`-Job in die Container-Campaign.
 * Worker pollt anschließend wie gewohnt; Personalisierung läuft über `customDataJson`.
 */
export function enqueueTriggerSend(input: EnqueueTriggerInput): EnqueueTriggerResult {
  return db.transaction((tx) => {
    const recipientId = crypto.randomUUID();
    const queueJobId = crypto.randomUUID();
    const now = new Date();

    // customDataJson hält ALLE übergebenen Daten + die expliziten to-Felder.
    // Reihenfolge: explizite Felder aus `to` zuerst, dann `data` (data-Keys gewinnen,
    // falls Caller bewusst `firstName` in `data` mitschickt).
    const customDataJson: Record<string, unknown> = {
      ...input.data,
    };

    tx.insert(recipients).values({
      id: recipientId,
      campaignId: input.template.containerCampaignId,
      variantId: input.template.containerVariantId,
      crmId: input.to.crmId ?? null,
      email: input.to.email,
      firstName: input.to.firstName ?? null,
      lastName: input.to.lastName ?? null,
      poolType: 'private',
      customDataJson,
    }).run();

    tx.insert(emailQueue).values({
      id: queueJobId,
      campaignId: input.template.containerCampaignId,
      recipientId,
      variantId: input.template.containerVariantId,
      status: 'pending',
      scheduledAt: now,
    }).run();

    return { recipientId, queueJobId };
  });
}

export interface LogTriggerEventInput {
  triggerTemplateId: string;
  recipientEmail: string;
  status: 'queued' | 'rejected_validation' | 'rejected_unsubscribed' | 'rejected_rate_limit' | 'rejected_auth' | 'duplicate';
  idempotencyKey?: string;
  errorMessage?: string;
  requestPayloadJson?: Record<string, unknown>;
  emailQueueJobId?: string;
}

export function logTriggerEvent(input: LogTriggerEventInput): void {
  db.insert(triggerLogs).values({
    id: crypto.randomUUID(),
    triggerTemplateId: input.triggerTemplateId,
    recipientEmail: input.recipientEmail,
    status: input.status,
    idempotencyKey: input.idempotencyKey ?? null,
    errorMessage: input.errorMessage ?? null,
    requestPayloadJson: input.requestPayloadJson ?? null,
    emailQueueJobId: input.emailQueueJobId ?? null,
  }).run();
}

/**
 * Idempotenz-Check: gleicher `idempotencyKey` in den letzten 24h für dasselbe Template?
 * Liefert den ursprünglichen `email_queue`-Job zurück, sodass die API mit
 * `{ duplicate: true, queueJobId: ... }` antworten kann.
 */
export function findRecentTriggerByIdempotency(
  triggerTemplateId: string,
  idempotencyKey: string,
): { emailQueueJobId: string | null } | undefined {
  const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
  const row = db.select()
    .from(triggerLogs)
    .where(and(
      eq(triggerLogs.triggerTemplateId, triggerTemplateId),
      eq(triggerLogs.idempotencyKey, idempotencyKey),
      eq(triggerLogs.status, 'queued'),
      gt(triggerLogs.createdAt, cutoff),
    ))
    .orderBy(desc(triggerLogs.createdAt))
    .limit(1)
    .get();
  return row ? { emailQueueJobId: row.emailQueueJobId } : undefined;
}

/**
 * Prüft ob das Trigger-Template das Stunden-Rate-Limit überschritten hat.
 * Zählt nur erfolgreich `queued` Events im 60-Minuten-Fenster.
 */
export function exceedsRateLimit(triggerTemplateId: string, limitPerHour: number): boolean {
  const cutoff = new Date(Date.now() - 60 * 60 * 1000);
  const row = db.select({ c: sql<number>`count(*)` })
    .from(triggerLogs)
    .where(and(
      eq(triggerLogs.triggerTemplateId, triggerTemplateId),
      eq(triggerLogs.status, 'queued'),
      gt(triggerLogs.createdAt, cutoff),
    ))
    .get();
  return (row?.c ?? 0) >= limitPerHour;
}

export function getRecentTriggerLogs(triggerTemplateId: string, limit = 100) {
  return db.select()
    .from(triggerLogs)
    .where(eq(triggerLogs.triggerTemplateId, triggerTemplateId))
    .orderBy(desc(triggerLogs.createdAt))
    .limit(limit)
    .all();
}

export function isUnsubscribed(email: string): boolean {
  const row = db.select().from(unsubscribes).where(eq(unsubscribes.email, email)).get();
  return !!row;
}

export interface ValidationResult {
  ok: boolean;
  errors: string[];
}

/**
 * Validiert eine eingehende Trigger-Payload gegen das Variablen-Schema des Templates.
 * - Alle `required: true` Variablen müssen in `data` (oder über `to`) vorhanden sein.
 * - Werte werden zu String gecastet — der Worker macht ohnehin `String(val)`.
 */
export function validateTriggerPayload(
  template: TriggerTemplate,
  body: { to?: { email?: string; firstName?: string; lastName?: string; crmId?: string }; data?: Record<string, unknown> },
): ValidationResult {
  const errors: string[] = [];

  // Email ist immer Pflicht, wird unten in der API-Route auch via z.string().email() gecheckt.
  if (!body.to?.email) errors.push('to.email ist erforderlich');

  const schema = (template.variableSchemaJson ?? []) as TriggerVariable[];
  const data = body.data ?? {};
  const provided = new Set([
    ...Object.keys(data).filter(k => data[k] !== undefined && data[k] !== null && data[k] !== ''),
    ...(body.to?.firstName ? ['firstName'] : []),
    ...(body.to?.lastName ? ['lastName'] : []),
  ]);

  for (const v of schema) {
    if (!v.required) continue;
    if (provided.has(v.key)) continue;
    if (v.defaultValue !== undefined) continue;
    errors.push(`Pflicht-Variable '${v.key}' fehlt in data`);
  }

  return { ok: errors.length === 0, errors };
}
