/**
 * Multi-Touch-Attribution-Engine.
 *
 * Pure Function — bekommt die touches vorgefetched und gibt gewichtete
 * Credit-Verteilung zurück. Wird vom Stats-Aggregat-Code und vom
 * Workspace-Dashboard genutzt um Conversion-Credits zwischen mehreren
 * Email-Touchpoints (Open/Click) aufzuteilen.
 *
 * Vier Modelle, pro Workspace konfigurierbar (`workspaces.attributionModel`):
 *  - last-click  : 100% auf letzten click (Fallback letzten open)
 *  - first-click : 100% auf ersten click (Fallback ersten open)
 *  - linear      : 1/N pro Touch
 *  - time-decay  : exponentiell, Halbwertszeit 7 Tage, normalisiert auf Σ=1
 *
 * Lookback-Filter (`workspaces.attributionLookbackDays`) wird VOR dem Modell
 * angewandt: alle touches älter als (conversionAt − lookbackDays) werden verworfen.
 */

export type AttributionModel = 'last-click' | 'first-click' | 'linear' | 'time-decay';

export interface AttributionTouch {
  campaignId: string;
  variantId: string | null;
  eventType: 'click' | 'open';
  createdAt: Date;
}

export interface AttributedTouch extends AttributionTouch {
  /** 0..1. Summe aller Weights pro Conversion = 1.0 (modulo Floating-Point). */
  weight: number;
}

export interface AttributeConversionInput {
  touches: AttributionTouch[];
  conversionAt: Date;
  model: AttributionModel;
  lookbackDays: number;
}

/** Halbwertszeit für time-decay (Tage). Konstante; könnte später pro Workspace werden. */
const TIME_DECAY_HALF_LIFE_DAYS = 7;

/**
 * Sortiert touches chronologisch und filtert nach Lookback.
 * Tie-Breaker bei identischem createdAt: campaignId asc, dann variantId asc.
 */
function preprocessTouches(input: AttributeConversionInput): AttributionTouch[] {
  const cutoffMs = input.conversionAt.getTime() - input.lookbackDays * 24 * 60 * 60 * 1000;
  return input.touches
    .filter((t) => t.createdAt.getTime() >= cutoffMs && t.createdAt.getTime() <= input.conversionAt.getTime())
    .slice() // copy vor sort
    .sort((a, b) => {
      const dt = a.createdAt.getTime() - b.createdAt.getTime();
      if (dt !== 0) return dt;
      if (a.campaignId !== b.campaignId) return a.campaignId < b.campaignId ? -1 : 1;
      const va = a.variantId ?? '';
      const vb = b.variantId ?? '';
      return va < vb ? -1 : va > vb ? 1 : 0;
    });
}

/**
 * Wählt die qualifizierenden Touches für click-priority-Modelle.
 * Wenn mindestens ein `click` existiert, nur clicks. Sonst Fallback auf opens.
 */
function clickPreferredOrAllOpens(touches: AttributionTouch[]): AttributionTouch[] {
  const clicks = touches.filter((t) => t.eventType === 'click');
  return clicks.length > 0 ? clicks : touches;
}

export function attributeConversion(input: AttributeConversionInput): AttributedTouch[] {
  const filtered = preprocessTouches(input);
  if (filtered.length === 0) return [];

  switch (input.model) {
    case 'last-click': {
      const candidates = clickPreferredOrAllOpens(filtered);
      const last = candidates[candidates.length - 1];
      return [{ ...last, weight: 1.0 }];
    }
    case 'first-click': {
      const candidates = clickPreferredOrAllOpens(filtered);
      const first = candidates[0];
      return [{ ...first, weight: 1.0 }];
    }
    case 'linear': {
      const candidates = clickPreferredOrAllOpens(filtered);
      const w = 1 / candidates.length;
      return candidates.map((t) => ({ ...t, weight: w }));
    }
    case 'time-decay': {
      const candidates = clickPreferredOrAllOpens(filtered);
      const conversionMs = input.conversionAt.getTime();
      // Raw weight pro Touch: 2^(-daysAgo / half-life)
      const raws = candidates.map((t) => {
        const daysAgo = (conversionMs - t.createdAt.getTime()) / (24 * 60 * 60 * 1000);
        return Math.pow(2, -daysAgo / TIME_DECAY_HALF_LIFE_DAYS);
      });
      const sum = raws.reduce((acc, x) => acc + x, 0);
      // Sum kann durch numerische Probleme 0 sein wenn alle daysAgo extrem hoch sind —
      // dann fallback gleichmäßige Verteilung (≈linear), damit kein NaN entsteht.
      if (sum === 0) {
        const w = 1 / candidates.length;
        return candidates.map((t) => ({ ...t, weight: w }));
      }
      return candidates.map((t, i) => ({ ...t, weight: raws[i] / sum }));
    }
  }
}

/**
 * Aggregiert eine Liste von AttributedTouches zu (campaignId, variantId) → weight-sum.
 * Wird vom Stats-Code genutzt um pro Variante den attribuierten Conversion-Anteil zu zeigen.
 */
export function aggregateAttribution(
  attributed: AttributedTouch[],
): Array<{ campaignId: string; variantId: string | null; weight: number }> {
  const map = new Map<string, { campaignId: string; variantId: string | null; weight: number }>();
  for (const t of attributed) {
    const key = `${t.campaignId}::${t.variantId ?? ''}`;
    const existing = map.get(key);
    if (existing) existing.weight += t.weight;
    else map.set(key, { campaignId: t.campaignId, variantId: t.variantId, weight: t.weight });
  }
  return [...map.values()];
}
