import { getScreenshotCropTemplate } from "./screenshotTemplates";
import type { ScreenshotParsedRow, ScreenshotType } from "./types";

type StatScreenshotType = Extract<
  ScreenshotType,
  "passing_stats" | "rushing_stats" | "receiving_stats"
>;

export type ScreenshotPlayerNameCandidate = {
  displayName: string;
  firstName: string | null;
  lastName: string | null;
  position: string | null;
  teamShortName: string | null;
  teamDisplayName: string | null;
};

export type ScreenshotGameScoreCandidate = {
  gameRow: number | null;
  awayTeamShortName: string | null;
  awayTeamDisplayName: string | null;
  awayScore: number | null;
  homeTeamShortName: string | null;
  homeTeamDisplayName: string | null;
  homeScore: number | null;
};

export type ScreenshotTextParserOptions = {
  gameScoreCandidates?: ScreenshotGameScoreCandidate[];
  playerCandidates?: ScreenshotPlayerNameCandidate[];
};

const SECTION_LABEL_PATTERN = /^\[[^\]]+\]$/;

const STAT_ROW_TYPES: Record<StatScreenshotType, string> = {
  passing_stats: "passing",
  rushing_stats: "rushing",
  receiving_stats: "receiving"
};

const STAT_ROW_POSITIONS: Record<StatScreenshotType, Set<string>> = {
  passing_stats: new Set(["QB"]),
  rushing_stats: new Set(["QB", "HB", "FB", "WR"]),
  receiving_stats: new Set(["HB", "FB", "WR", "TE"])
};

type HighlightScoreContext = {
  score: string | null;
  teamKeys: Set<string>;
};

export function parseScreenshotExtractionText(
  screenshotType: ScreenshotType,
  text: string,
  options: ScreenshotTextParserOptions = {}
): ScreenshotParsedRow[] {
  const lines = ocrContentLines(text);
  if (!lines.length) return [];
  if (screenshotType === "highlight_list") {
    return parseHighlightRows(lines, options, text);
  }
  if (isStatScreenshotType(screenshotType)) {
    return parseStatRows(screenshotType, lines, options);
  }
  return [];
}

function parseHighlightRows(
  lines: string[],
  options: ScreenshotTextParserOptions,
  text: string
): ScreenshotParsedRow[] {
  const scoreContext = inferHighlightScoreContext(
    text,
    options.gameScoreCandidates ?? []
  );

  return lines
    .map((line) => {
      const normalizedLine = normalizeHighlightLine(line);
      if (!isLikelyHighlightLine(normalizedLine)) return null;
      const score = normalizedLine.match(/\b\d{1,3}\s*[-]\s*\d{1,3}\b/)?.[0] ?? null;
      const period = parsePeriod(normalizedLine);
      const clock = normalizedLine.match(/\b\d{1,2}:\d{2}\b/)?.[0] ?? null;
      const summary = normalizeHighlightPlayerNames(
        normalizedLine.replace(/\s+/g, " ").trim(),
        options.playerCandidates ?? []
      );
      if (summary.length < 4) return null;
      const row: ScreenshotParsedRow = {
        rowType: "highlight",
        sequence: null,
        period,
        clock,
        team: inferHighlightTeam(
          summary,
          options.playerCandidates ?? [],
          scoreContext.teamKeys
        ),
        summary,
        score: score ? score.replace(/\s+/g, "") : scoreContext.score,
        rawText: line
      };
      return row;
    })
    .filter((row): row is ScreenshotParsedRow => row !== null)
    .map((row, index) => ({
      ...row,
      sequence: index + 1
    }));
}

function inferHighlightScoreContext(
  text: string,
  gameScoreCandidates: ScreenshotGameScoreCandidate[]
): HighlightScoreContext {
  const visibleTeamKeys = visibleTeamKeysFromText(text);
  if (visibleTeamKeys.size < 2 && gameScoreCandidates.length !== 1) {
    return {
      score: null,
      teamKeys: visibleTeamKeys
    };
  }

  const candidatesWithScores = gameScoreCandidates
    .filter(
      (candidate) =>
        candidate.awayScore !== null &&
        candidate.homeScore !== null
    );
  const matches = (candidatesWithScores.length === 1
    ? candidatesWithScores
    : candidatesWithScores.filter(
        (candidate) =>
          candidateTeamKeys(candidate, "away").some((key) =>
            visibleTeamKeys.has(key)
          ) &&
          candidateTeamKeys(candidate, "home").some((key) =>
            visibleTeamKeys.has(key)
          )
      )
  )
    .flatMap((candidate) => {
      const score = formatGameScoreCandidate(candidate);
      return score ? [{ candidate, score }] : [];
    });

  const uniqueMatches = Array.from(new Set(matches.map((match) => match.score)));
  if (uniqueMatches.length !== 1) {
    return {
      score: null,
      teamKeys: visibleTeamKeys
    };
  }

  return {
    score: uniqueMatches[0],
    teamKeys: gameScoreCandidateTeamKeys(matches[0].candidate)
  };
}

