import type {
  FraudAction,
  FraudAdapter,
  FraudSignal,
  AdapterConfig,
  DecisionPoint,
  PipelineResult,
} from './types.js';
import type { DrizzleClient } from '@/server/db/client';
import { insertFraudEvent } from '@/server/db/queries/fraud-events.js';
import { captureCaught } from '@/server/observability/capture.server.js';

const ACTION_SEVERITY: Record<string, number> = {
  pass: 0,
  flag: 1,
  hold: 2,
  block: 3,
};

/** Resolve MAX-severity across all signals. */
export function resolveAction(signals: FraudSignal[]): FraudAction {
  let max = 0;
  let result: FraudAction = 'pass';
  for (const s of signals) {
    const sev = ACTION_SEVERITY[s.action] ?? 0;
    if (sev > max) {
      max = sev;
      result = s.action;
    }
  }
  return result;
}

/** Downgrade signal action to at most cfg.action if specified. */
export function applyDowngrade(signal: FraudSignal, cfg: AdapterConfig): FraudSignal {
  if (!cfg.action) return signal;
  const natural = ACTION_SEVERITY[signal.action] ?? 0;
  const cap = ACTION_SEVERITY[cfg.action] ?? 3;
  if (natural <= cap) return signal;
  return { ...signal, action: cfg.action };
}

const ADAPTERS: FraudAdapter<unknown>[] = [];

export function registerAdapter<Ctx>(adapter: FraudAdapter<Ctx>): void {
  ADAPTERS.push({
    ...adapter,
    evaluate: (ctx, cfg) => adapter.evaluate(ctx as Ctx, cfg),
  });
}

export function getAdaptersForPoint(point: DecisionPoint): FraudAdapter<unknown>[] {
  return ADAPTERS.filter((a) => a.points.includes(point));
}

export interface RunPipelineInput<Ctx> {
  point: DecisionPoint;
  ctx: Ctx;
  userId: string;
  referralId?: string;
  fraudConfig: Record<string, AdapterConfig>;
  db: DrizzleClient;
  /** If false, skip fraud_events insert (e.g. CLICK → AE-only). */
  persistEvents?: boolean;
}

export async function runFraudPipeline<Ctx>(input: RunPipelineInput<Ctx>): Promise<PipelineResult> {
  const adapters = getAdaptersForPoint(input.point);
  const signals: FraudSignal[] = [];

  await Promise.all(
    adapters.map(async (adapter) => {
      const cfg: AdapterConfig = input.fraudConfig[adapter.key] ?? { enabled: true };
      if (!cfg.enabled) return;
      try {
        const raw = await adapter.evaluate(input.ctx, cfg);
        if (!raw || raw.action === 'pass') return;
        const signal = applyDowngrade(raw, cfg);
        signals.push(signal);
        if (input.persistEvents !== false) {
          await insertFraudEvent(input.db, {
            userId: input.userId,
            referralId: input.referralId,
            decisionPoint: input.point,
            signal,
          });
        }
      } catch (err) {
        console.error(`[fraud-pipeline] adapter=${adapter.key} point=${input.point} error`, err);
        captureCaught(err, {
          scope: 'fraud-pipeline.adapter-error',
          extra: { adapter: adapter.key, point: input.point },
        });
        // EARN only: fail-closed — adapter failure must not auto-release commission.
        if (input.point !== 'EARN') return;
        const signal: FraudSignal = {
          adapter: adapter.key,
          action: 'hold',
          codes: ['adapter_error'],
        };
        signals.push(signal);
        if (input.persistEvents !== false) {
          await insertFraudEvent(input.db, {
            userId: input.userId,
            referralId: input.referralId,
            decisionPoint: input.point,
            signal,
          });
        }
      }
    }),
  );

  return { action: resolveAction(signals), signals };
}
