/**
 * Free Pick of the Day — selects and serves the highest-confidence forecast daily.
 *
 * GET /api/free-pick — public, no auth required
 */

import { Router, Request, Response } from 'express';
import pool from '../db';
import { getLegacyCompositeView } from '../services/rie';
import { hasDvpData } from '../services/dvp';
import { hasHcwData } from '../services/home-field-scout';
import { deduplicateProps } from '../services/prop-dedup';
import { sanitizeMoneylinePair } from '../services/sgo';
import { normalizePublicForecastNarrative } from '../services/public-forecast-normalizer';

const router = Router();
const STEAM_UNLOCK_ENABLED = () => process.env.FEATURE_STEAM_UNLOCK === 'true';
const SHARP_UNLOCK_ENABLED = () => process.env.FEATURE_SHARP_UNLOCK === 'true';
const DVP_UNLOCK_ENABLED = () => process.env.FEATURE_DVP_UNLOCK !== 'false';
const HCW_UNLOCK_ENABLED = () => process.env.FEATURE_HCW_UNLOCK !== 'false';
const FREE_PICK_CONFIDENCE_BAND = 0.05;
const FREE_PICK_MIN_UPGRADE_SCORE_DELTA = 100;
const PROP_DEDUP_ENABLED = () => process.env.PROP_DEDUP_ENABLED === 'true';
const PI_SIBLING_SUPPRESSION_ENABLED = () => process.env.PI_SIBLING_SUPPRESSION_ENABLED === 'true';
const PRICED_PROP_COUNT_SUBQUERY = `LEFT JOIN (
  SELECT event_id,
         COALESCE(NULLIF(active_priced_count, 0), stale_priced_count, 0) AS priced_prop_count
  FROM (
    SELECT event_id,
           COUNT(*) FILTER (
             WHERE forecast_type = 'PLAYER_PROP'
               AND status = 'ACTIVE'
               AND COALESCE(forecast_payload->>'odds', forecast_payload->'signal_table_row'->>'odds', '') <> ''
           )::int AS active_priced_count,
           COUNT(*) FILTER (
             WHERE forecast_type = 'PLAYER_PROP'
               AND status = 'STALE'
               AND expires_at > NOW()
               AND COALESCE(forecast_payload->>'odds', forecast_payload->'signal_table_row'->>'odds', '') <> ''
           )::int AS stale_priced_count
    FROM rm_forecast_precomputed
    GROUP BY event_id
  ) priced_props
) pp ON pp.event_id =`;

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

function hasStarted(startsAt: string | Date | null | undefined): boolean {
  if (!startsAt) return false;
  const parsed = new Date(startsAt);
  if (Number.isNaN(parsed.getTime())) return false;
  return parsed.getTime() <= Date.now();
}

function sanitizeOddsBundle(odds: any): any {
  if (!odds || typeof odds !== 'object') return odds;
  return {
    ...odds,
    moneyline: sanitizeMoneylinePair(odds.moneyline),
  };
}

function buildTeamForecastEngineField(cached: any, league: string | null | undefined): Record<string, any> {
  const normalizedLeague = String(league || cached?.league || '').toLowerCase();
  const forecastData = typeof cached?.forecast_data === 'string'
    ? JSON.parse(cached.forecast_data)
    : (cached?.forecast_data || {});

  if (normalizedLeague !== 'mlb') {
    return {};
  }

  const directApplied = Boolean(forecastData?.mlb_direct_model?.applied);
  if (directApplied) {
    return {
      teamForecastEngine: {
        code: 'mlb_direct_moneyline',
        label: 'MLB Direct Model',
        detail: 'Direct full-game moneyline lock passed the live abstain gate.',
        applied: true,
      },
    };
  }

  return {
    teamForecastEngine: {
      code: 'mlb_baseline_fallback',
      label: 'MLB Baseline Fallback',
      detail: 'Direct model abstained or shaped as F5-only, so the live forecast fell back to the baseline team engine.',
      applied: false,
    },
  };
}

