import { pool } from '../db/index';
import { enrichGameData, type EnrichedGameData } from '../seo/data-enrichment';
import {
  getUpcomingGames,
  getPlayerProps,
  getRecentLineMovements,
  getRecentSharpMoves,
  getActiveLeagues,
  getCompletedGames,
  logJob,
  updateJobStatus,
} from '../seo/data-queries';
import type { TweetContent, EventDrivenAlert, GrokTweetResponse, GrokThreadResponse, PickDetails } from './types';
import {
  callGrokForTweet,
  gamePreviewTweetPrompt,
  propPickTweetPrompt,
  lineAlertTweetPrompt,
  hotTakeTweetPrompt,
  recapTweetPrompt,
  blogPromoTweetPrompt,
  threadPrompt,
  eventDrivenAlertPrompt,
  performanceRecapPrompt,
} from './twitter-prompts';
import {
  isDuplicateContent,
  getUntweetedBlogPosts,
  hasRecentEventAlert,
  getExistingPickForGame,
  getExistingPicksForGames,
  getPickTallySummary,
  getPicksForCompletedGame,
} from './twitter-data-queries';
import {
  getTopPiffLegs,
  getTeamAbbrev,
  getPiffMapCached,
  formatPiffForPrompt,
  refreshPiffCache,
  type PiffLeg,
} from './piff-loader';

/** Returns a Date adjusted so getUpcomingGames sees the correct ET calendar date */
function getETDate(): Date {
  const etDateStr = new Date().toLocaleDateString('en-CA', { timeZone: 'America/New_York' });
  return new Date(etDateStr + 'T12:00:00');
}

/** Format a raw UTC gameDate into a compact ET time string for context lines */
function formatTimeET(dateStr: string): string {
  try {
    const d = new Date(dateStr);
    if (isNaN(d.getTime())) return '';
    const time = d.toLocaleTimeString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit', hour12: true });
    return time.replace(/\s+/g, '') + ' ET';
  } catch {
    return '';
  }
}

// ── Fresh Data Queries (no caching) ─────────────────────────

async function getUpcomingGamesAllLeagues(date: Date): Promise<Array<{ league: string; games: any[] }>> {
  const leagues = await getActiveLeagues();
  const results: Array<{ league: string; games: any[] }> = [];
  for (const league of leagues) {
    const games = await getUpcomingGames(league, date);
    if (games.length > 0) results.push({ league, games });
  }
  return results;
}

async function getPropsForGames(league: string, gameIds: number[]): Promise<any[]> {
  if (gameIds.length === 0) return [];
  return getPlayerProps(league, gameIds, 30);
}

// ── Quality Gates ────────────────────────────────────────────

function isValidTweet(text: string): boolean {
  if (!text || text.length === 0) return false;
  // X Premium supports up to 25K chars — our new template runs ~400-500 chars
  if (text.length > 4000) {
    console.warn(`[content-gen] Tweet too long (${text.length} chars), skipping`);
    return false;
  }
  return true;
}

function countHashtags(text: string): number {
  return (text.match(/#\w+/g) || []).length;
}

async function passesQualityGates(text: string): Promise<boolean> {
  if (!isValidTweet(text)) return false;
  if (countHashtags(text) > 4) {
    console.warn('[content-gen] Too many hashtags, skipping');
    return false;
  }
  if (await isDuplicateContent(text)) {
    console.warn('[content-gen] Duplicate content detected, skipping');
    return false;
  }
  return true;
}

function shouldIncludeMedia(probability: number): boolean {
  return Math.random() < probability;
}

// ── Pick Contradiction Guard ─────────────────────────────────

function normalizeTeamName(name: string): string {
  return name.toLowerCase().trim().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '-');
}

function buildGameKey(league: string, away: string, home: string): string {
  return `${league.toLowerCase()}:${normalizeTeamName(away)}@${normalizeTeamName(home)}`;
}

function buildPropKey(league: string, player: string, propType: string): string {
  return `prop:${league.toLowerCase()}:${normalizeTeamName(player)}:${propType.toLowerCase().replace(/\s+/g, '-')}`;
}

const OPPOSITE_SIDES: Record<string, string> = { home: 'away', away: 'home', over: 'under', under: 'over' };

function isConflictingPick(existing: string, incoming: string): boolean {
  return OPPOSITE_SIDES[existing] === incoming;
}

function isValidPickSide(side: string | undefined): side is string {
  return side === 'home' || side === 'away' || side === 'over' || side === 'under';
}

/** Extract a clean short reason from Grok tweet text (strips emojis, headers, takes last meaningful line) */
function extractShortReason(grokText: string, fallback: string): string {
  const lines = grokText
    .split('\n')
    .map(l => l.trim())
    .filter(l => l.length > 10)                         // skip short/empty lines
    .filter(l => !/^[^\w]*$/.test(l))                   // skip emoji-only lines
    .filter(l => !l.startsWith('Current Market'))        // skip template headers
    .filter(l => !l.startsWith('Sports Claw'))
    .filter(l => !l.startsWith('Follow @'))
    .filter(l => !l.includes('|'))                       // skip stat lines like "Spread: -3 | Total: 220"
    .map(l => l.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, '').trim()); // strip emojis

  // Take the last meaningful line (usually the "take" / action sentence)
  const reason = lines[lines.length - 1] || fallback;
  return reason.length > 80 ? reason.substring(0, 77) + '...' : reason;
}

// ── Game Previews ────────────────────────────────────────────

