"""Logica de negocio para venta y programacion de pulseras."""
import uuid
import json
from datetime import datetime, date, timedelta
from sqlalchemy.orm import Session as DBSession
from models import Package, Session, AuditLog, SyncQueue, Agent, FolioCounter, TerminalPackage
from services import hardware_client
from config import VENUE_ID, VENUE_PREFIX


def _generar_folio(db: DBSession) -> str:
    """Genera folio secuencial: ICE-{PREFIX}-{NNN}. Se resetea cada dia."""
    hoy = date.today().isoformat()
    counter = db.query(FolioCounter).filter(
        FolioCounter.venue_id == VENUE_ID,
        FolioCounter.fecha == hoy,
    ).first()

    if not counter:
        counter = FolioCounter(venue_id=VENUE_ID, fecha=hoy, ultimo_numero=0)
        db.add(counter)
        db.flush()

    counter.ultimo_numero += 1
    db.flush()
    return f"ICE-{VENUE_PREFIX}-{counter.ultimo_numero:03d}"


def _get_agent_token(db: DBSession, agent_id: str) -> str | None:
    """Busca el token del agent en la DB."""
    if not agent_id:
        return None
    agent = db.query(Agent).filter(Agent.id == agent_id).first()
    return agent.token if agent else None


async def sell_and_program(db: DBSession, package_id: str, payment_method: str,
                           amount: float, cashier: str, agent_id: str = None,
                           timeout: int = 30,
                           red_minutes: int = None, green_minutes: int = None,
                           blue_minutes: int = None):
    """
    Flujo completo: crea sesion, programa pulsera, registra auditoria.
    Soporta paquetes globales y combos custom (C-...).
    Retorna dict con resultado.
    """
    # Fork: combo custom vs paquete global
    if package_id.startswith("C-"):
        # --- COMBO CUSTOM ---
        if not agent_id:
            return {'status': 'error', 'message': 'agent_id es obligatorio para combos custom'}

        # Validar que esta asignado a alguna terminal del venue (cualquier caja puede vender cualquier producto)
        tp = db.query(TerminalPackage).filter(
            TerminalPackage.combo_id == package_id,
            TerminalPackage.active == True,
        ).first()
        if not tp:
            return {'status': 'error', 'message': f'Combo "{package_id}" no esta adoptado por ninguna terminal'}

        # Los 3 RGB son obligatorios
        if red_minutes is None or green_minutes is None or blue_minutes is None:
            return {'status': 'error', 'message': 'red_minutes, green_minutes y blue_minutes son obligatorios para combos custom'}

        # Clamp min 1 por protocolo IR
        r = max(red_minutes, 1)
        g = max(green_minutes, 1)
        b = max(blue_minutes, 1)
        mins = max(r, g, b)
        combo_name = tp.name
        pkg_id = package_id
    else:
        # --- PAQUETE GLOBAL (logica original) ---
        package = db.query(Package).filter(Package.id == package_id, Package.active == True).first()
        if not package:
            return {'status': 'error', 'message': f'Paquete "{package_id}" no encontrado'}

        mins = package.minutes
        if package.color == 'red':
            r, g, b = mins, 1, 1
        elif package.color == 'green':
            r, g, b = 1, mins, 1
        elif package.color == 'blue':
            r, g, b = 1, 1, mins
        else:  # 'all'
            r, g, b = mins, mins, mins

        combo_name = package.name
        pkg_id = package.id

    # 3. Generar folio secuencial
    folio = _generar_folio(db)

    # 4. Crear sesion
    session_id = str(uuid.uuid4())[:8]
    session = Session(
        id=session_id,
        package_id=pkg_id,
        status='pending',
        red_minutes=r,
        green_minutes=g,
        blue_minutes=b,
        payment_method=payment_method,
        amount=amount,
        cashier=cashier,
        agent_id=agent_id,
        folio_number=folio,
    )
    db.add(session)
    db.commit()

    # 5. Obtener token del agent y programar pulsera
    token = _get_agent_token(db, agent_id)
    result = await hardware_client.program_wristband(
        r, g, b, session_id, timeout, agent_id=agent_id, token=token
    )

    # 6. Actualizar sesion segun resultado
    if result.get('status') == 'ok':
        now = datetime.now()
        session.status = 'active'
        session.programmed_at = now
        session.expires_at = now + timedelta(minutes=mins)

        # Auditoria
        db.add(AuditLog(
            action='sale',
            session_id=session_id,
            details=json.dumps({
                'package': pkg_id,
                'minutes': mins,
                'amount': amount,
                'payment': payment_method,
                'folio': folio,
            }),
            cashier=cashier,
        ))

        # Cola de sync (payload enriquecido para el panel cloud)
        db.add(SyncQueue(
            table_name='sessions',
            record_id=session_id,
            action='insert',
            payload=json.dumps({
                'id': session_id,
                'package_id': pkg_id,
                'package_name': combo_name,
                'minutes': mins,
                'amount': amount,
                'payment_method': payment_method,
                'cashier': cashier,
                'agent_id': agent_id,
                'status': 'active',
                'red_minutes': r,
                'green_minutes': g,
                'blue_minutes': b,
                'programmed_at': now.isoformat(),
                'folio_number': folio,
            }),
        ))

        db.commit()
        return {
            'status': 'ok',
            'session_id': session_id,
            'folio_number': folio,
            'package_name': combo_name,
            'minutes': mins,
            'amount': amount,
            'programmed_at': result.get('programmed_at'),
        }
    else:
        session.status = 'failed'
        db.add(AuditLog(
            action='program_failed',
            session_id=session_id,
            details=json.dumps(result),
            cashier=cashier,
        ))
        db.commit()
        return {
            'status': result.get('status', 'error'),
            'session_id': session_id,
            'folio_number': folio,
            'package_name': combo_name,
            'minutes': mins,
            'amount': amount,
            'message': result.get('message', 'Pulsera no detectada'),
        }