function hasPublicPlayerPropPricing(payload: any): boolean {
  const directOdds = payload?.odds;
  const tableOdds = payload?.signal_table_row?.odds;
  const hasFiniteOdds = (value: any) => value !== null && value !== undefined && value !== '' && Number.isFinite(Number(value));
  return hasFiniteOdds(directOdds) || hasFiniteOdds(tableOdds);
}

async function getFreePickPlayerPropsPreview(eventId: string) {
  const { rows } = await pool.query(
    `SELECT id, player_name, team_id, team_side, league, confidence_score, forecast_payload, status
     FROM rm_forecast_precomputed
     WHERE event_id = $1
       AND forecast_type = 'PLAYER_PROP'
       AND (
         status = 'ACTIVE'
         OR (status = 'STALE' AND expires_at > NOW())
       )
     ORDER BY
       CASE WHEN status = 'ACTIVE' THEN 0 ELSE 1 END,
       confidence_score DESC NULLS LAST`,
    [eventId]
  );

  const filterVisibleRows = async (candidateRows: any[]) => {
    let visibleRows = candidateRows;
    if (visibleRows.length > 0) {
      const outPlayers = await getOutPlayerNames(visibleRows[0].league);
      if (outPlayers.size > 0) {
        visibleRows = visibleRows.filter((row: any) => !outPlayers.has(row.player_name?.toLowerCase()));
      }
    }

    visibleRows = await filterSiblingSuppressedRows(visibleRows, eventId);
    visibleRows = visibleRows.filter((row: any) => hasPublicPlayerPropPricing(row.forecast_payload));
    if (PROP_DEDUP_ENABLED()) {
      visibleRows = deduplicateProps(visibleRows);
    }
    return visibleRows;
  };

  const activeRows = rows.filter((row: any) => String(row.status || 'ACTIVE').toUpperCase() === 'ACTIVE');
  const staleRows = rows.filter((row: any) => String(row.status || 'ACTIVE').toUpperCase() === 'STALE');
  let visibleRows = await filterVisibleRows(activeRows);
  if (visibleRows.length === 0) {
    visibleRows = await filterVisibleRows(staleRows);
  }

  return visibleRows.map((row: any) => ({
    assetId: row.id,
    player: row.player_name,
    team: row.team_id,
    teamSide: row.team_side,
    prop: row.forecast_payload?.prop || null,
    locked: true,
  }));
}

function hasRealInsightSignal(text: any, type: 'steam' | 'sharp'): boolean {
  if (!text || typeof text !== 'string') return false;
  const trimmed = text.trim();
  if (!trimmed) return false;

  const genericNoSignalPatterns = [
    /^no (line move|sharp|clear sharp|sharp money|line movement|significant line|notable movement)/i,
    /^(insufficient|without historical|with no tracked|no sharp move|no sharp money|no line move)/i,
    /^markets? (?:holding|hold|held|remain|remained|stayed) steady/i,
    /^markets? stable/i,
    /^lines? (?:have )?(?:remained|stayed) stable/i,
    /\bno notable (?:shift|movement|move)\b/i,
    /\bstable pricing\b/i,
    /\bconsensus on\b/i,
    /\bno clear sharp positioning\b/i,
    /\bno sharp moves detected\b/i,
    /\bno sharp market interest detected\b/i,
  ];
  if (genericNoSignalPatterns.some((pattern) => pattern.test(trimmed))) return false;

  const hasNumericEvidence = /(?:[+-]\d{2,4}|\b\d+(?:\.\d+)?\b)/.test(trimmed);
  const hasMovementEvidence = /\b(move|moved|moving|steam|steamed|shift|shifted|buyback|drift|drifted|tick|ticked|open(?:ed|ing)?|reopen(?:ed|ing)?|bet down|bet up|took money)\b/i.test(trimmed);
  const hasSharpEvidence = /\b(sharp|respected|syndicate|professional money|buyback)\b/i.test(trimmed);

  if (type === 'steam') {
    return hasMovementEvidence && hasNumericEvidence;
  }
  return hasSharpEvidence && (hasNumericEvidence || hasMovementEvidence);
}

