"""
Resource Status Tracker

Tracks download attempts, failures, and cooldowns in the database.
Provides persistent storage for failure classifications and retry eligibility.
"""

from datetime import datetime, timedelta, UTC
from typing import Optional, Dict, Any

from loguru import logger
from sqlalchemy.orm import sessionmaker, Session

from .models import ResourceDownloadStatus, Base
from .failure_classifier import BaseFailure

MAX_TOTAL_RETRIES = 5


def compute_retry_cooldown(
    attempt: int, default_cooldown: timedelta
) -> Optional[timedelta]:
    """Return cooldown for this attempt number, or None for permanent failure.

    Schedule:
        attempt 1: default_cooldown (from failure type)
        attempt 2: 1 day
        attempt 3-4: 30 days
        attempt >= 5: None (permanent failure)
    """
    if attempt >= MAX_TOTAL_RETRIES:
        return None
    if attempt >= 3:
        return timedelta(days=30)
    if attempt == 2:
        return timedelta(days=1)
    return default_cooldown


class ResourceStatusTracker:
    """Track download attempts, failures, and cooldowns in database"""

    def __init__(self, username: str, password: Optional[str] = None):
        """
        Initialize the status tracker for a user.

        Args:
            username: Username for database access
            password: Optional password for encrypted database
        """
        self.username = username
        self.password = password

        # Use the global db_manager singleton to share cached connections
        from ...database.encrypted_db import (
            DatabaseInitializationError,
            db_manager,
        )

        self.db_manager = db_manager
        try:
            self.engine = db_manager.open_user_database(username, password)
        except DatabaseInitializationError:
            # Surface init failures from the schedulers/library-init
            # callers as a plain RuntimeError — they all wrap construction
            # in try/except already, and propagating the typed exception
            # would couple every caller to encrypted_db's internals.
            # ``logger.warning`` (no traceback) rather than
            # ``logger.exception``: ``password`` is a live local in this
            # frame, so rendering a traceback under ``diagnose=True``
            # would dump the plaintext SQLCipher master password
            # (unrecoverable — TRUST.md §5). The redacted failure detail
            # is already logged at the raise site in
            # ``open_user_database`` (#4182).
            logger.warning(
                f"[STATUS_TRACKER] Database init failed for user: {username}"
            )
            raise RuntimeError(
                f"Database initialisation failed for user {username}"
            ) from None
        self.Session = sessionmaker(bind=self.engine)

        # Create tables if they don't exist
        Base.metadata.create_all(self.engine)
        logger.info(
            f"[STATUS_TRACKER] Initialized for user: {username} with encrypted database"
        )

    def _get_session(self) -> Session:
        """Get a database session"""
        return self.Session()

    def mark_failure(
        self,
        resource_id: int,
        failure: BaseFailure,
        session: Optional[Session] = None,
    ) -> None:
        """
        Mark a resource as failed with classification.

        Args:
            resource_id: Resource identifier
            failure: Classified failure object
            session: Optional existing database session to reuse
        """
        if session is not None:
            self._apply_failure(session, resource_id, failure)
            return

        with self._get_session() as session:
            self._apply_failure(session, resource_id, failure)
            session.commit()

    def _apply_failure(
        self, session: Session, resource_id: int, failure: BaseFailure
    ) -> None:
        """Apply failure status updates to a session (does not commit)."""
        # Get or create status record
        status = (
            session.query(ResourceDownloadStatus)
            .filter_by(resource_id=resource_id)
            .first()
        )

        if not status:
            status = ResourceDownloadStatus(resource_id=resource_id)
            session.add(status)

        # Update status information
        if failure.is_permanent():
            status.status = "permanently_failed"
            status.retry_after_timestamp = None
            status.failure_type = failure.error_type
            status.failure_message = failure.message
            status.permanent_failure_at = datetime.now(UTC)
            logger.info(
                f"[STATUS_TRACKER] Marked resource {resource_id} as permanently failed: {failure.error_type}"
            )
        else:
            status.failure_type = failure.error_type
            status.failure_message = failure.message

            attempt = (status.total_retry_count or 0) + 1
            cooldown = compute_retry_cooldown(attempt, failure.retry_after)

            if cooldown is None:
                status.status = "permanently_failed"
                status.permanent_failure_at = datetime.now(UTC)
                status.retry_after_timestamp = None
                logger.info(
                    f"[STATUS_TRACKER] Auto-promoted resource {resource_id} to "
                    f"permanently failed after {attempt} attempts"
                )
            else:
                status.status = "temporarily_failed"
                status.retry_after_timestamp = datetime.now(UTC) + cooldown
                logger.info(
                    f"[STATUS_TRACKER] Marked resource {resource_id} as temporarily failed: "
                    f"{failure.error_type}, attempt {attempt}, retry after: {cooldown}"
                )

        # Update retry statistics
        # Ensure total_retry_count is initialized (handle None from legacy data)
        if status.total_retry_count is None:
            status.total_retry_count = 0
        status.total_retry_count += 1

        # Check if last attempt was today (before overwriting last_attempt_at)
        today = datetime.now(UTC).date()
        last_attempt = (
            status.last_attempt_at.date() if status.last_attempt_at else None
        )
        status.last_attempt_at = datetime.now(UTC)
        if last_attempt == today:
            # Ensure today_retry_count is initialized (handle None from legacy data)
            if status.today_retry_count is None:
                status.today_retry_count = 0
            status.today_retry_count += 1
        else:
            status.today_retry_count = 1

        logger.debug(
            f"[STATUS_TRACKER] Updated failure status for resource {resource_id}"
        )

    def mark_success(
        self, resource_id: int, session: Optional[Session] = None
    ) -> None:
        """
        Mark a resource as successfully downloaded.

        Args:
            resource_id: Resource identifier
            session: Optional existing database session to reuse
        """
        if session is not None:
            self._apply_success(session, resource_id)
            return

        with self._get_session() as session:
            self._apply_success(session, resource_id)
            session.commit()

    def _apply_success(self, session: Session, resource_id: int) -> None:
        """Apply success status updates to a session (does not commit)."""
        status = (
            session.query(ResourceDownloadStatus)
            .filter_by(resource_id=resource_id)
            .first()
        )

        if status:
            status.status = "completed"
            status.failure_type = None
            status.failure_message = None
            status.retry_after_timestamp = None
            status.updated_at = datetime.now(UTC)
            logger.info(
                f"[STATUS_TRACKER] Marked resource {resource_id} as successfully completed"
            )

    def can_retry(self, resource_id: int) -> tuple[bool, Optional[str]]:
        """
        Check if a resource can be retried right now.

        Args:
            resource_id: Resource identifier

        Returns:
            Tuple of (can_retry, reason_if_not)
        """
        with self._get_session() as session:
            status = (
                session.query(ResourceDownloadStatus)
                .filter_by(resource_id=resource_id)
                .first()
            )

            if not status:
                # No status record, can retry
                return True, None

            # Check if permanently failed
            if status.status == "permanently_failed":
                return (
                    False,
                    f"Permanently failed: {status.failure_message or status.failure_type}",
                )

            # Check if temporarily failed and cooldown not expired
            if (
                status.status == "temporarily_failed"
                and status.retry_after_timestamp
            ):
                # Ensure retry_after_timestamp is timezone-aware (handle legacy data)
                retry_timestamp = status.retry_after_timestamp
                if retry_timestamp.tzinfo is None:
                    # Assume UTC for naive timestamps
                    retry_timestamp = retry_timestamp.replace(tzinfo=UTC)

                if datetime.now(UTC) < retry_timestamp:
                    return (
                        False,
                        f"Cooldown active, retry available at {retry_timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
                    )

            # Check daily retry limit (max 3 retries per day)
            # today_retry_count is only reset inside _apply_failure, so check
            # whether the stored count is actually from today before using it.
            today = datetime.now(UTC).date()
            last_attempt_date = (
                status.last_attempt_at.date()
                if status.last_attempt_at
                else None
            )
            today_count = (
                status.today_retry_count if last_attempt_date == today else 0
            )
            if today_count >= 3:
                return (
                    False,
                    f"Daily retry limit exceeded ({today_count}/3). Retry available tomorrow.",
                )

            # Check total retry limit (safety net for records not yet auto-promoted)
            if (status.total_retry_count or 0) >= MAX_TOTAL_RETRIES:
                return (
                    False,
                    f"Permanently failed after {status.total_retry_count} attempts. Will not retry.",
                )

            # Can retry
            return True, None

    def get_resource_status(self, resource_id: int) -> Optional[Dict[str, Any]]:
        """
        Get the current status of a resource.

        Args:
            resource_id: Resource identifier

        Returns:
            Status information dictionary or None if not found
        """
        with self._get_session() as session:
            status = (
                session.query(ResourceDownloadStatus)
                .filter_by(resource_id=resource_id)
                .first()
            )

            if not status:
                return None

            return {
                "resource_id": status.resource_id,
                "status": status.status,
                "failure_type": status.failure_type,
                "failure_message": status.failure_message,
                "retry_after_timestamp": status.retry_after_timestamp.isoformat()
                if status.retry_after_timestamp
                else None,
                "last_attempt_at": status.last_attempt_at.isoformat()
                if status.last_attempt_at
                else None,
                "total_retry_count": status.total_retry_count,
                "today_retry_count": status.today_retry_count,
                "created_at": status.created_at.isoformat(),
                "updated_at": status.updated_at.isoformat(),
            }

    def get_failed_resources_count(self) -> Dict[str, int]:
        """
        Get counts of resources by failure type.

        Returns:
            Dictionary mapping failure types to counts
        """
        with self._get_session() as session:
            failed_resources = (
                session.query(ResourceDownloadStatus)
                .filter(
                    ResourceDownloadStatus.status.in_(
                        ["temporarily_failed", "permanently_failed"]
                    )
                )
                .all()
            )

            counts = {}
            for resource in failed_resources:
                failure_type = resource.failure_type or "unknown"
                counts[failure_type] = counts.get(failure_type, 0) + 1

            return counts

    def clear_permanent_failures(self, older_than_days: int = 30) -> int:
        """
        Clear permanent failure statuses for old records.

        Args:
            older_than_days: Clear failures older than this many days

        Returns:
            Number of records cleared
        """
        cutoff_date = datetime.now(UTC) - timedelta(days=older_than_days)

        with self._get_session() as session:
            old_failures = (
                session.query(ResourceDownloadStatus)
                .filter(
                    ResourceDownloadStatus.status == "permanently_failed",
                    ResourceDownloadStatus.created_at < cutoff_date,
                )
                .all()
            )

            count = len(old_failures)
            for failure in old_failures:
                failure.status = "available"
                failure.failure_type = None
                failure.failure_message = None
                failure.retry_after_timestamp = None
                failure.permanent_failure_at = None
                failure.total_retry_count = 0
                failure.today_retry_count = 0
                failure.updated_at = datetime.now(UTC)

            session.commit()
            logger.info(
                f"[STATUS_TRACKER] Cleared {count} old permanent failure records"
            )
            return count
