/**
 * Forecast Runner — reusable generate/refresh functions extracted from daily-forecasts.ts.
 *
 * Called by both daily-forecasts.ts (batch cron) and forecast-scheduler.ts (per-event scheduler).
 */

import pool from '../db';
import { parseOdds, makeEventId, SgoEvent, deriveEventLifecycleStatus } from './sgo';
import { buildForecast } from './forecast-builder';
import { getCachedForecast, updateCachedOdds, resolveCanonicalEventId, RmForecast } from '../models/forecast';
import { archiveForecast, linkBlogPost } from './archive';
import { sanitizeForecastData } from './sanitizer';
import { getPiffPropsForGame, loadPiffPropsForDate } from './piff';
import { getDigimonForGame, loadDigimonPicksForDate } from './digimon';
import { getDvpForMatchup } from './dvp';
import { calculateComposite } from './composite-score';
import { generateBlogPost, generateSlug, saveBlogPost, sanitizeGeneratedBlogTitle } from './blog-generator';
import { runCompliancePipeline } from './compliance/pipeline';
import { generateHcwInsight } from './home-field-scout';
import { generateDvpInsight } from './dvp';
import { classifyNarrative, NarrativeContext } from './narrative-engine';
import { refreshIntelligence, mapToLegacyComposite, formatStoredModelSignals, getLegacyCompositeView } from './rie';
import { getCurrentEtDateKey, getEtDateKey } from '../lib/league-windows';
import { detectMaterialChange } from './material-change';

const RIE_ENABLED = process.env.RIE_ENABLED === 'true';

export interface GenerateResult {
  forecastId: string;
  confidence: number;
  compositeConfidence: number;
  success: boolean;
}

export interface RefreshResult {
  oldConfidence: number | null;
  newConfidence: number;
  confDelta: number;
  newOdds: { moneyline: any; spread: any; total: any };
  oldOdds: any;
  compositeVersion: string;
  stormCategory: number;
  refreshMode: 'composite_refresh' | 'full_rebuild';
}

function shouldRebuildForecastBody(params: {
  event: SgoEvent;
  cached: RmForecast;
  oldOdds: any;
  newOdds: any;
}): boolean {
  const cachedForecastData = typeof params.cached.forecast_data === 'string'
    ? JSON.parse(params.cached.forecast_data)
    : (params.cached.forecast_data || {});

  if (!cachedForecastData?.summary || !params.cached.narrative_metadata || !params.cached.input_quality) {
    return true;
  }

  if (deriveEventLifecycleStatus(params.event) !== 'scheduled') {
    return false;
  }

  return detectMaterialChange({
    preOdds: params.oldOdds,
    postOdds: params.newOdds,
    preConfidence: params.cached.composite_confidence || params.cached.confidence_score || 0,
    postConfidence: params.cached.composite_confidence || params.cached.confidence_score || 0,
  }).isMaterial;
}

/**
 * Generate a new forecast for an event (no existing forecast).
 * Wraps the "No forecast → generate one" path from daily-forecasts.ts.
 */