async def cortesia_and_program(db: DBSession, package_id: str, cashier: str,
                                supervisor: str, motivo: str,
                                agent_id: str = None, timeout: int = 30,
                                red_minutes: int = None, green_minutes: int = None,
                                blue_minutes: int = None):
    """
    Cortesia: pulsera gratis (amount=0, payment_method='cortesia').
    Misma logica que sell_and_program pero sin cobro.
    Soporta paquetes globales y combos custom (C-...).
    """
    # 1. Buscar paquete — combo custom vs paquete global
    if package_id.startswith("C-"):
        # --- COMBO CUSTOM ---
        tp = db.query(TerminalPackage).filter(
            TerminalPackage.combo_id == package_id,
            TerminalPackage.active == True,
        ).first()
        if not tp:
            return {'status': 'error', 'message': f'Combo "{package_id}" no esta adoptado por ninguna terminal'}

        if red_minutes is None or green_minutes is None or blue_minutes is None:
            return {'status': 'error', 'message': 'red_minutes, green_minutes y blue_minutes son obligatorios para combos custom'}

        r = max(red_minutes, 1)
        g = max(green_minutes, 1)
        b = max(blue_minutes, 1)
        mins = max(r, g, b)
        pkg_id = package_id
        pkg_name = tp.name
    else:
        # --- PAQUETE GLOBAL ---
        package = db.query(Package).filter(Package.id == package_id, Package.active == True).first()
        if not package:
            return {'status': 'error', 'message': f'Paquete "{package_id}" no encontrado'}

        mins = package.minutes
        if package.color == 'red':
            r, g, b = mins, 1, 1
        elif package.color == 'green':
            r, g, b = 1, mins, 1
        elif package.color == 'blue':
            r, g, b = 1, 1, mins
        else:  # 'all'
            r, g, b = mins, mins, mins
        pkg_id = package.id
        pkg_name = package.name

    # 2. Generar folio
    folio = _generar_folio(db)

    # 3. Crear sesion con amount=0
    session_id = str(uuid.uuid4())[:8]
    session = Session(
        id=session_id,
        package_id=pkg_id,
        status='pending',
        red_minutes=r,
        green_minutes=g,
        blue_minutes=b,
        payment_method='cortesia',
        amount=0,
        cashier=cashier,
        agent_id=agent_id,
        folio_number=folio,
    )
    db.add(session)
    db.commit()

    # 4. Programar pulsera
    token = _get_agent_token(db, agent_id)
    result = await hardware_client.program_wristband(
        r, g, b, session_id, timeout, agent_id=agent_id, token=token
    )

    # 5. Actualizar sesion
    if result.get('status') == 'ok':
        now = datetime.now()
        session.status = 'active'
        session.programmed_at = now
        session.expires_at = now + timedelta(minutes=mins)

        db.add(AuditLog(
            action='cortesia',
            session_id=session_id,
            details=json.dumps({
                'package': pkg_id,
                'minutes': mins,
                'amount': 0,
                'payment': 'cortesia',
                'folio': folio,
                'supervisor': supervisor,
                'motivo': motivo,
            }),
            cashier=cashier,
        ))

        db.add(SyncQueue(
            table_name='sessions',
            record_id=session_id,
            action='insert',
            payload=json.dumps({
                'id': session_id,
                'package_id': pkg_id,
                'package_name': pkg_name,
                'minutes': mins,
                'amount': 0,
                'payment_method': 'cortesia',
                'cashier': cashier,
                'status': 'active',
                'red_minutes': r,
                'green_minutes': g,
                'blue_minutes': b,
                'programmed_at': now.isoformat(),
                'folio_number': folio,
                'agent_id': agent_id,
            }),
        ))

        db.commit()
        return {
            'status': 'ok',
            'session_id': session_id,
            'folio_number': folio,
            'package_name': pkg_name,
            'minutes': mins,
            'supervisor': supervisor,
        }
    else:
        session.status = 'failed'
        db.add(AuditLog(
            action='cortesia_failed',
            session_id=session_id,
            details=json.dumps({**result, 'supervisor': supervisor, 'motivo': motivo}),
            cashier=cashier,
        ))
        db.commit()
        return {
            'status': result.get('status', 'error'),
            'session_id': session_id,
            'folio_number': folio,
            'package_name': pkg_name,
            'minutes': mins,
            'message': result.get('message', 'Pulsera no detectada'),
        }


