/**
 * Rain Man 2.0 Shadow Runner
 *
 * Runs in parallel with RM 1.0 forecasts, writing to v2 tables.
 * Reads its own config from rm_model_config (model_version = 'rm_2.0').
 * Initially produces identical results to RM 1.0 — the infrastructure
 * exists so we can diverge weights/signals without touching production.
 *
 * Usage: npx tsx src/workers/rm2-shadow-runner.ts [--dry-run]
 * Can also be called programmatically via runShadow().
 */

import 'dotenv/config';
import pool from '../db';
import { getLegacyCompositeView, isNativeIntelligenceResult } from '../services/rie';

const dryRun = process.argv.includes('--dry-run');
const RM2_SHADOW_ENABLED = process.env.RM2_SHADOW_ENABLED === 'true';

interface Rm2Config {
  weights: {
    nba: { grok: number; piff: number; digimon: number };
    default: { grok: number; piff: number; digimon: number };
  };
  signals: string[];
  composite_version: string;
}

interface LeagueDriftSummary {
  count: number;
  avgDelta: number;
  maxDelta: number;
  nulls: number;
  nullRate: number;
  nativeShapes: number;
}

export interface ShadowRunSummary {
  enabled: boolean;
  dryRun: boolean;
  copied: number;
  skipped: number;
  failed: number;
  accuracyCopied: number;
  perLeague: Record<string, LeagueDriftSummary>;
}

async function loadConfig(): Promise<Rm2Config> {
  const { rows } = await pool.query(
    `SELECT config_json FROM rm_model_config WHERE model_version = 'rm_2.0' AND is_active = true`
  );
  if (!rows[0]) throw new Error('No active rm_2.0 config found in rm_model_config');
  return rows[0].config_json as Rm2Config;
}

/**
 * Copy all RM 1.0 forecasts from today into rm_forecast_cache_v2,
 * re-scoring with RM 2.0 weights from config.
 */
function buildLeagueSummary(perLeague: Record<string, { count: number; sumDelta: number; maxDelta: number; nulls: number; nativeShapes: number }>): Record<string, LeagueDriftSummary> {
  return Object.fromEntries(
    Object.entries(perLeague).map(([league, drift]) => [
      league,
      {
        count: drift.count,
        avgDelta: drift.count > 0 ? drift.sumDelta / drift.count : 0,
        maxDelta: drift.maxDelta,
        nulls: drift.nulls,
        nullRate: drift.count > 0 ? drift.nulls / drift.count : 0,
        nativeShapes: drift.nativeShapes,
      },
    ]),
  );
}

