"""Core update execution logic for containers."""

from __future__ import annotations

import logging
import re
from datetime import datetime
from typing import TYPE_CHECKING, Callable

from gaol.distro import PackageManager, get_distro_info, get_package_manager_commands
from gaol.update.exceptions import (
    HealthCheckFailedError,
    PackageManagerError,
    UpdateExecutionError,
)
from gaol.update.models import (
    PackageUpdate,
    RollbackStrategy,
    UpdateCheck,
    UpdatePolicy,
    UpdateResult,
    UpdateStatus,
)

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime
    from gaol.store import StoredContainer


logger = logging.getLogger(__name__)


class UpdateExecutor:
    """Execute update operations on containers.

    Handles the low-level execution of package updates within containers,
    including checking for updates, applying them, and parsing results.

    Example:
        >>> executor = UpdateExecutor(runtime)
        >>> check = executor.check_updates(container_id, "mycontainer", "trixie")
        >>> if check.updates_available:
        ...     result = executor.apply_updates(container_id, stored, policy)
    """

    def __init__(self, runtime: ContainerRuntime) -> None:
        """Initialize the executor.

        Args:
            runtime: Container runtime to use for executing commands
        """
        self.runtime = runtime

    def check_updates(
        self,
        container_id: str,
        container_name: str,
        distro: str,
    ) -> UpdateCheck:
        """Check for available package updates.

        Args:
            container_id: Container ID
            container_name: Container name
            distro: Distribution name

        Returns:
            UpdateCheck with available updates
        """
        try:
            pm_commands = get_package_manager_commands(distro)
            distro_info = get_distro_info(distro)
            pm = distro_info.package_manager

            # Update package index
            exit_code, stdout, stderr = self.runtime.exec(
                container_id,
                ["sh", "-c", pm_commands["update"]],
            )

            if exit_code != 0 and pm != PackageManager.DNF:
                # DNF returns non-zero when updates are available
                return UpdateCheck(
                    container_name=container_name,
                    error_message=f"Failed to update package index: {stderr}",
                )

            # Get list of upgradable packages
            check_cmd = self._get_check_command(pm)
            exit_code, stdout, stderr = self.runtime.exec(
                container_id,
                ["sh", "-c", check_cmd],
            )

            packages = self._parse_upgradable_packages(stdout, pm)

            return UpdateCheck(
                container_name=container_name,
                updates_available=len(packages) > 0,
                package_count=len(packages),
                packages=packages,
            )

        except Exception as e:
            logger.error(f"Error checking updates for {container_name}: {e}")
            return UpdateCheck(
                container_name=container_name,
                error_message=str(e),
            )

    def apply_updates(
        self,
        container_id: str,
        stored: StoredContainer,
        policy: UpdatePolicy,
        snapshot_callback: Callable[[str], None] | None = None,
        health_check_callback: Callable[[str], bool] | None = None,
        rollback_callback: Callable[[str], None] | None = None,
    ) -> UpdateResult:
        """Apply updates to a container.

        Args:
            container_id: Container ID
            stored: Stored container info
            policy: Update policy
            snapshot_callback: Callback to create snapshot (name) -> None
            health_check_callback: Callback to run health check (container_id) -> bool
            rollback_callback: Callback to rollback (snapshot_name) -> None

        Returns:
            UpdateResult with operation outcome
        """
        result = UpdateResult(
            container_name=stored.config.name,
            status=UpdateStatus.CHECKING,
        )

        distro = stored.config.distro
        pm_commands = get_package_manager_commands(distro)
        distro_info = get_distro_info(distro)
        pm = distro_info.package_manager

        try:
            # Step 1: Create pre-update snapshot if requested
            if policy.pre_update_snapshot and snapshot_callback:
                result.status = UpdateStatus.DOWNLOADING
                snapshot_name = f"pre-update-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
                try:
                    snapshot_callback(snapshot_name)
                    result.snapshot_name = snapshot_name
                    logger.info(f"Created pre-update snapshot: {snapshot_name}")
                except Exception as e:
                    logger.warning(f"Failed to create snapshot: {e}")
                    if policy.rollback_strategy == RollbackStrategy.AUTO:
                        raise UpdateExecutionError(
                            f"Snapshot required but failed: {e}"
                        )

            # Step 2: Update package index
            result.status = UpdateStatus.DOWNLOADING
            exit_code, stdout, stderr = self.runtime.exec(
                container_id,
                ["sh", "-c", pm_commands["update"]],
            )
            if exit_code != 0 and pm != PackageManager.DNF:
                raise PackageManagerError(pm.value, "update", stderr)

            # Step 3: Check what will be upgraded
            check_cmd = self._get_check_command(pm)
            exit_code, stdout, stderr = self.runtime.exec(
                container_id,
                ["sh", "-c", check_cmd],
            )
            packages_before = self._parse_upgradable_packages(stdout, pm)
            result.packages_checked = len(packages_before)

            if not packages_before:
                # No updates available
                result.status = UpdateStatus.COMPLETED
                result.completed_at = datetime.now()
                return result

            # Step 4: Apply upgrades
            result.status = UpdateStatus.APPLYING
            upgrade_cmd = self._get_upgrade_command(pm, policy.packages_to_skip)
            exit_code, stdout, stderr = self.runtime.exec(
                container_id,
                ["sh", "-c", upgrade_cmd],
            )

            if exit_code != 0:
                raise PackageManagerError(pm.value, "upgrade", stderr)

            # Parse upgraded packages from output
            result.packages_updated = packages_before  # Assume all were updated
            result.packages_upgraded = len(packages_before)

            # Step 5: Clean up
            self.runtime.exec(
                container_id,
                ["sh", "-c", pm_commands["clean"]],
            )

            # Step 6: Health check verification
            if policy.health_check_after and health_check_callback:
                result.status = UpdateStatus.VERIFYING
                try:
                    health_ok = health_check_callback(container_id)
                    result.health_check_passed = health_ok

                    if not health_ok:
                        raise HealthCheckFailedError(
                            stored.config.name,
                            "Container health check failed after update",
                        )

                except HealthCheckFailedError:
                    # Attempt rollback
                    if (
                        policy.rollback_strategy == RollbackStrategy.AUTO
                        and result.snapshot_name
                        and rollback_callback
                    ):
                        result.status = UpdateStatus.ROLLING_BACK
                        result.rollback_reason = "Health check failed after update"
                        try:
                            rollback_callback(result.snapshot_name)
                            result.rolled_back = True
                        except Exception as re:
                            logger.error(f"Rollback failed: {re}")
                            result.error_message = (
                                f"Health check failed and rollback failed: {re}"
                            )

                        result.status = UpdateStatus.FAILED
                        result.completed_at = datetime.now()
                        return result
                    else:
                        raise

            # Success
            result.status = UpdateStatus.COMPLETED
            result.completed_at = datetime.now()

        except PackageManagerError as e:
            result.status = UpdateStatus.FAILED
            result.completed_at = datetime.now()
            result.error_message = e.message
            result.error_output = e.stderr

            # Attempt rollback if configured
            if (
                policy.rollback_strategy == RollbackStrategy.AUTO
                and result.snapshot_name
                and rollback_callback
            ):
                result.status = UpdateStatus.ROLLING_BACK
                result.rollback_reason = str(e)
                try:
                    rollback_callback(result.snapshot_name)
                    result.rolled_back = True
                except Exception as re:
                    logger.error(f"Rollback failed: {re}")

                result.status = UpdateStatus.FAILED

        except Exception as e:
            result.status = UpdateStatus.FAILED
            result.completed_at = datetime.now()
            result.error_message = str(e)

        return result

    def _get_check_command(self, pm: PackageManager) -> str:
        """Get command to list upgradable packages."""
        commands = {
            PackageManager.APT: "apt list --upgradable 2>/dev/null | tail -n +2 || true",
            PackageManager.DNF: "dnf check-update 2>/dev/null | grep -v '^$' | grep -v '^Last' || true",
            PackageManager.APK: "apk version -l '<' 2>/dev/null || true",
            PackageManager.PACMAN: "pacman -Qu 2>/dev/null || true",
        }
        return commands.get(pm, "echo ''")

    def _get_upgrade_command(
        self, pm: PackageManager, skip_packages: list[str] | None = None
    ) -> str:
        """Get full upgrade command for a package manager."""
        base_commands = {
            PackageManager.APT: "DEBIAN_FRONTEND=noninteractive apt-get upgrade -y",
            PackageManager.DNF: "dnf upgrade -y",
            PackageManager.APK: "apk upgrade",
            PackageManager.PACMAN: "pacman -Syu --noconfirm",
        }

        cmd = base_commands.get(pm, "echo 'unsupported'")

        # Add package exclusions if specified
        if skip_packages:
            if pm == PackageManager.APT:
                # APT uses apt-mark hold for package exclusion
                hold_cmds = " && ".join(
                    f"apt-mark hold {pkg}" for pkg in skip_packages
                )
                unhold_cmds = " && ".join(
                    f"apt-mark unhold {pkg}" for pkg in skip_packages
                )
                cmd = f"{hold_cmds} && {cmd} && {unhold_cmds}"
            elif pm == PackageManager.DNF:
                excludes = " ".join(f"--exclude={pkg}" for pkg in skip_packages)
                cmd = f"dnf upgrade -y {excludes}"
            elif pm == PackageManager.PACMAN:
                ignores = " ".join(f"--ignore {pkg}" for pkg in skip_packages)
                cmd = f"pacman -Syu --noconfirm {ignores}"
            # APK doesn't have a simple exclude mechanism

        return cmd

    def _parse_upgradable_packages(
        self, output: str, pm: PackageManager
    ) -> list[PackageUpdate]:
        """Parse package manager output to find upgradable packages."""
        packages = []

        if not output.strip():
            return packages

        lines = output.strip().split("\n")

        for line in lines:
            line = line.strip()
            if not line:
                continue

            pkg = self._parse_package_line(line, pm)
            if pkg:
                packages.append(pkg)

        return packages

    def _parse_package_line(
        self, line: str, pm: PackageManager
    ) -> PackageUpdate | None:
        """Parse a single line of package output."""
        try:
            if pm == PackageManager.APT:
                # Format: package/release version [upgradable from: old_version]
                match = re.match(
                    r"^([^/]+)/\S+\s+(\S+)\s+.*\[upgradable from:\s*(\S+)\]",
                    line,
                )
                if match:
                    return PackageUpdate(
                        package_name=match.group(1),
                        new_version=match.group(2),
                        old_version=match.group(3),
                    )

            elif pm == PackageManager.DNF:
                # Format: package.arch version repo
                parts = line.split()
                if len(parts) >= 2:
                    pkg_arch = parts[0]
                    pkg_name = pkg_arch.rsplit(".", 1)[0] if "." in pkg_arch else pkg_arch
                    return PackageUpdate(
                        package_name=pkg_name,
                        new_version=parts[1] if len(parts) > 1 else None,
                    )

            elif pm == PackageManager.APK:
                # Format: package-version < new-version
                # Version starts with digits, e.g., vim-8.2.4000-r0 < 9.0.1000-r0
                match = re.match(r"^(.+)-(\d[\d.\w-]*)\s+<\s+(\S+)", line)
                if match:
                    return PackageUpdate(
                        package_name=match.group(1),
                        old_version=match.group(2),
                        new_version=match.group(3),
                    )

            elif pm == PackageManager.PACMAN:
                # Format: package old_version -> new_version
                match = re.match(r"^(\S+)\s+(\S+)\s+->\s+(\S+)", line)
                if match:
                    return PackageUpdate(
                        package_name=match.group(1),
                        old_version=match.group(2),
                        new_version=match.group(3),
                    )
                # Alternative format: package new_version
                parts = line.split()
                if len(parts) >= 2:
                    return PackageUpdate(
                        package_name=parts[0],
                        new_version=parts[1],
                    )

        except Exception as e:
            logger.debug(f"Failed to parse package line '{line}': {e}")

        return None
