"""Scheduled Maintenance Windows for OCLAWMA.

This module provides the ability to pause job processing during maintenance windows,
with support for recurring schedules, timezone awareness, and web UI indicators.
"""

from __future__ import annotations

import json
import logging
import sqlite3
import threading
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from pathlib import Path
from typing import Any, Callable

from pydantic import BaseModel, Field, field_validator

logger = logging.getLogger(__name__)


class RecurrenceType(str, Enum):
    """Types of recurring maintenance windows."""

    NONE = "none"
    DAILY = "daily"
    WEEKLY = "weekly"
    MONTHLY = "monthly"


class MaintenanceStatus(str, Enum):
    """Status of a maintenance window."""

    SCHEDULED = "scheduled"
    ACTIVE = "active"
    COMPLETED = "completed"
    CANCELLED = "cancelled"


class MaintenanceWindow(BaseModel):
    """Model representing a maintenance window.

    Attributes:
        id: Unique identifier (None for new windows)
        name: Human-readable name for this window
        description: Optional description
        start_time: When the maintenance window starts
        end_time: When the maintenance window ends
        timezone: IANA timezone name (e.g., "America/New_York", "UTC")
        recurring: Type of recurrence (none, daily, weekly, monthly)
        recurrence_end: When recurring windows should stop (None = indefinite)
        created_at: When this window was created
        updated_at: When this window was last updated
        status: Current status of the window
        enabled: Whether this window is enabled
        metadata: Additional custom metadata
    """

    id: int | None = None
    name: str = Field(..., min_length=1, max_length=255)
    description: str | None = None
    start_time: datetime
    end_time: datetime
    timezone: str = Field(default="UTC")
    recurring: RecurrenceType = Field(default=RecurrenceType.NONE)
    recurrence_end: datetime | None = None
    created_at: datetime = Field(default_factory=lambda: datetime.utcnow())
    updated_at: datetime = Field(default_factory=lambda: datetime.utcnow())
    status: MaintenanceStatus = Field(default=MaintenanceStatus.SCHEDULED)
    enabled: bool = Field(default=True)
    metadata: dict[str, Any] = Field(default_factory=dict)

    @field_validator("end_time")
    @classmethod
    def end_time_after_start(cls, v: datetime, info: Any) -> datetime:
        """Validate that end_time is after start_time."""
        if "start_time" in info.data and v <= info.data["start_time"]:
            raise ValueError("end_time must be after start_time")
        return v

    @field_validator("timezone")
    @classmethod
    def validate_timezone(cls, v: str) -> str:
        """Validate timezone is a valid IANA timezone name."""
        try:
            import zoneinfo

            if not zoneinfo.available_timezones() or v not in zoneinfo.available_timezones():
                # Fallback: just check common timezones
                common_timezones = {
                    "UTC",
                    "GMT",
                    "US/Eastern",
                    "US/Central",
                    "US/Mountain",
                    "US/Pacific",
                    "Europe/London",
                    "Europe/Paris",
                    "Europe/Berlin",
                    "Asia/Tokyo",
                    "Asia/Shanghai",
                    "Australia/Sydney",
                    "America/New_York",
                    "America/Chicago",
                    "America/Denver",
                    "America/Los_Angeles",
                }
                if v not in common_timezones:
                    raise ValueError(f"Invalid timezone: {v}")
        except ImportError:
            # zoneinfo not available (Python < 3.9), use pytz or skip validation
            pass
        return v

    def is_active(self, now: datetime | None = None) -> bool:
        """Check if this maintenance window is currently active.

        Args:
            now: Optional datetime to check against (defaults to UTC now)

        Returns:
            True if currently in maintenance window
        """
        if not self.enabled:
            return False

        if now is None:
            now = datetime.utcnow()

        # Convert to UTC for comparison
        start = self.start_time
        end = self.end_time

        if start.tzinfo is not None:
            start = start.replace(tzinfo=None)
        if end.tzinfo is not None:
            end = end.replace(tzinfo=None)
        if now.tzinfo is not None:
            now = now.replace(tzinfo=None)

        return start <= now < end

    def get_next_occurrence(self, after: datetime | None = None) -> datetime | None:
        """Get the next occurrence of this recurring maintenance window.

        Args:
            after: datetime to start from (defaults to UTC now)

        Returns:
            Next start time or None if not recurring or past recurrence_end
        """
        if self.recurring == RecurrenceType.NONE:
            return None

        if after is None:
            after = datetime.utcnow()

        if after.tzinfo is not None:
            after = after.replace(tzinfo=None)

        # Get the base start time (naive)
        base_start = (
            self.start_time.replace(tzinfo=None) if self.start_time.tzinfo else self.start_time
        )

        # Check if we've exceeded recurrence_end
        if self.recurrence_end:
            recurrence_end_naive = (
                self.recurrence_end.replace(tzinfo=None)
                if self.recurrence_end.tzinfo
                else self.recurrence_end
            )
            if after >= recurrence_end_naive:
                return None

        # Calculate next occurrence based on the period since base_start
        if self.recurring == RecurrenceType.DAILY:
            # Calculate days since base start
            days_since_start = (after - base_start).days
            if days_since_start < 0:
                # The base start is in the future
                next_start = base_start
            else:
                # Add 1 day to get the next occurrence
                next_start = base_start + timedelta(days=days_since_start + 1)
                # Normalize to the same time of day as base_start
                next_start = next_start.replace(
                    hour=base_start.hour,
                    minute=base_start.minute,
                    second=base_start.second,
                    microsecond=base_start.microsecond,
                )
        elif self.recurring == RecurrenceType.WEEKLY:
            # Calculate weeks since base start
            days_since_start = (after - base_start).days
            if days_since_start < 0:
                next_start = base_start
            else:
                weeks_since = days_since_start // 7
                next_start = base_start + timedelta(weeks=weeks_since + 1)
        elif self.recurring == RecurrenceType.MONTHLY:
            if after < base_start:
                next_start = base_start
            else:
                # Calculate months since base start
                months_since = (after.year - base_start.year) * 12 + (
                    after.month - base_start.month
                )
                if after.day > base_start.day or (
                    after.day == base_start.day and after.time() >= base_start.time()
                ):
                    months_since += 1
                # Add months to base_start
                total_months = base_start.month + months_since
                year = base_start.year + (total_months - 1) // 12
                month = ((total_months - 1) % 12) + 1
                # Handle day overflow (e.g., Jan 31 -> Feb 28)
                import calendar

                last_day = calendar.monthrange(year, month)[1]
                day = min(base_start.day, last_day)
                next_start = base_start.replace(year=year, month=month, day=day)
        else:
            return None

        # Check recurrence_end again
        if self.recurrence_end:
            recurrence_end_naive = (
                self.recurrence_end.replace(tzinfo=None)
                if self.recurrence_end.tzinfo
                else self.recurrence_end
            )
            if next_start >= recurrence_end_naive:
                return None

        return next_start

    def duration_seconds(self) -> float:
        """Calculate duration in seconds."""
        return (self.end_time - self.start_time).total_seconds()

    def to_db_dict(self) -> dict[str, Any]:
        """Convert to dictionary for database storage."""
        return {
            "id": self.id,
            "name": self.name,
            "description": self.description,
            "start_time": self.start_time.isoformat(),
            "end_time": self.end_time.isoformat(),
            "timezone": self.timezone,
            "recurring": self.recurring.value,
            "recurrence_end": self.recurrence_end.isoformat() if self.recurrence_end else None,
            "created_at": self.created_at.isoformat(),
            "updated_at": self.updated_at.isoformat(),
            "status": self.status.value,
            "enabled": int(self.enabled),
            "metadata": json.dumps(self.metadata),
        }

    @classmethod
    def from_db_row(cls, row: sqlite3.Row) -> MaintenanceWindow:
        """Create from database row."""
        return cls(
            id=row["id"],
            name=row["name"],
            description=row["description"],
            start_time=datetime.fromisoformat(row["start_time"]),
            end_time=datetime.fromisoformat(row["end_time"]),
            timezone=row["timezone"],
            recurring=RecurrenceType(row["recurring"]),
            recurrence_end=(
                datetime.fromisoformat(row["recurrence_end"]) if row["recurrence_end"] else None
            ),
            created_at=datetime.fromisoformat(row["created_at"]),
            updated_at=datetime.fromisoformat(row["updated_at"]),
            status=MaintenanceStatus(row["status"]),
            enabled=bool(row["enabled"]),
            metadata=json.loads(row["metadata"]) if row["metadata"] else {},
        )