async function getOutPlayerNames(league: string): Promise<Set<string>> {
  const { rows } = await pool.query(`
    SELECT LOWER("playerName") AS name
    FROM "PlayerInjury"
    WHERE LOWER(league) = LOWER($1)
      AND status IN ('Out', 'IR', 'Suspension')
    GROUP BY LOWER("playerName")
    HAVING COUNT(DISTINCT source) >= 2
  `, [league]);
  return new Set(rows.map((row: any) => row.name));
}

async function filterSiblingSuppressedRows(rows: any[], eventId: string): Promise<any[]> {
  if (!PI_SIBLING_SUPPRESSION_ENABLED() || rows.length === 0) return rows;

  const { rows: suppressed } = await pool.query(
    `SELECT forecast_asset_id
     FROM pi_prop_eligibility
     WHERE game_id = $1
       AND publish_allowed = false
       AND suppress_reason = 'SUPPRESSED_SIBLING'`,
    [eventId],
  ).catch(() => ({ rows: [] as any[] }));

  if (suppressed.length === 0) return rows;
  const suppressedIds = new Set(suppressed.map((row: any) => row.forecast_asset_id));
  return rows.filter((row: any) => !suppressedIds.has(row.id));
}

function getFreePickContentScore(candidate: any): number {
  const pricedPropCount = Number(candidate.priced_prop_count || 0);
  const forecastData = candidate.forecast_data || {};
  const steam = STEAM_UNLOCK_ENABLED() && hasRealInsightSignal(forecastData.line_movement_analysis, 'steam');
  const sharp = SHARP_UNLOCK_ENABLED() && hasRealInsightSignal(forecastData.sharp_money_indicator, 'sharp');
  const dvp = DVP_UNLOCK_ENABLED() && !!candidate.league && ['nba', 'wnba', 'nfl', 'nhl', 'mlb', 'ncaab', 'ncaawb'].includes(String(candidate.league).toLowerCase());
  const hcw = HCW_UNLOCK_ENABLED() && !!candidate.league && hasHcwData(candidate.league);

  return (Math.min(pricedPropCount, 3) * 100)
    + (steam ? 30 : 0)
    + (sharp ? 30 : 0)
    + (dvp ? 10 : 0)
    + (hcw ? 10 : 0);
}

function shouldRefreshFreePick(candidate: any): boolean {
  return getFreePickContentScore(candidate) === 0;
}

function pickBestFreePickCandidate(rows: any[]) {
  if (rows.length === 0) return null;

  const maxConfidence = Number(rows[0].confidence_score || 0);
  const confidenceBand = rows.filter((row: any) => Number(row.confidence_score || 0) >= maxConfidence - FREE_PICK_CONFIDENCE_BAND);
  const ranked = [...confidenceBand].sort((a: any, b: any) => {
    const contentDelta = getFreePickContentScore(b) - getFreePickContentScore(a);
    if (contentDelta !== 0) return contentDelta;
    const confidenceDelta = Number(b.confidence_score || 0) - Number(a.confidence_score || 0);
    if (confidenceDelta !== 0) return confidenceDelta;
    return new Date(a.starts_at).getTime() - new Date(b.starts_at).getTime();
  });
  return ranked[0] || rows[0];
}

function shouldUpgradeFreePick(savedPick: any, candidate: any): boolean {
  if (!savedPick || !candidate) return false;
  if (savedPick.event_id === candidate.event_id) return false;

  const savedScore = getFreePickContentScore(savedPick);
  const candidateScore = getFreePickContentScore(candidate);
  if (candidateScore < savedScore + FREE_PICK_MIN_UPGRADE_SCORE_DELTA) return false;

  const savedConfidence = Number(savedPick.confidence_score || 0);
  const candidateConfidence = Number(candidate.confidence_score || 0);
  return candidateConfidence >= savedConfidence - FREE_PICK_CONFIDENCE_BAND;
}

