/**
 * Cumas-API-Polling für Conversion-Events (Phase 3b).
 *
 * Polled die Cumas/Nova-API auf neue Orders/eVBs **nach Campaign-Send-Zeit** und matcht
 * sie gegen unsere Recipients (über Email). Match → recordConversion(source='api_poll').
 *
 * Wird vom Worker periodisch via `runConversionPollingTick()` getickt.
 *
 * Designentscheidungen:
 *  - Email-Match (case-insensitive) als primäre Match-Strategie. Ehrlich: nicht 100%-präzise
 *    bei Shared-Mailboxes, aber das ist die einzige Spalte die in beiden Welten existiert.
 *  - State-Tracking via `app_settings.conversion_polling.lastPolledAt[source]` —
 *    SQL-Query-Filter `regDatetimeAfter=lastPolledAt` holt nur neue Records.
 *  - Bei einem Match wird `metaJson.orderId` UND `metaJson.payload` mitgespeichert für
 *    spätere Dedup-/Audit-Auswertungen.
 *  - Best-effort: Fehler werden geloggt, NICHT geworfen — der Worker-Tick läuft weiter.
 */

import { listOrders, listEvbs, isCumasConfigured, type CumasOrderRecord, type CumasEvbRecord } from '@/lib/crm/cumas-client';
import { recordConversion, getRecipientsByEmail, getRecipientsByIds } from '@/lib/db/campaign-repo';
import { getAppSetting, setAppSetting, getAllWorkspaces } from '@/lib/db/repositories';

export type ConversionPollingSource = 'cumas_orderPrivateShop' | 'cumas_evb';

export interface ConversionPollingSettings {
  enabled: boolean;
  intervalMinutes: number;
  sources: ConversionPollingSource[];
  /** Pro Quelle der ISO-Timestamp des letzten erfolgreichen Polls. Default: jetzt minus intervalMinutes. */
  lastPolledAt?: Partial<Record<ConversionPollingSource, string>>;
  /**
   * Optional: Pro Quelle der Cumas-Feldname (Dot-Path) der die MailForge-Recipient-ID
   * trägt — z.B. `extra_data.mf_rcp` oder `customer_note`. Wenn gesetzt, wird das Feld
   * vor dem Email-Match konsultiert; Email-Match bleibt als Fallback aktiv.
   */
  recipientIdField?: Partial<Record<ConversionPollingSource, string>>;
}

export const DEFAULT_POLLING_SETTINGS: ConversionPollingSettings = {
  enabled: false,
  intervalMinutes: 15,
  sources: ['cumas_orderPrivateShop'],
  lastPolledAt: {},
  recipientIdField: {},
};

const SETTINGS_KEY = 'conversion_polling';

/** Liest die aktuellen Polling-Settings aus app_settings (mit Defaults wenn noch nicht gesetzt). */
export function getConversionPollingSettings(): ConversionPollingSettings {
  const raw = getAppSetting(SETTINGS_KEY) as Partial<ConversionPollingSettings> | undefined;
  if (!raw) return DEFAULT_POLLING_SETTINGS;
  return {
    enabled: raw.enabled ?? false,
    intervalMinutes: Math.max(1, raw.intervalMinutes ?? 15),
    sources: raw.sources ?? ['cumas_orderPrivateShop'],
    lastPolledAt: raw.lastPolledAt ?? {},
    recipientIdField: raw.recipientIdField ?? {},
  };
}

export function setConversionPollingSettings(s: ConversionPollingSettings): void {
  setAppSetting(SETTINGS_KEY, s);
}

function updateLastPolledAt(source: ConversionPollingSource, isoTime: string): void {
  const cur = getConversionPollingSettings();
  setConversionPollingSettings({
    ...cur,
    lastPolledAt: { ...(cur.lastPolledAt ?? {}), [source]: isoTime },
  });
}

export interface PollResult {
  source: ConversionPollingSource;
  scanned: number;
  matched: number;
  conversionsRecorded: number;
  skippedDuplicates: number;
  errors: string[];
  pollWindowFrom: string;
  pollWindowTo: string;
}

/**
 * Konvertiert ein ISO-Datum zu Cumas-API-Datum (gleicher ISO-String, aber Cumas
 * akzeptiert beides). Hilfer-Funktion für klares Refactoring.
 */
function toCumasDate(iso: string): string {
  return iso;
}

