import pool from '../db';
import { scanContent } from './compliance/scanner';
import { callLLM } from './grok';
import { editorialGuardrails, classifyNarrative, getBlogNarrativePrompt, NarrativeContext, NarrativeClassification } from './narrative-engine';

export interface BlogPostContent {
  title: string;
  metaDescription: string;
  content: string;
  excerpt: string;
  tags: string[];
}

const DISALLOWED_TITLE_PATTERNS = [
  /\bfortress\b/i,
  /\bskeleton crew\b/i,
  /\bskeleton\b/i,
  /\bghost roster\b/i,
  /\bghost lineup\b/i,
  /\bghost starter\b/i,
  /\bghost spread\b/i,
  /\bghost\b/i,
  /\bopening day ghosts\b/i,
  /\bphantom\b/i,
  /\bbuzzsaw\b/i,
  /\bwalking wounded\b/i,
  /\bwinds of war\b/i,
  /\bbroken rosters?\b/i,
  /\bbroken lineups?\b/i,
  /\bbroken bats?\b/i,
  /\bshell game\b/i,
  /\bdesert mirage\b/i,
  /\btrapdoor\b/i,
  /\bgraveyard\b/i,
];
const DUPLICATE_MATCHUP_WINDOW_MS = 3 * 60 * 60 * 1000;

function cleanSlugPart(value: string): string {
  return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
}