async function queryFreePickCandidates(dateET: string) {
  const baseQuery = `SELECT fc.event_id, fc.league, fc.confidence_score,
            fc.home_team, fc.away_team, fc.starts_at, fc.forecast_data,
            e.home_short, e.away_short,
            COALESCE(pp.priced_prop_count, 0) AS priced_prop_count
     FROM rm_forecast_cache fc
     LEFT JOIN rm_events e ON fc.event_id = e.event_id
     ${PRICED_PROP_COUNT_SUBQUERY} fc.event_id
     WHERE (fc.starts_at AT TIME ZONE 'America/New_York')::date = $1::date
       AND fc.confidence_score IS NOT NULL`;

  const { rows } = await pool.query(
    `${baseQuery}
       AND fc.starts_at > NOW()
     ORDER BY fc.confidence_score DESC, fc.starts_at ASC
     LIMIT 25`,
    [dateET]
  );
  if (rows.length > 0) return rows;

  const fallback = await pool.query(
    `${baseQuery}
     ORDER BY fc.confidence_score DESC, fc.starts_at ASC
     LIMIT 25`,
    [dateET]
  );
  return fallback.rows;
}

/**
 * Compute the best forecast for today and persist it.
 * Selection: highest-confidence forecast inside a tight band, then prefer richer
 * public content so the free pick does not land on a dead board.
 */
async function computeFreePick(dateET: string, candidateRows?: any[]) {
  const rows = candidateRows || await queryFreePickCandidates(dateET);
  if (rows.length === 0) return null;
  const best = pickBestFreePickCandidate(rows);
  if (!best) return null;
  const matchup = `${best.away_team} @ ${best.home_team}`;

  await pool.query(
    `INSERT INTO rm_free_pick (pick_date, event_id, league, confidence_score, home_team, away_team, home_short, away_short, matchup)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
     ON CONFLICT (pick_date) DO UPDATE SET
       event_id = EXCLUDED.event_id,
       league = EXCLUDED.league,
       confidence_score = EXCLUDED.confidence_score,
       home_team = EXCLUDED.home_team,
       away_team = EXCLUDED.away_team,
       home_short = EXCLUDED.home_short,
       away_short = EXCLUDED.away_short,
       matchup = EXCLUDED.matchup,
       updated_at = NOW()`,
    [dateET, best.event_id, best.league, best.confidence_score, best.home_team, best.away_team, best.home_short || '', best.away_short || '', matchup]
  );

  return { ...best, matchup };
}

async function resolveTodaysFreePick(dateET: string, savedPick: any | null) {
  const candidateRows = await queryFreePickCandidates(dateET);
  const bestCandidate = savedPick ? pickBestFreePickCandidate(candidateRows) : null;
  const shouldRecompute = !savedPick
    || hasStarted(savedPick.starts_at)
    || shouldRefreshFreePick(savedPick)
    || shouldUpgradeFreePick(savedPick, bestCandidate);

  if (!shouldRecompute) {
    return savedPick;
  }

  const computed = await computeFreePick(dateET, candidateRows);
  return computed || savedPick;
}