/**
 * Dot-Path-Lookup auf einem beliebigen Record. `getByPath(obj, 'extra_data.mf_rcp')`
 * → `obj.extra_data.mf_rcp`. Liefert undefined, wenn irgendein Glied fehlt oder
 * der Wert kein String ist (Recipient-IDs sind UUIDs/Strings).
 */
function getByPath(obj: unknown, path: string): string | undefined {
  if (!path || obj === null || obj === undefined) return undefined;
  const parts = path.split('.');
  let cur: unknown = obj;
  for (const p of parts) {
    if (cur === null || cur === undefined || typeof cur !== 'object') return undefined;
    cur = (cur as Record<string, unknown>)[p];
  }
  if (typeof cur === 'string' && cur.trim().length > 0) return cur.trim();
  if (typeof cur === 'number') return String(cur);
  return undefined;
}

/**
 * Polled die `order_private_shops`-Collection auf neue Orders seit `lastPolledAt`.
 * Match-Strategie: Email-Lookup gegen unsere `recipients`-Tabelle (case-insensitive).
 */
async function pollOrders(): Promise<PollResult> {
  const settings = getConversionPollingSettings();
  const since = settings.lastPolledAt?.cumas_orderPrivateShop
    ?? new Date(Date.now() - settings.intervalMinutes * 60 * 1000).toISOString();
  const pollWindowTo = new Date().toISOString();

  const result: PollResult = {
    source: 'cumas_orderPrivateShop',
    scanned: 0,
    matched: 0,
    conversionsRecorded: 0,
    skippedDuplicates: 0,
    errors: [],
    pollWindowFrom: since,
    pollWindowTo,
  };

  try {
    // Hole alle Orders seit `since` — bei großen Volumes mehrere Pages, hier max 5
    const allOrders: CumasOrderRecord[] = [];
    for (let page = 1; page <= 5; page++) {
      const r = await listOrders({
        page,
        itemsPerPage: 100,
        regDatetimeAfter: toCumasDate(since),
      });
      allOrders.push(...r.records);
      if (!r.hasMore) break;
    }
    result.scanned = allOrders.length;

    // Recipient-ID-Match: Cascade Workspace-Feld → Global-Feld → Email.
    // Workspace-Felder kommen zuerst, weil sie die spezifischere Konfiguration sind.
    // Wir sammeln Workspace-Fields die nicht leer sind, plus das globale Feld als Fallback.
    const globalField = settings.recipientIdField?.cumas_orderPrivateShop;
    const brandFields = getAllWorkspaces()
      .map((b) => b.cumasRecipientField)
      .filter((f): f is string => !!f);
    const fieldsToTry = [...new Set([...brandFields, ...(globalField ? [globalField] : [])])];

    const ordersWithRcpId: Array<{ order: CumasOrderRecord; rcpId: string }> = [];
    if (fieldsToTry.length > 0) {
      for (const o of allOrders) {
        for (const field of fieldsToTry) {
          const rcpId = getByPath(o as unknown, field);
          if (rcpId) {
            ordersWithRcpId.push({ order: o, rcpId });
            break; // first match wins, kein doppeltes Insert
          }
        }
      }
    }
    const recipientById = new Map<string, { id: string; campaignId: string; email: string }>();
    if (ordersWithRcpId.length > 0) {
      const uniqueIds = [...new Set(ordersWithRcpId.map((x) => x.rcpId))];
      const rows = getRecipientsByIds(uniqueIds);
      for (const r of rows) recipientById.set(r.id, r);
    }

    // Sammle Emails der Orders, die NICHT per Recipient-ID gematcht wurden
    const orderIdToRcpMatch = new Map(ordersWithRcpId.map((x) => [x.order.id, x.rcpId]));
    const fallbackEmails = allOrders
      .filter((o) => !orderIdToRcpMatch.has(o.id) || !recipientById.has(orderIdToRcpMatch.get(o.id)!))
      .map((o) => o.email?.trim().toLowerCase())
      .filter((e): e is string => !!e);

    const recipientByEmail = new Map<string, { id: string; campaignId: string; email: string }>();
    if (fallbackEmails.length > 0) {
      const rows = getRecipientsByEmail(fallbackEmails);
      for (const r of rows) recipientByEmail.set(r.email.toLowerCase(), r);
    }

    if (recipientById.size === 0 && recipientByEmail.size === 0) {
      updateLastPolledAt('cumas_orderPrivateShop', pollWindowTo);
      return result;
    }

    // Pro Order: zuerst Recipient-ID, dann Email-Fallback
    for (const order of allOrders) {
      let recipient: { id: string; campaignId: string; email: string } | undefined;
      const candidateRcpId = orderIdToRcpMatch.get(order.id);
      if (candidateRcpId) recipient = recipientById.get(candidateRcpId);
      if (!recipient) {
        const email = order.email?.trim().toLowerCase();
        if (email) recipient = recipientByEmail.get(email);
      }
      if (!recipient) continue;

      result.matched++;
      const value = order.total ? Math.round(parseFloat(order.total) * 100) / 100 : undefined;
      const meta = {
        orderId: order.id,
        invoiceNumber: order.invoice_number ?? undefined,
        product: order.product ?? undefined,
        clientName: order.client_name ?? undefined,
        regDatetime: order.reg_datetime ?? undefined,
        kind: 'order',
      };

      const inserted = recordConversion({
        recipientId: recipient.id,
        value: value !== undefined ? Math.round(value) : undefined, // Cents → ganzzahlige Speicherung
        meta,
        source: 'api_poll',
      });

      if (inserted.ok) {
        result.conversionsRecorded++;
      } else {
        result.skippedDuplicates++;
      }
    }

    // Update lastPolledAt NUR bei erfolgreichem Lauf
    updateLastPolledAt('cumas_orderPrivateShop', pollWindowTo);
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    result.errors.push(msg);
  }

  return result;
}

