"""
VM Repository

Handles CRUD operations for VM instances in the SQLite database.
Extracted from StateManager to follow Single Responsibility Principle.
"""

import json
import re
import sqlite3
import time
import uuid
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple

if TYPE_CHECKING:
    from ..state import VMInstance

from ..constants import Intervals, Retries
from ..exceptions import StateError
from ..logger import LOG

# Optional dependency - check availability without importing
try:
    from importlib.util import find_spec
    PSUTIL_AVAILABLE = find_spec("psutil") is not None
except ImportError:
    PSUTIL_AVAILABLE = False


def _calculate_retry_delay(attempt: int, base_delay: float = 0.1, max_delay: float = 2.0) -> float:
    """Calculate retry delay with exponential backoff and decorrelated jitter.

    Uses decorrelated jitter (AWS recommended) to prevent thundering herd
    while strictly respecting max_delay. The delay is randomized between
    base_delay and the exponential backoff value, ensuring it never exceeds
    max_delay.

    Args:
        attempt: Current attempt number (0-indexed)
        base_delay: Base delay in seconds
        max_delay: Maximum delay cap in seconds

    Returns:
        Delay in seconds with jitter applied (always <= max_delay)
    """
    import random
    # Exponential backoff: base * 2^attempt, capped at max_delay
    exponential = min(base_delay * (2 ** attempt), max_delay)
    # Decorrelated jitter: random between base_delay and exponential
    # This prevents thundering herd while respecting max_delay
    return random.uniform(base_delay, exponential)