// GET /api/free-pick
router.get('/', async (_req: Request, res: Response) => {
  try {
    const dateET = todayET();

    // Check for existing pick — join events + forecast cache for odds/lines
    const { rows } = await pool.query(
      `SELECT fp.*,
              COALESCE(e.starts_at, fc.starts_at) as starts_at,
              fc.odds_updated_at as odds_updated_at,
              e.moneyline, e.spread, e.total,
              e.opening_moneyline, e.opening_spread, e.opening_total,
              fc.forecast_data,
              fc.league,
              COALESCE(pp.priced_prop_count, 0) AS priced_prop_count
       FROM rm_free_pick fp
       LEFT JOIN rm_events e ON fp.event_id = e.event_id
       LEFT JOIN rm_forecast_cache fc ON fp.event_id = fc.event_id
       ${PRICED_PROP_COUNT_SUBQUERY} fp.event_id
       WHERE fp.pick_date = $1`,
      [dateET]
    );

    let pick = rows[0] || null;
    pick = await resolveTodaysFreePick(dateET, pick);

    if (!pick) {
      res.json({ date: dateET, pick: null });
      return;
    }

    // BENCHMARK RULE: Use server-computed spread_edge from forecast_data
    const conf = pick.confidence_score || 0;
    const fcData = pick.forecast_data || {};
    let edge: number | null = fcData.spread_edge ?? null;
    if (edge == null) {
      // Fallback for legacy forecasts: compute from projected_margin vs market spread
      const projMargin = fcData.projected_margin;
      const sp = pick.spread || {};
      const marketHomeSpread = sp.home?.line ?? (typeof sp.home === 'string' ? parseFloat(sp.home) : null);
      if (projMargin != null && marketHomeSpread != null) {
        edge = Math.round(Math.abs(marketHomeSpread + projMargin) * 10) / 10;
      } else if (conf > 0) {
        edge = Math.round((conf - 0.5) * 120) / 10;
      }
    }

    res.json({
      date: dateET,
      pick: {
        eventId: pick.event_id,
        league: pick.league,
        confidence: Math.round((pick.confidence_score || 0) * 1000) / 10,
        edge: edge,
        homeTeam: pick.home_team,
        awayTeam: pick.away_team,
        homeShort: pick.home_short,
        awayShort: pick.away_short,
        matchup: pick.matchup,
        startsAt: pick.starts_at || null,
        oddsUpdatedAt: pick.odds_updated_at || null,
        forecastSide: fcData.forecast_side || fcData.winner_pick || null,
        winnerPick: fcData.winner_pick || null,
        projectedWinner: fcData.projected_winner || fcData.winner_pick || null,
        projectedMargin: fcData.projected_margin ?? null,
        totalDirection: fcData.total_direction || 'NONE',
        totalEdge: fcData.total_edge ?? null,
        projectedTotalPoints: fcData.projected_total_points ?? null,
        spread: pick.spread || null,
        openingSpread: pick.opening_spread || null,
        moneyline: pick.moneyline || null,
        openingMoneyline: pick.opening_moneyline || null,
      },
    });
  } catch (err) {
    console.error('Free forecast error:', err);
    res.status(500).json({ error: 'Failed to load free forecast' });
  }
});