export async function generateGamePreviews(date: Date): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];
  const allGames = await getUpcomingGamesAllLeagues(date);

  for (const { league, games } of allGames) {
    if (!league) continue; // skip games with undefined league
    // Pick top 1-2 games per league (by spread interest — games with odds set)
    const gamesWithOdds = games.filter(g => g.moneylineHome != null && g.homeTeam && g.awayTeam).slice(0, 2);

    for (const game of gamesWithOdds) {
      try {
        const enriched = await enrichGameData(game);
        if (!enriched?.league) {
          console.warn(`[content-gen] Skipping game preview — enrichment returned no league for ${game.homeTeam} vs ${game.awayTeam}`);
          continue;
        }

        // Contradiction guard: check for existing pick on this game
        const gameKey = buildGameKey(league, game.awayTeam, game.homeTeam);
        const existingPick = await getExistingPickForGame(gameKey);
        const prompt = gamePreviewTweetPrompt(enriched, existingPick?.pick_direction);
        const result = await callGrokForTweet<GrokTweetResponse>(prompt);

        if (result?.text && await passesQualityGates(result.text)) {
          // Validate pick consistency
          if (isValidPickSide(result.pickSide) && existingPick && isConflictingPick(existingPick.pick_direction, result.pickSide)) {
            console.warn(`[content-gen] BLOCKED contradicting game_preview on ${gameKey}: existing=${existingPick.pick_direction}, new=${result.pickSide}`);
            continue;
          }
          const pickDir = isValidPickSide(result.pickSide) ? result.pickSide : undefined;
          const pickSelection = pickDir
            ? (pickDir === 'home' ? `${game.homeTeam} ${enriched.consensus.spread ?? ''}` :
               pickDir === 'away' ? `${game.awayTeam} ${enriched.consensus.spread ? (enriched.consensus.spread * -1) : ''}` :
               pickDir === 'over' ? `Over ${enriched.consensus.total ?? ''}` :
               `Under ${enriched.consensus.total ?? ''}`)
            : 'N/A';
          tweets.push({
            text: result.text,
            contentType: 'game_preview',
            league,
            gameKey,
            pickDirection: pickDir,
            mediaType: shouldIncludeMedia(0.6) && result.imagePrompt ? 'image' : null,
            imagePrompt: result.imagePrompt,
            isPickType: !!pickDir,
            enrichedData: enriched,
            pickDetails: pickDir ? {
              gameKey,
              league,
              pickType: 'spread',
              selection: pickSelection.trim(),
              pickDirection: pickDir,
              lineValue: enriched.consensus.spread ?? undefined,
              oddsAmerican: pickDir === 'home' ? (enriched.consensus.homeML ?? undefined) : (enriched.consensus.awayML ?? undefined),
              confidence: enriched.modelPick?.confidence,
              edgePct: enriched.modelPick?.edge,
              shortReason: enriched.modelPick?.reasoning || extractShortReason(result.text, 'Data-driven edge identified'),
              gameDate: enriched.gameDate,
            } : undefined,
          });
        }
      } catch (err) {
        console.error(`[content-gen] Game preview failed for ${game.homeTeam} vs ${game.awayTeam}:`, (err as Error).message);
      }

      if (tweets.length >= 4) break;
    }
    if (tweets.length >= 4) break;
  }

  console.log(`[content-gen] Generated ${tweets.length} game preview tweets`);
  return tweets;
}

// ── Prop Picks (PIFF 3.0 Powered) ───────────────────────────