/**
 * Polled die `evbs`-Collection auf neue eVBs seit `lastPolledAt`.
 * eVBs haben keine direkte Email-Spalte — wir nutzen `idkdnr` zum Lookup auf Business-Records,
 * deren Emails dann gegen unsere recipients gematcht werden. **TODO** in Phase 3.4.b: aktuell
 * nur Skeleton, das Mapping-Detail folgt wenn der erste Use-Case existiert.
 */
async function pollEvbs(): Promise<PollResult> {
  const settings = getConversionPollingSettings();
  const since = settings.lastPolledAt?.cumas_evb
    ?? new Date(Date.now() - settings.intervalMinutes * 60 * 1000).toISOString();
  const pollWindowTo = new Date().toISOString();

  const result: PollResult = {
    source: 'cumas_evb',
    scanned: 0,
    matched: 0,
    conversionsRecorded: 0,
    skippedDuplicates: 0,
    errors: [],
    pollWindowFrom: since,
    pollWindowTo,
  };

  try {
    const allEvbs: CumasEvbRecord[] = [];
    for (let page = 1; page <= 5; page++) {
      const r = await listEvbs({
        page,
        itemsPerPage: 100,
        rechdatumAfter: toCumasDate(since),
      });
      allEvbs.push(...r.records);
      if (!r.hasMore) break;
    }
    result.scanned = allEvbs.length;

    // eVB → Email Mapping ist 2-step (idkdnr → Business → email). Skeleton:
    // Würde im Production-Use noch die Business-Lookup-Schritte brauchen.
    // Aktuell: skip, Settings-Toggle exposiert die Quelle aber bleibt no-op.
    if (allEvbs.length > 0) {
      result.errors.push(
        'eVB→Email-Lookup noch nicht implementiert — siehe TODO in cumas-poller.ts:pollEvbs(). Setze sources nur auf cumas_orderPrivateShop solange.',
      );
    }

    updateLastPolledAt('cumas_evb', pollWindowTo);
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    result.errors.push(msg);
  }

  return result;
}

/**
 * Public Worker-API: führt einen vollständigen Polling-Tick durch.
 * Returnt pro konfigurierter Quelle ein PollResult, oder leeres Array wenn disabled/nicht konfiguriert.
 */
export async function runConversionPolling(): Promise<PollResult[]> {
  const settings = getConversionPollingSettings();
  if (!settings.enabled) return [];
  if (!isCumasConfigured()) {
    console.warn('[conversion-poller] Cumas nicht konfiguriert (CRM_USERNAME/CRM_PASSWORD fehlen) — skip');
    return [];
  }

  const results: PollResult[] = [];
  for (const source of settings.sources) {
    if (source === 'cumas_orderPrivateShop') {
      results.push(await pollOrders());
    } else if (source === 'cumas_evb') {
      results.push(await pollEvbs());
    }
  }
  return results;
}