// GET /api/free-pick/content — public, returns full forecast for free pick (no auth, no deduction)
router.get('/content', async (_req: Request, res: Response) => {
  try {
    const dateET = todayET();

    // Get today's free pick
    const { rows: pickRows } = await pool.query(
      `SELECT fp.event_id, COALESCE(e.starts_at, fc.starts_at) as starts_at,
              fc.league, fc.forecast_data,
              COALESCE(pp.priced_prop_count, 0) AS priced_prop_count
       FROM rm_free_pick fp
       LEFT JOIN rm_events e ON fp.event_id = e.event_id
       LEFT JOIN rm_forecast_cache fc ON fp.event_id = fc.event_id
       ${PRICED_PROP_COUNT_SUBQUERY} fp.event_id
       WHERE fp.pick_date = $1`,
      [dateET]
    );

    const resolvedPick = await resolveTodaysFreePick(dateET, pickRows[0] || null);
    if (!resolvedPick) {
      res.json({ forecast: null });
      return;
    }
    const eventId = resolvedPick.event_id;

    // Fetch the full forecast from cache — same fields as authenticated endpoint
    const { rows: fcRows } = await pool.query(
      `SELECT fc.forecast_data, fc.confidence_score, fc.composite_confidence,
              fc.model_signals, fc.composite_version,
              fc.league, fc.home_team, fc.away_team, fc.odds_data,
              fc.opening_moneyline, fc.opening_spread, fc.opening_total,
              fc.created_at,
              e.home_short, e.away_short, e.moneyline, e.spread, e.total
       FROM rm_forecast_cache fc
       LEFT JOIN rm_events e ON fc.event_id = e.event_id
       WHERE fc.event_id = $1`,
      [eventId]
    );

    if (fcRows.length === 0) {
      res.json({ forecast: null });
      return;
    }

    const fc = fcRows[0];
    const legacyComposite = getLegacyCompositeView(
      fc.model_signals,
      fc.composite_confidence || fc.confidence_score || 0.5,
      fc.forecast_data?.value_rating || 5,
    );
    const liveOdds = sanitizeOddsBundle({
      moneyline: fc.moneyline || fc.odds_data?.moneyline || { home: null, away: null },
      spread: fc.spread || fc.odds_data?.spread || { home: null, away: null },
      total: fc.total || fc.odds_data?.total || { over: null, under: null },
    });
    const forecast = normalizePublicForecastNarrative(fc.forecast_data || {}, {
      homeTeam: fc.home_team,
      awayTeam: fc.away_team,
      homeShort: fc.home_short,
      awayShort: fc.away_short,
      odds: liveOdds,
    });

    // Build opening lines (same as authenticated endpoint)
    const openingLines: any = {};
    if (fc.opening_moneyline) openingLines.moneyline = sanitizeMoneylinePair(fc.opening_moneyline);
    if (fc.opening_spread) openingLines.spread = fc.opening_spread;
    if (fc.opening_total) openingLines.total = fc.opening_total;

    // Build model lines from forecast data — must match buildModelLines() in forecast.ts
    const fd = fc.forecast_data || {};
    const proj = fd.projected_lines;
    const modelLines = proj ? {
      moneyline: proj.moneyline || null,
      spread: proj.spread || null,
      total: proj.total ?? null,
    } : null;
    const playerPropsAssets = await getFreePickPlayerPropsPreview(eventId);
    const insightAvailability = {
      steam: STEAM_UNLOCK_ENABLED() && hasRealInsightSignal(fd.line_movement_analysis, 'steam'),
      sharp: SHARP_UNLOCK_ENABLED() && hasRealInsightSignal(fd.sharp_money_indicator, 'sharp'),
      dvp: DVP_UNLOCK_ENABLED() && !!fc.league && await hasDvpData(fc.league),
      hcw: HCW_UNLOCK_ENABLED() && !!fc.league && hasHcwData(fc.league),
    };

    res.json({
      forecast,
      confidence: fc.composite_confidence || fc.confidence_score || 0.5,
      homeTeam: fc.home_team || '',
      awayTeam: fc.away_team || '',
      homeShort: fc.home_short || '',
      awayShort: fc.away_short || '',
      league: fc.league || '',
      odds: liveOdds,
      openingLines: Object.keys(openingLines).length > 0 ? openingLines : null,
      modelLines: modelLines,
      compositeConfidence: fc.composite_confidence || null,
      modelSignals: legacyComposite?.modelSignals || null,
      ...buildTeamForecastEngineField(fc, fc.league),
      forecastVersion: '1.0',
      staticCommit: true,
      generatedAt: fc.created_at,
      insightAvailability,
      playerPropsMode: playerPropsAssets.length > 0 ? 'per_player' : 'team',
      playerPropsAssets,
      eventId,
      isFreePick: true,
    });
  } catch (err) {
    console.error('Free forecast content error:', err);
    res.status(500).json({ error: 'Failed to load free forecast content' });
  }
});

// Client error logging (helps debug mobile Safari crashes)
router.post('/client-error', (req, res) => {
  const payload = {
    type: typeof req.body?.type === 'string' ? req.body.type.slice(0, 100) : 'unknown',
    message: typeof req.body?.message === 'string' ? req.body.message.slice(0, 500) : 'Unknown client error',
    stack: typeof req.body?.stack === 'string' ? req.body.stack.slice(0, 2000) : null,
    source: typeof req.body?.source === 'string' ? req.body.source.slice(0, 300) : null,
    url: typeof req.body?.url === 'string' ? req.body.url.slice(0, 500) : null,
    ts: typeof req.body?.ts === 'string' ? req.body.ts : new Date().toISOString(),
    ip: req.ip,
    ua: req.get('user-agent') || null,
  };
  console.error('CLIENT ERROR:', JSON.stringify(payload));
  res.json({ ok: true });
});

export default router;
