"""Scheduler de automatizaciones basado en asyncio.

Ejecuta periódicamente las reglas de automatización habilitadas
según su `schedule_hint` (diario / semanal / horario). Al completar cada
ejecución notifica por Telegram con las propuestas generadas e incluye
botones inline de aprobación para las críticas o de riesgo alto.
"""
from __future__ import annotations

import asyncio
import logging
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from .orchestrator import MultiAgentOrchestrator
    from .storage import SupervisorStore
    from .telegram_bot import TelegramBotService

logger = logging.getLogger(__name__)

# Intervalos mínimos en segundos para cada hint
SCHEDULE_INTERVALS: dict[str, int] = {
    "horario":  3_600,
    "bidiario": 7_200,
    "diario":   86_400,
    "semanal":  604_800,
}

# Hora UTC preferida para primera ejecución de cada regla conocida
PREFERRED_RUN_HOUR: dict[str, int] = {
    "stack_health":             6,
    "ops_daily":                7,
    "seo_weekly":               4,
    "marketing_daily":          9,
    "reactivacion_anunciantes": 10,
    "seguridad_semanal":        3,
}

# Número máximo de ficheros de backup a conservar (rotación FIFO)
BACKUP_KEEP = 7

# Hora UTC a la que se hace el backup diario de la BD
BACKUP_HOUR_UTC = 2

# Hora UTC del digest diario por Telegram
DIGEST_HOUR_UTC = 8

# Minutos sin atender propuestas pendientes antes de enviar recordatorio (1 vez al día)
PENDING_REMINDER_MINUTES = 1440