export async function runShadow(): Promise<ShadowRunSummary> {
  if (!RM2_SHADOW_ENABLED) {
    console.log('[rm2-shadow] RM2_SHADOW_ENABLED=false — exiting without writes');
    return {
      enabled: false,
      dryRun,
      copied: 0,
      skipped: 0,
      failed: 0,
      accuracyCopied: 0,
      perLeague: {},
    };
  }

  const config = await loadConfig();
  const stats = { copied: 0, skipped: 0, failed: 0 };
  const perLeague: Record<string, { count: number; sumDelta: number; maxDelta: number; nulls: number; nativeShapes: number }> = {};

  console.log('='.repeat(60));
  console.log('RAIN MAN 2.0 SHADOW RUNNER');
  console.log(`Date: ${new Date().toISOString()}`);
  console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`);
  console.log(`Config weights:`, JSON.stringify(config.weights));
  console.log('='.repeat(60));

  // Get today's RM 1.0 forecasts
  const { rows: rm1Forecasts } = await pool.query(`
    SELECT id, event_id, league, home_team, away_team, forecast_data,
           confidence_score, starts_at, expires_at, odds_data, odds_updated_at,
           opening_moneyline, opening_spread, opening_total,
           composite_confidence, model_signals, composite_version,
           last_refresh_at, last_refresh_type, refresh_count,
           material_change, input_quality
    FROM rm_forecast_cache
    WHERE DATE(created_at AT TIME ZONE 'America/New_York') = (NOW() AT TIME ZONE 'America/New_York')::date
  `);

  console.log(`Found ${rm1Forecasts.length} RM 1.0 forecasts from today`);

  for (const fc of rm1Forecasts) {
    try {
      // Check if already shadowed
      const { rows: existing } = await pool.query(
        'SELECT id FROM rm_forecast_cache_v2 WHERE event_id = $1',
        [fc.event_id]
      );
      if (existing.length > 0) {
        stats.skipped++;
        continue;
      }

      const leagueKey = (fc.league || 'unknown').toLowerCase();
      perLeague[leagueKey] ||= { count: 0, sumDelta: 0, maxDelta: 0, nulls: 0, nativeShapes: 0 };

      // Re-calculate composite with RM 2.0 weights
      const isNba = fc.league === 'nba';
      const weights = isNba ? config.weights.nba : config.weights.default;
      let newComposite = fc.composite_confidence;

      // If model_signals exist, recalculate with v2 weights
      if (fc.model_signals) {
        const legacySignals = getLegacyCompositeView(
          fc.model_signals,
          fc.composite_confidence ?? fc.confidence_score ?? 0.5,
          fc.forecast_data?.value_rating || 5,
        );
        const signals: any = legacySignals?.modelSignals || {};
        const grokScore = signals.grok?.confidence ?? fc.confidence_score ?? 0.5;
        let piffScore = 0.5;
        if (signals.piff?.avgEdge != null) {
          piffScore = Math.max(0, Math.min(1, 0.5 + (signals.piff.avgEdge / 100) * 2));
        }
        let digimonScore = 0.5;
        if (signals.digimon?.available && signals.digimon?.avgMissRate != null) {
          digimonScore = Math.max(0, Math.min(1, 1 - signals.digimon.avgMissRate));
        }

        newComposite = (
          grokScore * weights.grok +
          piffScore * weights.piff +
          digimonScore * weights.digimon
        );
        newComposite = Math.max(0, Math.min(1, newComposite));
      }

      const baseline = fc.composite_confidence ?? null;
      const delta = baseline == null || newComposite == null ? null : Math.abs(newComposite - baseline);
      perLeague[leagueKey].count += 1;
      if (delta != null) {
        perLeague[leagueKey].sumDelta += delta;
        perLeague[leagueKey].maxDelta = Math.max(perLeague[leagueKey].maxDelta, delta);
      } else {
        perLeague[leagueKey].nulls += 1;
      }
      if (isNativeIntelligenceResult(fc.model_signals)) {
        perLeague[leagueKey].nativeShapes += 1;
      }

      if (dryRun) {
        console.log(`  [WOULD COPY] ${fc.away_team} @ ${fc.home_team} (${fc.league}) — composite: ${newComposite?.toFixed(3)} delta=${delta?.toFixed(3) ?? 'null'}`);
        stats.copied++;
        continue;
      }

      await pool.query(`
        INSERT INTO rm_forecast_cache_v2 (
          event_id, league, home_team, away_team, forecast_data,
          confidence_score, starts_at, expires_at, odds_data, odds_updated_at,
          opening_moneyline, opening_spread, opening_total,
          composite_confidence, model_signals, composite_version,
          last_refresh_at, last_refresh_type, refresh_count,
          material_change, input_quality, model_version
        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,'rm_2.0')
        ON CONFLICT (event_id) DO NOTHING
      `, [
        fc.event_id, fc.league, fc.home_team, fc.away_team,
        JSON.stringify(fc.forecast_data), fc.confidence_score,
        fc.starts_at, fc.expires_at,
        fc.odds_data ? JSON.stringify(fc.odds_data) : null, fc.odds_updated_at,
        fc.opening_moneyline ? JSON.stringify(fc.opening_moneyline) : null,
        fc.opening_spread ? JSON.stringify(fc.opening_spread) : null,
        fc.opening_total ? JSON.stringify(fc.opening_total) : null,
        newComposite,
        fc.model_signals ? JSON.stringify(fc.model_signals) : null,
        config.composite_version,
        fc.last_refresh_at, fc.last_refresh_type, fc.refresh_count,
        fc.material_change ? JSON.stringify(fc.material_change) : null,
        fc.input_quality ? JSON.stringify(fc.input_quality) : null,
      ]);

      console.log(`  [COPIED] ${fc.away_team} @ ${fc.home_team} (${fc.league}) — composite: ${newComposite?.toFixed(3)}`);
      stats.copied++;
    } catch (err: any) {
      console.error(`  [FAIL] ${fc.event_id}: ${err.message}`);
      stats.failed++;
    }
  }

  // Also shadow accuracy records
  const { rows: rm1Accuracy } = await pool.query(`
    SELECT a.* FROM rm_forecast_accuracy a
    LEFT JOIN rm_forecast_accuracy_v2 v2 ON v2.event_id = a.event_id
    WHERE v2.id IS NULL
      AND a.resolved_at >= NOW() - INTERVAL '30 days'
  `);

  let accuracyCopied = 0;
  for (const acc of rm1Accuracy) {
    try {
      // Find the v2 forecast_id if it exists
      const { rows: v2fc } = await pool.query(
        'SELECT id FROM rm_forecast_cache_v2 WHERE event_id = $1',
        [acc.event_id]
      );

      if (dryRun) {
        accuracyCopied++;
        continue;
      }

      await pool.query(`
        INSERT INTO rm_forecast_accuracy_v2 (
          forecast_id, event_id, league, predicted_winner, actual_winner,
          predicted_spread, actual_spread, predicted_total, actual_total,
          accuracy_bucket, accuracy_pct, resolved_at, model_version
        ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'rm_2.0')
      `, [
        v2fc[0]?.id || null, acc.event_id, acc.league,
        acc.predicted_winner, acc.actual_winner,
        acc.predicted_spread, acc.actual_spread,
        acc.predicted_total, acc.actual_total,
        acc.accuracy_bucket, acc.accuracy_pct, acc.resolved_at,
      ]);
      accuracyCopied++;
    } catch (err: any) {
      // Skip duplicates silently
    }
  }

  console.log('\n' + '='.repeat(60));
  console.log('SHADOW RUN COMPLETE');
  console.log(`Forecasts — Copied: ${stats.copied} | Skipped: ${stats.skipped} | Failed: ${stats.failed}`);
  console.log(`Accuracy records copied: ${accuracyCopied}`);
  const perLeagueSummary = buildLeagueSummary(perLeague);
  for (const [league, drift] of Object.entries(perLeagueSummary)) {
    console.log(`[DRIFT] ${league} n=${drift.count} avg_delta=${drift.avgDelta.toFixed(4)} max_delta=${drift.maxDelta.toFixed(4)} nulls=${drift.nulls} null_rate=${drift.nullRate.toFixed(4)} native_shapes=${drift.nativeShapes}`);
  }
  console.log('='.repeat(60));

  return {
    enabled: true,
    dryRun,
    copied: stats.copied,
    skipped: stats.skipped,
    failed: stats.failed,
    accuracyCopied,
    perLeague: perLeagueSummary,
  };
}

// CLI entry point
if (require.main === module) {
  runShadow()
    .then(() => pool.end())
    .then(() => process.exit(0))
    .catch((err) => {
      console.error('Fatal:', err);
      process.exit(1);
    });
}