export async function generatePropPicks(date: Date): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];
  const allGames = await getUpcomingGamesAllLeagues(date);

  // Refresh PIFF cache at start of each prop cycle
  refreshPiffCache();

  // ── PRIMARY: Use PIFF T1/T2 legs for prop picks ──
  const topPiffLegs = getTopPiffLegs(10);

  if (topPiffLegs.length > 0) {
    console.log(`[content-gen] PIFF 3.0: Found ${topPiffLegs.length} T1/T2 legs for prop picks`);

    // Build a lookup of all upcoming games by team abbreviation
    const gamesByTeam = new Map<string, { game: any; league: string }>();
    for (const { league, games } of allGames) {
      for (const game of games) {
        const homeAbbrev = getTeamAbbrev(game.homeTeam, league).toUpperCase();
        const awayAbbrev = getTeamAbbrev(game.awayTeam, league).toUpperCase();
        if (!gamesByTeam.has(homeAbbrev)) gamesByTeam.set(homeAbbrev, { game, league });
        if (!gamesByTeam.has(awayAbbrev)) gamesByTeam.set(awayAbbrev, { game, league });
      }
    }

    for (const piffLeg of topPiffLegs) {
      if (tweets.length >= 5) break;

      try {
        // Find the matching game for this PIFF leg
        const teamKey = piffLeg.team.toUpperCase();
        const matched = gamesByTeam.get(teamKey);
        if (!matched) {
          console.log(`[content-gen] PIFF leg ${piffLeg.name} (${teamKey}) — no matching game found, skipping`);
          continue;
        }

        const { game, league } = matched;
        const playerName = piffLeg.name;

        // Contradiction guard
        const propKey = buildPropKey(league, playerName, piffLeg.stat);
        const existingPick = await getExistingPickForGame(propKey);

        // If existing pick conflicts with PIFF direction, skip
        if (existingPick && piffLeg.direction) {
          const piffDir = piffLeg.direction.toLowerCase();
          if (isConflictingPick(existingPick.pick_direction, piffDir)) {
            console.warn(`[content-gen] PIFF leg ${playerName} ${piffLeg.direction} conflicts with existing ${existingPick.pick_direction}, skipping`);
            continue;
          }
        }

        // Build prompt with PIFF signal as primary directive
        const opponent = game.homeTeam === piffLeg.team || getTeamAbbrev(game.homeTeam, league).toUpperCase() === teamKey
          ? game.awayTeam : game.homeTeam;
        const team = game.homeTeam === piffLeg.team || getTeamAbbrev(game.homeTeam, league).toUpperCase() === teamKey
          ? game.homeTeam : game.awayTeam;

        const prompt = propPickTweetPrompt(
          playerName,
          piffLeg.stat,
          piffLeg.line,
          {
            team,
            opponent,
            league,
            gameDate: game?.gameDate,
            recentAvg: piffLeg.szn_hr ? Math.round(piffLeg.line * (1 + piffLeg.edge) * 10) / 10 : undefined,
            dvp: piffLeg.dvp_tier || undefined,
          },
          existingPick?.pick_direction,
          {
            name: piffLeg.name,
            stat: piffLeg.stat,
            line: piffLeg.line,
            direction: piffLeg.direction || 'over',
            tier_label: piffLeg.tier_label || 'T2_STRONG',
            edge: piffLeg.edge,
            prob: piffLeg.prob,
            dvp_tier: piffLeg.dvp_tier,
          }
        );

        const result = await callGrokForTweet<GrokTweetResponse>(prompt);

        if (result?.text && await passesQualityGates(result.text)) {
          // Force pickSide to match PIFF direction
          const pickDir = piffLeg.direction?.toLowerCase() as string || (isValidPickSide(result.pickSide) ? result.pickSide : 'over');
          const propPickType = pickDir === 'under' ? 'prop_under' : 'prop_over';

          let propEnriched: EnrichedGameData | undefined;
          try { propEnriched = await enrichGameData(game); } catch { /* enrichment optional */ }

          tweets.push({
            text: result.text,
            contentType: 'prop_pick',
            league,
            gameKey: propKey,
            pickDirection: pickDir,
            mediaType: shouldIncludeMedia(0.8) && result.imagePrompt ? 'image' : null,
            imagePrompt: result.imagePrompt,
            isPickType: !!propEnriched,
            enrichedData: propEnriched,
            pickDetails: propEnriched ? {
              gameKey: propKey,
              league,
              pickType: propPickType,
              selection: `${playerName} ${pickDir === 'over' ? 'Over' : 'Under'} ${piffLeg.line} ${piffLeg.stat}`,
              pickDirection: pickDir,
              lineValue: piffLeg.line,
              confidence: piffLeg.tier_label === 'T1_LOCK' ? 'HIGH' : 'MEDIUM',
              edgePct: Math.round(piffLeg.edge * 100),
              shortReason: `PIFF 3.0 ${piffLeg.tier_label}: +${(piffLeg.edge * 100).toFixed(0)}% edge, ${(piffLeg.prob * 100).toFixed(0)}% prob${piffLeg.dvp_tier ? `, DVP ${piffLeg.dvp_tier}` : ''}`,
              gameDate: game?.gameDate,
            } : undefined,
          });

          console.log(`[content-gen] PIFF prop pick: ${playerName} ${piffLeg.direction} ${piffLeg.line} ${piffLeg.stat} [${piffLeg.tier_label}] edge:+${(piffLeg.edge * 100).toFixed(0)}%`);
        }
      } catch (err) {
        console.error(`[content-gen] PIFF prop pick failed for ${piffLeg.name}:`, (err as Error).message);
      }
    }
  }

  // ── FALLBACK: Old flow if no PIFF legs available ──
  if (tweets.length === 0) {
    console.log('[content-gen] No PIFF legs available, falling back to PPL-based prop picks');

    for (const { league, games } of allGames) {
      const gameIds = games.map(g => g.id);
      const props = await getPropsForGames(league, gameIds);

      const seen = new Set<string>();
      const uniqueProps = props.filter(p => {
        const key = `${p.playerName || p.playerExternalId}-${p.propType}`;
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
      }).slice(0, 5);

      let leagueEnriched: EnrichedGameData | null = null;
      if (games.length > 0) {
        try { leagueEnriched = await enrichGameData(games[0]); } catch { /* enrichment optional */ }
      }

      for (const prop of uniqueProps) {
        try {
          const game = games.find(g => g.id === prop.gameId) || games[0];
          const playerName = prop.playerName || prop.playerExternalId;

          const propKey = buildPropKey(league, playerName, prop.propType);
          const existingPick = await getExistingPickForGame(propKey);
          const prompt = propPickTweetPrompt(
            playerName,
            prop.propType,
            prop.lineValue,
            {
              team: game?.homeTeam,
              opponent: game?.awayTeam,
              league,
              gameDate: game?.gameDate,
            },
            existingPick?.pick_direction
          );
          const result = await callGrokForTweet<GrokTweetResponse>(prompt);

          if (result?.text && await passesQualityGates(result.text)) {
            if (isValidPickSide(result.pickSide) && existingPick && isConflictingPick(existingPick.pick_direction, result.pickSide)) {
              console.warn(`[content-gen] BLOCKED contradicting prop_pick on ${propKey}: existing=${existingPick.pick_direction}, new=${result.pickSide}`);
              continue;
            }
            const pickDir = isValidPickSide(result.pickSide) ? result.pickSide : undefined;
            const propPickType = pickDir === 'over' ? 'prop_over' : pickDir === 'under' ? 'prop_under' : 'prop_over';
            let propEnriched = leagueEnriched;
            if (game && game.id !== games[0]?.id) {
              try { propEnriched = await enrichGameData(game); } catch { /* use cached */ }
            }
            tweets.push({
              text: result.text,
              contentType: 'prop_pick',
              league,
              gameKey: propKey,
              pickDirection: pickDir,
              mediaType: shouldIncludeMedia(0.8) && result.imagePrompt ? 'image' : null,
              imagePrompt: result.imagePrompt,
              isPickType: !!pickDir && !!propEnriched,
              enrichedData: propEnriched ?? undefined,
              pickDetails: pickDir && propEnriched ? {
                gameKey: propKey,
                league,
                pickType: propPickType,
                selection: `${playerName} ${pickDir === 'over' ? 'Over' : 'Under'} ${prop.lineValue} ${prop.propType}`,
                pickDirection: pickDir,
                lineValue: prop.lineValue,
                shortReason: extractShortReason(result.text, 'Data-driven edge identified'),
                gameDate: game?.gameDate,
              } : undefined,
            });
          }
        } catch (err) {
          console.error(`[content-gen] Prop pick failed:`, (err as Error).message);
        }

        if (tweets.length >= 5) break;
      }
      if (tweets.length >= 5) break;
    }
  }

  console.log(`[content-gen] Generated ${tweets.length} prop pick tweets${topPiffLegs.length > 0 ? ' (PIFF-powered)' : ' (fallback)'}`);
  return tweets;
}

