/**
 * Player name normalization and fuzzy matching.
 * Used by roster-reconciler to match ESPN players to SGO players.
 *
 * 3-tier matching:
 *   Tier 1 (confidence 1.0): Exact normalized match — strip accents, lowercase, remove non-alpha except spaces
 *   Tier 2 (confidence 0.95): Suffix-stripped match — also remove Jr/Sr/II/III/IV/V
 *   Tier 3 (confidence 0.80): Last name + first initial within same team
 */

// ---------------------------------------------------------------------------
// Core normalization helpers
// ---------------------------------------------------------------------------

/**
 * Normalize a player name for comparison.
 * - Decompose unicode (NFD) then strip combining diacritical marks
 * - Lowercase
 * - Remove non-alpha characters except spaces
 * - Collapse whitespace, trim
 *
 * Example: "P.J. Washington Jr." → "pj washington jr"
 */
export function normalizeName(name: string): string {
  return name
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .toLowerCase()
    .replace(/[^a-z ]/g, '')
    .replace(/\s+/g, ' ')
    .trim();
}

/**
 * Strip common name suffixes (jr, sr, ii, iii, iv, v) from an already-
 * normalized name.  Only removes a suffix that appears as a standalone
 * word at the very end of the string.
 *
 * Example: "jimmy butler iii" → "jimmy butler"
 * Example: "tim hardaway jr"  → "tim hardaway"
 * Safe:    "derrick ivory"    → "derrick ivory" (no change — "v" is not standalone at end)
 */
export function stripSuffix(normalized: string): string {
  return normalized.replace(/\s+(?:jr|sr|ii|iii|iv|v)$/, '').trim();
}

/**
 * Build a "first-initial + rest-of-name" key for tier-3 matching.
 * Takes the first character of the first word, then everything after the
 * first space.
 *
 * Example: "ronald holland"           → "r holland"
 * Example: "shai gilgeous alexander"  → "s gilgeous alexander"
 */
export function getInitialLastName(normalized: string): string {
  const spaceIdx = normalized.indexOf(' ');
  if (spaceIdx === -1) return normalized; // single-word name — return as-is
  const firstInitial = normalized[0];
  const rest = normalized.slice(spaceIdx + 1);
  return `${firstInitial} ${rest}`;
}

// ---------------------------------------------------------------------------
// Match result type
// ---------------------------------------------------------------------------

export interface MatchResult {
  sgoPlayerID: string;
  sgoName: string;
  espnName: string;
  tier: 1 | 2 | 3;
  confidence: number;
}

// ---------------------------------------------------------------------------
// Pre-built lookup maps for O(1) matching
// ---------------------------------------------------------------------------

interface PlayerEntry {
  playerID: string;
  name: string;
}

/** Sentinel value indicating that a key maps to more than one player. */
const AMBIGUOUS = Symbol('AMBIGUOUS');

type MapValue = PlayerEntry | typeof AMBIGUOUS;

export interface LookupMaps {
  tier1: Map<string, MapValue>;
  tier2: Map<string, MapValue>;
  tier3: Map<string, MapValue>;
}

/**
 * Pre-build Maps for all 3 matching tiers so that lookups are O(1).
 *
 * For tier 2 and tier 3, if two or more players produce the same key the
 * entry is marked ambiguous and will be skipped during matching (to avoid
 * false positives).
 */
export function buildLookupMaps(players: Array<PlayerEntry>): LookupMaps {
  const tier1 = new Map<string, MapValue>();
  const tier2 = new Map<string, MapValue>();
  const tier3 = new Map<string, MapValue>();

  for (const player of players) {
    const norm = normalizeName(player.name);
    const suff = stripSuffix(norm);
    const init = getInitialLastName(suff);

    // Tier 1 — exact normalized.  Duplicates are unlikely but mark ambiguous
    // just in case.
    if (tier1.has(norm)) {
      tier1.set(norm, AMBIGUOUS);
    } else {
      tier1.set(norm, player);
    }

    // Tier 2 — suffix-stripped
    if (tier2.has(suff)) {
      tier2.set(suff, AMBIGUOUS);
    } else {
      tier2.set(suff, player);
    }

    // Tier 3 — initial + last name
    if (tier3.has(init)) {
      tier3.set(init, AMBIGUOUS);
    } else {
      tier3.set(init, player);
    }
  }

  return { tier1, tier2, tier3 };
}

