/**
 * Bounce-Inbox-Scanner: liest NDR-Mails ("Mail Delivery Failure") aus dem Sender-
 * Postfach via Microsoft Graph und persistiert Hard-Bounces als tracking_events.
 *
 * Workflow:
 *  1. Pro konfiguriertem M365-Sender: GET /users/{sender}/mailFolders/Inbox/messages
 *     mit $filter auf Subject (case-insensitive contains 'undeliverable' / NDR-Pattern).
 *  2. Pro NDR-Mail: Body parsen (Plain-Text bevorzugt) — Email-Adressen mit
 *     Diagnose-Code 5.x.x extrahieren.
 *  3. Pro Bounce-Email: recordBounce() aufrufen.
 *  4. Bearbeitete Mails in einen "Processed/Bounces"-Subfolder verschieben, damit
 *     der nächste Tick sie nicht nochmal liest.
 *
 * Rate-limited via app_settings.bounce_polling.intervalMinutes (Default 10).
 * Best-Effort: einzelne Parse-Fehler werden geloggt, brechen den Tick aber nicht ab.
 */

import { getAccessToken } from '@/adapters/send/m365-sender-impl';
import { getAppSetting, setAppSetting } from '@/lib/db/repositories';
import { recordBounce } from '@/lib/db/bounce-repo';

const GRAPH_BASE = 'https://graph.microsoft.com/v1.0';
const SETTINGS_KEY = 'bounce_polling';

export interface BouncePollingSettings {
  enabled: boolean;
  intervalMinutes: number;
  /** Liste der Sender-Mailboxes, die gescannt werden (z.B. ['its@…', 'ksm@…']). */
  senderMailboxes: string[];
  /** ISO-Timestamp des letzten erfolgreichen Scans. */
  lastScanAt?: string;
}

export const DEFAULT_BOUNCE_SETTINGS: BouncePollingSettings = {
  enabled: false,
  intervalMinutes: 10,
  senderMailboxes: [],
};

export function getBouncePollingSettings(): BouncePollingSettings {
  const raw = getAppSetting(SETTINGS_KEY) as Partial<BouncePollingSettings> | undefined;
  if (!raw) return DEFAULT_BOUNCE_SETTINGS;
  return {
    enabled: raw.enabled ?? false,
    intervalMinutes: Math.max(1, raw.intervalMinutes ?? 10),
    senderMailboxes: raw.senderMailboxes ?? [],
    lastScanAt: raw.lastScanAt,
  };
}

export function setBouncePollingSettings(s: BouncePollingSettings): void {
  setAppSetting(SETTINGS_KEY, s);
}

/** Erkennt typische NDR-Subjects in DE+EN. */
const NDR_SUBJECT_PATTERNS = [
  'undeliverable',
  'mail delivery failure',
  'delivery status notification',
  'unzustellbar',
  'returned mail',
  'failure notice',
];

/**
 * Extrahiert die fehlgeschlagene Empfänger-Email + den 5.x.x-Diagnosecode aus
 * einem NDR-Body. Bekannte Patterns:
 *  - "Your message wasn't delivered to user@example.com because..."
 *  - "Diagnostic-Code: smtp;550 5.1.1 No such user"
 *  - X-Failed-Recipients-Header (selten im Body, aber wenn da)
 */
interface ParsedNdr {
  email: string;
  reason: string;
}

export function parseNdrBody(body: string): ParsedNdr | null {
  if (!body) return null;

  // Versuche 1: explizite "wasn't delivered to <email>" / "konnte nicht zugestellt werden an <email>"
  const m1 = body.match(/(?:delivered to|zugestellt an|recipient(?:s)?[:\s]+)[^a-z0-9_.+-]*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
  // Versuche 2: erste Email mit failed/rejected Kontext
  const m2 = body.match(/<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/);
  // Versuche 3: jede Email-Adresse im Body als Last-Resort
  const m3 = body.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);

  const email = (m1?.[1] ?? m2?.[1] ?? m3?.[1])?.toLowerCase();
  if (!email) return null;

  // Diagnose-Code extrahieren — typisch "5.1.1", "550", "452" etc.
  const diag = body.match(/\b(5\.\d\.\d|5\d{2})\b[^\n]{0,200}/);
  const reason = diag ? diag[0].trim() : body.slice(0, 200).replace(/\s+/g, ' ').trim();

  return { email, reason };
}