@dataclass
class MaintenanceState:
    """Current maintenance state."""

    in_maintenance: bool = False
    active_window: MaintenanceWindow | None = None
    next_window: MaintenanceWindow | None = None
    message: str | None = None
    maintenance_start: datetime | None = None
    maintenance_end: datetime | None = None


class MaintenanceError(Exception):
    """Raised when there's an error with maintenance operations."""

    pass


class MaintenanceNotFoundError(MaintenanceError):
    """Raised when a maintenance window is not found."""

    pass


class MaintenanceManager:
    """Manages scheduled maintenance windows.

    Provides functionality to:
    - Schedule maintenance windows (one-time or recurring)
    - Check if currently in maintenance
    - Pause job processing during maintenance
    - Auto-resume when maintenance ends
    - Web UI indicators

    Example:
        >>> manager = MaintenanceManager("/path/to/maintenance.db")
        >>> # Schedule weekly deploy window
        >>> window = manager.schedule_window(
        ...     name="Weekly Deploy",
        ...     start=datetime.utcnow() + timedelta(days=1),
        ...     duration_minutes=30,
        ...     recurring=RecurrenceType.WEEKLY
        ... )
        >>> if manager.is_in_maintenance():
        ...     print("Processing paused for maintenance")
    """

    SCHEMA = """
    CREATE TABLE IF NOT EXISTS maintenance_windows (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        description TEXT,
        start_time TEXT NOT NULL,
        end_time TEXT NOT NULL,
        timezone TEXT DEFAULT 'UTC',
        recurring TEXT DEFAULT 'none',
        recurrence_end TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
        status TEXT DEFAULT 'scheduled',
        enabled INTEGER DEFAULT 1,
        metadata TEXT DEFAULT '{}'
    );

    CREATE INDEX IF NOT EXISTS idx_maintenance_start ON maintenance_windows(start_time);
    CREATE INDEX IF NOT EXISTS idx_maintenance_enabled ON maintenance_windows(enabled);
    CREATE INDEX IF NOT EXISTS idx_maintenance_status ON maintenance_windows(status);
    """

    def __init__(
        self,
        db_path: str | Path = ":memory:",
        check_interval_seconds: float = 60.0,
        on_maintenance_start: Callable[[MaintenanceWindow], None] | None = None,
        on_maintenance_end: Callable[[MaintenanceWindow], None] | None = None,
    ) -> None:
        """Initialize the maintenance manager.

        Args:
            db_path: Path to SQLite database for persistence
            check_interval_seconds: How often to check for maintenance state changes
            on_maintenance_start: Optional callback when maintenance starts
            on_maintenance_end: Optional callback when maintenance ends
        """
        self.db_path = str(db_path)
        self.check_interval_seconds = check_interval_seconds
        self.on_maintenance_start = on_maintenance_start
        self.on_maintenance_end = on_maintenance_end

        self._lock = threading.RLock()
        self._current_state = MaintenanceState()
        self._stop_event = threading.Event()
        self._monitor_thread: threading.Thread | None = None
        self._last_check_time: datetime | None = None

        # Initialize database
        self._init_db()

    def _init_db(self) -> None:
        """Initialize the database schema."""
        with sqlite3.connect(self.db_path) as conn:
            conn.executescript(self.SCHEMA)
            conn.commit()

    def _get_connection(self) -> sqlite3.Connection:
        """Get a database connection with row factory.

        For in-memory databases, we need to ensure the schema is initialized
        on each new connection since each connection gets a fresh database.
        """
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row

        # For in-memory databases, initialize schema on every connection
        if self.db_path == ":memory:":
            conn.executescript(self.SCHEMA)
            conn.commit()

        return conn

    def schedule_window(
        self,
        name: str,
        start: datetime,
        duration_minutes: int = 60,
        description: str | None = None,
        timezone: str = "UTC",
        recurring: RecurrenceType = RecurrenceType.NONE,
        recurrence_end: datetime | None = None,
        enabled: bool = True,
        metadata: dict[str, Any] | None = None,
    ) -> MaintenanceWindow:
        """Schedule a new maintenance window.

        Args:
            name: Human-readable name for this window
            start: When the maintenance window starts
            duration_minutes: Duration of the maintenance window
            description: Optional description
            timezone: IANA timezone name
            recurring: Type of recurrence
            recurrence_end: When recurring windows should stop
            enabled: Whether this window is enabled
            metadata: Additional custom metadata

        Returns:
            The created MaintenanceWindow

        Raises:
            MaintenanceError: If there's an error creating the window
        """
        end = start + timedelta(minutes=duration_minutes)

        window = MaintenanceWindow(
            name=name,
            description=description,
            start_time=start,
            end_time=end,
            timezone=timezone,
            recurring=recurring,
            recurrence_end=recurrence_end,
            enabled=enabled,
            metadata=metadata or {},
        )

        with self._lock, self._get_connection() as conn:
            cursor = conn.execute(
                """
                    INSERT INTO maintenance_windows
                    (name, description, start_time, end_time, timezone, recurring,
                     recurrence_end, status, enabled, metadata)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    """,
                (
                    window.name,
                    window.description,
                    window.start_time.isoformat(),
                    window.end_time.isoformat(),
                    window.timezone,
                    window.recurring.value,
                    window.recurrence_end.isoformat() if window.recurrence_end else None,
                    window.status.value,
                    int(window.enabled),
                    json.dumps(window.metadata),
                ),
            )
            window.id = cursor.lastrowid
            conn.commit()

        logger.info(f"Scheduled maintenance window: {window.name} (ID: {window.id})")
        return window

    def get_window(self, window_id: int) -> MaintenanceWindow:
        """Get a maintenance window by ID.

        Args:
            window_id: The window ID

        Returns:
            The MaintenanceWindow

        Raises:
            MaintenanceNotFoundError: If window not found
        """
        with self._get_connection() as conn:
            row = conn.execute(
                "SELECT * FROM maintenance_windows WHERE id = ?",
                (window_id,),
            ).fetchone()

        if row is None:
            raise MaintenanceNotFoundError(f"Maintenance window {window_id} not found")

        return MaintenanceWindow.from_db_row(row)

    def list_windows(
        self,
        active_only: bool = False,
        enabled_only: bool = False,
        limit: int = 100,
        offset: int = 0,
    ) -> list[MaintenanceWindow]:
        """List maintenance windows.

        Args:
            active_only: Only return currently active windows
            enabled_only: Only return enabled windows
            limit: Maximum number of results
            offset: Offset for pagination

        Returns:
            List of MaintenanceWindow objects
        """
        now = datetime.utcnow().isoformat()

        query = "SELECT * FROM maintenance_windows WHERE 1=1"
        params: list[Any] = []

        if active_only:
            query += " AND start_time <= ? AND end_time > ? AND enabled = 1"
            params.extend([now, now])

        if enabled_only:
            query += " AND enabled = 1"

        query += " ORDER BY start_time DESC LIMIT ? OFFSET ?"
        params.extend([limit, offset])

        with self._get_connection() as conn:
            rows = conn.execute(query, params).fetchall()

        return [MaintenanceWindow.from_db_row(row) for row in rows]

    def update_window(
        self,
        window_id: int,
        name: str | None = None,
        start_time: datetime | None = None,
        end_time: datetime | None = None,
        description: str | None = None,
        enabled: bool | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> MaintenanceWindow:
        """Update a maintenance window.

        Args:
            window_id: The window ID
            name: New name (optional)
            start_time: New start time (optional)
            end_time: New end time (optional)
            description: New description (optional)
            enabled: New enabled state (optional)
            metadata: New metadata (merged with existing if provided)

        Returns:
            Updated MaintenanceWindow

        Raises:
            MaintenanceNotFoundError: If window not found
        """
        window = self.get_window(window_id)

        if name is not None:
            window.name = name
        if start_time is not None:
            window.start_time = start_time
        if end_time is not None:
            window.end_time = end_time
        if description is not None:
            window.description = description
        if enabled is not None:
            window.enabled = enabled
        if metadata is not None:
            window.metadata = {**window.metadata, **metadata}

        window.updated_at = datetime.utcnow()

        with self._lock, self._get_connection() as conn:
            conn.execute(
                """
                    UPDATE maintenance_windows SET
                        name = ?,
                        description = ?,
                        start_time = ?,
                        end_time = ?,
                        enabled = ?,
                        metadata = ?,
                        updated_at = ?
                    WHERE id = ?
                    """,
                (
                    window.name,
                    window.description,
                    window.start_time.isoformat(),
                    window.end_time.isoformat(),
                    int(window.enabled),
                    json.dumps(window.metadata),
                    window.updated_at.isoformat(),
                    window_id,
                ),
            )
            conn.commit()

        logger.info(f"Updated maintenance window: {window.name} (ID: {window_id})")
        return window

    def delete_window(self, window_id: int) -> None:
        """Delete a maintenance window.

        Args:
            window_id: The window ID

        Raises:
            MaintenanceNotFoundError: If window not found
        """
        with self._lock, self._get_connection() as conn:
            cursor = conn.execute(
                "DELETE FROM maintenance_windows WHERE id = ?",
                (window_id,),
            )
            if cursor.rowcount == 0:
                raise MaintenanceNotFoundError(f"Maintenance window {window_id} not found")
            conn.commit()

        logger.info(f"Deleted maintenance window: {window_id}")

    def is_in_maintenance(self, now: datetime | None = None) -> bool:
        """Check if currently in a maintenance window.

        This method checks all enabled maintenance windows and returns
        True if any window is currently active.

        Args:
            now: Optional datetime to check against (defaults to UTC now)

        Returns:
            True if currently in maintenance
        """
        if now is None:
            now = datetime.utcnow()

        # Use cached state if recent check
        with self._lock:
            if (
                self._last_check_time
                and (now - self._last_check_time).total_seconds() < self.check_interval_seconds
            ):
                return self._current_state.in_maintenance

        now_str = now.isoformat()

        with self._get_connection() as conn:
            # Check if any enabled window is currently active
            row = conn.execute(
                """
                SELECT * FROM maintenance_windows
                WHERE enabled = 1
                AND start_time <= ?
                AND end_time > ?
                ORDER BY start_time
                LIMIT 1
                """,
                (now_str, now_str),
            ).fetchone()

        in_maintenance = row is not None
        active_window = MaintenanceWindow.from_db_row(row) if row else None

        # Get next upcoming window
        next_row = (
            conn.execute(
                """
            SELECT * FROM maintenance_windows
            WHERE enabled = 1
            AND start_time > ?
            ORDER BY start_time
            LIMIT 1
            """,
                (now_str,),
            ).fetchone()
            if not in_maintenance
            else None
        )
        next_window = MaintenanceWindow.from_db_row(next_row) if next_row else None

        # Check for state change
        with self._lock:
            was_in_maintenance = self._current_state.in_maintenance

            self._current_state = MaintenanceState(
                in_maintenance=in_maintenance,
                active_window=active_window,
                next_window=next_window,
                message=active_window.description if active_window else None,
                maintenance_start=active_window.start_time if active_window else None,
                maintenance_end=active_window.end_time if active_window else None,
            )
            self._last_check_time = now

            # Trigger callbacks on state change
            if (
                in_maintenance
                and not was_in_maintenance
                and active_window
                and self.on_maintenance_start
            ):
                try:
                    self.on_maintenance_start(active_window)
                except Exception as e:
                    logger.error(f"Error in maintenance start callback: {e}")

            if (
                not in_maintenance
                and was_in_maintenance
                and self.on_maintenance_end
                and active_window
            ):
                # Maintenance just ended
                try:
                    self.on_maintenance_end(active_window)
                except Exception as e:
                    logger.error(f"Error in maintenance end callback: {e}")

        return in_maintenance

    def get_maintenance_state(self) -> MaintenanceState:
        """Get the current maintenance state.

        Returns:
            MaintenanceState with current status and active/next windows
        """
        # Refresh state
        self.is_in_maintenance()
        with self._lock:
            return self._current_state

    def start_monitoring(self) -> None:
        """Start background monitoring thread.

        This will periodically check for maintenance state changes
        and trigger callbacks when entering/exiting maintenance.
        """
        if self._monitor_thread and self._monitor_thread.is_alive():
            return

        self._stop_event.clear()
        self._monitor_thread = threading.Thread(
            target=self._monitor_loop,
            daemon=True,
            name="maintenance-monitor",
        )
        self._monitor_thread.start()
        logger.info("Started maintenance monitoring")

    def stop_monitoring(self) -> None:
        """Stop the background monitoring thread."""
        if self._monitor_thread:
            self._stop_event.set()
            self._monitor_thread.join(timeout=5.0)
            logger.info("Stopped maintenance monitoring")

    def _monitor_loop(self) -> None:
        """Background monitoring loop."""
        while not self._stop_event.is_set():
            try:
                self.is_in_maintenance()
            except Exception as e:
                logger.error(f"Error in maintenance monitoring: {e}")

            # Wait for next check interval
            self._stop_event.wait(self.check_interval_seconds)

    def cleanup_expired(self, before: datetime | None = None) -> int:
        """Clean up expired maintenance windows.

        Args:
            before: Delete windows ending before this time (defaults to now)

        Returns:
            Number of windows deleted
        """
        if before is None:
            before = datetime.utcnow()

        with self._lock, self._get_connection() as conn:
            cursor = conn.execute(
                "DELETE FROM maintenance_windows WHERE end_time < ? AND enabled = 0",
                (before.isoformat(),),
            )
            deleted = cursor.rowcount
            conn.commit()

        if deleted > 0:
            logger.info(f"Cleaned up {deleted} expired maintenance windows")
        return deleted

    def get_stats(self) -> dict[str, Any]:
        """Get maintenance statistics.

        Returns:
            Dictionary with statistics
        """
        with self._get_connection() as conn:
            total = conn.execute("SELECT COUNT(*) FROM maintenance_windows").fetchone()[0]
            enabled = conn.execute(
                "SELECT COUNT(*) FROM maintenance_windows WHERE enabled = 1"
            ).fetchone()[0]
            active = conn.execute(
                """
                SELECT COUNT(*) FROM maintenance_windows
                WHERE enabled = 1 AND start_time <= ? AND end_time > ?
                """,
                (datetime.utcnow().isoformat(), datetime.utcnow().isoformat()),
            ).fetchone()[0]
            upcoming = conn.execute(
                """
                SELECT COUNT(*) FROM maintenance_windows
                WHERE enabled = 1 AND start_time > ?
                """,
                (datetime.utcnow().isoformat(),),
            ).fetchone()[0]

        return {
            "total_windows": total,
            "enabled_windows": enabled,
            "active_windows": active,
            "upcoming_windows": upcoming,
            "in_maintenance": self.is_in_maintenance(),
        }

    def close(self) -> None:
        """Clean up resources."""
        self.stop_monitoring()


# Global instance for integration
_maintenance_manager: MaintenanceManager | None = None


def get_maintenance_manager(
    db_path: str | Path | None = None,
) -> MaintenanceManager:
    """Get or create the global maintenance manager instance.

    Args:
        db_path: Optional path to database (uses default if not provided)

    Returns:
        MaintenanceManager instance
    """
    global _maintenance_manager

    if _maintenance_manager is None:
        if db_path is None:
            data_dir = Path.home() / ".oclawma"
            data_dir.mkdir(parents=True, exist_ok=True)
            db_path = data_dir / "maintenance.db"

        _maintenance_manager = MaintenanceManager(db_path)

    return _maintenance_manager


def set_maintenance_manager(manager: MaintenanceManager | None) -> None:
    """Set the global maintenance manager instance.

    Args:
        manager: MaintenanceManager instance or None to reset
    """
    global _maintenance_manager
    _maintenance_manager = manager