function normalizeTeamForDuplicateMatch(value: string): string {
  return value
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/&/g, ' and ')
    .replace(/[^a-z0-9 ]+/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function teamNamesLikelySame(a: string, b: string): boolean {
  const normA = normalizeTeamForDuplicateMatch(a);
  const normB = normalizeTeamForDuplicateMatch(b);
  if (!normA || !normB) return false;
  if (normA === normB) return true;

  const tokensA = normA.split(' ');
  const tokensB = normB.split(' ');
  const shorter = normA.length <= normB.length ? normA : normB;
  const longer = normA.length <= normB.length ? normB : normA;
  if ((tokensA.length === 1 || tokensB.length === 1) && longer.startsWith(`${shorter} `)) {
    return true;
  }

  return false;
}

function buildFallbackBlogTitle(context: {
  homeTeam: string;
  awayTeam: string;
  league: string;
}): string {
  const templates = [
    `Why ${context.awayTeam} at ${context.homeTeam} is tighter than it looks`,
    `${context.awayTeam} at ${context.homeTeam}: where this matchup tilts`,
    `${context.homeTeam} vs ${context.awayTeam}: the pressure points that matter`,
    `${context.awayTeam}-${context.homeTeam}: the details that could decide it`,
  ];
  const seed = `${context.league}:${context.awayTeam}:${context.homeTeam}`;
  const index = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % templates.length;
  const title = templates[index];
  return title.length <= 80 ? title : title.slice(0, 77).trimEnd() + '...';
}

export function sanitizeGeneratedBlogTitle(title: string, context: {
  homeTeam: string;
  awayTeam: string;
  league: string;
}): string {
  const cleaned = title.trim().replace(/\s+/g, ' ');
  if (!cleaned) return buildFallbackBlogTitle(context);
  if (DISALLOWED_TITLE_PATTERNS.some((pattern) => pattern.test(cleaned))) {
    return buildFallbackBlogTitle(context);
  }
  return cleaned;
}

export function blogPostsLikelyDuplicate(candidate: {
  sport: string;
  gameDate: string;
  homeTeam: string;
  awayTeam: string;
  startsAt: string;
}, existing: {
  sport: string;
  game_date: string;
  home_team: string;
  away_team: string;
  starts_at: string | null;
}): boolean {
  if (candidate.sport !== existing.sport || candidate.gameDate !== existing.game_date) {
    return false;
  }

  const directMatch = teamNamesLikelySame(candidate.homeTeam, existing.home_team)
    && teamNamesLikelySame(candidate.awayTeam, existing.away_team);
  const reversedMatch = teamNamesLikelySame(candidate.homeTeam, existing.away_team)
    && teamNamesLikelySame(candidate.awayTeam, existing.home_team);

  if (!directMatch && !reversedMatch) return false;

  if (!existing.starts_at) return true;
  const candidateTs = new Date(candidate.startsAt).getTime();
  const existingTs = new Date(existing.starts_at).getTime();
  if (Number.isNaN(candidateTs) || Number.isNaN(existingTs)) return true;
  return Math.abs(candidateTs - existingTs) <= DUPLICATE_MATCHUP_WINDOW_MS;
}

export function generateSlug(league: string, homeTeam: string, awayTeam: string, gameDate: string): string {
  return `${cleanSlugPart(league)}-${cleanSlugPart(awayTeam)}-vs-${cleanSlugPart(homeTeam)}-${gameDate}`;
}

export async function generateBlogPost(context: {
  league: string;
  homeTeam: string;
  awayTeam: string;
  startsAt: string;
  sanitizedSummary: string;
  keyFactors: string[];
  spreadAnalysis: string;
  totalAnalysis: string;
  historicalTrend: string;
  injuryNotes: string;
  oddsData?: any;
  narrativeClassification?: NarrativeClassification;
}): Promise<BlogPostContent> {
  const oddsInfo = context.oddsData
    ? `Current markets:
- Moneyline: Home ${context.oddsData.moneyline?.home ?? 'N/A'} | Away ${context.oddsData.moneyline?.away ?? 'N/A'}
- Spread: Home ${context.oddsData.spread?.home?.line ?? 'N/A'} | Away ${context.oddsData.spread?.away?.line ?? 'N/A'}
- Total: ${context.oddsData.total?.over?.line ?? 'N/A'}`
    : 'Current markets not available.';

  const gameDate = new Date(context.startsAt).toLocaleDateString('en-US', {
    weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
  });

  // Team logo URLs (ESPN CDN)
  const teamLogoUrl = (teamName: string, league: string) => {
    const slug = teamName.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-');
    return `https://a.espncdn.com/combiner/i?img=/i/teamlogos/${league.toLowerCase()}/500/${slug}.png&h=100&w=100`;
  };

  const prompt = `You are generating a Rain Wire post for a sports matchup within the Rainmaker ecosystem.

TERMINOLOGY: Blogs and Rain Wire are the same thing.

MATCHUP: ${context.awayTeam} @ ${context.homeTeam}
GAME DATE: ${gameDate}
LEAGUE: ${context.league.toUpperCase()}

BACKGROUND RESEARCH (weave into narrative naturally — do NOT quote directly, do NOT include hard numbers):
- Summary: ${context.sanitizedSummary}
- Key Factors: ${context.keyFactors.join('; ')}
- Spread Context: ${context.spreadAnalysis}
- Total Context: ${context.totalAnalysis}
- Historical Trend: ${context.historicalTrend}
- Injury Notes: ${context.injuryNotes || 'None reported'}

PRIMARY OBJECTIVE:
The sole objective of this Rain Wire post is to persuade the reader to click the Forecast link by delivering rich context, storytelling, and intrigue — WITHOUT revealing proprietary analysis, picks, or hard numbers. If the reader does not feel compelled to click the Forecast link at the end, the post has failed.

VISUAL HEADER (REQUIRED — place at the very top):
<div style="display:flex;align-items:center;justify-content:center;gap:2rem;margin:1.5rem 0 2rem;">
  <div style="text-align:center;"><img src="AWAY_LOGO_URL" alt="${context.awayTeam}" style="width:80px;height:80px;object-fit:contain;" /><p style="font-weight:bold;margin-top:0.5rem;">${context.awayTeam}</p></div>
  <span style="font-size:2rem;font-weight:bold;color:#f97316;">VS</span>
  <div style="text-align:center;"><img src="HOME_LOGO_URL" alt="${context.homeTeam}" style="width:80px;height:80px;object-fit:contain;" /><p style="font-weight:bold;margin-top:0.5rem;">${context.homeTeam}</p></div>
</div>
(Use real ESPN team logo URLs if possible. Format: https://a.espncdn.com/i/teamlogos/${context.league.toLowerCase()}/500/ABBREV.png)

CONTENT REQUIREMENTS:
- Minimum 250 words, target around 300-320 words. Start winding down at 320. Going slightly over 350 is fine but do NOT write novels.
- Highly descriptive, visual, and narrative-driven
- Written in a confident, authoritative tone
- Must present an ORIGINAL angle on the matchup
- Informative but INTENTIONALLY INCOMPLETE — hold back deeper insights for the Forecast

RESEARCH TO WEAVE IN NATURALLY:
- Matchup storylines and situational dynamics
- Recent team form (last 5–10 games, trends, momentum)
- Injury considerations and roster availability
- Weather and environmental factors when relevant
- Context that serious analysts would consider before forming an opinion

⚠️ DO NOT INCLUDE:
- Exact statistics or projections
- Explicit picks or recommendations
- Hard numbers of any kind
- Internal model name references or probabilities (never mention PIFF, DIGIMON, Grok — only Rain Man)

LANGUAGE & TERMINOLOGY STANDARD (CRITICAL — extra weight):
Ethos: "Smarter language for better existence."
All copy must avoid explicit gambling language. Use market-based, analytical phrasing instead.

NEVER USE: betting lines, betting markets, bettors, wagers, gambling. AVOID: sportsbooks (use "market" or "market venues" instead), odds (use "market price" or "market pricing"), betting (use "investing", "market interest", "positioning")

USE INSTEAD:
- "markets" or "current market" instead of "betting markets"
- "market consensus" instead of "betting consensus"
- "market positioning" or "market movement" instead of "line movement"
- "early market interest" or "speculative interest" instead of "early action"
- "sharp interest" or "informed interest" instead of "sharp money"
- "pricing behavior" instead of "odds movement"
- "consensus signals" instead of "betting signals"
- "bookmakers" (sparingly, only when necessary)

TONE: Treat markets as signals, not wagers. Frame activity as analysis of collective behavior, not gambling. Maintain a calm, authoritative, institutional voice. Assume the reader understands the underlying mechanics without naming them explicitly.

GOAL: Elevate the brand into sports intelligence and forecasting. Keep content compliant, professional, and broadly distributable across media, search, and AI platforms.

NARRATIVE STYLE:
- You are a skeptical market analyst, not a storyteller. Question the number, not the narrative.
- Every headline MUST reference a specific market detail: the spread, total, a key stat, or a situational factor that changes the price. Headlines without a concrete angle get rejected.
- Name the edge: is the market too high, too low, ignoring an injury, overreacting to a streak?
- Avoid dramatic metaphors, war imagery, and horror-movie language. No "fortress", "skeleton crew", "ghost roster", "buzzsaw", "phantom", "graveyard", "trapdoor", "walking wounded", "winds of war", "broken rosters", "desert mirage", or any similar cliché.
- Write titles a sharp reader would click — someone who wants to know WHY a number is wrong, not who is going to win.
- Title formula: [Situation or factor] + [market price or stat] + [skeptical take on the mispricing]
- Examples of GOOD titles: "Memphis Missing 3 Starters but PHX -11.5 Still Looks Overpriced", "BOS/HOU Opens -125 With Whitlock on Short Rest — Market Isn't Pricing Fatigue", "Avalanche 9-2 Rout Masks a Makar Injury That Shifts the Series Price"
- Examples of BAD titles: "Ghost Roster Walks Into a Buzzsaw", "The Fortress Has a Trapdoor", "Skeleton Crew vs. Star Power"
- Open with the sharpest analytical angle, not atmospheric drama
- Body should show the reasoning: matchup factors, rest advantages, injury context, pace mismatches, travel, weather — the things that move numbers

CONTEST COVERAGE & FUNNEL STRUCTURE:
- This is exactly ONE Rain Wire post for ONE contest
- Each Rain Wire corresponds to a single matching Forecast for the same contest
- The Rain Wire is the awareness + intrigue layer; the Forecast is the proof + reasoning layer
- NEVER use the word "prediction(s)" — always call it a "Forecast"

SUGGESTIVE CTA STANDARD:
- End with language that implies we have a strong directional read on the matchup WITHOUT stating a pick or using hard numbers
- Position the Forecast as where readers find the math, logic, and model-driven reasoning behind the edge
- Be confidence-coded and suggestive — allude to there being a strong read on the likely outcome
- Withhold the "why" — direct users to the Forecast where analysis, logic, and model reasoning are shown

FORECAST CTA (REQUIRED — end every post with this):
<div style="margin-top:2rem;padding:1.5rem;border-radius:12px;background:rgba(249,115,22,0.08);border:1px solid rgba(249,115,22,0.25);text-align:center;">
  <p style="font-size:1.1rem;font-weight:bold;color:#f97316;margin-bottom:0.5rem;">🌧️ Want the Full Forecast?</p>
  <p style="color:#94a3b8;margin-bottom:1rem;">There are subtle edges and hidden value in this matchup that only deeper analysis reveals. The surface doesn't tell the full story.</p>
  <a href="https://rainmakersports.app/forecast" style="display:inline-block;padding:0.75rem 2rem;background:#f97316;color:white;font-weight:bold;border-radius:8px;text-decoration:none;">View Full Forecast →</a>
</div>

SEO & DISTRIBUTION:
- SEO-optimized with clean heading hierarchy (h2 → h3)
- Semantic HTML: <h2>, <h3>, <p>, <ul>, <li>, <strong>, <em>
- Crawler-friendly and LLM/frontier-model compliant
- Structured for search engines, AI discovery, social sharing, and news syndication
- Use team names, league, and game date naturally in headings
- Include relevant long-tail keywords naturally

Return ONLY this JSON:
{
  "title": "Skeptical, market-aware headline (60-75 chars). Must reference a number, stat, or situational factor. Frame the market question: what is the price missing? Not generic 'Team A vs Team B Preview' and not dramatic metaphors.",
  "metaDescription": "Meta description (150-160 chars, enticing preview)",
  "content": "<div>...</div><h2>...</h2><p>...</p>...",
  "excerpt": "2-3 sentence preview (under 200 chars)",
  "tags": ["tag1", "tag2", "tag3", "tag4", "tag5"]
}`;

  // Build narrative-aware system prompt if classification is available
  let narrativeDirectives = '';
  if (context.narrativeClassification) {
    narrativeDirectives = getBlogNarrativePrompt(context.narrativeClassification);
  }

  const systemPrompt = `You are Rain Man, the skeptical analyst behind Rainmaker Sports, writing Rain Wire posts. Your voice is analytical, contrarian, and market-aware — you question whether the number is right, not who wins. Write sharp, analytical previews (250-350 words) that identify what the market may be mispricing. Be intentionally incomplete — hold back the model's specific edge for the Forecast. No exact projections or picks. Never reference internal model names (PIFF, DIGIMON, Grok, etc.) — you are Rain Man, the sole source. LANGUAGE RULE: Always use "current markets" for pricing references, "market speculators" for audience references, and "market venues" for venue references. Headlines MUST reference a specific number, stat, or situational factor — never generic or dramatic. Return JSON only.
${narrativeDirectives}`;

  try {
    let content = await callLLM(systemPrompt, prompt, {
      maxTokens: 3000,
      temperature: 0.75,
    }, {
      category: 'blog',
      league: context.league,
    });

    content = content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();

    const parsed = JSON.parse(content) as BlogPostContent;
    if (!parsed.title || !parsed.content || parsed.content.length < 400) {
      throw new Error('Blog content too short or missing fields');
    }

    // ── NARRATIVE ENGINE: Apply editorial guardrails to blog content ──
    parsed.content = editorialGuardrails(parsed.content);
    parsed.title = sanitizeGeneratedBlogTitle(editorialGuardrails(parsed.title), {
      homeTeam: context.homeTeam,
      awayTeam: context.awayTeam,
      league: context.league,
    });
    parsed.excerpt = editorialGuardrails(parsed.excerpt);
    if (parsed.metaDescription) {
      parsed.metaDescription = editorialGuardrails(parsed.metaDescription);
    }

    // Quick compliance pre-scan (warning only, full pipeline runs in worker)
    try {
      const quickScan = await scanContent({
        title: parsed.title,
        content: parsed.content,
        excerpt: parsed.excerpt || '',
      });
      if (quickScan.findings.length > 0) {
        console.warn(`[COMPLIANCE-PRESCAN] ${quickScan.findings.length} violation(s) detected in blog — will be handled by pipeline`);
      }
    } catch { /* non-critical pre-scan */ }

    return parsed;
  } catch (err) {
    console.error('[blog-generator] LLM call failed:', err);
    return getDefaultBlogContent(context);
  }
}

function getDefaultBlogContent(context: {
  league: string;
  homeTeam: string;
  awayTeam: string;
  startsAt: string;
  sanitizedSummary: string;
  keyFactors: string[];
  spreadAnalysis: string;
  totalAnalysis: string;
  injuryNotes: string;
  oddsData?: any;
}): BlogPostContent {
  const gameDate = new Date(context.startsAt).toLocaleDateString('en-US', {
    weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
  });

  const ml = context.oddsData?.moneyline;
  const spread = context.oddsData?.spread;
  const total = context.oddsData?.total;

  const content = `
<h2>Matchup Overview</h2>
<p>The ${context.awayTeam} travel to take on the ${context.homeTeam} on ${gameDate} in ${context.league.toUpperCase()} action. ${context.sanitizedSummary}</p>

<h2>Key Storylines</h2>
<ul>
${context.keyFactors.map(f => `<li>${f}</li>`).join('\n')}
</ul>

<h2>Current Markets</h2>
<p>Here are the current markets for this matchup:</p>
<ul>
<li><strong>Moneyline:</strong> ${context.homeTeam} ${ml?.home ?? 'N/A'} | ${context.awayTeam} ${ml?.away ?? 'N/A'}</li>
<li><strong>Spread:</strong> ${context.homeTeam} ${spread?.home?.line ?? 'N/A'} | ${context.awayTeam} ${spread?.away?.line ?? 'N/A'}</li>
<li><strong>Total:</strong> Over/Under ${total?.over?.line ?? 'N/A'}</li>
</ul>

<h2>Spread Analysis</h2>
<p>${context.spreadAnalysis}</p>

<h2>Total Analysis</h2>
<p>${context.totalAnalysis}</p>

${context.injuryNotes ? `<h2>Injury Report</h2>\n<p>${context.injuryNotes}</p>` : ''}

<h2>What to Watch</h2>
<p>This game features several compelling narratives to follow. Keep an eye on matchup dynamics, pace of play, and how both teams adjust throughout the contest.</p>

<h3>What time does ${context.awayTeam} vs ${context.homeTeam} start?</h3>
<p>The game is scheduled for ${gameDate}.</p>

<h3>What are the current markets?</h3>
<p>The moneyline is ${context.homeTeam} ${ml?.home ?? 'N/A'} / ${context.awayTeam} ${ml?.away ?? 'N/A'}, with a spread of ${spread?.home?.line ?? 'N/A'} and a total of ${total?.over?.line ?? 'N/A'}.</p>

<h3>What are the key factors in this game?</h3>
<p>${context.keyFactors.slice(0, 3).join('. ')}.</p>
`.trim();

  return {
    title: `${context.awayTeam} vs ${context.homeTeam} Preview - ${context.league.toUpperCase()} ${gameDate.split(',')[0]}`,
    metaDescription: `${context.awayTeam} at ${context.homeTeam} ${context.league.toUpperCase()} game preview with current markets, key storylines, and matchup analysis.`,
    content,
    excerpt: `${context.awayTeam} face ${context.homeTeam} in ${context.league.toUpperCase()} action. Get the full matchup breakdown.`,
    tags: [context.league.toLowerCase(), context.homeTeam.toLowerCase().split(' ').pop() || '', context.awayTeam.toLowerCase().split(' ').pop() || '', 'preview', 'current-markets'],
  };
}

export async function saveBlogPost(params: {
  archivedForecastId: string;
  slug: string;
  sport: string;
  startsAt: string;
  title: string;
  metaDescription: string;
  content: string;
  excerpt: string;
  tags: string[];
  homeTeam: string;
  awayTeam: string;
  gameDate: string;
  status?: string;
}): Promise<string> {
  const canonicalMatchups = await pool.query(
    `SELECT bp.id, bp.slug, bp.sport, bp.game_date, bp.home_team, bp.away_team, af.starts_at
     FROM rm_blog_posts bp
     LEFT JOIN rm_archived_forecasts af ON af.id = bp.archived_forecast_id
     WHERE bp.sport = $1
       AND bp.game_date = $2`,
    [params.sport, params.gameDate],
  );

  const existingMatch = canonicalMatchups.rows.find((row) => blogPostsLikelyDuplicate({
    sport: params.sport,
    gameDate: params.gameDate,
    homeTeam: params.homeTeam,
    awayTeam: params.awayTeam,
    startsAt: params.startsAt,
  }, row));

  if (existingMatch) {
    const { rows } = await pool.query(
      `UPDATE rm_blog_posts
       SET archived_forecast_id = $1,
           title = $2,
           meta_description = $3,
           content = $4,
           excerpt = $5,
           tags = $6,
           home_team = $7,
           away_team = $8,
           status = $9,
           published_at = $10,
           updated_at = NOW()
       WHERE id = $11
       RETURNING id`,
      [
        params.archivedForecastId,
        params.title,
        params.metaDescription,
        params.content,
        params.excerpt,
        params.tags,
        params.homeTeam,
        params.awayTeam,
        params.status || 'published',
        params.status === 'draft' ? null : new Date(),
        existingMatch.id,
      ],
    );
    return rows[0].id;
  }

  const { rows } = await pool.query(
    `INSERT INTO rm_blog_posts
      (archived_forecast_id, slug, sport, title, meta_description, content, excerpt, tags,
       home_team, away_team, game_date, status, published_at, canonical_url)
     VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
     ON CONFLICT (slug) DO UPDATE SET
       title = EXCLUDED.title, content = EXCLUDED.content, meta_description = EXCLUDED.meta_description,
       excerpt = EXCLUDED.excerpt, tags = EXCLUDED.tags, updated_at = NOW()
     RETURNING id`,
    [
      params.archivedForecastId,
      params.slug,
      params.sport,
      params.title,
      params.metaDescription,
      params.content,
      params.excerpt,
      params.tags,
      params.homeTeam,
      params.awayTeam,
      params.gameDate,
      params.status || 'published',
      params.status === 'draft' ? null : new Date(),
      `https://rainmakersports.app/rain-wire/${params.sport.toLowerCase()}/${params.slug}`,
    ]
  );
  return rows[0].id;
}
