import 'dotenv/config';

import dbPool from '../db';
import {
  buildNormalizedMlbMarketsForEvent,
  fetchActiveMlbEvents,
  storeNormalizedMlbMarketsForEvent,
} from '../services/mlb-market-completion-engine';
import {
  createOperationalCronRun,
  ensureOperationalCronRegistration,
  finalizeOperationalCronRun,
  MLB_MARKET_COMPLETION_CRON,
  refreshOperationalCronMetrics,
  summarizeNormalizedMlbMarkets,
} from '../services/mlb-market-reporter';

const LOCK_KEY = 90328011;
const DEFAULT_EVENT_TIMEOUT_MS = Number(process.env.MLB_MARKET_COMPLETION_TIMEOUT_MS || 45000);

type DbClient = {
  query: (sql: string, params?: any[]) => Promise<{ rows: any[] }>;
  release: () => void;
};

type DbPoolLike = {
  connect: () => Promise<DbClient>;
  end: () => Promise<void>;
};

export function getDateET(): string {
  return new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });
}

function mergeCounts(target: Record<string, number>, next: Record<string, number>): void {
  for (const [key, value] of Object.entries(next)) {
    target[key] = (target[key] || 0) + Number(value || 0);
  }
}

function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
    promise
      .then((value) => {
        clearTimeout(timer);
        resolve(value);
      })
      .catch((err) => {
        clearTimeout(timer);
        reject(err);
      });
  });
}