def anular_sesion(db: DBSession, session_id: str, cashier: str,
                  supervisor: str, motivo: str):
    """
    Anula una sesion activa. No desprograma la pulsera (fisicamente imposible).
    """
    session = db.query(Session).filter(Session.id == session_id).first()
    if not session:
        return {'status': 'error', 'message': 'Sesion no encontrada'}
    if session.status != 'active':
        return {'status': 'error', 'message': f'Solo se pueden anular sesiones activas (estado actual: "{session.status}")'}

    # Obtener nombre del paquete para la respuesta
    if session.package_id.startswith("C-"):
        tp = db.query(TerminalPackage).filter(
            TerminalPackage.combo_id == session.package_id,
            TerminalPackage.agent_id == session.agent_id,
        ).first()
        package_name = tp.name if tp else session.package_id
    else:
        package = db.query(Package).filter(Package.id == session.package_id).first()
        package_name = package.name if package else session.package_id

    session.status = 'anulada'

    db.add(AuditLog(
        action='anulacion',
        session_id=session_id,
        details=json.dumps({
            'folio': session.folio_number,
            'package': session.package_id,
            'amount': session.amount,
            'supervisor': supervisor,
            'motivo': motivo,
        }),
        cashier=cashier,
    ))

    db.add(SyncQueue(
        table_name='sessions',
        record_id=session_id,
        action='update',
        payload=json.dumps({
            'id': session_id,
            'status': 'anulada',
        }),
    ))

    db.commit()
    return {
        'status': 'ok',
        'session_id': session_id,
        'folio_number': session.folio_number,
        'package_name': package_name,
        'minutes': max(session.red_minutes, session.green_minutes, session.blue_minutes),
        'supervisor': supervisor,
    }


