import type {
  PublicTopGameCard,
  PublicTopPickEntry,
  PublicTopPicksResponseV1,
  PublicTopPropCard,
} from '../contracts/forecast-public-contract';

export interface SportsWeatherMapOptions {
  clipSeconds?: number;
  clipCount?: number;
  aspectRatio?: string;
}

export interface SportsWeatherMapClip {
  index: number;
  durationSeconds: number;
  slug: string;
  visualGoal: string;
  voiceover: string;
  overlay: string;
  prompt: string;
}

export interface SportsWeatherMapPromptResult {
  sport: string;
  league: string;
  title: string;
  totalDurationSeconds: number;
  clipSeconds: number;
  sourceSummary: string[];
  clips: SportsWeatherMapClip[];
  prompt: string;
}

type SportTemplate = {
  sport: string;
  title: string;
  introHook: string;
  introVisual: string;
  outroLine: string;
  mapLabel: string;
  sceneTone: string;
};

const MAX_CLIP_SECONDS = 8;

const SPORT_TEMPLATES: Record<string, SportTemplate> = {
  baseball: {
    sport: 'baseball',
    title: 'Rainmaker Diamond Weather Map',
    introHook: 'Partly cloudy with a chance of extra-base damage.',
    introVisual: 'green-screen baseball radar with diamond zones, wind arrows, and bullpen heat markers',
    outroLine: 'Keep the card tight and let the board do the work.',
    mapLabel: 'Diamond Radar',
    sceneTone: 'crisp studio energy with ballpark-style overlays and wind-direction graphics',
  },
  basketball: {
    sport: 'basketball',
    title: 'Rainmaker Hardwood Weather Map',
    introHook: 'Check the radar. Shot volume and pace are building fast tonight.',
    introVisual: 'green-screen hardwood weather board with court overlays, pace arrows, and shot-zone heat clouds',
    outroLine: 'One unit discipline. Let the numbers stay louder than the sweat.',
    mapLabel: 'Hardwood Forecast',
    sceneTone: 'fast studio pacing, clean analyst delivery, and bright arena color cues',
  },
  football: {
    sport: 'football',
    title: 'Rainmaker Gridiron Weather Map',
    introHook: 'Pressure fronts are colliding and the board is finally honest.',
    introVisual: 'green-screen football weather board with field stripes, pressure systems, and spread arrows',
    outroLine: 'Play the edge, not the helmet sticker.',
    mapLabel: 'Gridiron Forecast',
    sceneTone: 'big-slate urgency with clean telestrator movements and bold field graphics',
  },
  hockey: {
    sport: 'hockey',
    title: 'Rainmaker Ice Front Weather Map',
    introHook: 'Cold front on the glass tonight. Shot volume and chaos are live.',
    introVisual: 'green-screen hockey map with rink overlays, shot-lane streaks, and ice-storm icons',
    outroLine: 'Stay selective and let the ice tilt come to you.',
    mapLabel: 'Ice Front',
    sceneTone: 'sharp studio delivery with icy highlights and fast on-screen transitions',
  },
  soccer: {
    sport: 'soccer',
    title: 'Rainmaker Pitch Pressure Map',
    introHook: 'Possession pressure is building and the value pockets are obvious.',
    introVisual: 'green-screen pitch map with passing lanes, heat zones, and pressure arrows',
    outroLine: 'Trust the shape, trust the price, and stop forcing variance.',
    mapLabel: 'Pitch Pressure',
    sceneTone: 'measured analyst cadence with clean tactical overlays and motion lines',
  },
  generic: {
    sport: 'generic',
    title: 'Rainmaker Sports Weather Map',
    introHook: 'The board is moving and the cleanest edges are already showing.',
    introVisual: 'green-screen sports weather board with weather icons, market arrows, and confidence badges',
    outroLine: 'Small card. Clean process. Let the model breathe.',
    mapLabel: 'Sports Weather',
    sceneTone: 'premium broadcast delivery with energetic but controlled movements',
  },
};