// ── Line Movement Alerts ─────────────────────────────────────

export async function generateLineAlerts(): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];
  const leagues = await getActiveLeagues();

  for (const league of leagues) {
    try {
      const moves = await getRecentLineMovements(league, 10);
      const sharps = await getRecentSharpMoves(league, 10);

      // Only alert on significant moves (>1 point)
      const significantMoves = moves.filter(m => Math.abs(m.lineMovement || 0) > 1);
      if (significantMoves.length === 0) continue;

      const moveData = significantMoves.slice(0, 4).map(m => ({
        game: `${m.awayTeam} @ ${m.homeTeam}`,
        market: m.marketType || 'spread',
        from: m.openLine || 0,
        to: m.currentLine || 0,
        sharp: sharps.some(s => s.homeTeam === m.homeTeam && s.awayTeam === m.awayTeam),
        steam: m.steamMove || false,
        gameDate: m.gameDate || undefined,
      }));

      // Contradiction guard: check existing pick on the biggest move's game
      const biggestMove = significantMoves[0];
      const gameKey = buildGameKey(league, biggestMove.awayTeam || '', biggestMove.homeTeam || '');
      const existingPick = await getExistingPickForGame(gameKey);
      const prompt = lineAlertTweetPrompt(moveData, league, existingPick?.pick_direction);
      const result = await callGrokForTweet<GrokTweetResponse>(prompt);

      if (result?.text && await passesQualityGates(result.text)) {
        // Validate pick consistency
        if (isValidPickSide(result.pickSide) && existingPick && isConflictingPick(existingPick.pick_direction, result.pickSide)) {
          console.warn(`[content-gen] BLOCKED contradicting line_alert on ${gameKey}: existing=${existingPick.pick_direction}, new=${result.pickSide}`);
          continue;
        }
        const pickDir = isValidPickSide(result.pickSide) ? result.pickSide : undefined;
        // Enrich the biggest-move game for breakdown
        let lineEnriched: EnrichedGameData | undefined;
        if (pickDir && biggestMove.homeTeam && biggestMove.awayTeam) {
          try {
            // Build a minimal game object for enrichment
            const enrichGame = { id: 0, league, homeTeam: biggestMove.homeTeam, awayTeam: biggestMove.awayTeam, gameDate: biggestMove.gameDate || '' };
            lineEnriched = await enrichGameData(enrichGame as any);
          } catch { /* enrichment optional for line alerts */ }
        }
        tweets.push({
          text: result.text,
          contentType: 'line_alert',
          league,
          gameKey,
          pickDirection: pickDir,
          isPickType: !!pickDir && !!lineEnriched,
          enrichedData: lineEnriched,
          pickDetails: pickDir && lineEnriched ? {
            gameKey,
            league,
            pickType: (pickDir === 'over' || pickDir === 'under') ? 'total' : 'spread',
            selection: `${pickDir === 'home' ? biggestMove.homeTeam : pickDir === 'away' ? biggestMove.awayTeam : pickDir === 'over' ? 'Over' : 'Under'} ${biggestMove.currentLine ?? ''}`.trim(),
            pickDirection: pickDir,
            lineValue: biggestMove.currentLine ?? undefined,
            shortReason: extractShortReason(result.text, 'Data-driven edge identified'),
            gameDate: biggestMove.gameDate || undefined,
          } : undefined,
        });
      }
    } catch (err) {
      console.error(`[content-gen] Line alert failed for ${league}:`, (err as Error).message);
    }

    if (tweets.length >= 4) break;
  }

  console.log(`[content-gen] Generated ${tweets.length} line alert tweets`);
  return tweets;
}

// ── Hot Takes ────────────────────────────────────────────────