export async function generateForEvent(
  event: SgoEvent,
  league: string,
  label: string = '',
): Promise<GenerateResult> {
  const homeTeam = event.teams.home.names.long;
  const awayTeam = event.teams.away.names.long;

  const result = await buildForecast(event, league);
  const conf = result.composite?.compositeConfidence ?? result.confidenceScore;
  console.log(`  [OK] ${label} — ${result.forecast.winner_pick} (${Math.round(conf * 100)}% conf)`);

  // Snapshot opening lines from rm_events
  try {
    const homeShort = event.teams.home.names.short || '';
    const awayShort = event.teams.away.names.short || '';
    const startsAt = event.status?.startsAt || '';
    let evDateStr = '';
    if (startsAt) {
      const d = new Date(startsAt);
      const etStr = d.toLocaleString('en-US', { timeZone: 'America/New_York' });
      const et = new Date(etStr);
      evDateStr = `${et.getFullYear()}-${String(et.getMonth() + 1).padStart(2, '0')}-${String(et.getDate()).padStart(2, '0')}`;
    }
    const rmEventId = evDateStr ? makeEventId(league, awayShort, homeShort, evDateStr) : '';
    if (rmEventId) {
      const { rows: evRows } = await pool.query(
        'SELECT opening_moneyline, opening_spread, opening_total FROM rm_events WHERE event_id = $1',
        [rmEventId]
      );
      if (evRows[0] && (evRows[0].opening_moneyline || evRows[0].opening_spread || evRows[0].opening_total)) {
        await pool.query(
          `UPDATE rm_forecast_cache
           SET opening_moneyline = $1, opening_spread = $2, opening_total = $3
           WHERE event_id = $4`,
          [
            evRows[0].opening_moneyline ? JSON.stringify(evRows[0].opening_moneyline) : null,
            evRows[0].opening_spread ? JSON.stringify(evRows[0].opening_spread) : null,
            evRows[0].opening_total ? JSON.stringify(evRows[0].opening_total) : null,
            event.eventID,
          ]
        );
      }
    }
  } catch (snapErr: any) {
    console.error(`  [SNAP] Opening line snapshot failed for ${event.eventID}: ${snapErr.message}`);
  }

  // Archive + blog generation
  try {
    const probRaw = (result.composite?.compositeConfidence ?? result.confidenceScore) * 100;
    const archivedId = await archiveForecast({
      forecastId: result.forecastId,
      eventId: event.eventID,
      league,
      homeTeam,
      awayTeam,
      startsAt: event.status.startsAt || new Date().toISOString(),
      winnerPick: result.forecast.winner_pick,
      probabilityRaw: probRaw,
      forecastData: result.forecast,
      confidenceScore: result.rawConfidenceScore ?? result.confidenceScore,
      compositeConfidence: result.composite?.compositeConfidence ?? result.confidenceScore,
      oddsData: result.odds,
      modelSignals: result.composite,
    });

    if (archivedId) {
      console.log(`  [ARCHIVED] ${label} (bucket: ${Math.floor(probRaw / 10) * 10})`);

      try {
        const sanitized = sanitizeForecastData(result.forecast);
        const gameDate = event.status.startsAt
          ? new Date(event.status.startsAt).toISOString().split('T')[0]
          : new Date().toISOString().split('T')[0];
        const slug = generateSlug(league, homeTeam, awayTeam, gameDate);

        // Build narrative classification for blog tone alignment
        let narrativeClassification;
        try {
          const liveOdds = parseOdds(event);
          const blogNarrativeCtx: NarrativeContext = {
            homeTeam, awayTeam, league,
            marketSpread: liveOdds.spread?.home?.line ?? null,
            projectedMargin: result.forecast.projected_margin,
            marketTotal: liveOdds.total?.over?.line ?? null,
            projectedTotal: result.forecast.projected_total_points,
            confidence: result.composite?.compositeConfidence ?? result.confidenceScore,
            valueRating: result.forecast.value_rating || 5,
            stormCategory: result.composite?.stormCategory || 2,
            forecastSide: result.forecast.forecast_side || homeTeam,
            spreadEdge: result.forecast.spread_edge || 0,
            totalDirection: result.forecast.total_direction || 'NONE',
            totalEdge: result.forecast.total_edge || 0,
          };
          narrativeClassification = classifyNarrative(blogNarrativeCtx);
        } catch { /* non-fatal — blog generates without classification */ }

        const blogContent = await generateBlogPost({
          league,
          homeTeam,
          awayTeam,
          startsAt: event.status.startsAt || new Date().toISOString(),
          sanitizedSummary: sanitized.summary,
          keyFactors: sanitized.keyFactors,
          spreadAnalysis: sanitized.spreadAnalysis,
          totalAnalysis: sanitized.totalAnalysis,
          historicalTrend: sanitized.historicalTrend,
          injuryNotes: sanitized.injuryNotes,
          oddsData: result.odds,
          narrativeClassification,
        });

        // Compliance guard
        let blogStatus = 'published';
        try {
          const complianceResult = await runCompliancePipeline({
            propertyId: 'rainmaker_web',
            contentType: 'blog_post',
            contentSource: 'daily-forecasts-worker',
            fields: {
              title: blogContent.title,
              metaDescription: blogContent.metaDescription,
              content: blogContent.content,
              excerpt: blogContent.excerpt,
            },
          });

          if (complianceResult.finalStatus === 'BLOCK') {
            blogStatus = 'draft';
            console.log(`  [COMPLIANCE-BLOCK] ${label} — severity ${complianceResult.severityScore}, saved as draft`);
          } else if (complianceResult.rewrittenText && complianceResult.rewritePassed) {
            try {
              const rewritten = JSON.parse(complianceResult.rewrittenText);
              if (rewritten.title) blogContent.title = sanitizeGeneratedBlogTitle(rewritten.title, { homeTeam, awayTeam, league });
              if (rewritten.metaDescription) blogContent.metaDescription = rewritten.metaDescription;
              if (rewritten.content) blogContent.content = rewritten.content;
              if (rewritten.excerpt) blogContent.excerpt = rewritten.excerpt;
              console.log(`  [COMPLIANCE-REWRITE] ${label} — auto-fixed ${complianceResult.findings.length} violations`);
            } catch { /* use original if parse fails */ }
          }
        } catch (compErr: any) {
          console.error(`  [COMPLIANCE-ERR] ${label}: ${compErr.message} — publishing anyway`);
        }

        const blogId = await saveBlogPost({
          archivedForecastId: archivedId,
          slug,
          sport: league.toLowerCase(),
          startsAt: event.status.startsAt || new Date().toISOString(),
          title: blogContent.title,
          metaDescription: blogContent.metaDescription,
          content: blogContent.content,
          excerpt: blogContent.excerpt,
          tags: blogContent.tags,
          homeTeam,
          awayTeam,
          gameDate,
          status: blogStatus,
        });

        if (blogStatus === 'draft') {
          try {
            await pool.query(
              `UPDATE rm_compliance_scans SET content_id = $1
               WHERE content_source = 'daily-forecasts-worker' AND content_id IS NULL
               AND created_at > NOW() - INTERVAL '5 minutes'
               ORDER BY created_at DESC LIMIT 1`,
              [blogId]
            );
          } catch { /* non-critical */ }
        }

        await linkBlogPost(archivedId, blogId);
        console.log(`  [BLOG] ${label} → /rain-wire/${league}/${slug} (${blogStatus})`);
      } catch (blogErr: any) {
        console.error(`  [BLOG-FAIL] ${label}: ${blogErr.message}`);
      }
    }
  } catch (archiveErr: any) {
    console.error(`  [ARCHIVE-FAIL] ${label}: ${archiveErr.message}`);
  }

  // ─── PRE-GENERATE SIGNAL INSIGHTS (HCW + DVP) ───
  // RULE: Home Crowd + Weather and Defense vs Position insights must ALWAYS be
  // generated at forecast creation time. No forecast should ever lack these.
  try {
    const homeShortName = event.teams.home.names.short || homeTeam.split(' ').pop() || '';
    const awayShortName = event.teams.away.names.short || awayTeam.split(' ').pop() || '';

    // 1) Home Crowd + Weather
    try {
      const hcwData = await generateHcwInsight({
        homeTeam, awayTeam, league,
        homeShort: homeShortName, awayShort: awayShortName,
        startsAt: event.status.startsAt || new Date().toISOString(),
        eventId: event.eventID,
      });
      if (hcwData) {
        await pool.query(
          `INSERT INTO rm_insight_cache (event_id, insight_type, league, home_team, away_team, insight_data)
           VALUES ($1, 'HCW', $2, $3, $4, $5)
           ON CONFLICT (event_id, insight_type) DO UPDATE SET insight_data = $5, created_at = NOW()`,
          [event.eventID, league, homeTeam, awayTeam, JSON.stringify(hcwData)]
        );
        console.log(`  [HCW] ${label} — pre-generated (impact: ${hcwData.impact_rating || 'N/A'})`);
      }
    } catch (hcwErr: any) {
      console.error(`  [HCW-FAIL] ${label}: ${hcwErr.message}`);
    }

    // 2) Defense vs Position
    try {
      const dvpData = await generateDvpInsight(homeTeam, awayTeam, league, homeShortName, awayShortName);
      if (dvpData) {
        await pool.query(
          `INSERT INTO rm_insight_cache (event_id, insight_type, league, home_team, away_team, insight_data)
           VALUES ($1, 'DVP', $2, $3, $4, $5)
           ON CONFLICT (event_id, insight_type) DO UPDATE SET insight_data = $5, created_at = NOW()`,
          [event.eventID, league, homeTeam, awayTeam, JSON.stringify(dvpData)]
        );
        console.log(`  [DVP] ${label} — pre-generated`);
      }
    } catch (dvpErr: any) {
      console.error(`  [DVP-FAIL] ${label}: ${dvpErr.message}`);
    }
  } catch (insightErr: any) {
    console.error(`  [INSIGHT-FAIL] ${label}: ${insightErr.message}`);
  }

  return {
    forecastId: result.forecastId,
    confidence: result.confidenceScore,
    compositeConfidence: result.composite?.compositeConfidence ?? result.confidenceScore,
    success: true,
  };
}

