"""Periodic device scan task scheduler (0.2.2.6).

Task types (extensible):
- inventory_apps — refresh .desktop inventory + discovered_apps_allowlist
- inventory_browsers — refresh browser count in device profile
- diagnostics_snapshot — light RAM/disk refresh without full app rescan

Security (unchanged by this module):
- Scans populate the dynamic launch allowlist but do NOT bypass application_control.
- PolicyEngine deny-by-default and trusted-device requirements remain upstream.
- Launches still require .desktop resolution (no arbitrary binary paths).
"""

from __future__ import annotations

import os
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Callable

from .device_capability_scanner import (
    get_scan_scheduler,
    load_profile,
    refresh_profile_light,
    save_profile,
)

SCAN_TASK_INVENTORY_APPS = "inventory_apps"
SCAN_TASK_INVENTORY_BROWSERS = "inventory_browsers"
SCAN_TASK_DIAGNOSTICS_SNAPSHOT = "diagnostics_snapshot"

SCAN_TASK_TYPES = frozenset(
    {
        SCAN_TASK_INVENTORY_APPS,
        SCAN_TASK_INVENTORY_BROWSERS,
        SCAN_TASK_DIAGNOSTICS_SNAPSHOT,
    }
)

DEFAULT_DEVICE_PROFILE_SCAN_INTERVAL_SEC = int(
    os.environ.get("DEVICE_PROFILE_SCAN_INTERVAL_SEC", "300")
)

AppsDiscoveredCallback = Callable[[list[dict[str, Any]]], None]


def _read_scan_interval_sec() -> int:
    raw = os.environ.get("DEVICE_PROFILE_SCAN_INTERVAL_SEC", "").strip()
    if raw.isdigit():
        value = int(raw)
        return max(60, value)
    return DEFAULT_DEVICE_PROFILE_SCAN_INTERVAL_SEC


@dataclass
class DeviceScanScheduler:
    """Background periodic scan orchestrator — does not block GTK or heartbeat."""

    interval_sec: int = field(default_factory=_read_scan_interval_sec)
    capability_scheduler: Any | None = None
    profile_path: Any | None = None
    on_apps_discovered: AppsDiscoveredCallback | None = None

    def __post_init__(self) -> None:
        self._lock = threading.Lock()
        self._stop = threading.Event()
        self._thread: threading.Thread | None = None
        if self.capability_scheduler is None:
            self.capability_scheduler = get_scan_scheduler()
        if self.on_apps_discovered is not None:
            self.capability_scheduler.on_apps_discovered = self.on_apps_discovered

    def start(self) -> None:
        with self._lock:
            if self._thread is not None and self._thread.is_alive():
                return
            self._stop.clear()
            self._thread = threading.Thread(
                target=self._run_loop,
                name="iuve-device-scan-scheduler",
                daemon=True,
            )
            self._thread.start()

    def stop(self) -> None:
        self._stop.set()
        with self._lock:
            thread = self._thread
        if thread is not None:
            thread.join(timeout=2)

    def trigger_manual_refresh(self) -> bool:
        """Force full inventory scan (Diagnostics / dashboard manual refresh)."""
        return self.run_task(SCAN_TASK_INVENTORY_APPS, force=True, reason="manual_refresh")

    def run_periodic_tasks(self) -> None:
        for task_type in (
            SCAN_TASK_INVENTORY_APPS,
            SCAN_TASK_INVENTORY_BROWSERS,
            SCAN_TASK_DIAGNOSTICS_SNAPSHOT,
        ):
            self.run_task(task_type, force=False, reason=f"periodic_{task_type}")

    def run_task(
        self,
        task_type: str,
        *,
        force: bool = False,
        reason: str = "",
    ) -> bool:
        normalized = str(task_type or "").strip().lower()
        if normalized not in SCAN_TASK_TYPES:
            return False

        if normalized == SCAN_TASK_INVENTORY_APPS:
            scan_reason = reason or "periodic_inventory_apps"
            return bool(
                self.capability_scheduler.schedule_if_needed(
                    force=force,
                    reason=scan_reason,
                )
            )

        if normalized == SCAN_TASK_INVENTORY_BROWSERS:
            profile = load_profile(self.profile_path)
            if profile is None:
                return self.run_task(SCAN_TASK_INVENTORY_APPS, force=force, reason=reason or "inventory_browsers")
            from .device_capability_scanner import _browsers_from_profile

            browsers = _browsers_from_profile(profile)
            profile = dict(profile)
            profile["browsers_count"] = len(browsers)
            profile["scan_reason"] = reason or "periodic_inventory_browsers"
            save_profile(profile, self.profile_path)
            return True

        if normalized == SCAN_TASK_DIAGNOSTICS_SNAPSHOT:
            profile = load_profile(self.profile_path)
            if profile is None:
                return False
            updated = refresh_profile_light(profile)
            updated["scan_reason"] = reason or "periodic_diagnostics_snapshot"
            save_profile(updated, self.profile_path)
            return True

        return False

    def _run_loop(self) -> None:
        while not self._stop.wait(self.interval_sec):
            try:
                self.run_periodic_tasks()
            except Exception:
                pass


_GLOBAL_DEVICE_SCAN_SCHEDULER: DeviceScanScheduler | None = None
_GLOBAL_DEVICE_SCAN_SCHEDULER_INIT = threading.Lock()


def get_device_scan_scheduler(**kwargs: Any) -> DeviceScanScheduler:
    """Process-wide periodic scan scheduler singleton."""
    global _GLOBAL_DEVICE_SCAN_SCHEDULER
    with _GLOBAL_DEVICE_SCAN_SCHEDULER_INIT:
        if _GLOBAL_DEVICE_SCAN_SCHEDULER is None:
            _GLOBAL_DEVICE_SCAN_SCHEDULER = DeviceScanScheduler(**kwargs)
        return _GLOBAL_DEVICE_SCAN_SCHEDULER