export async function generateHotTakes(): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];
  const allGames = await getUpcomingGamesAllLeagues(getETDate());

  // Build context summary and collect game keys for contradiction check
  const contextLines: string[] = [];
  const gameKeyMap: Map<string, { away: string; home: string; league: string }> = new Map();
  for (const { league, games } of allGames.slice(0, 3)) {
    for (const g of games.slice(0, 3)) {
      const timeStr = g.gameDate ? formatTimeET(g.gameDate) : '';
      contextLines.push(
        `${league.toUpperCase()}: ${g.awayTeam} @ ${g.homeTeam}${timeStr ? ` (${timeStr})` : ''} — spread ${g.spreadHome ?? 'N/A'}, total ${g.total ?? 'N/A'}`
      );
      const gk = buildGameKey(league, g.awayTeam, g.homeTeam);
      gameKeyMap.set(gk, { away: g.awayTeam, home: g.homeTeam, league });
    }
  }

  if (contextLines.length === 0) return tweets;

  try {
    // Batch-check existing positions for all candidate games
    const existingPicks = await getExistingPicksForGames(Array.from(gameKeyMap.keys()));
    let positionLines = '';
    if (existingPicks.size > 0) {
      const lines: string[] = [];
      existingPicks.forEach((direction, gk) => {
        const info = gameKeyMap.get(gk);
        if (info) {
          const sideName = direction === 'home' ? info.home : direction === 'away' ? info.away : direction;
          lines.push(`${info.away} @ ${info.home}: we already backed "${sideName}" (${direction})`);
        }
      });
      positionLines = lines.join('\n');
    }

    // Build PIFF context for hot takes
    const piffMap = getPiffMapCached();
    const allPiffLegs: PiffLeg[] = [];
    for (const legs of Object.values(piffMap)) {
      for (const leg of legs) {
        if (leg.tier_label === 'T1_LOCK' || leg.tier_label === 'T2_STRONG') {
          allPiffLegs.push(leg);
        }
      }
    }
    allPiffLegs.sort((a, b) => b.edge - a.edge);
    const piffContext = allPiffLegs.length > 0 ? formatPiffForPrompt(allPiffLegs.slice(0, 8)) : undefined;

    const prompt = hotTakeTweetPrompt(contextLines.join('\n'), positionLines || undefined, piffContext);
    const result = await callGrokForTweet<GrokTweetResponse>(prompt);

    if (result?.text && await passesQualityGates(result.text)) {
      // Determine gameKey from Grok's response (gameHome + gameAway) or best-effort match
      let gameKey: string | undefined;
      let pickDirection: string | undefined;

      if (result.gameHome && result.gameAway) {
        // Find the matching league from our game map
        const normHome = normalizeTeamName(result.gameHome);
        const normAway = normalizeTeamName(result.gameAway);
        gameKeyMap.forEach((_info, gk) => {
          if (!gameKey && gk.includes(normHome) && gk.includes(normAway)) {
            gameKey = gk;
          }
        });
        // Fallback: build from Grok's team names using first available league
        if (!gameKey && allGames.length > 0) {
          gameKey = buildGameKey(allGames[0].league, result.gameAway, result.gameHome);
        }
      }

      if (isValidPickSide(result.pickSide)) {
        pickDirection = result.pickSide;
      }

      // Final contradiction check
      if (gameKey && pickDirection) {
        const existing = existingPicks.get(gameKey);
        if (existing && isConflictingPick(existing, pickDirection)) {
          console.warn(`[content-gen] BLOCKED contradicting hot_take on ${gameKey}: existing=${existing}, new=${pickDirection}`);
          return tweets; // empty
        }
      }

      // Enrich the matched game for breakdown
      let hotTakeEnriched: EnrichedGameData | undefined;
      if (gameKey && pickDirection) {
        const matchedInfo = gameKeyMap.get(gameKey);
        if (matchedInfo) {
          const matchedGame = allGames.flatMap(a => a.games).find(
            g => g.homeTeam === matchedInfo.home && g.awayTeam === matchedInfo.away
          );
          if (matchedGame) {
            try { hotTakeEnriched = await enrichGameData(matchedGame); } catch { /* optional */ }
          }
        }
      }
      tweets.push({
        text: result.text,
        contentType: 'hot_take',
        gameKey,
        pickDirection,
        mediaType: shouldIncludeMedia(0.5) && result.imagePrompt ? 'image' : null,
        imagePrompt: result.imagePrompt,
        isPickType: !!pickDirection && !!hotTakeEnriched,
        enrichedData: hotTakeEnriched,
        pickDetails: pickDirection && hotTakeEnriched ? {
          gameKey: gameKey!,
          league: hotTakeEnriched.league,
          pickType: (pickDirection === 'over' || pickDirection === 'under') ? 'total' : 'moneyline',
          selection: `${pickDirection === 'home' ? hotTakeEnriched.homeTeamFull : pickDirection === 'away' ? hotTakeEnriched.awayTeamFull : pickDirection === 'over' ? 'Over' : 'Under'} ${hotTakeEnriched.consensus.total ?? hotTakeEnriched.consensus.spread ?? ''}`.trim(),
          pickDirection,
          lineValue: (pickDirection === 'over' || pickDirection === 'under') ? (hotTakeEnriched.consensus.total ?? undefined) : (hotTakeEnriched.consensus.spread ?? undefined),
          confidence: hotTakeEnriched.modelPick?.confidence,
          edgePct: hotTakeEnriched.modelPick?.edge,
          shortReason: extractShortReason(result.text, 'Data-driven edge identified'),
          gameDate: hotTakeEnriched.gameDate,
        } : undefined,
      });
    }
  } catch (err) {
    console.error('[content-gen] Hot take failed:', (err as Error).message);
  }

  console.log(`[content-gen] Generated ${tweets.length} hot take tweets`);
  return tweets;
}