function inferSport(league: string): keyof typeof SPORT_TEMPLATES {
  const normalized = String(league || '').trim().toLowerCase();
  if (['mlb'].includes(normalized)) return 'baseball';
  if (['nba', 'wnba', 'ncaab'].includes(normalized)) return 'basketball';
  if (['nfl', 'ncaaf'].includes(normalized)) return 'football';
  if (['nhl'].includes(normalized)) return 'hockey';
  if (['epl', 'la_liga', 'bundesliga', 'serie_a', 'ligue_1', 'champions_league', 'mls'].includes(normalized)) {
    return 'soccer';
  }
  return 'generic';
}

function toLeagueLabel(league: string): string {
  const normalized = String(league || '').trim().toLowerCase();
  if (!normalized) return 'sports';
  return normalized.toUpperCase().replace(/_/g, ' ');
}

function clampClipSeconds(value?: number): number {
  const parsed = Number(value ?? MAX_CLIP_SECONDS);
  if (!Number.isFinite(parsed)) return MAX_CLIP_SECONDS;
  return Math.max(4, Math.min(MAX_CLIP_SECONDS, Math.round(parsed)));
}

function toOneDecimal(value: number | null | undefined): string {
  if (value == null || !Number.isFinite(Number(value))) return '0.0';
  return Number(value).toFixed(1);
}

function formatOdds(value: number | null | undefined): string {
  if (value == null || !Number.isFinite(Number(value))) return 'EVEN';
  const numeric = Number(value);
  return numeric > 0 ? `+${numeric}` : `${numeric}`;
}

function buildPickLabel(entry: PublicTopPickEntry): string {
  if (entry.kind === 'prop' && entry.prop) {
    const prop = entry.prop;
    return `${prop.playerName} ${prop.forecastDirection} ${toOneDecimal(prop.marketLine)} ${prop.propType}`;
  }
  if (entry.kind === 'game' && entry.game) {
    const game = entry.game;
    const side = game.forecastSide || `${game.awayTeam} at ${game.homeTeam}`;
    return `${side} game edge`;
  }
  return `Pick ${entry.rankPosition}`;
}

function buildSourceLine(entry: PublicTopPickEntry): string {
  if (entry.kind === 'prop' && entry.prop) {
    const prop = entry.prop;
    return [
      `#${entry.rankPosition}`,
      buildPickLabel(entry),
      `${toOneDecimal(prop.projectedProbability)}% model`,
      `${toOneDecimal(prop.edgePct)}% edge`,
      `${formatOdds(prop.odds)} odds`,
    ].join(' | ');
  }
  if (entry.kind === 'game' && entry.game) {
    const game = entry.game;
    return [
      `#${entry.rankPosition}`,
      buildPickLabel(entry),
      `${toOneDecimal(game.confidencePct)}% confidence`,
      `${toOneDecimal(game.edge)} edge`,
      `${toOneDecimal(game.valueRating)} value`,
    ].join(' | ');
  }
  return `#${entry.rankPosition} | ${buildPickLabel(entry)}`;
}

function entryIdentity(entry: PublicTopPickEntry): string {
  if (entry.kind === 'prop' && entry.prop) {
    return `prop:${entry.prop.playerName}:${entry.prop.eventId}`;
  }
  if (entry.kind === 'game' && entry.game) {
    return `game:${entry.game.eventId}`;
  }
  return `${entry.kind}:${entry.rankPosition}`;
}

function selectStoryEntries(entries: PublicTopPickEntry[], targetCount: number): PublicTopPickEntry[] {
  const chosen: PublicTopPickEntry[] = [];
  const seen = new Set<string>();

  for (const entry of entries) {
    if (chosen.length >= targetCount) break;
    const identity = entryIdentity(entry);
    if (seen.has(identity)) continue;
    seen.add(identity);
    chosen.push(entry);
  }

  if (chosen.length >= targetCount) return chosen;

  for (const entry of entries) {
    if (chosen.length >= targetCount) break;
    if (chosen.includes(entry)) continue;
    chosen.push(entry);
  }

  return chosen;
}