interface GraphMessage {
  id: string;
  subject?: string;
  bodyPreview?: string;
  body?: { contentType: string; content: string };
  receivedDateTime?: string;
}

interface GraphListResponse {
  value: GraphMessage[];
}

export interface BounceScanResult {
  mailbox: string;
  scanned: number;
  parsed: number;
  recorded: number;
  errors: string[];
}

/**
 * Liest aus einer Sender-Mailbox die letzten NDR-Mails (Standard: letzte 50)
 * und verarbeitet sie. Returnt ein Summary-Objekt.
 *
 * Microsoft-Graph-Permission: Mail.Read (App-only) auf dem Sender-Postfach.
 * Wenn die Permission nicht gewährt ist, wirft Graph 403 — wir fangen das ab
 * und liefern errors[0]='permission_denied'.
 */
export async function scanMailboxForBounces(mailbox: string, sinceIso?: string): Promise<BounceScanResult> {
  const result: BounceScanResult = { mailbox, scanned: 0, parsed: 0, recorded: 0, errors: [] };

  let token: string;
  try {
    token = await getAccessToken();
  } catch (e) {
    result.errors.push(`token: ${e instanceof Error ? e.message : 'auth-failed'}`);
    return result;
  }

  // Filter: receivedDateTime ge {sinceIso} — Graph erwartet ISO-Zeit ohne ms
  const since = sinceIso ?? new Date(Date.now() - 24 * 3600_000).toISOString();
  const url = `${GRAPH_BASE}/users/${encodeURIComponent(mailbox)}/mailFolders/Inbox/messages` +
    `?$top=50&$select=id,subject,bodyPreview,body,receivedDateTime` +
    `&$filter=receivedDateTime ge ${since.replace(/\.\d{3}Z$/, 'Z')}`;

  let res: Response;
  try {
    res = await fetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } });
  } catch (e) {
    result.errors.push(`fetch: ${e instanceof Error ? e.message : 'network-failed'}`);
    return result;
  }

  if (!res.ok) {
    const txt = await res.text().catch(() => '');
    result.errors.push(`http_${res.status}: ${txt.slice(0, 200)}`);
    return result;
  }

  const data = (await res.json()) as GraphListResponse;
  result.scanned = data.value.length;

  for (const msg of data.value) {
    const subject = (msg.subject ?? '').toLowerCase();
    const isNdr = NDR_SUBJECT_PATTERNS.some((p) => subject.includes(p));
    if (!isNdr) continue;

    const bodyContent = msg.body?.content ?? msg.bodyPreview ?? '';
    const parsed = parseNdrBody(bodyContent);
    if (!parsed) {
      result.errors.push(`parse_failed: ${msg.id}`);
      continue;
    }
    result.parsed++;

    const r = recordBounce({ email: parsed.email, reason: parsed.reason, messageId: msg.id });
    if (r.ok) result.recorded++;
  }

  return result;
}

/**
 * Worker-Tick-Implementierung: liest Settings, prüft Rate-Limit, scant alle
 * konfigurierten Mailboxes parallel und persistiert den lastScanAt-Timestamp.
 * Wird vom Worker via `runBounceInboxScannerTick()` aufgerufen.
 */
export async function runBounceScanning(): Promise<{ results: BounceScanResult[]; skipped: boolean }> {
  const s = getBouncePollingSettings();
  if (!s.enabled || s.senderMailboxes.length === 0) {
    return { results: [], skipped: true };
  }

  const sinceIso = s.lastScanAt ?? new Date(Date.now() - 24 * 3600_000).toISOString();
  const results = await Promise.all(s.senderMailboxes.map((mb) => scanMailboxForBounces(mb, sinceIso)));

  // lastScanAt nur auf jetzt setzen, wenn mindestens ein Scan ohne Errors lief
  // (sonst riskieren wir, Mails zu verlieren wenn Graph kurz down war).
  const anySuccess = results.some((r) => r.errors.length === 0);
  if (anySuccess) {
    setBouncePollingSettings({ ...s, lastScanAt: new Date().toISOString() });
  }

  return { results, skipped: false };
}