function visibleTeamKeysFromText(text: string): Set<string> {
  return new Set(
    Array.from(text.matchAll(/\b[A-Z]{2,4}\b/g))
      .map((match) => normalizeTeamKey(match[0]))
      .filter((key) => key && !["na", "pat", "qtr", "ot"].includes(key))
  );
}

function gameScoreCandidateTeamKeys(
  candidate: ScreenshotGameScoreCandidate
): Set<string> {
  return new Set([
    ...candidateTeamKeys(candidate, "away"),
    ...candidateTeamKeys(candidate, "home")
  ]);
}

function candidateTeamKeys(
  candidate: ScreenshotGameScoreCandidate,
  side: "away" | "home"
): string[] {
  const values =
    side === "away"
      ? [candidate.awayTeamShortName, candidate.awayTeamDisplayName]
      : [candidate.homeTeamShortName, candidate.homeTeamDisplayName];
  return values.flatMap((value) => (value ? [normalizeTeamKey(value)] : []));
}

function formatGameScoreCandidate(
  candidate: ScreenshotGameScoreCandidate
): string | null {
  if (candidate.awayScore === null || candidate.homeScore === null) return null;
  const awayTeam = candidate.awayTeamShortName ?? candidate.awayTeamDisplayName;
  const homeTeam = candidate.homeTeamShortName ?? candidate.homeTeamDisplayName;
  if (!awayTeam || !homeTeam) return null;
  return `${awayTeam} ${candidate.awayScore} - ${homeTeam} ${candidate.homeScore}`;
}

function inferHighlightTeam(
  summary: string,
  playerCandidates: ScreenshotPlayerNameCandidate[],
  allowedTeamKeys: Set<string>
): string | null {
  const summaryKey = highlightNameKey(summary);
  if (!summaryKey || !playerCandidates.length) return null;

  const matches = playerCandidates
    .flatMap((candidate) => {
      const team =
        candidate.teamShortName?.trim() || candidate.teamDisplayName?.trim() || null;
      if (!team) return [];
      const teamKeys = playerCandidateTeamKeys(candidate);
      if (
        allowedTeamKeys.size > 0 &&
        !teamKeys.some((key) => allowedTeamKeys.has(key))
      ) {
        return [];
      }
      return highlightPlayerNameLabels(candidate).flatMap((label) => {
        const labelKey = highlightNameKey(label);
        if (!labelKey || labelKey.length < 6) return [];
        const index = summaryKey.indexOf(labelKey);
        return index >= 0
          ? [
              {
                index,
                team,
                labelLength: labelKey.length
              }
            ]
          : [];
      });
    })
    .sort(
      (left, right) =>
        left.index - right.index || right.labelLength - left.labelLength
    );

  const first = matches[0];
  if (!first) return null;

  const earliestTeams = new Set(
    matches
      .filter((match) => match.index === first.index)
      .map((match) => normalizeTeamKey(match.team))
  );
  return earliestTeams.size === 1 ? first.team : null;
}

function normalizeHighlightPlayerNames(
  summary: string,
  playerCandidates: ScreenshotPlayerNameCandidate[]
): string {
  let normalized = summary;
  for (const candidate of playerCandidates) {
    for (const variant of highlightPlayerNameOcrVariants(candidate.displayName)) {
      normalized = normalized.replace(
        new RegExp(`\\b${escapeRegex(variant)}\\b`, "gi"),
        candidate.displayName
      );
    }
  }
  return normalized;
}

function highlightPlayerNameLabels(
  candidate: ScreenshotPlayerNameCandidate
): string[] {
  const labels = [candidate.displayName];
  const firstName = candidate.firstName?.trim();
  const lastName = candidate.lastName?.trim();
  if (firstName && lastName) labels.push(`${firstName} ${lastName}`);
  return Array.from(new Set(labels));
}

function highlightPlayerNameOcrVariants(displayName: string): string[] {
  const suffixMatch = displayName.match(/^(.+)\s+(II|III)$/i);
  if (!suffixMatch) return [];

  const base = suffixMatch[1];
  const suffix = suffixMatch[2].toUpperCase();
  if (suffix === "II") return [`${base} Il`, `${base} ll`, `${base} I]`];
  return [`${base} Ill`, `${base} lll`];
}