class AutomationScheduler:
    """Scheduler asyncio que comprueba cada minuto si hay reglas vencidas."""

    def __init__(self, store: "SupervisorStore", orchestrator: "MultiAgentOrchestrator") -> None:
        self.store = store
        self.orchestrator = orchestrator
        self._telegram: "TelegramBotService | None" = None
        self._task: asyncio.Task[None] | None = None
        self._running = False
        self._executing: set[str] = set()  # reglas en ejecución ahora mismo
        self._last_backup_date: str = ""   # YYYY-MM-DD del último backup realizado
        self._last_digest_date: str = ""   # YYYY-MM-DD del último digest enviado
        self._last_security_scan_week: str = ""  # ISO year-week del último security scan automático
        self._last_reminder_ts: float = 0  # timestamp (epoch) del último recordatorio de pendientes

    def attach_telegram(self, telegram: "TelegramBotService") -> None:
        """Conecta el bot de Telegram para notificaciones de resultado."""
        self._telegram = telegram

    # ------------------------------------------------------------------ #
    # Ciclo de vida                                                         #
    # ------------------------------------------------------------------ #

    async def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._task = asyncio.create_task(self._scheduler_loop(), name="automation-scheduler")
        logger.info("AutomationScheduler iniciado")

    async def stop(self) -> None:
        self._running = False
        if self._task:
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
            self._task = None
        logger.info("AutomationScheduler detenido")

    # ------------------------------------------------------------------ #
    # Loop principal                                                        #
    # ------------------------------------------------------------------ #

    async def _scheduler_loop(self) -> None:
        while self._running:
            try:
                await self._tick()
            except asyncio.CancelledError:
                raise
            except Exception as exc:
                logger.exception("Error inesperado en scheduler loop: %s", exc)
            await asyncio.sleep(60)

    async def _tick(self) -> None:
        rules = self.store.list_automation_rules(limit=50)
        now = datetime.now(timezone.utc)
        fired: list[str] = []
        for rule in rules:
            if not rule.get("enabled"):
                continue
            key = rule.get("rule_key", "")
            if key in self._executing:
                continue  # evitar ejecuciones solapadas
            if self._is_rule_due(rule, now):
                fired.append(key)
                asyncio.create_task(self._safe_execute(rule), name=f"rule-{key}")
        if fired:
            self.store.add_audit("scheduler.tick.fired", {"rules": fired, "ts": now.isoformat()})

        # Backup diario a las BACKUP_HOUR_UTC UTC
        today_str = now.strftime("%Y-%m-%d")
        if now.hour == BACKUP_HOUR_UTC and now.minute < 2 and self._last_backup_date != today_str:
            self._last_backup_date = today_str
            asyncio.create_task(self._run_backup(now), name="db-backup")

        # Digest diario a las DIGEST_HOUR_UTC UTC
        if now.hour == DIGEST_HOUR_UTC and now.minute < 2 and self._last_digest_date != today_str:
            self._last_digest_date = today_str
            asyncio.create_task(self._send_daily_digest(), name="daily-digest")

        # Recordatorio de propuestas pendientes si llevan >PENDING_REMINDER_MINUTES sin atender
        import time as _time
        elapsed_since_reminder = _time.monotonic() - self._last_reminder_ts
        if elapsed_since_reminder >= PENDING_REMINDER_MINUTES * 60:
            asyncio.create_task(self._check_pending_proposals(), name="pending-reminder")

        # Escaneo de seguridad automático: lunes a las 03:00 UTC (siempre activo, sin depender de reglas)
        iso_week = now.strftime("%G-W%V")  # e.g. "2025-W03"
        if (
            now.weekday() == 0  # lunes
            and now.hour == 3
            and now.minute < 2
            and self._last_security_scan_week != iso_week
        ):
            self._last_security_scan_week = iso_week
            _dummy_rule: dict[str, Any] = {"rule_key": "seguridad_semanal", "name": "Auto Security Scan"}
            asyncio.create_task(self._run_security_scan(_dummy_rule), name="auto-security-scan")

    # ------------------------------------------------------------------ #
    # Lógica de "¿toca ejecutar?"                                          #
    # ------------------------------------------------------------------ #

    def _is_rule_due(self, rule: dict[str, Any], now: datetime) -> bool:
        hint = str(rule.get("schedule_hint", "diario")).lower()
        interval = SCHEDULE_INTERVALS.get(hint, SCHEDULE_INTERVALS["diario"])
        last_run = rule.get("last_run_at")
        key = rule.get("rule_key", "unknown")

        if not last_run:
            # Primera vez: dispara en la hora preferida del día
            preferred = PREFERRED_RUN_HOUR.get(key, 6)
            return now.hour == preferred and now.minute < 2

        try:
            last_dt = datetime.fromisoformat(last_run.replace("Z", "+00:00"))
        except (ValueError, AttributeError):
            return False

        elapsed = (now - last_dt).total_seconds()
        return elapsed >= interval

    # ------------------------------------------------------------------ #
    # Ejecución con manejo seguro de errores                               #
    # ------------------------------------------------------------------ #

    async def _safe_execute(self, rule: dict[str, Any]) -> None:
        key = rule.get("rule_key", "?")
        self._executing.add(key)
        try:
            logger.info("Scheduler: ejecutando regla '%s'", key)
            if key == "seguridad_semanal":
                await self._run_security_scan(rule)
            else:
                result = await self.orchestrator.run_automation_rule(
                    key, actor="scheduler", note="Ejecución automática por scheduler"
                )
                await self._notify_success(rule, result)
        except Exception as exc:
            logger.error("Scheduler: fallo en regla '%s': %s", key, exc)
            await self._notify_error(rule, exc)
        finally:
            self._executing.discard(key)

    async def _run_security_scan(self, rule: dict[str, Any]) -> None:
        """Ejecuta el escaneo de seguridad semanal con pip-audit + trivy."""
        from . import security_scanner as _scanner
        result = await _scanner.run_full_scan(actor="scheduler:seguridad_semanal")
        self.store.save_security_scan(result)
        if not self._telegram:
            return
        status_icon = {"clean": "✅", "vulnerabilities_found": "⚠️", "error": "❌"}.get(
            result.get("status", ""), "ℹ️"
        )
        tools = ", ".join(result.get("tools_used", [])) or "ninguna"
        text = (
            f"{status_icon} *Seguridad semanal*\n\n"
            f"Estado: {result.get('status', '?')}\n"
            f"Herramientas: {tools}\n"
            f"Total CVE: {result.get('total_vulnerabilities', 0)}\n"
            f"  🔴 Crítico: {result.get('critical', 0)}\n"
            f"  🟠 Alto:    {result.get('high', 0)}\n"
            f"  🟡 Medio:   {result.get('medium', 0)}\n"
            f"  🟢 Bajo:    {result.get('low', 0)}\n\n"
            f"{result.get('summary', '')}"
        )
        await self._telegram.broadcast_to_admins(text, [])

    # ------------------------------------------------------------------ #
    # Notificaciones Telegram                                               #
    # ------------------------------------------------------------------ #

    async def _notify_success(self, rule: dict[str, Any], result: dict[str, Any]) -> None:
        if not self._telegram:
            return
        inner = result.get("result") or {}
        study = inner.get("study") or {}
        proposals = inner.get("proposals") or []
        title = rule.get("title", rule.get("rule_key", "?"))
        summary = str(study.get("summary") or "Sin resumen disponible")
        n = len(proposals)
        text = (
            f"🤖 *Scheduler — {title}*\n\n"
            f"📋 {summary}\n\n"
            f"💡 _{n} propuesta(s) generada(s)_"
        )
        await self._telegram.broadcast_to_admins(text, proposals[:4])

    async def _notify_error(self, rule: dict[str, Any], exc: Exception) -> None:
        if not self._telegram:
            return
        title = rule.get("title", rule.get("rule_key", "?"))
        text = f"⚠️ *Scheduler Error — {title}*\n\n`{str(exc)[:350]}`"
        await self._telegram.broadcast_to_admins(text, [])

    # ------------------------------------------------------------------ #
    # Backup SQLite diario                                                  #
    # BACKUP_KEEP ficheros más recientes; los anteriores se eliminan.       #
    # ------------------------------------------------------------------ #

    async def _run_backup(self, now: datetime) -> None:
        try:
            db_path = Path(self.store.db_path)  # type: ignore[attr-defined]
        except AttributeError:
            # Si el store no expone db_path (versiones antiguas) usamos la ruta convencional
            db_path = Path("data/supervisor.sqlite3")

        if not db_path.exists():
            logger.warning("Backup: fichero BD no encontrado en %s", db_path)
            return

        backup_dir = db_path.parent / "backups"
        backup_dir.mkdir(parents=True, exist_ok=True)

        stamp = now.strftime("%Y%m%d_%H%M%S")
        dest  = backup_dir / f"supervisor_{stamp}.sqlite3"
        try:
            await asyncio.to_thread(shutil.copy2, db_path, dest)
            logger.info("Backup BD creado: %s", dest.name)
            self.store.add_audit("scheduler.backup.ok", {"file": dest.name})
        except Exception as exc:
            logger.error("Backup BD fallido: %s", exc)
            self.store.add_audit("scheduler.backup.error", {"error": str(exc)})
            return

        # Rotación: eliminar backups antiguos conservando solo BACKUP_KEEP
        existing = sorted(backup_dir.glob("supervisor_*.sqlite3"))
        to_remove = existing[: max(0, len(existing) - BACKUP_KEEP)]
        for old in to_remove:
            try:
                old.unlink()
                logger.debug("Backup antiguo eliminado: %s", old.name)
            except OSError as exc:
                logger.warning("No se pudo eliminar backup %s: %s", old.name, exc)

    # ------------------------------------------------------------------ #
    # Digest diario automático                                             #
    # ------------------------------------------------------------------ #

    async def _send_daily_digest(self) -> None:
        """Envía el digest diario a todos los admins de Telegram."""
        if not self._telegram:
            return
        for chat_id in self._telegram.allowed_chat_ids:
            try:
                await self._telegram._send_digest(chat_id)  # noqa: SLF001
            except Exception as exc:
                logger.warning("Digest diario: fallo enviando a %s: %s", chat_id, exc)
        self.store.add_audit("scheduler.digest.sent", {"chat_ids": list(self._telegram.allowed_chat_ids)})
        logger.info("Digest diario enviado a %d admin(s)", len(self._telegram.allowed_chat_ids))

    # ------------------------------------------------------------------ #
    # Recordatorio de propuestas pendientes sin atender                   #
    # ------------------------------------------------------------------ #

    async def _check_pending_proposals(self) -> None:
        """Si hay propuestas pendientes sin atender, recuerda por Telegram."""
        import time as _time
        if not self._telegram:
            return
        pending = self.store.list_proposals(status="pending", limit=10)
        if not pending:
            return

        # Filtrar las que llevan más de PENDING_REMINDER_MINUTES sin atender
        now = datetime.now(timezone.utc)
        stale: list[dict[str, Any]] = []
        for p in pending:
            created_raw = p.get("created_at", "")
            try:
                created_dt = datetime.fromisoformat(str(created_raw).replace("Z", "+00:00"))
                age_minutes = (now - created_dt).total_seconds() / 60
                if age_minutes >= PENDING_REMINDER_MINUTES:
                    stale.append(p)
            except (ValueError, TypeError):
                stale.append(p)

        if not stale:
            return

        self._last_reminder_ts = _time.monotonic()
        n = len(stale)
        risk_icons = {"low": "🟢", "medium": "🟡", "high": "🔴"}
        lines = [
            f"⏰ *{n} propuesta(s) pendientes sin revisar*",
            f"_Llevan más de {PENDING_REMINDER_MINUTES} min esperando tu aprobación_",
            "",
        ]
        for p in stale[:5]:
            ri = risk_icons.get(p.get("risk_level", ""), "⚪")
            lines.append(f"  {ri} {p.get('title', 'Propuesta')[:44]}")

        text = "\n".join(lines)
        keyboard: dict[str, Any] = {"inline_keyboard": [[
            {"text": "📋 Ver propuestas", "url": self._telegram.settings.public_base_url or ""},
        ]]} if getattr(self._telegram.settings, "public_base_url", None) else None  # type: ignore[assignment]

        for chat_id in self._telegram.allowed_chat_ids:
            try:
                await self._telegram._send_message(chat_id, text, reply_markup=keyboard)  # noqa: SLF001
            except Exception as exc:
                logger.warning("Recordatorio pendientes: fallo enviando a %s: %s", chat_id, exc)

        self.store.add_audit("scheduler.reminder.pending", {"count": n})
        logger.info("Recordatorio de %d propuesta(s) pendientes enviado", n)

    # ------------------------------------------------------------------ #
    # Estado público (para dashboard)                                      #
    # ------------------------------------------------------------------ #

    def get_status(self) -> dict[str, Any]:
        return {
            "running": self._running,
            "executing_now": list(self._executing),
            "next_check_in_seconds": 60,
        }