// ---------------------------------------------------------------------------
// Single-player matching
// ---------------------------------------------------------------------------

/**
 * Validate that a tier-3 (initial + last name) match is plausible.
 * Requires that the first names share at least the first 3 characters,
 * OR that one is a known short form of the other (e.g. "Sam" / "Samuel").
 * This prevents false matches like "Cameron Johnson" ≠ "Chaney Johnson".
 */
function validateTier3(espnNorm: string, sgoNorm: string): boolean {
  const espnFirst = espnNorm.split(' ')[0] || '';
  const sgoFirst = sgoNorm.split(' ')[0] || '';

  // If first names are very short (≤2 chars like "CJ", "PJ"), just check initial
  if (espnFirst.length <= 2 || sgoFirst.length <= 2) {
    return espnFirst[0] === sgoFirst[0];
  }

  // Check first 3 chars match (catches Sam/Samuel, Max/Maxim, Ron/Ronald, etc.)
  const minLen = Math.min(espnFirst.length, sgoFirst.length, 3);
  return espnFirst.slice(0, minLen) === sgoFirst.slice(0, minLen);
}

/**
 * Match an ESPN player name against a list of SGO players using the 3-tier
 * system.  Returns the best match or null.
 *
 * If `prebuilt` lookup maps are provided they will be used for O(1) lookups;
 * otherwise the function falls back to a linear scan of `sgoPlayers`.
 */
export function matchPlayer(
  espnName: string,
  sgoPlayers: Array<PlayerEntry>,
  prebuilt?: LookupMaps,
): MatchResult | null {
  const espnNorm = normalizeName(espnName);
  const espnSuff = stripSuffix(espnNorm);
  const espnInit = getInitialLastName(espnSuff);

  // ----- Fast path: use pre-built maps if available -----
  if (prebuilt) {
    // Tier 1
    const t1 = prebuilt.tier1.get(espnNorm);
    if (t1 && t1 !== AMBIGUOUS) {
      return {
        sgoPlayerID: t1.playerID,
        sgoName: t1.name,
        espnName,
        tier: 1,
        confidence: 1.0,
      };
    }

    // Tier 2
    const t2 = prebuilt.tier2.get(espnSuff);
    if (t2 && t2 !== AMBIGUOUS) {
      return {
        sgoPlayerID: t2.playerID,
        sgoName: t2.name,
        espnName,
        tier: 2,
        confidence: 0.95,
      };
    }

    // Tier 3
    const t3 = prebuilt.tier3.get(espnInit);
    if (t3 && t3 !== AMBIGUOUS) {
      const t3Norm = stripSuffix(normalizeName(t3.name));
      if (validateTier3(espnSuff, t3Norm)) {
        return {
          sgoPlayerID: t3.playerID,
          sgoName: t3.name,
          espnName,
          tier: 3,
          confidence: 0.80,
        };
      }
    }

    return null;
  }

  // ----- Slow path: linear scan -----

  // Tier 1 — exact normalized match
  for (const sgo of sgoPlayers) {
    if (normalizeName(sgo.name) === espnNorm) {
      return {
        sgoPlayerID: sgo.playerID,
        sgoName: sgo.name,
        espnName,
        tier: 1,
        confidence: 1.0,
      };
    }
  }

  // Tier 2 — suffix-stripped match
  for (const sgo of sgoPlayers) {
    if (stripSuffix(normalizeName(sgo.name)) === espnSuff) {
      return {
        sgoPlayerID: sgo.playerID,
        sgoName: sgo.name,
        espnName,
        tier: 2,
        confidence: 0.95,
      };
    }
  }

  // Tier 3 — first initial + last name (with first-name validation)
  for (const sgo of sgoPlayers) {
    const sgoNorm = normalizeName(sgo.name);
    const sgoSuff = stripSuffix(sgoNorm);
    if (getInitialLastName(sgoSuff) === espnInit && validateTier3(espnSuff, sgoSuff)) {
      return {
        sgoPlayerID: sgo.playerID,
        sgoName: sgo.name,
        espnName,
        tier: 3,
        confidence: 0.80,
      };
    }
  }

  return null;
}