export async function runMlbMarketCompletionCron(deps?: {
  pool?: DbPoolLike;
  eventTimeoutMs?: number;
  now?: () => number;
  dateET?: () => string;
  fetchEvents?: typeof fetchActiveMlbEvents;
  buildMarkets?: typeof buildNormalizedMlbMarketsForEvent;
  storeMarkets?: typeof storeNormalizedMlbMarketsForEvent;
  ensureCronRegistration?: typeof ensureOperationalCronRegistration;
  createRun?: typeof createOperationalCronRun;
  finalizeRun?: typeof finalizeOperationalCronRun;
  refreshCronMetrics?: typeof refreshOperationalCronMetrics;
  summarizeMarkets?: typeof summarizeNormalizedMlbMarkets;
}): Promise<{ status: 'SUCCESS' | 'FAILED' | 'SKIPPED'; metrics: Record<string, any> }> {
  const runtimePool = deps?.pool || (dbPool as unknown as DbPoolLike);
  const now = deps?.now || Date.now;
  const dateET = deps?.dateET || getDateET;
  const fetchEvents = deps?.fetchEvents || fetchActiveMlbEvents;
  const buildMarkets = deps?.buildMarkets || buildNormalizedMlbMarketsForEvent;
  const storeMarkets = deps?.storeMarkets || storeNormalizedMlbMarketsForEvent;
  const ensureCronRegistration = deps?.ensureCronRegistration || ensureOperationalCronRegistration;
  const createRun = deps?.createRun || createOperationalCronRun;
  const finalizeRun = deps?.finalizeRun || finalizeOperationalCronRun;
  const refreshCronMetrics = deps?.refreshCronMetrics || refreshOperationalCronMetrics;
  const summarizeMarkets = deps?.summarizeMarkets || summarizeNormalizedMlbMarkets;
  const eventTimeoutMs = deps?.eventTimeoutMs ?? DEFAULT_EVENT_TIMEOUT_MS;

  const startedAt = now();
  const client = await runtimePool.connect();
  let lockAcquired = false;
  let runId = '';
  let finalized = false;

  const failMetrics: Record<string, any> = {
    cron: MLB_MARKET_COMPLETION_CRON.slug,
    date_et: dateET(),
  };

  try {
    await ensureCronRegistration(client as any);
    runId = await createRun(client as any);

    const { rows: lockRows } = await client.query('SELECT pg_try_advisory_lock($1) AS locked', [LOCK_KEY]);
    lockAcquired = Boolean(lockRows[0]?.locked);

    if (!lockAcquired) {
      const skippedMetrics = {
        ...failMetrics,
        reason: 'lock_not_acquired',
      };
      await finalizeRun(client as any, {
        runId,
        status: 'SKIPPED',
        durationMs: now() - startedAt,
        metrics: skippedMetrics,
        logBlob: '[mlb-market-completion] skipped: another run is already active',
      });
      finalized = true;
      await refreshCronMetrics(client as any);
      return { status: 'SKIPPED', metrics: skippedMetrics };
    }

    const events = await fetchEvents();
    const metrics: Record<string, any> = {
      cron: MLB_MARKET_COMPLETION_CRON.slug,
      date_et: dateET(),
      total_events: events.length,
      processed_events: 0,
      failed_events: 0,
      total_markets: 0,
      source_complete: 0,
      multi_source_complete: 0,
      incomplete: 0,
      gap_filled_markets: 0,
      under_only_markets: 0,
      over_only_markets: 0,
      two_way_markets: 0,
      zero_gap_fill_events: 0,
      gap_fill_attempts: 0,
      gap_fill_successes: 0,
      breakdown_by_prop_type: {},
      source_distribution: {},
      failed_gap_fill_attempts: 0,
      event_breakdown: [] as Record<string, any>[],
    };
    const logs: string[] = [];

    for (const event of events) {
      try {
        const markets = await withTimeout(
          buildMarkets(event),
          eventTimeoutMs,
          `event ${event.eventId}`,
        );

        await client.query('BEGIN');
        await storeMarkets(event, markets, client as any);
        await client.query('COMMIT');

        const summary = summarizeMarkets(markets);
        metrics.processed_events += 1;
        metrics.total_markets += summary.totalMarkets;
        metrics.source_complete += summary.sourceComplete;
        metrics.multi_source_complete += summary.multiSourceComplete;
        metrics.incomplete += summary.incomplete;
        metrics.gap_filled_markets += summary.gapFilled;
        metrics.under_only_markets += summary.underOnlyMarkets;
        metrics.over_only_markets += summary.overOnlyMarkets;
        metrics.two_way_markets += summary.twoWayMarkets;
        metrics.zero_gap_fill_events += summary.zeroGapFillEvents;
        metrics.gap_fill_attempts += summary.gapFillAttempts;
        metrics.gap_fill_successes += summary.gapFillSuccesses;
        metrics.failed_gap_fill_attempts = metrics.gap_fill_attempts - metrics.gap_fill_successes;
        mergeCounts(metrics.breakdown_by_prop_type, summary.breakdownByPropType);
        mergeCounts(metrics.source_distribution, summary.sourceDistribution);
        metrics.event_breakdown.push(...summary.eventBreakdown.map((entry) => ({
          event_id: entry.eventId,
          starts_at: entry.startsAt,
          home_team: entry.homeTeam,
          away_team: entry.awayTeam,
          total_markets: entry.totalMarkets,
          source_complete: entry.sourceComplete,
          multi_source_complete: entry.multiSourceComplete,
          incomplete: entry.incomplete,
          gap_filled_markets: entry.gapFilledMarkets,
          under_only_markets: entry.underOnlyMarkets,
          over_only_markets: entry.overOnlyMarkets,
          two_way_markets: entry.twoWayMarkets,
          zero_gap_fill_event: entry.zeroGapFillEvent,
          completion_rate: entry.completionRate,
        })));

        logs.push(
          `[ok] ${event.eventId} ${event.awayTeam}@${event.homeTeam} markets=${summary.totalMarkets} source_complete=${summary.sourceComplete} multi_source=${summary.multiSourceComplete} incomplete=${summary.incomplete} under_only=${summary.underOnlyMarkets} two_way=${summary.twoWayMarkets} gap_filled=${summary.gapFilled} zero_gap_fill_events=${summary.zeroGapFillEvents}`,
        );
      } catch (err: any) {
        await client.query('ROLLBACK').catch(() => {});
        metrics.failed_events += 1;
        logs.push(`[fail] ${event.eventId} ${event.awayTeam}@${event.homeTeam} ${err.message || String(err)}`);
      }
    }

    metrics.complete_rate = metrics.total_markets > 0
      ? (metrics.source_complete + metrics.multi_source_complete) / metrics.total_markets
      : 0;
    metrics.gap_fill_success_rate = metrics.gap_fill_attempts > 0
      ? metrics.gap_fill_successes / metrics.gap_fill_attempts
      : 0;

    const status: 'SUCCESS' | 'FAILED' = metrics.failed_events > 0 && metrics.processed_events === 0 ? 'FAILED' : 'SUCCESS';

    await finalizeRun(client as any, {
      runId,
      status,
      durationMs: now() - startedAt,
      metrics,
      errorMessage: status === 'FAILED' ? 'All event completions failed' : null,
      logBlob: logs.join('\n'),
    });
    finalized = true;
    await refreshCronMetrics(client as any);
    return { status, metrics };
  } catch (err: any) {
    if (runId && !finalized) {
      await finalizeRun(client as any, {
        runId,
        status: 'FAILED',
        durationMs: now() - startedAt,
        metrics: failMetrics,
        errorMessage: err.message || String(err),
        logBlob: `[fatal] ${err.message || String(err)}`,
      }).catch(() => {});
      await refreshCronMetrics(client as any).catch(() => {});
    }
    throw err;
  } finally {
    if (lockAcquired) {
      await client.query('SELECT pg_advisory_unlock($1)', [LOCK_KEY]).catch(() => {});
    }
    client.release();
  }
}

async function main(): Promise<void> {
  try {
    const result = await runMlbMarketCompletionCron();
    console.log(JSON.stringify(result, null, 2));
    if (result.status === 'FAILED') process.exitCode = 1;
  } finally {
    await (dbPool as unknown as DbPoolLike).end().catch(() => {});
  }
}

if (require.main === module) {
  main().catch(async (err) => {
    console.error('[mlb-market-completion] Fatal:', err);
    await (dbPool as unknown as DbPoolLike).end().catch(() => {});
    process.exit(1);
  });
}