function buildOverlay(entry: PublicTopPickEntry, metaphor: string): string {
  if (entry.kind === 'prop' && entry.prop) {
    const prop = entry.prop;
    return `${buildPickLabel(entry)} ${metaphor} | ${toOneDecimal(prop.projectedProbability)}% | ${toOneDecimal(prop.edgePct)}% edge`;
  }
  const game = entry.game as PublicTopGameCard;
  return `${buildPickLabel(entry)} ${metaphor} | ${toOneDecimal(game.confidencePct)}% | ${toOneDecimal(game.edge)} edge`;
}

function buildVoiceover(entry: PublicTopPickEntry, metaphor: string): string {
  if (entry.kind === 'prop' && entry.prop) {
    const prop = entry.prop;
    const verification = prop.verificationLabel ? `${prop.verificationLabel.toLowerCase()}` : 'source-backed odds';
    return `${metaphor} on ${prop.team}. ${prop.playerName} is ${prop.forecastDirection.toLowerCase()} ${toOneDecimal(prop.marketLine)} ${prop.propType} at ${formatOdds(prop.odds)}. Rainmaker has this at ${toOneDecimal(prop.projectedProbability)} percent with a ${toOneDecimal(prop.edgePct)} percent edge on ${verification}.`;
  }
  const game = entry.game as PublicTopGameCard;
  const side = game.forecastSide || `${game.awayTeam} at ${game.homeTeam}`;
  return `${metaphor} on the main board. ${side} leads this slate at ${toOneDecimal(game.confidencePct)} percent confidence with a ${toOneDecimal(game.edge)} point edge and ${toOneDecimal(game.valueRating)} value rating.`;
}

function sceneMetaphor(entry: PublicTopPickEntry, index: number): string {
  if (index === 0) return 'Sun lock';
  if (entry.kind === 'game') return 'Storm front';
  if (entry.prop?.forecastDirection === 'UNDER') return 'Cold front';
  if (entry.prop?.forecastDirection === 'OVER') return 'Lightning strike';
  return 'Pressure edge';
}

function sceneVisual(entry: PublicTopPickEntry, metaphor: string): string {
  if (entry.kind === 'prop' && entry.prop) {
    const prop = entry.prop;
    return `${metaphor} icon animates over ${prop.team} while the presenter points to ${prop.playerName}'s line card and the stat ticker flashes ${prop.propType}.`;
  }
  const game = entry.game as PublicTopGameCard;
  return `${metaphor} graphic rolls across the matchup board while the presenter traces the forecast side and highlights the live edge meter.`;
}

function scenePrompt(params: {
  template: SportTemplate;
  clipSeconds: number;
  aspectRatio: string;
  entry: PublicTopPickEntry;
  metaphor: string;
  role: string;
}): string {
  const { template, clipSeconds, aspectRatio, entry, metaphor, role } = params;
  const label = buildPickLabel(entry);
  const visual = sceneVisual(entry, metaphor);
  const voiceover = buildVoiceover(entry, metaphor);
  return [
    `Create clip ${role} for a stitched ${template.title} sequence.`,
    `Hard cap: ${clipSeconds} seconds. Aspect ratio: ${aspectRatio}.`,
    'The bot will stitch this clip with the others, so end on a clean beat with no fade to black.',
    `Presenter: glamorous, confident, green-screen sports forecaster; broadcast-safe, non-explicit, no nudity, no wardrobe malfunction, just polished charisma and high-energy delivery.`,
    `Studio style: ${template.sceneTone}. Background: ${template.introVisual}.`,
    `Featured pick: ${label}.`,
    `Visual action: ${visual}`,
    `Voiceover target: ${voiceover}`,
    `Overlay text: ${buildOverlay(entry, metaphor)}.`,
    'Camera: medium shot with a short push-in, crisp telestrator gestures, clean freeze-frame ending for stitch points.',
  ].join(' ');
}

