"""
Shared loader for league_map.json and team_map.json.
Single source of truth for team/league name translations across:
  - API-Football (signals, live scores)
  - football-data.co.uk (bet resolution)
  - Kalshi (exchange prices)

Hot-reload: call reload() to pick up changes without restarting scripts.
"""
import json
import re
import unicodedata
from pathlib import Path

LEAGUE_MAP_FILE = Path(r'E:\soccer\data\league_map.json')
TEAM_MAP_FILE   = Path(r'E:\soccer\data\team_map.json')

# Module-level state
_league_map = {}   # div -> {name, api_football_id, kalshi_series}
_team_map   = {}   # api_football_name -> {div, fdco, kalshi, aliases}
_alias_idx  = {}   # lowercased alias/name -> api_football_name (for reverse lookup)
_mtimes     = {'league': 0.0, 'team': 0.0}


def _fold(s):
    """Lowercase + strip accents/diacritics for accent-insensitive matching.

    API-Football sends accented names (e.g. 'Castellón', 'Almería') while
    team_map keys are plain ASCII ('Castellon', 'Almeria'); folding both sides
    makes the lookup accent-insensitive so the Kalshi leg doesn't silently miss.
    """
    if not s:
        return ''
    nfkd = unicodedata.normalize('NFKD', s)
    return ''.join(c for c in nfkd if not unicodedata.combining(c)).lower().strip()


_STOP = {'fc', 'cf', 'ac', 'sc', 'afc', 'de', 'do', 'da', 'the'}


def _tokens(s):
    """Whole words, accent-folded, minus noise tokens. Substring matching on
    space-stripped names produced three separate wrong-club bugs -- see _lookup."""
    return {t for t in re.split(r'[^a-z0-9]+', _fold(s))
            if len(t) >= 3 and t not in _STOP}


def _load():
    global _league_map, _team_map, _alias_idx
    with open(LEAGUE_MAP_FILE, encoding='utf-8') as f:
        _league_map = json.load(f)
    with open(TEAM_MAP_FILE, encoding='utf-8') as f:
        raw = json.load(f)

    _team_map = {k: v for k, v in raw.items() if not k.startswith('_')}

    # Build alias index: any name/alias -> canonical api_football key.
    # Keys are accent-folded so accented API-Football names still match.
    _alias_idx = {}
    for key, info in _team_map.items():
        _alias_idx[_fold(key)] = key
        if info.get('fdco'):
            _alias_idx[_fold(info['fdco'])] = key
        for a in info.get('aliases', []):
            _alias_idx[_fold(a)] = key


def reload():
    """Re-read JSON files. Call from hot-reload loops."""
    lm = LEAGUE_MAP_FILE.stat().st_mtime
    tm = TEAM_MAP_FILE.stat().st_mtime
    if lm != _mtimes['league'] or tm != _mtimes['team']:
        _load()
        _mtimes['league'] = lm
        _mtimes['team']   = tm
        return True   # changed
    return False      # unchanged


def changed():
    """Return True if either JSON file has been modified since last load."""
    try:
        lm = LEAGUE_MAP_FILE.stat().st_mtime
        tm = TEAM_MAP_FILE.stat().st_mtime
        return lm != _mtimes['league'] or tm != _mtimes['team']
    except:
        return False


# ── League helpers ────────────────────────────────────────────────────────

def league_api_id(div):
    """API-Football league ID for a div code."""
    return _league_map.get(div, {}).get('api_football_id')

def league_name(div):
    """Display name for a div code."""
    return _league_map.get(div, {}).get('name', div)

def league_kalshi_series(div):
    """Kalshi series ticker for a div code, or None."""
    return _league_map.get(div, {}).get('kalshi_series')

def league_season(div, default=None):
    """API-Football season for a div code. Returns the per-league override if set, else default."""
    return _league_map.get(div, {}).get('season', default)


# ── Current-season resolution ─────────────────────────────────────────────
# A league's API-Football season year is NOT reliably the calendar year, and a
# league can silently vanish when the two diverge. J1 League (JP1) is the case
# that exposed this on 2026-08-07: Japan moved to an autumn-spring calendar, so
# API-Football labels the season that opened 2026-08-07 as **2027** (running to
# 2027-06-06). daily_watchlist only ever queried [year, year-1] = 2026/2025 and
# league_map pinned season=2026 -- the finished Feb-Jun transitional season --
# so all 380 fixtures were invisible. Zero JP1 rows had EVER been captured,
# while Kalshi (11 open events) and Poly.com (140) both carried the league.
#
# Rather than hardcode 2027, ask API-Football which season it considers
# `current` and cache the answer. That way the next calendar shift -- in any
# league -- corrects itself instead of dropping the league without a trace.
SEASON_CACHE_FILE = Path(r'E:\soccer\data\af_season_cache.json')
SEASON_CACHE_TTL  = 7 * 86400          # a season label changes at most yearly

_season_cache = None


def _season_cache_load():
    global _season_cache
    if _season_cache is not None:
        return _season_cache
    try:
        with open(SEASON_CACHE_FILE, encoding='utf-8') as f:
            _season_cache = json.load(f)
    except (FileNotFoundError, ValueError):
        _season_cache = {'fetched': 0, 'seasons': {}}
    return _season_cache


