"""Hermes V3D: propuestas de asignación desde tarjetas+EPG+Sling. Sin escritura en DB.
Solo muestra datos no sensibles al modelo. Las propuestas requieren aprobación independiente.
"""
import os, re, base64, sqlite3, urllib.request, json, secrets, hashlib
from pathlib import Path
from datetime import datetime, timezone
from urllib.parse import urlsplit
from hermes_football_link_manager_v2_1 import tokens, epg_date, channel_num, epg_num

DB='/var/lib/hermes-nexus/football.db'
EPG='/var/lib/hermes-nexus/epg.db'
PENDING=Path('/var/lib/hermes-nexus/football_pending')
HOST='15.204.47.5'

def _playlist():
    user=os.environ.get('HERMES_O11_USER')
    password=os.environ.get('HERMES_O11_PASSWORD')
    if not user or not password: raise RuntimeError('Credenciales O11 no cargadas')
    auth=base64.b64encode((user+':'+password).encode()).decode()
    req=urllib.request.Request('http://15.204.47.5:1234/playlist.m3u',headers={'Authorization':'Basic '+auth,'User-Agent':'Hermes-V3D'})
    with urllib.request.urlopen(req,timeout=25) as resp: content=resp.read(25*1024*1024+1)
    if len(content)>25*1024*1024: raise ValueError('M3U excede límite')
    channels={}; name=''
    for line in content.decode('utf-8-sig',errors='replace').splitlines():
        line=line.strip()
        if line.startswith('#EXTINF'): name=line.rsplit(',',1)[-1].strip()
        elif line.startswith('http') and name:
            p=urlsplit(line)
            key=channel_num(name)
            if key and p.scheme=='http' and p.hostname==HOST and p.port==1234 and p.path.startswith('/stream/sling/') and not p.username and not p.password:
                channels.setdefault(key,[]).append({'nombre':name,'url':line})
            name=''
    return channels

def preparar_asignacion_futbol(event_id):
    if not isinstance(event_id,str) or not re.fullmatch(r'[a-zA-Z0-9_.:-]{5,100}',event_id):
        raise ValueError('ID de partido inválido')
    with sqlite3.connect('file:'+DB+'?mode=ro',uri=True) as db:
        db.row_factory=sqlite3.Row
        event=db.execute('SELECT * FROM events WHERE id=?',(event_id,)).fetchone()
        if not event:return {'estado':'EVENTO_NO_ENCONTRADO'}
        event=dict(event)
        existing=db.execute('SELECT manual_url FROM football_links WHERE event_id=?',(event_id,)).fetchone()
        if existing and existing[0]:return {'estado':'ENLACE_MANUAL_PROTEGIDO'}
    when=datetime.fromisoformat(event['starts_utc'].replace('Z','+00:00')).astimezone(timezone.utc)
    with sqlite3.connect('file:'+EPG+'?mode=ro',uri=True) as db:
        rows=db.execute("SELECT canal,titulo,inicio,fin FROM epg_programas WHERE fuente='sling' AND (canal LIKE 'vixpremiumdeportes%' OR canal LIKE 'vixdeportes%') AND inicio LIKE ? LIMIT 500",(when.strftime('%Y%m%d')+'%',)).fetchall()
    # V4.2: comparación de equipos en cualquier orden, con nombres alternativos.
    from difflib import SequenceMatcher
    def clean(text):
        import unicodedata
        text=''.join(c for c in unicodedata.normalize('NFKD',str(text).casefold()) if not unicodedata.combining(c))
        return re.sub(r'[^a-z0-9]+',' ',text).strip()
    def score_team(team, title):
        t=clean(team);words=clean(title).split()
        if not t or not words:return 0.0
        if t in clean(title):return 1.0
        variants=[t]+t.split()
        return max(SequenceMatcher(None,x,w).ratio() for x in variants for w in words if len(x)>2)
    candidates=[]
    for canal,titulo,inicio,fin in rows:
        h=score_team(event['home'],titulo)
        a=score_team(event['away'],titulo)
        if min(h,a)<0.53:continue
        try:delta=round((epg_date(inicio)-when).total_seconds()/60)
        except (ValueError,TypeError):continue
        if abs(delta)>120:continue
        key=epg_num(canal)
        if key:candidates.append({'canal':canal,'titulo':titulo,'inicio':inicio,'delta':delta,'key':key,'score':h+a-abs(delta)/300})
    # No descarga catálogo si no hay coincidencia EPG.
    if not candidates:return {'estado':'SIN_COINCIDENCIA_EPG','partido':event['away']+' vs '+event['home']}
    catalog=_playlist()
    matches=[]
    for c in candidates:
        entries=catalog.get(c['key'],[])
        if entries:matches.append((c,entries[0]))
    if not matches:return {'estado':'SIN_CANAL_EN_CATALOGO','coincidencias_epg':len(candidates)}
    matches.sort(key=lambda m:(-m[0]['score'],abs(m[0]['delta'])))
    c,entry=matches[0]
    PENDING.mkdir(mode=0o700,parents=True,exist_ok=True)
    os.chmod(PENDING,0o700)
    ticket=secrets.token_hex(12)
    proposal={'event_id':event_id,'partido':event['away']+' vs '+event['home'],'epg_canal':c['canal'],
      'epg_titulo':c['titulo'],'diferencia_minutos':c['delta'],'catalogo_nombre':entry['nombre'],
      'url':entry['url'],'fuente_publica':'','verificado_por':'',
      'created_utc':datetime.now(timezone.utc).isoformat()}
    file=PENDING/(ticket+'.json')
    with open(file,'x',encoding='utf-8') as fh:json.dump(proposal,fh,ensure_ascii=False,indent=2)
    os.chmod(file,0o600)
    return {'estado':'PROPUESTA_PENDIENTE','ticket':ticket,'event_id':event_id,
      'partido':proposal['partido'],'canal_epg':c['canal'],'canal_catalogo':entry['nombre'],
      'diferencia_minutos':c['delta'],'verificacion_publica':'PENDIENTE',
      'guardado':False,'online':'NO_COMPROBADO',
      'instruccion':'Revisar fuente pública independiente y autorizar fuera de Gemini antes de guardar.'}