export function buildSportsWeatherMapPrompt(
  response: PublicTopPicksResponseV1,
  options: SportsWeatherMapOptions = {},
): SportsWeatherMapPromptResult {
  const clipSeconds = clampClipSeconds(options.clipSeconds);
  const clipCount = Math.max(2, Math.min(4, Math.round(options.clipCount ?? 4)));
  const aspectRatio = options.aspectRatio || '9:16';
  const selected = selectStoryEntries(response.entries, Math.max(1, clipCount - 1));
  const sportKey = inferSport(response.league || '');
  const template = SPORT_TEMPLATES[sportKey];

  const clips: SportsWeatherMapClip[] = [
    {
      index: 1,
      durationSeconds: Math.min(clipSeconds, 7),
      slug: 'intro',
      visualGoal: `Open on the ${template.mapLabel} board and establish the slate fast.`,
      voiceover: `${template.introHook} This is the ${template.title}, built from today's live Rainmaker board.`,
      overlay: `${template.title} | ${toLeagueLabel(response.league)} | Live Rainmaker Data`,
      prompt: [
        `Create clip 1 of a stitched ${template.title} sequence.`,
        `Hard cap: ${Math.min(clipSeconds, 7)} seconds. Aspect ratio: ${aspectRatio}.`,
        'The bot will stitch this clip with the others, so end on a crisp hold for the next cut.',
        'Presenter: glamorous, confident, green-screen sports forecaster; broadcast-safe, polished, playful, non-explicit.',
        `Background: ${template.introVisual}.`,
        `Voiceover target: ${template.introHook} This is the ${template.title}, built from today's live Rainmaker board.`,
        `On-screen board should preview the next ${selected.length} ranked picks with weather icons and confidence badges.`,
        'Camera: medium-wide opening, one clean step toward the board, finish centered for stitching.',
      ].join(' '),
    },
  ];

  selected.forEach((entry, idx) => {
    const metaphor = sceneMetaphor(entry, idx);
    clips.push({
      index: clips.length + 1,
      durationSeconds: clipSeconds,
      slug: `pick-${idx + 1}`,
      visualGoal: sceneVisual(entry, metaphor),
      voiceover: buildVoiceover(entry, metaphor),
      overlay: buildOverlay(entry, metaphor),
      prompt: scenePrompt({
        template,
        clipSeconds,
        aspectRatio,
        entry,
        metaphor,
        role: `${idx + 2}`,
      }),
    });
  });

  const lastClip = clips[clips.length - 1];
  lastClip.voiceover = `${lastClip.voiceover} ${template.outroLine}`;
  lastClip.prompt = `${lastClip.prompt} End with a direct-to-camera responsible betting line and a clean 1-beat hold.`;

  const sourceSummary = selected.map(buildSourceLine);
  const totalDurationSeconds = clips.reduce((sum, clip) => sum + clip.durationSeconds, 0);
  const promptLines = [
    `${template.title}`,
    '',
    `League: ${toLeagueLabel(response.league)}`,
    `Clip format: ${clips.length} stitched clips, each ${clipSeconds} seconds max, ${aspectRatio}`,
    '',
    'Use these real Rainmaker picks only:',
    ...sourceSummary.map((line) => `- ${line}`),
    '',
    'Clip plan:',
    ...clips.map((clip) => [
      `Clip ${clip.index} (${clip.durationSeconds}s max)`,
      `Goal: ${clip.visualGoal}`,
      `Voiceover: ${clip.voiceover}`,
      `Overlay: ${clip.overlay}`,
      `Prompt: ${clip.prompt}`,
    ].join('\n')),
    '',
    'Stitch notes:',
    '- Keep every clip self-contained and end on a clean hold for seamless stitching.',
    '- Reuse the same presenter, wardrobe palette, studio lighting, and weather-board style across all clips.',
    '- Keep the tone glamorous and high-energy but broadcast-safe. No nudity, no explicit sexual content, no wardrobe malfunction beats.',
    '- The final stitched piece should feel like one fast forecast segment built from Rainmaker picks, not four unrelated shots.',
  ];

  return {
    sport: template.sport,
    league: response.league,
    title: template.title,
    totalDurationSeconds,
    clipSeconds,
    sourceSummary,
    clips,
    prompt: promptLines.join('\n\n'),
  };
}
