import os
import logging
import httpx
from dotenv import load_dotenv
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.error import TimedOut, NetworkError
from telegram.ext import (
    Application, CommandHandler, CallbackQueryHandler, 
    MessageHandler, filters, ContextTypes, ConversationHandler
)

# Import our custom structural components
import db_manager
from parser import extract_available_nrcs

# Load environmental configurations
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
ADMIN_ID = os.getenv("ADMIN_ID")
CICLO = os.getenv("CICLO", "202620")

# Setup logging architecture
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)

# Define Finite State Machine (FSM) targets for ConversationHandler
TYPING_CLAVE, TYPING_NRC = range(2)

# Global Network Assets shared across asynchronous operations
URL = 'https://siiauescolar.siiau.udg.mx/wal/sspseca.consulta_oferta'
HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
    'Accept-Language': 'es-419,es;q=0.9,en;q=0.8',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Origin': 'https://siiauescolar.siiau.udg.mx',
    'Referer': 'https://siiauescolar.siiau.udg.mx/wal/sspseca.forma_consulta',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36'
}

# HARDCODED CENTERS DICTIONARY (Extracted from your original sample script)
CENTERS = {
    '3': 'CUTLAJOMULCO',
    '4': 'CU GUADALAJARA',
    '5': 'CUTLAQUEPAQUE',
    '6': 'CU CHAPALA',
    'A': 'CUAAD',
    'B': 'CUCBA',
    'C': 'CUCEA',
    'D': 'CUCEI',
    'E': 'CUCS',
    'F': 'CUCSH',
    'G': 'CUALTOS',
    'H': 'CUCIENEGA',
    'I': 'CUCOSTA',
    'J': 'CUCOSTA SUR',
    'K': 'CUSUR',
    'M': 'CUVALLES',
    'N': 'CUNORTE',
    'O': 'CUCEI - VALLES',
    'P': 'CUCSUR - VALLES',
    'Q': 'CUCEI - NORTE',
    'R': 'CUALTOS - NORTE',
    'S': 'CUCOSTA - NORTE',
    'T': 'SEDE TLAJOMULCO',
    'U': 'CULAGOS',
    'V': 'CICLO VERANO',
    'W': 'CUCEA - VALLE',
    'X': 'SUV (VIRTUAL)',
    'Y': 'INCORPORADAS',
    'Z': 'CUTONALA'
}