class VMRepository:
    """Repository for VM instance CRUD operations."""

    def __init__(self, get_connection: callable, get_pooled_connection: callable) -> None:
        """
        Initialize VM Repository.

        Args:
            get_connection: Callable that returns a context manager yielding sqlite3.Connection
            get_pooled_connection: Callable that returns a context manager yielding pooled connection
        """
        self._get_connection = get_connection
        self._get_pooled_connection = get_pooled_connection

    def create_vm(
        self,
        name: str,
        config_data: Dict[str, Any],
        config_path: Optional[str] = None,
        get_vm_callback: callable = None,
    ) -> str:
        """
        Create a new VM instance.

        Args:
            name: VM name (must be unique)
            config_data: VM configuration dictionary
            config_path: Optional path to config file
            get_vm_callback: Callback to check if VM exists (to avoid circular dependency)

        Returns:
            VM instance ID

        Raises:
            StateError: If validation fails or DB operation fails
        """
        # Validation: Check VM name
        if not name or not name.strip():
            raise StateError("VM name cannot be empty")

        # Check name length (prevent filesystem issues with very long names)
        if len(name) > 255:
            raise StateError(
                f"VM name too long ({len(name)} chars). Maximum is 255 characters."
            )

        # Validate name contains only safe characters (alphanumeric, dash,
        # underscore, dot)
        if not re.match(r"^[a-zA-Z0-9._-]+$", name):
            raise StateError(
                f"VM name '{name}' contains invalid characters. "
                f"Only alphanumeric, dash (-), underscore (_), and dot (.) are allowed."
            )

        # Check for name conflicts BEFORE attempting insert
        # Provides clearer error message than SQLite IntegrityError
        if get_vm_callback:
            existing_vm = get_vm_callback(name)
            if existing_vm:
                raise StateError(
                    f"VM with name '{name}' already exists (ID: {existing_vm.id}). "
                    f"Use 'maqet rm {name}' to remove it first, "
                    f"or choose a different name."
                )

        # Validation: Check config_data size to prevent DB bloat
        config_json = json.dumps(config_data)
        config_size = len(config_json.encode("utf-8"))
        max_config_size = 10 * 1024 * 1024  # 10MB limit

        if config_size > max_config_size:
            raise StateError(
                f"Configuration data too large ({config_size} bytes, max {max_config_size}). "
                f"Consider reducing storage device configurations or using external config files."
            )

        vm_id = str(uuid.uuid4())

        # NOTE: auth_secret no longer stored in database (ephemeral file-based secrets)
        # Secret is generated by VMRunner at runtime and stored in {socket_path}.auth

        # Retry logic for transient lock errors
        max_retries = Retries.DB_OPERATION
        for attempt in range(max_retries):
            try:
                with self._get_pooled_connection(readonly=False) as conn:
                    conn.execute(
                        """
                        INSERT INTO vm_instances (id, name, config_path, config_data, status)
                        VALUES (?, ?, ?, ?, 'created')
                    """,
                        (vm_id, name, config_path, config_json),
                    )
                return vm_id  # Success
            except sqlite3.IntegrityError:
                raise StateError(f"VM with name '{name}' already exists")
            except sqlite3.OperationalError as e:
                error_msg = str(e).lower()

                # Handle transient lock errors with retry
                if "locked" in error_msg and attempt < max_retries - 1:
                    # Exponential backoff with jitter
                    wait_time = _calculate_retry_delay(attempt, Intervals.DB_RETRY_BASE)
                    LOG.debug(
                        f"Database locked while creating VM (attempt {attempt + 1}/{max_retries}), "
                        f"retrying in {wait_time:.2f}s"
                    )
                    time.sleep(wait_time)
                    continue

                # Non-retryable or final attempt - raise appropriate error
                if "disk" in error_msg or "space" in error_msg:
                    raise StateError(
                        f"Cannot create VM: Disk full or insufficient space. "
                        f"Free up disk space and try again. Error: {e}"
                    )
                elif "locked" in error_msg:
                    raise StateError(
                        f"Failed to create VM after {max_retries} retries: Database is locked. "
                        f"Another process may be accessing it. Error: {e}"
                    )
                else:
                    raise StateError(
                        f"Database error while creating VM: {e}"
                    )
            except Exception as e:
                # Log unexpected errors with context
                LOG.error(
                    f"Unexpected error creating VM '{name}' (ID: {vm_id}): {type(e).__name__}: {e}"
                )
                raise StateError(f"Failed to create VM: {e}")

        # Should not reach here due to return in success case or exception
        raise StateError(f"Failed to create VM after {max_retries} retries")

    def get_vm(self, identifier: str) -> Optional["VMInstance"]:
        """
        Get VM instance by ID or name.

        Optimized to use indexes by trying ID lookup first (PRIMARY KEY),
        then name lookup (idx_vm_name index) if not found.

        Args:
            identifier: VM ID or name

        Returns:
            VM instance or None if not found
        """
        # NOTE: SECURITY - Uses parameterized queries, safe from SQL injection
        with self._get_pooled_connection(readonly=True) as conn:
            # Try ID first (PRIMARY KEY index - O(log n))
            row = conn.execute(
                "SELECT * FROM vm_instances WHERE id = ?",
                (identifier,),
            ).fetchone()

            if row:
                return self._row_to_vm_instance(row)

            # Try name (idx_vm_name index - O(log n))
            row = conn.execute(
                "SELECT * FROM vm_instances WHERE name = ?",
                (identifier,),
            ).fetchone()

            if row:
                return self._row_to_vm_instance(row)

        return None

    def list_vms(
        self, status_filter: Optional[str] = None
    ) -> List["VMInstance"]:
        """
        List all VM instances.

        Args:
            status_filter: Optional status to filter by

        Returns:
            List of VM instances
        """
        with self._get_pooled_connection(readonly=True) as conn:
            if status_filter:
                rows = conn.execute(
                    "SELECT * FROM vm_instances WHERE status = ? ORDER BY created_at",
                    (status_filter,),
                ).fetchall()
            else:
                rows = conn.execute(
                    "SELECT * FROM vm_instances ORDER BY created_at"
                ).fetchall()

            return [self._row_to_vm_instance(row) for row in rows]

    def update_vm_status(
        self,
        identifier: str,
        status: str,
        pid: Optional[int] = None,
        runner_pid: Optional[int] = None,
        socket_path: Optional[str] = None,
        qmp_socket_path: Optional[str] = None,
        validate_pid_callback: Optional[callable] = None,
    ) -> bool:
        """
        Update VM status and process information.

        Optimized to use indexes by trying ID lookup first (PRIMARY KEY),
        then name lookup (idx_vm_name index) if not found.

        Args:
            identifier: VM ID or name
            status: New status
            pid: QEMU process ID (if running)
            runner_pid: VM runner process ID (if running)
            socket_path: QMP socket path (if running)
            qmp_socket_path: QMP socket path for cross-process communication
            validate_pid_callback: Callback to validate PID ownership (security concern)

        Returns:
            True if updated, False if VM not found

        Raises:
            ValueError: If PID ownership validation fails
        """
        # Security: Validate PID ownership if provided and callback available
        if validate_pid_callback:
            if pid is not None:
                try:
                    validate_pid_callback(pid)
                except ValueError as e:
                    LOG.error(f"PID validation failed: {e}")
                    raise

            if runner_pid is not None:
                try:
                    validate_pid_callback(runner_pid)
                except ValueError as e:
                    LOG.error(f"Runner PID validation failed: {e}")
                    raise

        # Retry logic for transient lock errors
        max_retries = Retries.DB_OPERATION
        for attempt in range(max_retries):
            try:
                with self._get_pooled_connection(readonly=False) as conn:
                    # Try ID first (PRIMARY KEY index - O(log n))
                    cursor = conn.execute(
                        """
                        UPDATE vm_instances
                        SET status = ?, pid = ?, runner_pid = ?, socket_path = ?, qmp_socket_path = ?
                        WHERE id = ?
                    """,
                        (status, pid, runner_pid, socket_path, qmp_socket_path, identifier),
                    )

                    if cursor.rowcount > 0:
                        return True

                    # Try name (idx_vm_name index - O(log n))
                    cursor = conn.execute(
                        """
                        UPDATE vm_instances
                        SET status = ?, pid = ?, runner_pid = ?, socket_path = ?, qmp_socket_path = ?
                        WHERE name = ?
                    """,
                        (status, pid, runner_pid, socket_path, qmp_socket_path, identifier),
                    )

                    return cursor.rowcount > 0

            except sqlite3.OperationalError as e:
                if "locked" in str(e).lower() and attempt < max_retries - 1:
                    # Exponential backoff with jitter
                    wait_time = _calculate_retry_delay(attempt, Intervals.DB_RETRY_BASE)
                    LOG.debug(
                        f"Database locked while updating VM status (attempt {attempt + 1}/{max_retries}), "
                        f"retrying in {wait_time:.2f}s"
                    )
                    time.sleep(wait_time)
                    continue
                raise StateError(f"Failed to update VM status after {max_retries} retries: {e}")

        # Should not reach here
        return False

    def batch_update_vm_statuses(
        self,
        updates: Dict[str, Tuple[str, Optional[int], Optional[int], Optional[str]]]
    ) -> int:
        """Update multiple VM statuses in a single transaction.

        Efficiently updates status, pid, runner_pid, and socket_path for multiple VMs
        in one database transaction, avoiding N+1 query pattern.

        Args:
            updates: Dict mapping VM name to tuple of (status, pid, runner_pid, socket_path)

        Returns:
            Number of rows updated

        Raises:
            StateError: If database operation fails
        """
        if not updates:
            return 0

        max_retries = Retries.DB_OPERATION
        for attempt in range(max_retries):
            try:
                with self._get_pooled_connection(readonly=False) as conn:
                    # Use executemany for batch update (single transaction)
                    conn.executemany(
                        """
                        UPDATE vm_instances
                        SET status = ?, pid = ?, runner_pid = ?, socket_path = ?
                        WHERE name = ?
                        """,
                        [
                            (status, pid, runner_pid, socket_path, name)
                            for name, (status, pid, runner_pid, socket_path) in updates.items()
                        ]
                    )
                    LOG.debug(f"Batch updated {len(updates)} VM status(es)")
                    return len(updates)

            except sqlite3.OperationalError as e:
                if "locked" in str(e).lower() and attempt < max_retries - 1:
                    # Exponential backoff with jitter
                    wait_time = _calculate_retry_delay(attempt, Intervals.DB_RETRY_BASE)
                    LOG.debug(
                        f"Database locked during batch update (attempt {attempt + 1}/{max_retries}), "
                        f"retrying in {wait_time:.2f}s"
                    )
                    time.sleep(wait_time)
                    continue
                raise StateError(
                    f"Failed to batch update VM statuses after {max_retries} retries: {e}"
                )

        return 0

    def remove_vm(self, identifier: str) -> bool:
        """
        Remove VM instance from database.

        Optimized to use indexes by trying ID lookup first (PRIMARY KEY),
        then name lookup (idx_vm_name index) if not found.

        Args:
            identifier: VM ID or name

        Returns:
            True if removed, False if VM not found
        """
        with self._get_pooled_connection(readonly=False) as conn:
            # Try ID first (PRIMARY KEY index - O(log n))
            cursor = conn.execute(
                "DELETE FROM vm_instances WHERE id = ?",
                (identifier,),
            )

            if cursor.rowcount > 0:
                return True

            # Try name (idx_vm_name index - O(log n))
            cursor = conn.execute(
                "DELETE FROM vm_instances WHERE name = ?",
                (identifier,),
            )

            return cursor.rowcount > 0

    def update_vm_config(
        self, identifier: str, new_config: Dict[str, Any]
    ) -> bool:
        """
        Update VM configuration in database.

        Optimized to use indexes by trying ID lookup first (PRIMARY KEY),
        then name lookup (idx_vm_name index) if not found.

        Args:
            identifier: VM name or ID
            new_config: New configuration data

        Returns:
            True if update successful, False if VM not found

        Raises:
            StateError: If database operation fails
        """
        try:
            with self._get_pooled_connection(readonly=False) as conn:
                # Try ID first (PRIMARY KEY index - O(log n))
                cursor = conn.execute(
                    """
                    UPDATE vm_instances
                    SET config_data = ?
                    WHERE id = ?
                """,
                    (json.dumps(new_config), identifier),
                )

                if cursor.rowcount > 0:
                    return True

                # Try name (idx_vm_name index - O(log n))
                cursor = conn.execute(
                    """
                    UPDATE vm_instances
                    SET config_data = ?
                    WHERE name = ?
                """,
                    (json.dumps(new_config), identifier),
                )

                return cursor.rowcount > 0

        except sqlite3.Error as e:
            raise StateError(f"Database error updating VM config: {e}")
        except Exception as e:
            raise StateError(f"Error updating VM config: {e}")

    def _row_to_vm_instance(self, row: sqlite3.Row) -> "VMInstance":
        """Convert database row to VMInstance object."""
        # Import at runtime to avoid circular dependency
        from ..state import VMInstance

        config_data = json.loads(row["config_data"])

        # Handle runner_pid which may not exist in older schemas
        runner_pid = row["runner_pid"] if "runner_pid" in row.keys() else None

        # Handle auth_secret which may not exist in older schemas
        auth_secret = row["auth_secret"] if "auth_secret" in row.keys() else None

        # Handle qmp_socket_path which may not exist in older schemas
        qmp_socket_path = row["qmp_socket_path"] if "qmp_socket_path" in row.keys() else None

        return VMInstance(
            id=row["id"],
            name=row["name"],
            config_path=row["config_path"],
            config_data=config_data,
            status=row["status"],
            pid=row["pid"],
            runner_pid=runner_pid,
            socket_path=row["socket_path"],
            qmp_socket_path=qmp_socket_path,
            created_at=datetime.fromisoformat(row["created_at"]),
            updated_at=datetime.fromisoformat(row["updated_at"]),
            auth_secret=auth_secret,
        )