// ── Recaps ───────────────────────────────────────────────────

export async function generateRecaps(): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];
  const today = getETDate();
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  const leagues = await getActiveLeagues();
  for (const league of leagues) {
    try {
      const completed = await getCompletedGames(league, yesterday, today);
      if (completed.length === 0) continue;

      // Pick top 2 most interesting completed games
      for (const game of completed.slice(0, 2)) {
        // Look up whether we had a pick on this game
        const gameKey = buildGameKey(league, game.awayTeam, game.homeTeam);
        const ourGradedPick = await getPicksForCompletedGame(gameKey);

        const ourPick = ourGradedPick?.grade_result && ['win', 'loss', 'push'].includes(ourGradedPick.grade_result)
          ? { side: ourGradedPick.selection, result: ourGradedPick.grade_result as 'win' | 'loss' | 'push' }
          : undefined;

        const prompt = recapTweetPrompt({
          home: game.homeTeam,
          away: game.awayTeam,
          homeScore: game.homeScore || 0,
          awayScore: game.awayScore || 0,
          league,
        }, ourPick);
        const result = await callGrokForTweet<GrokTweetResponse>(prompt);

        if (result?.text && await passesQualityGates(result.text)) {
          tweets.push({
            text: result.text,
            contentType: 'recap',
            league,
          });
        }
      }
    } catch (err) {
      console.error(`[content-gen] Recap failed for ${league}:`, (err as Error).message);
    }

    if (tweets.length >= 4) break;
  }

  console.log(`[content-gen] Generated ${tweets.length} recap tweets`);
  return tweets;
}

// ── Blog Promos ──────────────────────────────────────────────

export async function generateBlogPromos(): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];

  try {
    const posts = await getUntweetedBlogPosts(3);
    for (const post of posts.slice(0, 2)) {
      const url = `https://sportsclaw.guru/blog/${post.slug}`;
      const prompt = blogPromoTweetPrompt(post.title, post.excerpt || '', url);
      const result = await callGrokForTweet<GrokTweetResponse>(prompt);

      if (result?.text && await passesQualityGates(result.text)) {
        tweets.push({
          text: result.text,
          contentType: 'blog_promo',
          league: post.league,
          blogPostId: post.id,
        });
      }
    }
  } catch (err) {
    console.error('[content-gen] Blog promo failed:', (err as Error).message);
  }

  console.log(`[content-gen] Generated ${tweets.length} blog promo tweets`);
  return tweets;
}

// ── Threads ──────────────────────────────────────────────────

export async function generateThread(topic: string, dataContext: string): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];

  try {
    const prompt = threadPrompt(topic, dataContext);
    const result = await callGrokForTweet<GrokThreadResponse>(prompt);

    if (!result?.hook || !result?.parts?.length) return tweets;

    if (await passesQualityGates(result.hook)) {
      // Hook tweet
      tweets.push({
        text: result.hook,
        contentType: 'thread_hook',
        mediaType: shouldIncludeMedia(0.2) && result.imagePrompt ? 'image' : null,
        imagePrompt: result.imagePrompt,
      });

      // Thread parts (will be linked as replies to hook)
      for (const part of result.parts) {
        if (part && part.length <= 280) {
          tweets.push({
            text: part,
            contentType: 'thread_part',
          });
        }
      }
    }
  } catch (err) {
    console.error('[content-gen] Thread generation failed:', (err as Error).message);
  }

  console.log(`[content-gen] Generated thread with ${tweets.length} parts`);
  return tweets;
}

// ── Recap Thread (end of day wrap) ───────────────────────────

export async function generateRecapThread(): Promise<TweetContent[]> {
  const today = getETDate();
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  const leagues = await getActiveLeagues();
  const recapLines: string[] = [];

  for (const league of leagues) {
    const completed = await getCompletedGames(league, yesterday, today);
    for (const g of completed.slice(0, 3)) {
      recapLines.push(`${league.toUpperCase()}: ${g.awayTeam} ${g.awayScore} - ${g.homeTeam} ${g.homeScore}`);
    }
  }

  if (recapLines.length === 0) return [];
  return generateThread('Daily Recap & Tomorrow Preview', recapLines.join('\n'));
}

// ── Weekly Recap Thread (Monday) ─────────────────────────────

export async function generateWeeklyRecapThread(): Promise<TweetContent[]> {
  const today = getETDate();
  const weekStart = new Date(today);
  weekStart.setDate(weekStart.getDate() - 7);

  const leagues = await getActiveLeagues();
  const summaryLines: string[] = [];

  for (const league of leagues) {
    const completed = await getCompletedGames(league, weekStart, today);
    if (completed.length > 0) {
      summaryLines.push(`${league.toUpperCase()}: ${completed.length} games completed`);
    }
  }

  if (summaryLines.length === 0) return [];
  return generateThread('Weekly Wrap-Up', summaryLines.join('\n'));
}

// ── Video Content ────────────────────────────────────────────

