"""Group manager for orchestrating container groups."""

from __future__ import annotations

import shlex
import subprocess
import time
from collections.abc import Iterator
from typing import TYPE_CHECKING

from gaol.groups.models import (
    ContainerRef,
    GroupSpec,
    GroupState,
    GroupStatus,
    HealthCheckConfig,
    HealthCheckType,
    MemberStatus,
)
from gaol.groups.networking import GroupNetworkManager
from gaol.groups.resolver import CircularDependencyError, DependencyResolver
from gaol.groups.store import GroupStore, StoredGroup, get_group_store
from gaol.models.container import ContainerConfig, ContainerStatus, NetworkConfig
from gaol.runtimes import get_runtime

if TYPE_CHECKING:
    from gaol.runtimes.base import ContainerRuntime


class GroupError(Exception):
    """Error during group operations."""

    pass


class GroupManager:
    """Orchestrate container group lifecycle.

    This manager handles creating, starting, stopping, and destroying
    container groups with proper dependency ordering and shared networking.
    """

    def __init__(
        self,
        store: GroupStore | None = None,
        network_manager: GroupNetworkManager | None = None,
    ) -> None:
        """Initialize the group manager.

        Args:
            store: GroupStore for persistence (default: global store)
            network_manager: Network manager (default: new instance)
        """
        self._store = store or get_group_store()
        self._network_manager = network_manager or GroupNetworkManager()
        self._runtimes: dict[str, ContainerRuntime] = {}

    def _get_runtime(self, name: str) -> ContainerRuntime:
        """Get a cached runtime instance.

        Args:
            name: Runtime name

        Returns:
            ContainerRuntime instance
        """
        if name not in self._runtimes:
            self._runtimes[name] = get_runtime(name)
        return self._runtimes[name]

    def up(
        self,
        spec: GroupSpec,
        detach: bool = True,
        wait_for_healthy: bool = True,
        timeout: int = 60,
    ) -> GroupStatus:
        """Bring up a container group.

        Creates the shared network, creates all containers in dependency order,
        and starts them.

        Args:
            spec: Group specification
            detach: Run containers in background
            wait_for_healthy: Wait for health checks to pass
            timeout: Timeout for health checks in seconds

        Returns:
            GroupStatus with current state

        Raises:
            GroupError: If group creation fails
            CircularDependencyError: If dependencies have a cycle
        """
        # Check if group already exists and is running
        existing = self._store.get(spec.name)
        if existing and existing.state == GroupState.RUNNING:
            raise GroupError(f"Group '{spec.name}' is already running")

        # Resolve dependency order
        resolver = DependencyResolver(spec.containers)
        try:
            start_order = resolver.resolve_start_order()
        except CircularDependencyError:
            raise

        # Create shared network if enabled
        network_name = None
        if spec.shared_network:
            network_name = self._network_manager.create_network(
                spec.name, runtime=spec.default_runtime
            )

        # Update store with starting state
        self._store.save(
            spec,
            state=GroupState.STARTING,
            network_name=network_name,
        )

        container_ids: dict[str, str] = {}
        try:
            # Create and start containers in dependency order
            for container_name in start_order:
                container_ref = spec.containers[container_name]
                full_name = spec.get_container_full_name(container_name)
                runtime_name = container_ref.runtime or spec.default_runtime
                runtime = self._get_runtime(runtime_name)

                # Build container config
                config = self._build_container_config(
                    full_name, container_ref, spec.default_runtime
                )

                # Create container
                container_id = runtime.create(config)
                container_ids[container_name] = container_id

                # Connect to group network if using Docker
                if spec.shared_network and runtime_name == "docker":
                    self._connect_to_network(container_id, network_name, container_name)

                # Start container
                runtime.start(container_id)

                # Wait for health check if configured
                if wait_for_healthy and container_ref.healthcheck:
                    self._wait_for_healthy(
                        runtime,
                        container_id,
                        container_ref.healthcheck,
                        timeout,
                    )

            # Update store with running state
            self._store.save(
                spec,
                state=GroupState.RUNNING,
                network_name=network_name,
                container_ids=container_ids,
            )

            return self.status(spec.name)

        except Exception as e:
            # Cleanup on failure
            self._cleanup_failed_up(spec.name, container_ids, network_name)
            raise GroupError(f"Failed to bring up group: {e}") from e

    def down(self, group_name: str, remove_volumes: bool = False) -> None:
        """Bring down a container group.

        Stops all containers in reverse dependency order, removes them,
        and destroys the shared network.

        Args:
            group_name: Name of the group
            remove_volumes: Also remove volumes

        Raises:
            GroupError: If group doesn't exist or teardown fails
        """
        stored = self._store.get(group_name)
        if not stored:
            raise GroupError(f"Group '{group_name}' not found")

        spec = stored.spec

        # Resolve stop order (reverse of start)
        resolver = DependencyResolver(spec.containers)
        stop_order = resolver.resolve_stop_order()

        # Update state
        self._store.update_state(group_name, GroupState.STOPPING)

        errors: list[str] = []

        # Stop and remove containers in order
        for container_name in stop_order:
            container_ref = spec.containers[container_name]
            runtime_name = container_ref.runtime or spec.default_runtime
            runtime = self._get_runtime(runtime_name)
            container_id = stored.container_ids.get(container_name)

            if container_id:
                try:
                    # Stop container
                    try:
                        runtime.stop(container_id, timeout=10)
                    except Exception:
                        pass  # Container might already be stopped

                    # Remove container
                    runtime.destroy(container_id, force=True)
                except Exception as e:
                    errors.append(f"Failed to remove {container_name}: {e}")

        # Destroy network
        if stored.network_name:
            try:
                self._network_manager.destroy_network(
                    group_name, runtime=spec.default_runtime
                )
            except Exception as e:
                errors.append(f"Failed to destroy network: {e}")

        # Remove from store
        self._store.delete(group_name)

        if errors:
            raise GroupError(f"Errors during teardown: {'; '.join(errors)}")

    def start(self, group_name: str) -> GroupStatus:
        """Start a stopped group.

        Args:
            group_name: Name of the group

        Returns:
            GroupStatus with current state

        Raises:
            GroupError: If group doesn't exist or is already running
        """
        stored = self._store.get(group_name)
        if not stored:
            raise GroupError(f"Group '{group_name}' not found")

        if stored.state == GroupState.RUNNING:
            raise GroupError(f"Group '{group_name}' is already running")

        spec = stored.spec

        # Resolve start order
        resolver = DependencyResolver(spec.containers)
        start_order = resolver.resolve_start_order()

        self._store.update_state(group_name, GroupState.STARTING)

        try:
            for container_name in start_order:
                container_ref = spec.containers[container_name]
                runtime_name = container_ref.runtime or spec.default_runtime
                runtime = self._get_runtime(runtime_name)
                container_id = stored.container_ids.get(container_name)

                if container_id:
                    runtime.start(container_id)

            self._store.update_state(group_name, GroupState.RUNNING)
            return self.status(group_name)

        except Exception as e:
            self._store.update_state(group_name, GroupState.ERROR)
            raise GroupError(f"Failed to start group: {e}") from e

    def stop(self, group_name: str) -> None:
        """Stop a running group without removing containers.

        Args:
            group_name: Name of the group

        Raises:
            GroupError: If group doesn't exist
        """
        stored = self._store.get(group_name)
        if not stored:
            raise GroupError(f"Group '{group_name}' not found")

        spec = stored.spec

        # Resolve stop order
        resolver = DependencyResolver(spec.containers)
        stop_order = resolver.resolve_stop_order()

        self._store.update_state(group_name, GroupState.STOPPING)

        for container_name in stop_order:
            container_ref = spec.containers[container_name]
            runtime_name = container_ref.runtime or spec.default_runtime
            runtime = self._get_runtime(runtime_name)
            container_id = stored.container_ids.get(container_name)

            if container_id:
                try:
                    runtime.stop(container_id, timeout=10)
                except Exception:
                    pass  # Ignore stop errors

        self._store.update_state(group_name, GroupState.STOPPED)

    def status(self, group_name: str) -> GroupStatus:
        """Get the current status of a group.

        Args:
            group_name: Name of the group

        Returns:
            GroupStatus with container statuses

        Raises:
            GroupError: If group doesn't exist
        """
        stored = self._store.get(group_name)
        if not stored:
            raise GroupError(f"Group '{group_name}' not found")

        spec = stored.spec
        members: list[MemberStatus] = []
        running_count = 0

        for container_name, container_ref in spec.containers.items():
            runtime_name = container_ref.runtime or spec.default_runtime
            runtime = self._get_runtime(runtime_name)
            container_id = stored.container_ids.get(container_name)
            full_name = spec.get_container_full_name(container_name)

            if container_id:
                try:
                    state = runtime.status(container_id)
                    if state.status == ContainerStatus.RUNNING:
                        running_count += 1
                    members.append(
                        MemberStatus(
                            name=container_name,
                            full_name=full_name,
                            state=state.status,
                            runtime=runtime_name,
                            container_id=container_id,
                            ip_address=state.ip_address,
                            ports=state.ports,
                        )
                    )
                except Exception:
                    members.append(
                        MemberStatus(
                            name=container_name,
                            full_name=full_name,
                            state=ContainerStatus.UNKNOWN,
                            runtime=runtime_name,
                            container_id=container_id,
                        )
                    )
            else:
                members.append(
                    MemberStatus(
                        name=container_name,
                        full_name=full_name,
                        state=ContainerStatus.UNKNOWN,
                        runtime=runtime_name,
                    )
                )

        # Determine overall state
        total = len(members)
        if running_count == total:
            overall_state = GroupState.RUNNING
        elif running_count == 0:
            overall_state = GroupState.STOPPED
        else:
            overall_state = GroupState.PARTIALLY_RUNNING

        return GroupStatus(
            name=group_name,
            state=overall_state,
            containers=members,
            network_name=stored.network_name,
            created_at=stored.created_at,
            started_at=stored.started_at,
        )

    def logs(
        self,
        group_name: str,
        container_name: str | None = None,
        follow: bool = False,
        tail: int | None = None,
    ) -> str:
        """Get logs from group containers.

        Args:
            group_name: Name of the group
            container_name: Specific container (None for all)
            follow: Stream logs continuously
            tail: Number of lines from end

        Returns:
            Log output

        Raises:
            GroupError: If group or container doesn't exist
        """
        stored = self._store.get(group_name)
        if not stored:
            raise GroupError(f"Group '{group_name}' not found")

        spec = stored.spec

        if container_name:
            if container_name not in spec.containers:
                raise GroupError(f"Container '{container_name}' not in group")
            containers = [container_name]
        else:
            containers = list(spec.containers.keys())

        logs_output: list[str] = []

        for name in containers:
            container_ref = spec.containers[name]
            runtime_name = container_ref.runtime or spec.default_runtime
            runtime = self._get_runtime(runtime_name)
            container_id = stored.container_ids.get(name)

            if container_id:
                try:
                    log = runtime.logs(container_id, follow=follow, tail=tail)
                    if len(containers) > 1:
                        # Prefix each line with container name
                        prefixed = "\n".join(
                            f"[{name}] {line}" for line in log.splitlines()
                        )
                        logs_output.append(prefixed)
                    else:
                        logs_output.append(log)
                except Exception as e:
                    logs_output.append(f"[{name}] Error getting logs: {e}")

        return "\n".join(logs_output)

    def exec(
        self,
        group_name: str,
        container_name: str,
        command: list[str],
        interactive: bool = False,
        tty: bool = False,
    ) -> tuple[int, str, str]:
        """Execute a command in a group container.

        Args:
            group_name: Name of the group
            container_name: Container to exec in
            command: Command to execute
            interactive: Keep STDIN open
            tty: Allocate pseudo-TTY

        Returns:
            Tuple of (exit_code, stdout, stderr)

        Raises:
            GroupError: If group or container doesn't exist
        """
        stored = self._store.get(group_name)
        if not stored:
            raise GroupError(f"Group '{group_name}' not found")

        spec = stored.spec

        if container_name not in spec.containers:
            raise GroupError(f"Container '{container_name}' not in group")

        container_ref = spec.containers[container_name]
        runtime_name = container_ref.runtime or spec.default_runtime
        runtime = self._get_runtime(runtime_name)
        container_id = stored.container_ids.get(container_name)

        if not container_id:
            raise GroupError(f"Container '{container_name}' has no ID")

        return runtime.exec(
            container_id,
            command,
            interactive=interactive,
            tty=tty,
        )

    def list(self, include_stopped: bool = True) -> list[GroupStatus]:
        """List all groups.

        Args:
            include_stopped: Include stopped groups

        Returns:
            List of GroupStatus
        """
        stored_groups = self._store.list(include_stopped=include_stopped)
        return [self.status(g.spec.name) for g in stored_groups]

    def _build_container_config(
        self,
        full_name: str,
        container_ref: ContainerRef,
        default_runtime: str,
    ) -> ContainerConfig:
        """Build a ContainerConfig from a ContainerRef.

        Args:
            full_name: Full container name (group-container)
            container_ref: Container reference from spec
            default_runtime: Default runtime to use

        Returns:
            ContainerConfig
        """
        # Parse port mappings
        ports: dict[int, int] = {}
        for port_str in container_ref.ports:
            parts = port_str.split(":")
            if len(parts) == 2:
                host_port, container_port = parts
                # Remove protocol suffix if present
                container_port = container_port.split("/")[0]
                ports[int(host_port)] = int(container_port)

        # Parse volume mounts
        volumes: dict[str, str] = {}
        for vol_str in container_ref.volumes:
            parts = vol_str.split(":")
            if len(parts) >= 2:
                host_path = parts[0]
                container_path = parts[1]
                volumes[host_path] = container_path

        # Parse command
        command: list[str] | None = None
        if container_ref.command:
            if isinstance(container_ref.command, str):
                command = shlex.split(container_ref.command)
            else:
                command = container_ref.command

        return ContainerConfig(
            name=full_name,
            runtime=container_ref.runtime or default_runtime,
            image=container_ref.image,
            profiles=container_ref.profiles,
            network=NetworkConfig(ports=ports),
            volumes=volumes,
            environment=container_ref.environment,
            command=command,
            working_dir=container_ref.working_dir,
        )

    def _connect_to_network(
        self, container_id: str, network_name: str | None, alias: str
    ) -> None:
        """Connect a container to the group network.

        Args:
            container_id: Container ID
            network_name: Network name
            alias: DNS alias for the container
        """
        if not network_name:
            return

        # Use Docker CLI to connect (works with all Docker versions)
        result = subprocess.run(
            [
                "docker",
                "network",
                "connect",
                "--alias",
                alias,
                network_name,
                container_id,
            ],
            capture_output=True,
            text=True,
        )

        if result.returncode != 0:
            # Ignore "already connected" errors
            if "already exists" not in result.stderr.lower():
                raise GroupError(f"Failed to connect to network: {result.stderr}")

    def _wait_for_healthy(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        healthcheck: HealthCheckConfig,
        timeout: int,
    ) -> None:
        """Wait for a container to become healthy.

        Args:
            runtime: Container runtime
            container_id: Container ID
            healthcheck: Health check configuration
            timeout: Maximum time to wait in seconds

        Raises:
            GroupError: If health check fails after timeout
        """
        start_time = time.time()
        interval = healthcheck.interval

        # Initial delay for start period
        if healthcheck.start_period > 0:
            time.sleep(healthcheck.start_period)

        consecutive_failures = 0

        while time.time() - start_time < timeout:
            try:
                if self._check_health(runtime, container_id, healthcheck):
                    return  # Healthy!
                consecutive_failures = 0  # Reset on successful check
            except Exception:
                consecutive_failures += 1
                if consecutive_failures >= healthcheck.retries:
                    raise GroupError(
                        f"Container health check failed after {healthcheck.retries} retries"
                    )

            time.sleep(interval)

        raise GroupError(f"Container health check timed out after {timeout}s")

    def _check_health(
        self,
        runtime: ContainerRuntime,
        container_id: str,
        healthcheck: HealthCheckConfig,
    ) -> bool:
        """Perform a single health check.

        Args:
            runtime: Container runtime
            container_id: Container ID
            healthcheck: Health check configuration

        Returns:
            True if healthy
        """
        if healthcheck.type == HealthCheckType.COMMAND:
            if healthcheck.command:
                exit_code, _, _ = runtime.exec(container_id, healthcheck.command)
                return exit_code == 0

        elif healthcheck.type == HealthCheckType.TCP:
            if healthcheck.port:
                # Use nc to check TCP port
                exit_code, _, _ = runtime.exec(
                    container_id,
                    ["nc", "-z", "localhost", str(healthcheck.port)],
                )
                return exit_code == 0

        elif healthcheck.type == HealthCheckType.HTTP:
            if healthcheck.port:
                path = healthcheck.path or "/"
                # Use wget or curl to check HTTP
                exit_code, _, _ = runtime.exec(
                    container_id,
                    [
                        "wget",
                        "-q",
                        "-O",
                        "/dev/null",
                        f"http://localhost:{healthcheck.port}{path}",
                    ],
                )
                return exit_code == 0

        return False

    def _cleanup_failed_up(
        self,
        group_name: str,
        container_ids: dict[str, str],
        network_name: str | None,
    ) -> None:
        """Clean up after a failed up operation.

        Args:
            group_name: Group name
            container_ids: Container IDs created so far
            network_name: Network name if created
        """
        # Try to remove containers
        for container_name, container_id in container_ids.items():
            try:
                # Use docker CLI for simplicity
                subprocess.run(
                    ["docker", "rm", "-f", container_id],
                    capture_output=True,
                )
            except Exception:
                pass

        # Try to remove network
        if network_name:
            try:
                self._network_manager.destroy_network(group_name)
            except Exception:
                pass

        # Update store to error state
        try:
            self._store.update_state(group_name, GroupState.ERROR)
        except Exception:
            pass


def get_group_manager() -> GroupManager:
    """Get a GroupManager instance.

    Returns:
        GroupManager
    """
    return GroupManager()