def current_seasons(af_headers, force=False):
    """{div: season_year} for every div with an api_football_id.

    Cached to disk for SEASON_CACHE_TTL. `af_headers` is passed in by the caller
    so this module never holds the API key. On any network/parse failure the
    league falls back to its league_map `season` value, so a dead API degrades
    to today's behaviour rather than emptying the watchlist.
    """
    import time
    cache = _season_cache_load()
    fresh = (time.time() - cache.get('fetched', 0)) < SEASON_CACHE_TTL
    if fresh and not force and cache.get('seasons'):
        return {k: v for k, v in cache['seasons'].items()}

    import requests
    seasons = {}
    for div in all_divs():
        lid = league_api_id(div)
        if not lid:
            continue
        year = None
        try:
            r = requests.get('https://v3.football.api-sports.io/leagues',
                             headers=af_headers, params={'id': lid}, timeout=20)
            entry = (r.json().get('response') or [{}])[0]
            for s in (entry.get('seasons') or []):
                if s.get('current'):
                    year = s.get('year')
                    break
        except Exception:
            year = None
        seasons[div] = year if year is not None else league_season(div)

    if any(v is not None for v in seasons.values()):
        cache = {'fetched': time.time(), 'seasons': seasons}
        try:
            SEASON_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
            with open(SEASON_CACHE_FILE, 'w', encoding='utf-8') as f:
                json.dump(cache, f, indent=2)
        except OSError:
            pass
        globals()['_season_cache'] = cache
    return seasons


def league_current_season(div, af_headers=None, default=None):
    """Season year API-Football currently uses for `div`.

    Prefers the cached `current` season; falls back to the league_map override
    and then `default`. With af_headers=None it is read-only (cache or config),
    so callers on a hot path never trigger a refresh by accident.
    """
    cache = _season_cache_load().get('seasons', {})
    year = cache.get(div)
    if year is None and af_headers is not None:
        year = current_seasons(af_headers).get(div)
    if year is None:
        year = league_season(div, default)
    return year


def season_years(af_headers=None, extra=()):
    """Sorted distinct season years covering every tracked league.

    daily_watchlist queries /fixtures by DATE, where `season` filters globally
    rather than per league -- so it must sweep each distinct year our leagues
    live in, or leagues on an off-calendar season are dropped.
    """
    if af_headers is not None:
        current_seasons(af_headers)          # refresh if stale
    cache = _season_cache_load().get('seasons', {})
    years = {v for v in cache.values() if isinstance(v, int)}
    years |= {v for v in (league_season(d) for d in all_divs()) if isinstance(v, int)}
    years |= {y for y in extra if isinstance(y, int)}
    return sorted(years)

def all_divs():
    """All div codes in order."""
    return list(_league_map.keys())

def league_map_full():
    """Full league map dict."""
    return dict(_league_map)


# ── Team helpers ──────────────────────────────────────────────────────────

def _lookup(name):
    """Find team entry by any known name (exact, then alias, then fuzzy)."""
    n = _fold(name)
    # Exact / alias match
    key = _alias_idx.get(n)
    if key:
        return key, _team_map[key]
    # Fuzzy: WORD-LEVEL, not substring, and ambiguity-safe.
    #
    # History of this function, because each fix exposed the next bug:
    #   1. Original returned the FIRST substring hit in dict order. "Estudiantes
    #      de Rio Cuarto" -> ELP (Estudiantes LA PLATA) despite a correct RCU
    #      entry existing; "Gimnasia M." (Mendoza) -> GLP (La Plata). Silently
    #      built tickers for the wrong club.
    #   2. "Refuse if >1 candidate" over-corrected: the alias "mito" (Mito
    #      Hollyhock) is a substring of "atroMITOsathinon", so Atromitos lost a
    #      correct match to a spurious collision.
    #   3. "Prefer alias contained in query" was worse still: "aris" is a
    #      substring of "pARIS", so Paris -> Aris Thessaloniki.
    # The root cause throughout is SUBSTRING matching on space-stripped names.
    # Matching on WORD tokens kills all three: {aris} is not a subset of
    # {paris}, {mito} is not a subset of {atromitos, athinon}.
    #
    # Rule: a candidate must share whole words -- one side's token set contained
    # in the other's. Rank by how many words overlap, so "central cordoba" (2)
    # beats "cordoba" (1). Refuse on a genuine tie: returning None just skips the
    # fixture, while a wrong abbr prices the wrong match.
    nt = _tokens(name)
    if nt:
        scored = []
        for alias, k in _alias_idx.items():
            at = _tokens(alias)
            if at and (at <= nt or nt <= at):
                scored.append((len(at & nt), k))
        if scored:
            best = max(sc for sc, _ in scored)
            top = {k for sc, k in scored if sc == best}
            if len(top) == 1:
                k = top.pop()
                return k, _team_map[k]
            print(f"  [maps] ambiguous team name {name!r} -> {sorted(top)}; "
                  f"refusing to guess (add an alias to data/team_map.json)")
    return None, None


def fdco_name(api_name):
    """Translate API-Football team name to football-data.co.uk name."""
    _, info = _lookup(api_name)
    if info and info.get('fdco'):
        return info['fdco']
    return api_name   # fallback: use as-is


def kalshi_abr(name):
    """Get Kalshi abbreviation for a team name (from any system)."""
    _, info = _lookup(name)
    if info:
        return info.get('kalshi')
    return None


def team_info(name):
    """Full team entry dict, or None."""
    _, info = _lookup(name)
    return info


# ── Init ─────────────────────────────────────────────────────────────────
_load()
_mtimes['league'] = LEAGUE_MAP_FILE.stat().st_mtime
_mtimes['team']   = TEAM_MAP_FILE.stat().st_mtime
