"""
BackgroundSyncScheduler — periodically pulls classifications from registered catalogs.

Uses stdlib threading.Timer in a self-rescheduling loop; no external scheduler
dependency required. Each CatalogConnection gets its own timer thread.

Resilience behaviour
--------------------
Consecutive failures trigger exponential backoff before the next retry:

    failure 1  → retry in BACKOFF_MINUTES[0]  (5 min)
    failure 2  → retry in BACKOFF_MINUTES[1]  (15 min)
    failure 3+ → retry in BACKOFF_MINUTES[2]  (60 min), connection marked "degraded"

On the next *successful* sync the failure count resets and the connection is
restored to "active" status with its normal sync_interval_minutes schedule.
"""

from __future__ import annotations

import logging
import secrets
import threading
import uuid
from datetime import datetime, timedelta
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from autopil.db.base import CatalogStoreBase

from autopil.catalog.registry import CatalogRegistry
from autopil.models import CatalogConnection, SyncResult

logger = logging.getLogger(__name__)

# Backoff schedule (minutes): index = consecutive failure count − 1 (capped at last element)
BACKOFF_MINUTES = [5, 15, 60]

# Number of consecutive failures before marking a connection "degraded"
DEGRADED_THRESHOLD = 3


class BackgroundSyncScheduler:
    """
    Manages per-connection background sync timers.

    Usage (inside create_app):
        scheduler = BackgroundSyncScheduler(catalog_store, encryption_key)
        for tenant in all_tenants:
            scheduler.start_all(tenant["tenant_id"])

    The scheduler is intentionally lightweight — it never holds DB connections
    between runs and each sync is fully independent.
    """

    def __init__(
        self,
        catalog_store: "CatalogStoreBase",
        encryption_key: str | None = None,
    ) -> None:
        self._store = catalog_store
        self._encryption_key = encryption_key
        self._timers: dict[str, threading.Timer] = {}
        self._failure_counts: dict[str, int] = {}   # consecutive failure counter per connection
        self._lock = threading.Lock()

    # ── public API ───────────────────────────────────────────────────────────

    def start_all(self, tenant_id: str) -> None:
        """Start background timers for all enabled connections for a tenant."""
        connections = self._store.list_connections(tenant_id)
        for conn in connections:
            if conn.is_enabled:
                self.start_connection(conn)

    def start_connection(self, connection: CatalogConnection, *, delay_minutes: int | None = None) -> None:
        """
        Start (or restart) the background sync timer for a connection.

        Parameters
        ----------
        connection:
            The CatalogConnection to schedule.
        delay_minutes:
            Override the delay before the first sync. Defaults to
            connection.sync_interval_minutes.
        """
        self.stop_connection(connection.connection_id)
        interval_s = (delay_minutes if delay_minutes is not None else connection.sync_interval_minutes) * 60
        timer = threading.Timer(
            interval=interval_s,
            function=self._sync_task,
            args=(connection,),
        )
        timer.daemon = True
        timer.name = f"autopil-sync-{connection.connection_id[:8]}"
        with self._lock:
            self._timers[connection.connection_id] = timer
        timer.start()
        logger.debug(
            "Scheduled sync for connection=%s (%s) in %d min",
            connection.connection_id, connection.display_name,
            delay_minutes if delay_minutes is not None else connection.sync_interval_minutes,
        )

    def stop_connection(self, connection_id: str) -> None:
        """Cancel the background timer for a connection."""
        with self._lock:
            timer = self._timers.pop(connection_id, None)
        if timer:
            timer.cancel()

    def stop_all(self) -> None:
        """Cancel all active timers (e.g. on application shutdown)."""
        with self._lock:
            ids = list(self._timers)
        for cid in ids:
            self.stop_connection(cid)

    def trigger_now(self, connection_id: str, tenant_id: str) -> SyncResult:
        """
        Run a sync immediately, synchronously. Returns the SyncResult.
        Called from the POST /v1/catalog/connections/{id}/sync endpoint.
        """
        conn = self._store.get_connection(connection_id, tenant_id)
        if conn is None:
            raise ValueError(f"Connection not found: {connection_id}")
        result = self._run_sync(conn)
        self._handle_result(conn, result)
        return result

    def get_failure_count(self, connection_id: str) -> int:
        """Return the current consecutive failure count for a connection."""
        with self._lock:
            return self._failure_counts.get(connection_id, 0)

    # ── internal ─────────────────────────────────────────────────────────────

    def _sync_task(self, connection: CatalogConnection) -> None:
        """Timer callback: run sync, handle backoff/recovery, reschedule."""
        try:
            result = self._run_sync(connection)
            self._handle_result(connection, result)
        except Exception:
            logger.exception(
                "Unhandled error in sync task for connection=%s", connection.connection_id
            )
            # Count as a failure and still reschedule
            self._increment_failure(connection)

    def _handle_result(self, connection: CatalogConnection, result: SyncResult) -> None:
        """Update failure counts, connection status, and reschedule."""
        if result.status == "success":
            self._on_success(connection)
        else:
            self._increment_failure(connection)

    def _on_success(self, connection: CatalogConnection) -> None:
        """Reset failure state and reschedule at normal interval."""
        with self._lock:
            was_degraded = self._failure_counts.get(connection.connection_id, 0) >= DEGRADED_THRESHOLD
            self._failure_counts[connection.connection_id] = 0

        if was_degraded:
            logger.info(
                "Connection %s (%s) recovered — restoring to 'active'",
                connection.connection_id, connection.display_name,
            )
            self._store.update_connection(
                connection.connection_id,
                {"status": "active"},
                connection.tenant_id,
            )
            # Refresh connection object from store so we reschedule with up-to-date settings
            fresh = self._store.get_connection(connection.connection_id, connection.tenant_id)
            if fresh:
                self.start_connection(fresh)
                return

        self.start_connection(connection)

    def _increment_failure(self, connection: CatalogConnection) -> None:
        """Increment failure counter, apply backoff, mark degraded if threshold reached."""
        with self._lock:
            count = self._failure_counts.get(connection.connection_id, 0) + 1
            self._failure_counts[connection.connection_id] = count

        backoff_idx = min(count - 1, len(BACKOFF_MINUTES) - 1)
        backoff = BACKOFF_MINUTES[backoff_idx]

        if count == DEGRADED_THRESHOLD:
            logger.warning(
                "Connection %s (%s) reached %d consecutive failures — marking degraded, "
                "will retry every %d min",
                connection.connection_id, connection.display_name, count, backoff,
            )
            self._store.update_connection(
                connection.connection_id,
                {"status": "degraded"},
                connection.tenant_id,
            )
        elif count > DEGRADED_THRESHOLD:
            logger.warning(
                "Connection %s (%s) still failing (count=%d) — retry in %d min",
                connection.connection_id, connection.display_name, count, backoff,
            )
        else:
            logger.warning(
                "Connection %s (%s) sync failed (count=%d) — retry in %d min",
                connection.connection_id, connection.display_name, count, backoff,
            )

        self.start_connection(connection, delay_minutes=backoff)

    def _run_sync(self, connection: CatalogConnection) -> SyncResult:
        """Execute one full sync cycle for a connection. Returns SyncResult."""
        sync_id = "syn_" + secrets.token_hex(8)
        started_at = datetime.utcnow()
        logger.info(
            "Starting catalog sync: connection=%s (%s)",
            connection.connection_id, connection.catalog_type,
        )

        try:
            connector = CatalogRegistry.get_connector(connection)
            classifications = connector.fetch_classifications()

            # Set expiry = fetched_at + sync_interval * 2 (so stale entries auto-expire)
            now = datetime.utcnow()
            expires = now + timedelta(minutes=connection.sync_interval_minutes * 2)
            for cls in classifications:
                cls.fetched_at = now
                cls.expires_at = expires
                cls.tenant_id  = connection.tenant_id

            written = self._store.upsert_classifications(classifications)
            skipped = len(classifications) - written
            self._store.delete_stale_classifications(connection.connection_id)
            self._store.mark_sync_completed(connection.connection_id, now)

            result = SyncResult(
                sync_id=sync_id,
                connection_id=connection.connection_id,
                tenant_id=connection.tenant_id,
                status="success",
                assets_synced=written,
                assets_skipped=skipped,
                started_at=started_at,
                completed_at=datetime.utcnow(),
            )
            logger.info(
                "Catalog sync complete: connection=%s synced=%d skipped=%d",
                connection.connection_id, written, skipped,
            )

        except Exception as exc:
            result = SyncResult(
                sync_id=sync_id,
                connection_id=connection.connection_id,
                tenant_id=connection.tenant_id,
                status="failed",
                assets_synced=0,
                assets_skipped=0,
                started_at=started_at,
                completed_at=datetime.utcnow(),
                error_message=str(exc),
            )
            logger.error(
                "Catalog sync failed: connection=%s error=%s",
                connection.connection_id, exc,
            )

        self._store.record_sync(result)
        return result
