"""Scheduler for recurring scans and signature updates.

Wraps the ``schedule`` library to provide cron-like recurring tasks that
drive the :class:`ScanEngine` and signature updater in a long-running
daemon loop.
"""

from __future__ import annotations

import logging
import time
from typing import Optional

import schedule

from ayn_antivirus.config import Config
from ayn_antivirus.core.engine import ScanEngine, ScanResult
from ayn_antivirus.core.event_bus import EventType, event_bus

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Cron expression helpers
# ---------------------------------------------------------------------------

def _parse_cron_field(field: str, min_val: int, max_val: int) -> list[int]:
    """Parse a single cron field (e.g. ``*/5``, ``1,3,5``, ``0-23``, ``*``).

    Returns a sorted list of matching integer values.
    """
    values: set[int] = set()

    for part in field.split(","):
        part = part.strip()

        # */step
        if part.startswith("*/"):
            step = int(part[2:])
            values.update(range(min_val, max_val + 1, step))
        # range with optional step  (e.g. 1-5 or 1-5/2)
        elif "-" in part:
            range_part, _, step_part = part.partition("/")
            lo, hi = range_part.split("-", 1)
            step = int(step_part) if step_part else 1
            values.update(range(int(lo), int(hi) + 1, step))
        # wildcard
        elif part == "*":
            values.update(range(min_val, max_val + 1))
        # literal
        else:
            values.add(int(part))

    return sorted(values)


def _cron_to_schedule(cron_expr: str) -> dict:
    """Convert a 5-field cron expression into components.

    Returns a dict with keys ``minutes``, ``hours``, ``days``, ``months``,
    ``weekdays`` — each a list of integers.

    Only *minute* and *hour* are used by the ``schedule`` library adapter
    below; the rest are validated but not fully honoured (``schedule`` lacks
    calendar-level granularity).
    """
    parts = cron_expr.strip().split()
    if len(parts) != 5:
        raise ValueError(f"Expected 5-field cron expression, got: {cron_expr!r}")

    return {
        "minutes": _parse_cron_field(parts[0], 0, 59),
        "hours": _parse_cron_field(parts[1], 0, 23),
        "days": _parse_cron_field(parts[2], 1, 31),
        "months": _parse_cron_field(parts[3], 1, 12),
        "weekdays": _parse_cron_field(parts[4], 0, 6),
    }


# ---------------------------------------------------------------------------
# Scheduler
# ---------------------------------------------------------------------------

class Scheduler:
    """Manages recurring scan and update jobs.

    Parameters
    ----------
    config:
        Application configuration — used to build a :class:`ScanEngine` and
        read schedule expressions.
    engine:
        Optional pre-built engine instance.  If ``None``, one is created from
        *config*.
    """

    def __init__(self, config: Config, engine: Optional[ScanEngine] = None) -> None:
        self.config = config
        self.engine = engine or ScanEngine(config)
        self._scheduler = schedule.Scheduler()

    # ------------------------------------------------------------------
    # Job builders
    # ------------------------------------------------------------------

    def schedule_scan(self, cron_expr: str, scan_type: str = "full") -> None:
        """Schedule a recurring scan using a cron expression.

        Parameters
        ----------
        cron_expr:
            Standard 5-field cron string (``minute hour dom month dow``).
        scan_type:
            One of ``"full"``, ``"quick"``, or ``"deep"``.
        """
        parsed = _cron_to_schedule(cron_expr)

        # ``schedule`` doesn't natively support cron, so we approximate by
        # scheduling at every matching hour:minute combination.  For simple
        # expressions like ``0 2 * * *`` this is exact.
        for hour in parsed["hours"]:
            for minute in parsed["minutes"]:
                time_str = f"{hour:02d}:{minute:02d}"
                self._scheduler.every().day.at(time_str).do(
                    self._run_scan, scan_type=scan_type
                )
                logger.info("Scheduled %s scan at %s daily", scan_type, time_str)

    def schedule_update(self, interval_hours: int = 6) -> None:
        """Schedule recurring signature updates.

        Parameters
        ----------
        interval_hours:
            How often (in hours) to pull fresh signatures.
        """
        self._scheduler.every(interval_hours).hours.do(self._run_update)
        logger.info("Scheduled signature update every %d hour(s)", interval_hours)

    # ------------------------------------------------------------------
    # Daemon loop
    # ------------------------------------------------------------------

    def run_daemon(self) -> None:
        """Start the blocking scheduler loop.

        Runs all pending jobs and sleeps between iterations.  Designed to be
        the main loop of a background daemon process.

        Press ``Ctrl+C`` (or send ``SIGINT``) to exit cleanly.
        """
        logger.info("AYN scheduler daemon started — %d job(s)", len(self._scheduler.get_jobs()))

        try:
            while True:
                self._scheduler.run_pending()
                time.sleep(30)
        except KeyboardInterrupt:
            logger.info("Scheduler daemon stopped by user")

    # ------------------------------------------------------------------
    # Job implementations
    # ------------------------------------------------------------------

    def _run_scan(self, scan_type: str = "full") -> None:
        """Execute a scan job."""
        logger.info("Starting scheduled %s scan", scan_type)
        try:
            if scan_type == "quick":
                result: ScanResult = self.engine.quick_scan()
            else:
                # "full" and "deep" both scan all paths; deep adds process/network
                # via full_scan on the engine, but here we keep it simple.
                result = ScanResult()
                for path in self.config.scan_paths:
                    partial = self.engine.scan_path(path, recursive=True)
                    result.files_scanned += partial.files_scanned
                    result.files_skipped += partial.files_skipped
                    result.threats.extend(partial.threats)

            logger.info(
                "Scheduled %s scan complete — %d files, %d threats",
                scan_type,
                result.files_scanned,
                len(result.threats),
            )
        except Exception:
            logger.exception("Scheduled %s scan failed", scan_type)

    def _run_update(self) -> None:
        """Execute a signature update job."""
        logger.info("Starting scheduled signature update")
        try:
            from ayn_antivirus.signatures.manager import SignatureManager

            manager = SignatureManager(self.config)
            summary = manager.update_all()
            total = summary.get("total_new", 0)
            errors = summary.get("errors", [])
            logger.info(
                "Scheduled signature update complete: %d new, %d errors",
                total,
                len(errors),
            )
            if errors:
                for err in errors:
                    logger.warning("Feed error: %s", err)
            manager.close()
            event_bus.publish(EventType.SIGNATURE_UPDATED, {
                "total_new": total,
                "feeds": list(summary.get("feeds", {}).keys()),
                "errors": errors,
            })
        except Exception:
            logger.exception("Scheduled signature update failed")