function highlightNameKey(value: string): string {
  return value
    .toLowerCase()
    .replace(/\b(?:il|ll|i\])\b/g, "ii")
    .replace(/[^a-z0-9]+/g, "");
}

function escapeRegex(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function parseStatRows(
  screenshotType: StatScreenshotType,
  lines: string[],
  options: ScreenshotTextParserOptions
): ScreenshotParsedRow[] {
  const template = getScreenshotCropTemplate(screenshotType);
  if (!template) return [];

  const statColumns = template.columns
    .map((column) => column.key)
    .filter((key) => key !== "player" && key !== "team");

  return lines
    .map((line) =>
      parseStatLine(
        line,
        screenshotType,
        STAT_ROW_TYPES[screenshotType],
        statColumns,
        options.playerCandidates ?? []
      )
    )
    .filter((row): row is ScreenshotParsedRow => row !== null);
}

function parseStatLine(
  line: string,
  screenshotType: StatScreenshotType,
  rowType: string,
  statColumns: string[],
  playerCandidates: ScreenshotPlayerNameCandidate[]
): ScreenshotParsedRow | null {
  if (isLikelyStatHeader(line)) return null;

  const firstValue = firstValueToken(line);
  if (!firstValue || firstValue.index <= 0) return null;

  const playerParts = splitTeamPlayer(line.slice(0, firstValue.index));
  const playerOcr = playerParts.player;
  if (!playerOcr || isLikelyStatHeader(playerOcr)) return null;

  const values = statValues(line.slice(firstValue.index));
  const numericCount = values.filter((value) => typeof value === "number").length;
  if (numericCount < 2) return null;

  const playerMatch = reconcilePlayerName(
    screenshotType,
    playerParts.team,
    playerOcr,
    playerCandidates
  );
  const player = playerMatch ?? fallbackPlayerNameFromOcr(playerOcr, playerCandidates);
  const row: ScreenshotParsedRow = {
    rowType,
    player: player ?? playerOcr,
    team: playerParts.team,
    rawText: line
  };
  if (player && player !== playerOcr) row.playerOcr = playerOcr;

  statColumns.forEach((column, index) => {
    row[column] = values[index] ?? null;
  });

  return row;
}

function reconcilePlayerName(
  screenshotType: StatScreenshotType,
  team: string | null,
  playerOcr: string,
  playerCandidates: ScreenshotPlayerNameCandidate[]
): string | null {
  if (!playerCandidates.length) return null;

  const positionSet = STAT_ROW_POSITIONS[screenshotType];
  const teamKey = team ? normalizeTeamKey(team) : null;
  const candidateScores = playerCandidates
    .filter((candidate) => candidate.displayName.trim())
    .filter((candidate) => {
      const position = candidate.position?.toUpperCase() ?? null;
      return position ? positionSet.has(position) : true;
    })
    .filter((candidate) => !teamKey || playerCandidateTeamKeys(candidate).includes(teamKey))
    .map((candidate) => ({
      candidate,
      score: playerNameScore(playerOcr, candidate)
    }))
    .sort((left, right) => right.score - left.score);

  const best = candidateScores[0];
  if (!best || best.score < 0.75) return null;

  const second = candidateScores[1];
  if (second && best.score - second.score < 0.08) return null;

  return best.candidate.displayName;
}

function fallbackPlayerNameFromOcr(
  playerOcr: string,
  playerCandidates: ScreenshotPlayerNameCandidate[]
): string | null {
  const abbreviated = abbreviatedOcrName(playerOcr);
  if (!abbreviated) return null;

  const lastName = fallbackLastNameWithSuffix(
    abbreviated.lastName,
    playerCandidates
  );
  if (!lastName) return null;

  return `${abbreviated.initial}.${lastName}`;
}

function abbreviatedOcrName(
  value: string
): { initial: string; lastName: string } | null {
  const normalized = normalizePlayerName(value);
  const match = normalized.match(/^([a-zA-Z1Il])\.?\s*([a-zA-Z'-]{3,})$/);
  if (!match) return null;

  return {
    initial: normalizeOcrInitial(match[1]),
    lastName: match[2]
  };
}

function normalizeOcrInitial(value: string): string {
  if (value === "1") return "I";
  return value.toUpperCase();
}

function fallbackLastNameWithSuffix(
  value: string,
  playerCandidates: ScreenshotPlayerNameCandidate[]
): string | null {
  const suffixMatch = value.match(/^([a-zA-Z'-]{3,}?)(lll|iii|ll|ii)$/i);
  if (!suffixMatch) return null;

  const stem = suffixMatch[1];
  const suffixText = suffixMatch[2].toLowerCase();
  const knownLastNames = knownLastNameKeys(playerCandidates);
  const stemKey = simpleNameKey(stem);
  if (!knownLastNames.has(stemKey)) return null;

  const knownSuffix = knownLastNameSuffixes(playerCandidates).get(stemKey);
  const suffix =
    knownSuffix ??
    (suffixText === "iii" ? "III" : "II");
  return `${titleCaseName(stem)} ${suffix}`;
}

function knownLastNameKeys(
  playerCandidates: ScreenshotPlayerNameCandidate[]
): Set<string> {
  const keys = new Set<string>();
  for (const candidate of playerCandidates) {
    const lastName = candidate.lastName?.trim();
    if (!lastName) continue;
    keys.add(simpleNameKey(lastNameWithoutRomanSuffix(lastName)));
  }
  return keys;
}

function knownLastNameSuffixes(
  playerCandidates: ScreenshotPlayerNameCandidate[]
): Map<string, string> {
  const suffixes = new Map<string, string>();
  for (const candidate of playerCandidates) {
    const lastName = candidate.lastName?.trim();
    if (!lastName) continue;
    const suffix = lastName.match(/\b(II|III|IV|V)\b$/i)?.[1]?.toUpperCase();
    if (!suffix) continue;
    suffixes.set(simpleNameKey(lastNameWithoutRomanSuffix(lastName)), suffix);
  }
  return suffixes;
}

function lastNameWithoutRomanSuffix(value: string): string {
  return value.replace(/\s+\b(?:II|III|IV|V)\b$/i, "").trim();
}

function titleCaseName(value: string): string {
  return value.replace(/\b[a-z]/gi, (letter) => letter.toUpperCase());
}

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

function playerCandidateTeamKeys(candidate: ScreenshotPlayerNameCandidate): string[] {
  return [candidate.teamShortName, candidate.teamDisplayName]
    .flatMap((value) => (value ? [normalizeTeamKey(value)] : []))
    .filter(Boolean);
}

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

function playerNameScore(
  playerOcr: string,
  candidate: ScreenshotPlayerNameCandidate
): number {
  const ocrKeys = normalizedPlayerNameKeys(playerOcr);
  const candidateKeys = candidatePlayerNameKeys(candidate);
  if (!ocrKeys.length || !candidateKeys.length) return 0;

  let best = 0;
  for (const ocrKey of ocrKeys) {
    for (const candidateKey of candidateKeys) {
      best = Math.max(best, stringSimilarity(ocrKey, candidateKey));
    }
  }
  return best;
}

function candidatePlayerNameKeys(candidate: ScreenshotPlayerNameCandidate): string[] {
  const firstName = candidate.firstName?.trim() ?? "";
  const lastName = candidate.lastName?.trim() ?? "";
  const firstInitial = firstName[0] ?? "";
  const labels = [candidate.displayName, lastName];
  if (firstInitial && lastName) {
    labels.push(`${firstInitial}.${lastName}`, `${firstInitial}${lastName}`);
  }
  return Array.from(new Set(labels.flatMap(normalizedPlayerNameKeys)));
}

function normalizedPlayerNameKeys(value: string): string[] {
  const base = value
    .toLowerCase()
    .replace(/^[1|](?=[a-z.])/, "l")
    .replace(/\b([a-z])\s+(?=[a-z])/g, "$1.")
    .replace(/[^a-z0-9]+/g, "");
  if (!base) return [];

  const variants = new Set([base]);
  variants.add(base.replace(/^l(?=[a-z])/, "i"));
  variants.add(base.replace(/^i(?=[a-z])/, "l"));
  variants.add(base.replace(/ll$/g, "ii"));
  variants.add(base.replace(/iii$/g, "ii"));
  variants.add(base.replace(/ii$/g, ""));
  variants.add(base.replace(/ll$/g, ""));
  return Array.from(variants).filter(Boolean);
}

function stringSimilarity(left: string, right: string): number {
  if (left === right) return 1;
  if (!left.length || !right.length) return 0;
  const distance = editDistance(left, right);
  return 1 - distance / Math.max(left.length, right.length);
}

function editDistance(left: string, right: string): number {
  const previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
  const current = Array.from({ length: right.length + 1 }, () => 0);

  for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
    current[0] = leftIndex;
    for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
      const cost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
      current[rightIndex] = Math.min(
        previous[rightIndex] + 1,
        current[rightIndex - 1] + 1,
        previous[rightIndex - 1] + cost
      );
    }
    previous.splice(0, previous.length, ...current);
  }

  return previous[right.length] ?? 0;
}

function firstValueToken(line: string): { index: number } | null {
  const matches = line.matchAll(/\S+/g);
  for (const match of matches) {
    if (match.index === undefined) continue;
    if (statTokenValue(match[0]) !== undefined) {
      return { index: match.index };
    }
  }
  return null;
}

function normalizeHighlightLine(line: string): string {
  return line.replace(/^\s*(?:N\/A|NA|RATING)\s+/i, "").replace(/\s+/g, " ").trim();
}

function isLikelyHighlightLine(line: string): boolean {
  const normalized = line.toLowerCase();
  return (
    /\b[1-4](?:st|nd|rd|th)\s*&\s*\d+\b/i.test(line) ||
    normalized.includes("kickoff") ||
    normalized.includes("pat") ||
    normalized.includes("extra point") ||
    normalized.includes("field goal") ||
    normalized.includes("punt") ||
    normalized.includes("touchdown") ||
    normalized.includes("pass ") ||
    normalized.includes("rush ")
  );
}

function ocrContentLines(text: string): string[] {
  return text
    .split(/\r?\n/)
    .map((line) => line.replace(/[|]/g, " ").replace(/\s+/g, " ").trim())
    .filter(Boolean)
    .filter((line) => !SECTION_LABEL_PATTERN.test(line))
    .filter((line) => !isLikelyNoiseLine(line));
}

function splitTeamPlayer(value: string): { team: string | null; player: string } {
  const normalized = normalizePlayerName(value);
  const parts = normalized.split(" ");
  if (parts.length >= 2 && /^[A-Z]{2,4}$/.test(parts[0])) {
    return {
      team: parts[0],
      player: parts.slice(1).join(" ")
    };
  }
  return {
    team: null,
    player: normalized
  };
}

function statValues(value: string): Array<number | null> {
  return value
    .trim()
    .split(/\s+/)
    .flatMap((token) => {
      if (token.includes("/")) {
        return token
          .split("/")
          .map(statTokenValue)
          .filter((part): part is number | null => part !== undefined);
      }
      const parsed = statTokenValue(token);
      return parsed === undefined ? [] : [parsed];
    });
}

function statTokenValue(token: string): number | null | undefined {
  const cleaned = token.trim().replace(/,/g, "");
  if (!cleaned) return undefined;

  const bracketless = cleaned.replace(/^[\[\(]+/, "").replace(/[\]\)]+$/, "");
  if (/^-?\d+(?:\.\d+)?$/.test(bracketless)) {
    return Number(bracketless);
  }

  if (/^[\[\(]?[iIlL1][\]\)]?$/.test(cleaned)) return 1;
  if (/^[\[\(]?[xX:]+[\]\)]?$/.test(cleaned)) return null;

  const clippedNumber = cleaned.match(/^-?\d+(?:\.\d+)?(?=[\[\]])/);
  if (clippedNumber) return Number(clippedNumber[0]);

  return undefined;
}

function parsePeriod(line: string): string | null {
  const prefixQuarter = line.match(/\bQ([1-4])\b/i);
  if (prefixQuarter) return `Q${prefixQuarter[1]}`;
  const suffixQuarter = line.match(/\b([1-4])Q\b/i);
  if (suffixQuarter) return `Q${suffixQuarter[1]}`;
  const namedQuarter = line.match(/\b([1-4])(?:ST|ND|RD|TH)\s+(?:QUARTER|QTR)\b/i);
  if (namedQuarter) return `Q${namedQuarter[1]}`;
  const prefixedQuarter = line.match(/\b(?:QUARTER|QTR)\s*([1-4])\b/i);
  if (prefixedQuarter) return `Q${prefixedQuarter[1]}`;
  if (/\bOT\b/i.test(line)) return "OT";
  return null;
}

function normalizePlayerName(value: string): string {
  return value
    .replace(/[^a-zA-Z0-9 .'-]/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function isLikelyStatHeader(value: string): boolean {
  const normalized = value.toLowerCase();
  return (
    normalized === "player" ||
    normalized === "name" ||
    normalized.includes("team player") ||
    normalized.includes("player team") ||
    normalized.includes("team att") ||
    normalized.includes("team rec")
  );
}

function isLikelyNoiseLine(line: string): boolean {
  const normalized = line.toLowerCase();
  if (normalized.length <= 2) return true;
  return normalized === "ocr text" || normalized === "source screenshot";
}

function isStatScreenshotType(
  screenshotType: ScreenshotType
): screenshotType is StatScreenshotType {
  return (
    screenshotType === "passing_stats" ||
    screenshotType === "rushing_stats" ||
    screenshotType === "receiving_stats"
  );
}
