"""
Novig live price checker (read-only) — soccer match-winner markets. SKELETON.

Third exchange leg alongside kalshi_live.py / polymarket_live.py. Exposes the
same {home,draw,away} x {bid,ask,mid} dict shape from get_all_prices() so the
rest of the pipeline treats all three books uniformly.

STATUS (2026-07-04): written against docs.novig.com before credentials exist.
Auth flow, events and open-markets endpoints are per the official docs; the
ORDER-BOOK response shape is UNVERIFIED (docs index lists "Get order book" but
the schema page wasn't public) — verify + fix _best_bid_ask() on first run
with real credentials. Everything degrades to None without creds, so importing
this module never breaks the monitor.

Auth: OAuth2 client-credentials -> 30-min bearer tokens (re-request on expiry).
Put in E:\soccer\.env:
    NOVIG_CLIENT_ID=...
    NOVIG_CLIENT_SECRET=...
Credentials come from Novig directly (account holder API access).

READ-ONLY BY DESIGN: no order-placement endpoints in this module. If Novig
execution is ever wanted it gets its own novig_trader.py with hard caps,
mirroring kalshi_trader.py.
"""
import os, time, re
import requests

BASE       = "https://api.novig.us"
TOKEN_URL  = f"{BASE}/nbx/v1/auth/emm-token"
EVENTS_URL = f"{BASE}/nbx/v2/emm/events"
OPEN_MKTS  = f"{BASE}/nbx/v2/emm/markets/open"

# div code -> Novig league string (their identifiers are short strings: EPL, MLS...).
# Fill in as coverage is audited; None = not on Novig (skip silently, like poly_tag).
NOVIG_LEAGUE = {
    'E0':  'EPL',
    'MLS': 'MLS',
    'SP1': 'LALIGA',      # TODO verify exact identifier once creds exist
    'I1':  'SERIEA',      # TODO verify
    'D1':  'BUNDESLIGA',  # TODO verify
    # audit the full league list via list_leagues() on first authenticated run
}

_CACHE_TTL = 60          # seconds; per-league event list reuse within a poll cycle
_cache = {}              # league -> {"ts": float, "events": list}


# ── Env / auth ────────────────────────────────────────────────────────────
def _load_env(path=r'E:\soccer\.env'):
    env = {}
    try:
        for line in open(path):
            line = line.strip()
            if line and not line.startswith('#') and '=' in line:
                k, v = line.split('=', 1)
                env[k.strip()] = v.strip()
    except Exception:
        pass
    return env

_env = _load_env()
CLIENT_ID     = _env.get('NOVIG_CLIENT_ID', '')
CLIENT_SECRET = _env.get('NOVIG_CLIENT_SECRET', '')

_token = {'value': None, 'expires': 0.0}
_warned_no_creds = False

def _get_token():
    """Bearer token via client-credentials; cached, renewed 5 min early."""
    global _warned_no_creds
    if not CLIENT_ID or not CLIENT_SECRET:
        if not _warned_no_creds:
            print("  [novig] NOVIG_CLIENT_ID/SECRET not in .env — novig prices disabled")
            _warned_no_creds = True
        return None
    if _token['value'] and time.time() < _token['expires']:
        return _token['value']
    try:
        r = requests.post(TOKEN_URL, data={
            'grant_type':    'client_credentials',
            'client_id':     CLIENT_ID,
            'client_secret': CLIENT_SECRET,
        }, timeout=15)
        r.raise_for_status()
        j = r.json()
        _token['value']   = j.get('access_token')
        # tokens live 30 min; renew at 25 to avoid mid-cycle expiry
        _token['expires'] = time.time() + min(int(j.get('expires_in', 1800)), 1800) - 300
        return _token['value']
    except Exception as e:
        print(f"  [novig] auth failed: {e}")
        return None

def _get(url, params=None):
    tok = _get_token()
    if not tok:
        return None
    try:
        r = requests.get(url, headers={'Authorization': f'Bearer {tok}'},
                         params=params, timeout=15)
        if r.status_code != 200:
            return None
        return r.json()
    except Exception:
        return None


# ── Team-name bridging (reuse polymarket_live's team_map alias machinery) ──
try:
    from polymarket_live import _team_match, _norm
except ImportError:
    def _norm(s):
        import unicodedata
        s = unicodedata.normalize('NFKD', s or '').encode('ascii', 'ignore').decode().lower()
        return re.sub(r'[^a-z0-9 ]', ' ', s).strip()
    def _team_match(name, team):
        na, nb = _norm(name), _norm(team)
        return na == nb or ((na in nb or nb in na) and min(len(na), len(nb)) >= 5)


# ── Events / markets ──────────────────────────────────────────────────────
def _fetch_events(novig_league, force=False):
    """Open pregame + ingame events for a Novig league string, cached _CACHE_TTL."""
    now = time.time()
    c = _cache.get(novig_league)
    if not force and c is not None and now - c['ts'] < _CACHE_TTL:
        return c['events']
    events = []
    for status in ('OPEN_PREGAME', 'OPEN_INGAME'):
        j = _get(EVENTS_URL, params={'league': novig_league, 'status': status, 'limit': 100})
        if j:
            events += j if isinstance(j, list) else j.get('events', j.get('data', []))
    _cache[novig_league] = {'ts': now, 'events': events}
    return events