export async function generateVideoTweet(): Promise<TweetContent | null> {
  const allGames = await getUpcomingGamesAllLeagues(getETDate());
  if (allGames.length === 0) return null;

  // Pick the biggest game (first with odds)
  for (const { league, games } of allGames) {
    const bigGame = games.find(g => g.moneylineHome != null);
    if (!bigGame) continue;

    try {
      const enriched = await enrichGameData(bigGame);

      // Contradiction guard
      const gameKey = buildGameKey(league, bigGame.awayTeam, bigGame.homeTeam);
      const existingPick = await getExistingPickForGame(gameKey);
      const prompt = gamePreviewTweetPrompt(enriched, existingPick?.pick_direction);
      const result = await callGrokForTweet<GrokTweetResponse>(prompt);

      if (result?.text && await passesQualityGates(result.text)) {
        if (isValidPickSide(result.pickSide) && existingPick && isConflictingPick(existingPick.pick_direction, result.pickSide)) {
          console.warn(`[content-gen] BLOCKED contradicting video tweet on ${gameKey}: existing=${existingPick.pick_direction}, new=${result.pickSide}`);
          continue;
        }
        const vidPickDir = isValidPickSide(result.pickSide) ? result.pickSide : undefined;
        return {
          text: result.text,
          contentType: 'video',
          league,
          gameKey,
          pickDirection: vidPickDir,
          mediaType: 'video',
          imagePrompt: result.imagePrompt || `Dramatic sports betting preview: ${enriched.awayTeamFull} vs ${enriched.homeTeamFull}, primetime matchup, dynamic camera movement, bold graphics`,
          isPickType: !!vidPickDir,
          enrichedData: enriched,
          pickDetails: vidPickDir ? {
            gameKey,
            league,
            pickType: 'spread',
            selection: `${vidPickDir === 'home' ? enriched.homeTeamFull : vidPickDir === 'away' ? enriched.awayTeamFull : vidPickDir === 'over' ? 'Over' : 'Under'} ${enriched.consensus.spread ?? enriched.consensus.total ?? ''}`.trim(),
            pickDirection: vidPickDir,
            lineValue: enriched.consensus.spread ?? undefined,
            confidence: enriched.modelPick?.confidence,
            edgePct: enriched.modelPick?.edge,
            shortReason: enriched.modelPick?.reasoning || extractShortReason(result.text, 'Data-driven edge identified'),
            gameDate: enriched.gameDate,
          } : undefined,
        };
      }
    } catch (err) {
      console.error(`[content-gen] Video tweet failed:`, (err as Error).message);
    }
  }

  return null;
}

// ── Performance Recap ────────────────────────────────────────

export async function generatePerformanceRecap(): Promise<TweetContent | null> {
  try {
    // Run grading as safety net before building recap
    const { gradeAllPendingPicks } = await import('./grading-engine');
    const gradingReport = await gradeAllPendingPicks();
    console.log(`[content-gen] Pre-recap grading: ${gradingReport.graded} graded (${gradingReport.wins}W-${gradingReport.losses}L-${gradingReport.pushes}P)`);

    // Pull tally from bot's own sc_picks (graded picks)
    const tally = await getPickTallySummary(36);

    // Skip if no recent graded picks
    if (tally.recent.wins + tally.recent.losses === 0) {
      console.log('[content-gen] No graded picks in last 36h, skipping performance recap');
      return null;
    }

    const prompt = performanceRecapPrompt({
      recentWins: tally.recent.wins,
      recentLosses: tally.recent.losses,
      recentPushes: tally.recent.pushes,
      recentByLeague: tally.recent.byLeague,
      overallWins: tally.overall.wins,
      overallLosses: tally.overall.losses,
      overallPushes: tally.overall.pushes,
      overallTotal: tally.overall.total,
      overallWinPct: tally.overall.winPct,
      overallByLeague: tally.overall.byLeague,
    });

    const result = await callGrokForTweet<{ text: string }>(prompt);

    if (result?.text && await passesQualityGates(result.text)) {
      console.log(`[content-gen] Generated performance recap tweet (from sc_picks)`);
      return {
        text: result.text,
        contentType: 'performance_recap',
      };
    }

    return null;
  } catch (err) {
    console.error('[content-gen] Performance recap failed:', (err as Error).message);
    return null;
  }
}

// ── Event-Driven Alerts ──────────────────────────────────────

const MAX_EVENT_ALERTS_PER_CYCLE = 2;