async def reprogram_and_program(db: DBSession, session_id: str, package_id: str,
                                  cashier: str, supervisor: str, motivo: str,
                                  agent_id: str = None, timeout: int = 30):
    """
    Reprograma una pulsera activa con un paquete diferente.
    Requiere aprobacion de supervisor.
    """
    # 1. Buscar sesion existente
    session = db.query(Session).filter(Session.id == session_id).first()
    if not session:
        return {'status': 'error', 'message': 'Sesion no encontrada'}
    if session.status != 'active':
        return {'status': 'error', 'message': f'Solo se pueden reprogramar sesiones activas (estado actual: "{session.status}")'}

    # 2. Buscar nuevo paquete y calcular RGB
    package = db.query(Package).filter(Package.id == package_id, Package.active == True).first()
    if not package:
        return {'status': 'error', 'message': f'Paquete "{package_id}" no encontrado'}

    mins = package.minutes
    if package.color == 'red':
        r, g, b = mins, 1, 1
    elif package.color == 'green':
        r, g, b = 1, mins, 1
    elif package.color == 'blue':
        r, g, b = 1, 1, mins
    else:  # 'all'
        r, g, b = mins, mins, mins

    # Guardar datos del paquete viejo para auditoria
    old_package_id = session.package_id
    old_minutes = max(session.red_minutes, session.green_minutes, session.blue_minutes)

    # 3. Programar pulsera fisicamente
    use_agent = agent_id or session.agent_id
    token = _get_agent_token(db, use_agent)
    result = await hardware_client.program_wristband(
        r, g, b, session_id, timeout, agent_id=use_agent, token=token
    )

    if result.get('status') == 'ok':
        now = datetime.now()
        # 4. Actualizar sesion con nuevo paquete
        session.package_id = package.id
        session.red_minutes = r
        session.green_minutes = g
        session.blue_minutes = b
        session.programmed_at = now
        session.expires_at = now + timedelta(minutes=mins)

        # 5. Auditoria
        db.add(AuditLog(
            action='reprogram',
            session_id=session_id,
            details=json.dumps({
                'old_package': old_package_id,
                'old_minutes': old_minutes,
                'new_package': package.id,
                'new_minutes': mins,
                'folio': session.folio_number,
                'supervisor': supervisor,
                'motivo': motivo,
            }),
            cashier=cashier,
        ))

        # 6. Cola de sync
        db.add(SyncQueue(
            table_name='sessions',
            record_id=session_id,
            action='update',
            payload=json.dumps({
                'id': session_id,
                'package_id': package.id,
                'package_name': package.name,
                'minutes': mins,
                'status': 'active',
                'red_minutes': r,
                'green_minutes': g,
                'blue_minutes': b,
                'programmed_at': now.isoformat(),
                'folio_number': session.folio_number,
            }),
        ))

        db.commit()
        return {
            'status': 'ok',
            'session_id': session_id,
            'folio_number': session.folio_number,
            'package_name': package.name,
            'minutes': mins,
            'supervisor': supervisor,
        }
    else:
        db.add(AuditLog(
            action='reprogram_failed',
            session_id=session_id,
            details=json.dumps({
                **result,
                'new_package': package_id,
                'supervisor': supervisor,
                'motivo': motivo,
            }),
            cashier=cashier,
        ))
        db.commit()
        return {
            'status': result.get('status', 'error'),
            'session_id': session_id,
            'folio_number': session.folio_number,
            'package_name': package.name,
            'minutes': mins,
            'message': result.get('message', 'Pulsera no detectada'),
        }


async def retry_program(db: DBSession, session_id: str, timeout: int = 30):
    """Reintenta programar una sesion fallida."""
    session = db.query(Session).filter(Session.id == session_id).first()
    if not session:
        return {'status': 'error', 'message': 'Sesion no encontrada'}
    if session.status not in ('failed', 'pending'):
        return {'status': 'error', 'message': f'Sesion en estado "{session.status}", no se puede reintentar'}

    # Obtener token del agent
    token = _get_agent_token(db, session.agent_id)

    result = await hardware_client.program_wristband(
        session.red_minutes, session.green_minutes, session.blue_minutes,
        session_id, timeout, agent_id=session.agent_id, token=token
    )

    # Obtener nombre del paquete para la respuesta
    if session.package_id.startswith("C-"):
        tp = db.query(TerminalPackage).filter(
            TerminalPackage.combo_id == session.package_id,
        ).first()
        pkg_name = tp.name if tp else session.package_id
    else:
        pkg = db.query(Package).filter(Package.id == session.package_id).first()
        pkg_name = pkg.name if pkg else session.package_id

    mins = max(session.red_minutes, session.green_minutes, session.blue_minutes)

    if result.get('status') == 'ok':
        now = datetime.now()
        session.status = 'active'
        session.programmed_at = now
        session.expires_at = now + timedelta(minutes=mins)
        db.add(AuditLog(action='program_retry_ok', session_id=session_id, details=json.dumps(result)))
        db.commit()
        return {
            'status': 'ok',
            'session_id': session_id,
            'folio_number': session.folio_number,
            'package_name': pkg_name,
            'minutes': mins,
            'amount': session.amount,
            'programmed_at': result.get('programmed_at'),
        }
    else:
        db.add(AuditLog(action='program_retry_failed', session_id=session_id, details=json.dumps(result)))
        db.commit()
        return {
            'status': result.get('status', 'error'),
            'session_id': session_id,
            'folio_number': session.folio_number,
            'package_name': pkg_name,
            'minutes': mins,
            'amount': session.amount,
            'message': result.get('message', 'Timeout'),
        }