def find_event(game_date, home_team, away_team, league=None):
    """Novig event dict for this fixture, or None.
       game_date: 'YYYY-MM-DD' string or datetime.date (normalized here — see the
       polymarket_live date-object lesson, 2026-07-03).
       league: our div code (e.g. 'E0') -> Novig league string via NOVIG_LEAGUE."""
    if hasattr(game_date, 'isoformat'):
        game_date = game_date.isoformat()
    game_date = str(game_date)[:10]
    nl = NOVIG_LEAGUE.get(league) if league else None
    leagues = [nl] if nl else [v for v in NOVIG_LEAGUE.values() if v]
    for lg in leagues:
        for e in _fetch_events(lg):
            g = e.get('game') or {}
            h = (g.get('homeTeam') or {}).get('name', '')
            a = (g.get('awayTeam') or {}).get('name', '')
            if not (h and a):
                continue
            if not ((_team_match(h, home_team) and _team_match(a, away_team)) or
                    (_team_match(h, away_team) and _team_match(a, home_team))):
                continue
            start = str(e.get('scheduledStart', ''))[:10]
            if start and start != game_date:
                continue
            return e
    return None


def _money_market(event):
    """The match-winner (MONEY) market dict for an event, or None.
    Markets ride on the event via marketIds; fetch open markets for the league
    and join. TODO(creds): confirm whether markets are embedded on the event
    (EventWithMarketsDto) — if so, read them directly and skip the join."""
    mkts = event.get('markets')
    if mkts:
        for m in mkts:
            if m.get('type') == 'MONEY':
                return m
    ids = set(event.get('marketIds') or [])
    if not ids:
        return None
    j = _get(OPEN_MKTS, params={'league': event.get('league'), 'marketType': 'MONEY'})
    for m in (j if isinstance(j, list) else (j or {}).get('markets', [])):
        if m.get('id') in ids:
            return m
    return None


def _best_bid_ask(market_id, outcome_id):
    """(bid, ask) for one outcome from the order book.
    TODO(creds): UNVERIFIED — the docs index lists a 'Get order book' endpoint but
    the path/schema weren't public. Likely GET /nbx/v2/emm/markets/{id}/orderbook.
    Adjust path + parsing on first authenticated run; until then returns (None, None)
    and get_all_prices falls back to last-trade for mid with no bid/ask."""
    j = _get(f"{BASE}/nbx/v2/emm/markets/{market_id}/orderbook")
    if not j:
        return None, None
    try:
        book = j.get('outcomes', j)
        for o in (book if isinstance(book, list) else book.get('orderbook', [])):
            if o.get('outcomeId', o.get('id')) != outcome_id:
                continue
            bids = o.get('bids') or []
            asks = o.get('asks') or []
            bid = float(bids[0]['price']) if bids else None
            ask = float(asks[0]['price']) if asks else None
            return bid, ask
    except Exception:
        pass
    return None, None


def _classify_outcome(o, home_team, away_team):
    """'home' / 'draw' / 'away' for an OutcomeResponseDto, or None."""
    desc = o.get('description', '') or ''
    comp = (o.get('competitor') or {}).get('name', '') if isinstance(o.get('competitor'), dict) else ''
    if 'draw' in desc.lower() or 'tie' in desc.lower():
        return 'draw'
    for cand in (comp, desc):
        if cand and _team_match(cand, home_team):
            return 'home'
        if cand and _team_match(cand, away_team):
            return 'away'
    return None


def get_all_prices(game_date, home_team, away_team, league=None):
    """Fetch home/draw/away prices from Novig. Returns dict or None.
       Shape matches kalshi_live/polymarket_live:
         {home_bid,home_ask,home_mid, draw_*, away_*}
       Prices are probabilities in [0,1] (Novig quotes 0.55-style).
       mid = (bid+ask)/2 when bid present, else ask, else last trade."""
    ev = find_event(game_date, home_team, away_team, league=league)
    if not ev:
        return None
    m = _money_market(ev)
    if not m:
        return None
    result = {}
    for o in (m.get('outcomes') or []):
        label = _classify_outcome(o, home_team, away_team)
        if not label:
            continue
        bid, ask = _best_bid_ask(m.get('id'), o.get('id'))
        last = o.get('last')
        try:
            last = float(last) if last is not None else None
        except (TypeError, ValueError):
            last = None
        mid = ((bid + ask) / 2) if (bid and ask) else (ask or last)
        if mid is None:
            continue
        result[f'{label}_bid'] = round(bid, 4) if bid else 0.0
        result[f'{label}_ask'] = round(ask, 4) if ask else round(mid, 4)
        result[f'{label}_mid'] = round(mid, 4)
    return result or None


# ── Discovery helpers (run these on first authenticated session) ──────────
def list_leagues():
    """All league identifiers Novig currently lists — use to fill NOVIG_LEAGUE."""
    j = _get(f"{BASE}/nbx/v2/emm/events/leagues")
    print(j)
    return j

def list_matches(league=None):
    """Print open events for a league (or all mapped leagues)."""
    leagues = [NOVIG_LEAGUE[league]] if league and NOVIG_LEAGUE.get(league) \
              else [v for v in NOVIG_LEAGUE.values() if v]
    for lg in leagues:
        evs = _fetch_events(lg, force=True)
        print(f"[{lg}] {len(evs)} open events")
        for e in evs:
            g = e.get('game') or {}
            h = (g.get('homeTeam') or {}).get('name', '?')
            a = (g.get('awayTeam') or {}).get('name', '?')
            print(f"   {str(e.get('scheduledStart',''))[:16]}  {h} vs {a}  [{e.get('status')}]")


if __name__ == '__main__':
    import sys
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    if not CLIENT_ID:
        print("No NOVIG_CLIENT_ID in .env — add credentials, then run:")
        print("  python main\\novig_live.py           # auth check + open events")
        sys.exit(0)
    print("auth:", "OK" if _get_token() else "FAILED")
    print("\nleagues on Novig:")
    list_leagues()
    print("\nopen events in mapped leagues:")
    list_matches()