# --- 1. CORE BOT COMMAND HANDLERS ---

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Greets the user and checks if they need to configure their campus."""
    chat_id = update.effective_chat.id
    username = update.effective_chat.username or "Estudiante"
    
    # Track/Save user entry inside database mapping layer
    db_manager.save_or_update_user(chat_id, username)
    profile = db_manager.get_user_profile(chat_id)
    
    if profile and profile['preferred_center']:
        await show_main_menu(update, context)
    else:
        await ask_for_center(update, context)

async def ask_for_center(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Renders inline keyboard options for choosing a Centro Universitario."""
    keyboard = []
    # Build 2-column center selection panel
    row = []
    for code, name in CENTERS.items():
        row.append(InlineKeyboardButton(name, callback_data=f"set_center:{code}"))
        if len(row) == 2:
            keyboard.append(row)
            row = []
    if row: keyboard.append(row)
    
    msg_text = "👋 ¡Hola! Para empezar, selecciona tu **Centro Universitario**:"
    if update.message:
        await update.message.reply_text(msg_text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
    elif update.callback_query:
        await update.callback_query.edit_message_text(msg_text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")

async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Displays user dashboard options."""
    chat_id = update.effective_chat.id
    profile = db_manager.get_user_profile(chat_id)
    center_name = CENTERS.get(profile['preferred_center'], "No definido")
    
    keyboard = [
        [InlineKeyboardButton("➕ Monitorear Nuevo NRC", callback_data="menu_add")],
        [InlineKeyboardButton("📋 Ver Mis Monitores Activos", callback_data="menu_list")],
        [InlineKeyboardButton("🏢 Cambiar Centro Opciones", callback_data="menu_change_center")]
    ]
    
    msg_text = f"⚙️ **Panel de Control - SIIAU Monitor**\n\n🏫 Centro Actual: `{center_name}`\n📅 Ciclo: `{CICLO}`\n\n¿Qué deseas hacer?"
    
    if update.message:
        await update.message.reply_text(msg_text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")
    elif update.callback_query:
        await update.callback_query.edit_message_text(msg_text, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")

# --- 2. CALLBACK PROCESSING GATEWAY ---

async def handle_callbacks(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    chat_id = update.effective_chat.id
    username = update.effective_chat.username or "Estudiante"
    
    if query.data.startswith("set_center:"):
        center_code = query.data.split(":")[1]
        db_manager.save_or_update_user(chat_id, username, preferred_center=center_code)
        await query.edit_message_text(f"✅ Centro guardado con éxito.")
        await show_main_menu(update, context)
        
    elif query.data == "menu_change_center":
        await ask_for_center(update, context)
        
    elif query.data == "menu_list":
        await display_monitors_panel(query, chat_id)
        
    elif query.data.startswith("delete_mon:"):
        monitor_id = int(query.data.split(":")[1])
        db_manager.remove_monitor(monitor_id)
        await display_monitors_panel(query, chat_id)
        
    elif query.data == "menu_back":
        # Redirects the user directly back to the clean dashboard layout interface
        await show_main_menu(update, context)

async def display_monitors_panel(query, chat_id):
    """Renders a dynamic list of tracked targets featuring ❌ deletion keys."""
    monitors = db_manager.get_user_monitors(chat_id)
    keyboard = []
    
    if not monitors:
        msg = "📋 **No tienes monitores activos actualmente.**"
        keyboard.append([InlineKeyboardButton("⬅️ Volver", callback_data="menu_back")])
    else:
        msg = "📋 **Tus Monitores Activos:**\n\nCualquier elemento listado aquí significa que se está buscando activamente en segundo plano.\n"
        for mon in monitors:
            status_icon = "🟢 ¡CUPOS DISPONIBLES!" if mon['was_present'] == 1 else "🔴 Sin cupos"
            msg += f"\n• `{mon['clave_materia']}` | NRC: `{mon['nrc']}` ({status_icon})"
            keyboard.append([InlineKeyboardButton(f"❌ Eliminar {mon['clave_materia']} ({mon['nrc']})", callback_data=f"delete_mon:{mon['id']}")])
        
        keyboard.append([InlineKeyboardButton("⬅️ Volver al Menú", callback_data="menu_back")])
        
    if query.data == "menu_back":
        await show_main_menu(query, context=None) # We catch this via state mapping instead
    else:
        await query.edit_message_text(msg, reply_markup=InlineKeyboardMarkup(keyboard), parse_mode="Markdown")

# --- 3. CONVERSATION INPUT ROUTINES (FSM) ---

async def start_add_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()
    await query.edit_message_text("📝 Escribe la **Clave de la Materia** (ej. `IJ289`):")
    return TYPING_CLAVE

async def save_clave_get_nrc(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data['temp_clave'] = update.message.text.upper().strip()
    await update.message.reply_text(f"Recibido: `{context.user_data['temp_clave']}`.\nAhora escribe el **NRC de 5 o 6 dígitos** que deseas rastrear:")
    return TYPING_NRC

async def complete_add_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE):
    chat_id = update.effective_chat.id
    nrc = update.message.text.strip()
    clave = context.user_data.get('temp_clave')
    
    if not nrc.isdigit() or len(nrc) not in (5, 6):
        await update.message.reply_text("⚠️ El NRC debe ser un número válido de 5 o 6 dígitos. Inténtalo de nuevo:")
        return TYPING_NRC
        
    success = db_manager.add_monitor(chat_id, clave, nrc)
    if success:
        await update.message.reply_text(f"🚀 **¡Monitor Iniciado!**\nBuscando `{clave}` con NRC `{nrc}`. Te avisaré inmediatamente cuando detecte disponibilidad.")
    else:
        await update.message.reply_text("⚠️ Ya estás monitoreando este NRC exacto para este ciclo.")
        
    # Clear session tracking dictionary values
    context.user_data.clear()
    
    # Return user seamlessly to the dashboard dashboard menu layout
    # Passing fake update wrappers to cheat standard route context rules safely
    class FakeUpdate:
        def __init__(self, msg): self.message = msg; self.effective_chat = msg.chat; self.callback_query = None
    await show_main_menu(FakeUpdate(update.message), context)
    return ConversationHandler.END

async def cancel_conversation(update: Update, context: ContextTypes.DEFAULT_TYPE):
    context.user_data.clear()
    await update.message.reply_text("❌ Acción cancelada.")
    class FakeUpdate:
        def __init__(self, msg): self.message = msg; self.effective_chat = msg.chat; self.callback_query = None
    await show_main_menu(FakeUpdate(update.message), context)
    return ConversationHandler.END

# --- 4. CENTRAL SCRAPER BACKGROUND WORKER JOB ---

async def central_scraping_job(context: ContextTypes.DEFAULT_TYPE):
    """
    Central background orchestrator execution thread.
    De-duplicates requests across user bases and enforces presence notification logic matrices.
    """
    targets = db_manager.get_unique_targets_to_scrape()
    if not targets:
        logger.info("💤 Background scan skipped: No active NRC monitors registered in database.")
        return
    
    logger.info(f"🔍 Starting batch scan for {len(targets)} unique subject configurations...")
        
    # Standard httpx asynchronous context client matching your tested browser identity variables
    async with httpx.AsyncClient(headers=HEADERS, verify=False, timeout=20.0) as client:
        for target in targets:
            center, ciclo, clave = target['preferred_center'], target['ciclo'], target['clave_materia']

            logger.info(f"📡 Requesting SIIAU -> Campus: [{center}] | Materia: [{clave}]")
            
            payload = {
                'ciclop': ciclo, 'cup': center, 'majrp': '', 'crsep': clave,
                'materiap': '', 'horaip': '', 'horafp': '', 'edifp': '', 'aulap': '',
                'dispp': 'D', 'ordenp': '0', 'mostrarp': '500'
            }
            
            try:
                response = await client.post(URL, data=payload)
                if response.status_code != 200:
                    continue
                    
                # Mine live response strings using verified parser engine sets
                open_nrcs_on_page = extract_available_nrcs(response.text)
                logger.info(f"📊 Live Scrape Result for [{clave}]: Found open NRCs: {open_nrcs_on_page}")
                
                # Fetch target dependencies across student bases
                # We need to evaluate every individual user monitor row against what was extracted
                # Because we don't know the NRC pool until we read the target row subqueries
                with db_manager.get_db_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute("""
                        SELECT m.id, m.chat_id, m.nrc, m.was_present, u.username
                        FROM monitors m 
                        JOIN users u ON m.chat_id = u.chat_id
                        WHERE m.ciclo = ? AND m.clave_materia = ? AND u.preferred_center = ?;
                    """, (ciclo, clave, center))
                    subs = cursor.fetchall()
                
                for sub in subs:
                    mon_id, chat_id, nrc, was_present, username = sub['id'], sub['chat_id'], sub['nrc'], sub['was_present'], sub['username']
                    is_now_available = nrc in open_nrcs_on_page
                    
                    # PRESENCE ENGINE NOTIFICATION CONFIGURATION MATRIX
                    if is_now_available and was_present == 0:
                        # Case A: Seats just opened up!
                        msg = f"🔔 **¡CUPOS DISPONIBLES EN SIIAU!**\n\n📚 Materia: `{clave}`\n🔢 NRC: `{nrc}`\n🏫 Campus: `{CENTERS.get(center)}`\n\n💻 ¡Ingresa a SIIAU para registrar tu materia!"
                        try:
                            await context.bot.send_message(chat_id=chat_id, text=msg, parse_mode="Markdown")
                            db_manager.update_monitor_state(mon_id, 1)
                        except Exception as te:
                            logger.error(f"Failed sending alert to {chat_id}: {te}")
                            
                    elif not is_now_available and was_present == 1:
                        # Case B: The seat was taken!
                        msg = f"⚠️ **Cupo Agotado**\n\nEl NRC `{nrc}` (`{clave}`) ya no muestra disponibilidad. No te preocupes, seguiré buscando..."
                        try:
                            await context.bot.send_message(chat_id=chat_id, text=msg, parse_mode="Markdown")
                            db_manager.update_monitor_state(mon_id, 0)
                        except Exception as te:
                            logger.error(f"Failed sending status update to {chat_id}: {te}")
                            
            except Exception as e:
                logger.error(f"Network processing exception during target scan {clave}: {e}")
                # Optional: Send exception traces to your ADMIN_ID via .env strings here

async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Catches unhandled exceptions, intercepts network timeouts, and logs them cleanly."""
    error = context.error
    
    # Check if the failure is simply a transient network timeout to Telegram's infrastructure
    if isinstance(error, (TimedOut, NetworkError)):
        logger.warning(
            f"⚠️ Transient Telegram network timeout detected. "
            f"The request will be retried automatically by the long-polling engine. Context: {error}"
        )
        
        # If a user message triggered this, gently let them know the server is experiencing lag
        if isinstance(update, Update) and update.effective_message:
            try:
                await update.effective_message.reply_text(
                    "⚠️ Hubo un pequeño retraso en la red con los servidores de Telegram. "
                    "Por favor, intenta enviar tu último mensaje de nuevo si no ves respuesta."
                )
            except Exception:
                pass  # Ignore secondary issues if connection is completely down temporarily
        return

    # Log any other unexpected exceptions with a full stack trace for debugging
    logger.error("💥 An unhandled exception occurred during execution:", exc_info=error)

# --- 5. RUNTIME INITIALIZATION ENGINE ---

if __name__ == '__main__':
    # Guarantee local database instances exist before setting up routes
    db_manager.init_db()
    
    # Instantiate the standard python-telegram-bot application runtime wrapper
    app = Application.builder().token(TOKEN).build()

    # REGISTER THE GLOBAL ERROR HANDLER HERE 👇
    app.add_error_handler(global_error_handler)
    
    # Mount background worker jobs into system event pools.
    # We poll every 15 seconds. Safe, respectful, and fast enough for course adjustments.
    app.job_queue.run_repeating(central_scraping_job, interval=5, first=5)
    
    # Configure multi-step user state wizards
    add_conv = ConversationHandler(
        entry_points=[CallbackQueryHandler(start_add_conversation, pattern="^menu_add$")],
        states={
            TYPING_CLAVE: [MessageHandler(filters.TEXT & ~filters.COMMAND, save_clave_get_nrc)],
            TYPING_NRC: [MessageHandler(filters.TEXT & ~filters.COMMAND, complete_add_conversation)]
        },
        fallbacks=[CommandHandler("cancel", cancel_conversation)]
    )
    
    # Map command, wizard, and link handlers into application pools
    app.add_handler(CommandHandler("start", start))
    app.add_handler(add_conv)
    app.add_handler(CallbackQueryHandler(
        handle_callbacks, 
        pattern="^(set_center:|menu_change_center|menu_list|delete_mon:|menu_back$)"
    ))
    print("🤖 Bot operational. Polling initiated...")
    app.run_polling()