/**
 * Refresh an existing forecast — update odds and either re-crunch composite signals
 * or rebuild the full forecast body when the line move is meaningful enough.
 * Wraps the "CRUNCH THE NUMBERS" path from daily-forecasts.ts.
 * Returns pre/post state for material change detection.
 */
export async function refreshForEvent(
  event: SgoEvent,
  league: string,
  cached: RmForecast,
  label: string = '',
): Promise<RefreshResult> {
  const liveOdds = parseOdds(event);
  const oddsPayload = {
    moneyline: liveOdds.moneyline,
    spread: liveOdds.spread,
    total: liveOdds.total,
  };

  const oldOdds = cached.odds_data;
  const oldConf = cached.composite_confidence;

  if (shouldRebuildForecastBody({ event, cached, oldOdds, newOdds: oddsPayload })) {
    const rebuilt = await buildForecast(event, league, { ignoreCache: true });
    const rebuiltConfidence = rebuilt.composite?.compositeConfidence ?? rebuilt.confidenceScore;
    const confDelta = oldConf ? Math.abs(rebuiltConfidence - oldConf) : 0;

    console.log(`  [REBUILT] ${label} — full forecast body refreshed`);

    return {
      oldConfidence: oldConf,
      newConfidence: rebuiltConfidence,
      confDelta,
      newOdds: rebuilt.odds,
      oldOdds,
      compositeVersion: rebuilt.composite?.compositeVersion || cached.composite_version || 'v1',
      stormCategory: rebuilt.composite?.stormCategory || 2,
      refreshMode: 'full_rebuild',
    };
  }

  await updateCachedOdds(event.eventID, oddsPayload);

  // Re-crunch composite with latest model signals
  const homeShort = event.teams.home.names.short || '';
  const awayShort = event.teams.away.names.short || '';
  const homeTeam = event.teams.home.names.long;
  const awayTeam = event.teams.away.names.long;

  const cachedForecastData = typeof cached.forecast_data === 'string'
    ? JSON.parse(cached.forecast_data)
    : (cached.forecast_data || {});
  const cachedLegacy = getLegacyCompositeView(
    cached.model_signals,
    cached.confidence_score || cachedForecastData?.confidence || cached.composite_confidence || 0.5,
    cachedForecastData?.value_rating || 5,
  );
  const grokConf = cachedLegacy?.modelSignals?.grok?.confidence
    || cached.confidence_score
    || cachedForecastData?.confidence
    || cached.composite_confidence
    || 0.5;
  const grokValue = cachedLegacy?.modelSignals?.grok?.valueRating
    || cachedForecastData?.value_rating
    || 5;

  let composite;
  let storedModelSignals;
  let intel = null;

  if (RIE_ENABLED) {
    // ── RIE pipeline: unified refresh ──
    intel = await refreshIntelligence({
      event,
      league,
      homeTeam,
      awayTeam,
      homeShort,
      awayShort,
      startsAt: event.status?.startsAt || new Date().toISOString(),
      eventId: event.eventID,
      cachedGrokConfidence: grokConf,
      cachedGrokValueRating: grokValue,
    });
    composite = mapToLegacyComposite(intel, grokConf, grokValue);
    storedModelSignals = formatStoredModelSignals(intel, composite);
  } else {
    // ── Legacy pipeline ──
    const eventDateKey = getEtDateKey(event.status?.startsAt || '') || getCurrentEtDateKey();
    const piffProps = homeShort && awayShort
      ? getPiffPropsForGame(homeShort, awayShort, loadPiffPropsForDate(eventDateKey), league)
      : [];
    const digimonPicks = homeShort && awayShort && league === 'nba'
      ? getDigimonForGame(homeShort, awayShort, loadDigimonPicksForDate(eventDateKey))
      : [];
    const dvpData = await getDvpForMatchup(homeShort, awayShort, league);

    composite = calculateComposite({
      grokConfidence: grokConf,
      grokValueRating: grokValue,
      piffProps,
      digimonPicks,
      dvp: dvpData,
      league,
    });
    storedModelSignals = composite;
  }

  const newConf = composite.compositeConfidence;
  const confDelta = oldConf ? Math.abs(newConf - oldConf) : 0;

  await pool.query(
    `UPDATE rm_forecast_cache
     SET composite_confidence = $1,
         model_signals = $2,
         composite_version = $3,
         forecast_data = jsonb_set(
           COALESCE(forecast_data, '{}'::jsonb),
           '{confidence}',
           to_jsonb(($5)::float8),
           true
         )
     WHERE id = $4`,
    [newConf, JSON.stringify(storedModelSignals || composite), composite.compositeVersion, cached.id, newConf]
  );

  if (confDelta > 0.03) {
    console.log(`  [CRUNCHED] ${label} — conf ${oldConf ? Math.round(oldConf * 100) : '?'}% → ${Math.round(newConf * 100)}% (Δ${Math.round(confDelta * 100)}%) Cat ${composite.stormCategory}`);
  } else {
    console.log(`  [ODDS+CRUNCH] ${label} — conf steady ${Math.round(newConf * 100)}%`);
  }

  return {
    oldConfidence: oldConf,
    newConfidence: newConf,
    confDelta,
    newOdds: oddsPayload,
    oldOdds,
    compositeVersion: composite.compositeVersion,
    stormCategory: composite.stormCategory,
    refreshMode: 'composite_refresh',
  };
}