export async function checkEventDrivenAlerts(): Promise<TweetContent[]> {
  const tweets: TweetContent[] = [];
  const leagues = await getActiveLeagues();
  const alertedGames = new Set<string>(); // dedup by game matchup

  for (const league of leagues) {
    if (tweets.length >= MAX_EVENT_ALERTS_PER_CYCLE) break;

    try {
      const moves = await getRecentLineMovements(league, 20);
      const sharps = await getRecentSharpMoves(league, 10);

      // Line movement > 1.5 pts — pick biggest per unique game
      const bigMoves = moves.filter(m => Math.abs(m.lineMovement || 0) > 1.5);
      for (const move of bigMoves) {
        if (tweets.length >= MAX_EVENT_ALERTS_PER_CYCLE) break;
        const alertKey = `${move.awayTeam}@${move.homeTeam}`;
        if (alertedGames.has(alertKey)) continue;
        if (await hasRecentEventAlert(alertKey, 4)) { alertedGames.add(alertKey); continue; }
        alertedGames.add(alertKey);

        // Contradiction guard
        const pickGameKey = buildGameKey(league, move.awayTeam || '', move.homeTeam || '');
        const existingPick = await getExistingPickForGame(pickGameKey);

        const details = `${move.awayTeam} @ ${move.homeTeam}\n${move.marketType}: ${move.openLine} → ${move.currentLine} (${(move.lineMovement || 0) > 0 ? '+' : ''}${move.lineMovement} pts)`;
        const prompt = eventDrivenAlertPrompt('Major Line Movement', details, move.gameDate || undefined, existingPick?.pick_direction, league);
        const result = await callGrokForTweet<GrokTweetResponse>(prompt);

        if (result?.text && await passesQualityGates(result.text)) {
          if (isValidPickSide(result.pickSide) && existingPick && isConflictingPick(existingPick.pick_direction, result.pickSide)) {
            console.warn(`[content-gen] BLOCKED contradicting event_driven on ${pickGameKey}: existing=${existingPick.pick_direction}, new=${result.pickSide}`);
            continue;
          }
          const evPickDir = isValidPickSide(result.pickSide) ? result.pickSide : undefined;
          let evEnriched: EnrichedGameData | undefined;
          if (evPickDir && move.homeTeam && move.awayTeam) {
            try {
              evEnriched = await enrichGameData({ id: 0, league, homeTeam: move.homeTeam, awayTeam: move.awayTeam, gameDate: move.gameDate || '' } as any);
            } catch { /* optional */ }
          }
          tweets.push({
            text: result.text,
            contentType: 'event_driven',
            league,
            gameKey: pickGameKey,
            pickDirection: evPickDir,
            isPickType: !!evPickDir && !!evEnriched,
            enrichedData: evEnriched,
            pickDetails: evPickDir && evEnriched ? {
              gameKey: pickGameKey,
              league,
              pickType: (evPickDir === 'over' || evPickDir === 'under') ? 'total' : 'spread',
              selection: `${evPickDir === 'home' ? move.homeTeam : evPickDir === 'away' ? move.awayTeam : evPickDir === 'over' ? 'Over' : 'Under'} ${move.currentLine ?? ''}`.trim(),
              pickDirection: evPickDir,
              lineValue: move.currentLine ?? undefined,
              shortReason: extractShortReason(result.text, 'Data-driven edge identified'),
              gameDate: move.gameDate || undefined,
            } : undefined,
          });
        }
      }

      // Steam moves — only if game not already alerted
      const steamMoves = sharps.filter(s => s.isSteamMove);
      for (const steam of steamMoves.slice(0, 1)) {
        if (tweets.length >= MAX_EVENT_ALERTS_PER_CYCLE) break;
        const alertKey = `${steam.awayTeam}@${steam.homeTeam}`;
        if (alertedGames.has(alertKey)) continue;
        if (await hasRecentEventAlert(alertKey, 4)) { alertedGames.add(alertKey); continue; }
        alertedGames.add(alertKey);

        // Contradiction guard
        const pickGameKey = buildGameKey(league, steam.awayTeam || '', steam.homeTeam || '');
        const existingPick = await getExistingPickForGame(pickGameKey);

        const details = `${steam.awayTeam} @ ${steam.homeTeam}\n${steam.market}: ${steam.lineFrom} → ${steam.lineTo}\nSTEAM MOVE detected at ${steam.moveDetectedAt}`;
        const prompt = eventDrivenAlertPrompt('Steam Move', details, steam.gameDate || undefined, existingPick?.pick_direction, league);
        const result = await callGrokForTweet<GrokTweetResponse>(prompt);

        if (result?.text && await passesQualityGates(result.text)) {
          if (isValidPickSide(result.pickSide) && existingPick && isConflictingPick(existingPick.pick_direction, result.pickSide)) {
            console.warn(`[content-gen] BLOCKED contradicting event_driven (steam) on ${pickGameKey}: existing=${existingPick.pick_direction}, new=${result.pickSide}`);
            continue;
          }
          const steamPickDir = isValidPickSide(result.pickSide) ? result.pickSide : undefined;
          let steamEnriched: EnrichedGameData | undefined;
          if (steamPickDir && steam.homeTeam && steam.awayTeam) {
            try {
              steamEnriched = await enrichGameData({ id: 0, league, homeTeam: steam.homeTeam, awayTeam: steam.awayTeam, gameDate: steam.gameDate || '' } as any);
            } catch { /* optional */ }
          }
          tweets.push({
            text: result.text,
            contentType: 'event_driven',
            league,
            gameKey: pickGameKey,
            pickDirection: steamPickDir,
            isPickType: !!steamPickDir && !!steamEnriched,
            enrichedData: steamEnriched,
            pickDetails: steamPickDir && steamEnriched ? {
              gameKey: pickGameKey,
              league,
              pickType: (steamPickDir === 'over' || steamPickDir === 'under') ? 'total' : 'spread',
              selection: `${steamPickDir === 'home' ? steam.homeTeam : steamPickDir === 'away' ? steam.awayTeam : steamPickDir === 'over' ? 'Over' : 'Under'} ${steam.lineTo ?? ''}`.trim(),
              pickDirection: steamPickDir,
              lineValue: steam.lineTo ?? undefined,
              shortReason: extractShortReason(result.text, 'Data-driven edge identified'),
              gameDate: steam.gameDate || undefined,
            } : undefined,
          });
        }
      }
    } catch (err) {
      console.error(`[content-gen] Event-driven check failed for ${league}:`, (err as Error).message);
    }
  }

  console.log(`[content-gen] Generated ${tweets.length} event-driven alerts`);
  return tweets;
}
