"""Tor proxy and hidden service managers.

This module provides managers for:
- TorProxyManager: Manage Tor proxies for containers
- HiddenServiceManager: Manage Tor hidden services (.onion)
"""

from __future__ import annotations

import os
from datetime import datetime
from pathlib import Path

import yaml

from gaol.privacy.exceptions import (
    HiddenServiceAlreadyExistsError,
    HiddenServiceNotFoundError,
    TorProxyAlreadyExistsError,
    TorProxyNotFoundError,
)
from gaol.privacy.models import (
    HiddenServiceConfig,
    HiddenServiceState,
    PrivacyStatus,
    TorProxyConfig,
    TorProxyState,
)
from gaol.privacy.tor.backend import TorBackend


class TorProxyManager:
    """Manage Tor proxy instances for containers.

    Handles starting, stopping, and tracking Tor proxies
    that route container traffic through the Tor network.

    Example:
        >>> manager = TorProxyManager()
        >>> config = TorProxyConfig(socks_port=9050)
        >>> proxy = manager.create("myproxy", "mycontainer", config)
        >>> print(f"SOCKS port: {proxy.socks_port}")
        >>> manager.delete("myproxy")
    """

    def __init__(self, state_dir: Path | None = None) -> None:
        """Initialize the Tor proxy manager.

        Args:
            state_dir: Directory for state files. Defaults to
                       ~/.local/state/gaol/tor
        """
        if state_dir is None:
            state_dir = Path.home() / ".local" / "state" / "gaol" / "tor"
        self.state_dir = state_dir
        self.state_dir.mkdir(parents=True, exist_ok=True)
        self._store_path = self.state_dir / "proxies.yaml"
        self._backend = TorBackend(state_dir)

    def create(
        self,
        name: str,
        container_name: str,
        config: TorProxyConfig | None = None,
    ) -> TorProxyState:
        """Create and start a Tor proxy for a container.

        Args:
            name: Unique name for this proxy.
            container_name: Associated container name.
            config: Proxy configuration. Uses defaults if not specified.

        Returns:
            TorProxyState with proxy information.

        Raises:
            TorProxyAlreadyExistsError: If proxy with name already exists.
            TorBootstrapError: If Tor fails to bootstrap.
        """
        # Check for existing
        existing = self.get(name)
        if existing and existing.status == PrivacyStatus.RUNNING:
            raise TorProxyAlreadyExistsError(name)

        # Clean up stale state
        if existing:
            self._remove_state(name)

        if config is None:
            config = TorProxyConfig()

        # Start Tor
        state = self._backend.start(name, container_name, config)

        # Save state
        self._save_state(state)
        return state

    def delete(self, name: str) -> None:
        """Stop and remove a Tor proxy.

        Args:
            name: Name of the proxy to delete.

        Raises:
            TorProxyNotFoundError: If proxy is not found.
        """
        state = self.get(name)
        if not state:
            raise TorProxyNotFoundError(name)

        # Stop the daemon
        self._backend.stop(name)

        # Clean up data directory
        self._backend.cleanup(name)

        # Remove state
        self._remove_state(name)

    def get(self, name: str) -> TorProxyState | None:
        """Get a proxy by name.

        Args:
            name: Proxy name.

        Returns:
            TorProxyState if found, None otherwise.
        """
        states = self._load_states()
        for state in states:
            if state.name == name:
                # Update status
                state.status = self._backend.get_status(name)
                return state
        return None

    def list(self, container_name: str | None = None) -> list[TorProxyState]:
        """List all Tor proxies.

        Args:
            container_name: If specified, filter by container.

        Returns:
            List of proxy states.
        """
        states = self._load_states()

        # Update statuses and filter
        active = []
        for state in states:
            state.status = self._backend.get_status(state.name)
            if container_name is None or state.container_name == container_name:
                active.append(state)

        return active

    def status(self, name: str) -> TorProxyState | None:
        """Get detailed status of a proxy.

        Args:
            name: Proxy name.

        Returns:
            TorProxyState with current status, None if not found.
        """
        state = self.get(name)
        if not state:
            return None

        # Get additional info if running
        if state.status == PrivacyStatus.RUNNING:
            state.bootstrap_progress = 100
            # Could add circuit info here

        return state

    def _is_process_running(self, pid: int | None) -> bool:
        """Check if a process is running.

        Args:
            pid: Process ID.

        Returns:
            True if process is running.
        """
        if pid is None:
            return False
        try:
            os.kill(pid, 0)
            return True
        except (ProcessLookupError, PermissionError):
            return False

    def _save_state(self, state: TorProxyState) -> None:
        """Save proxy state.

        Args:
            state: State to save.
        """
        states = self._load_states()
        states = [s for s in states if s.name != state.name]
        states.append(state)
        self._write_store(states)

    def _remove_state(self, name: str) -> None:
        """Remove proxy state.

        Args:
            name: Name of proxy to remove.
        """
        states = self._load_states()
        states = [s for s in states if s.name != name]
        self._write_store(states)

    def _load_states(self) -> list[TorProxyState]:
        """Load proxy states from store.

        Returns:
            List of proxy states.
        """
        if not self._store_path.exists():
            return []

        try:
            with open(self._store_path) as f:
                data = yaml.safe_load(f)
            if not data or "proxies" not in data:
                return []
            return [TorProxyState.model_validate(p) for p in data["proxies"]]
        except Exception:
            return []

    def _write_store(self, states: list[TorProxyState]) -> None:
        """Write proxy states to store.

        Args:
            states: List of states to write.
        """
        self._store_path.parent.mkdir(parents=True, exist_ok=True)
        data = {"proxies": [s.model_dump(mode="json") for s in states]}
        with open(self._store_path, "w") as f:
            yaml.safe_dump(data, f, default_flow_style=False)


class HiddenServiceManager:
    """Manage Tor hidden services for containers.

    Handles creating and managing .onion addresses that expose
    container ports through the Tor network.

    Example:
        >>> manager = HiddenServiceManager()
        >>> config = HiddenServiceConfig(
        ...     name="web",
        ...     container_name="nginx",
        ...     container_port=80,
        ... )
        >>> service = manager.create(config)
        >>> print(f"Onion: {service.onion_address}")
        >>> manager.delete("web")
    """

    def __init__(self, state_dir: Path | None = None) -> None:
        """Initialize the hidden service manager.

        Args:
            state_dir: Directory for state files.
        """
        if state_dir is None:
            state_dir = Path.home() / ".local" / "state" / "gaol" / "tor"
        self.state_dir = state_dir
        self.state_dir.mkdir(parents=True, exist_ok=True)
        self._store_path = self.state_dir / "hidden_services.yaml"
        self._backend = TorBackend(state_dir)

    def create(self, config: HiddenServiceConfig) -> HiddenServiceState:
        """Create a hidden service for a container.

        This creates a .onion address that routes to the specified
        container port through Tor.

        Args:
            config: Hidden service configuration.

        Returns:
            HiddenServiceState with .onion address.

        Raises:
            HiddenServiceAlreadyExistsError: If service with name exists.
        """
        # Check for existing
        existing = self.get(config.name)
        if existing:
            raise HiddenServiceAlreadyExistsError(config.name)

        # Create hidden service directory
        hs_dir = self.state_dir / "hidden_services" / config.name
        hs_dir.mkdir(parents=True, exist_ok=True)
        os.chmod(hs_dir, 0o700)  # Tor requires strict permissions

        # Create Tor config for this hidden service
        # This will be used by a shared Tor instance
        hs_config = self._generate_hs_torrc(config, hs_dir)
        config_file = hs_dir / "torrc"
        config_file.write_text(hs_config)

        # Start Tor for this hidden service
        # In a real implementation, we'd use a shared Tor instance
        # For now, create dedicated instance per service
        tor_config = TorProxyConfig(
            socks_port=0,  # Disabled
            control_port=9051 + hash(config.name) % 1000,
            dns_port=0,  # Disabled
        )

        # Create state
        state = HiddenServiceState(
            name=config.name,
            container_name=config.container_name,
            container_port=config.container_port,
            hidden_service_port=config.hidden_service_port,
            data_dir=hs_dir,
            status=PrivacyStatus.STARTING,
            created_at=datetime.now(),
        )

        # Start Tor with hidden service config
        self._start_hidden_service_tor(config, hs_dir, state)

        # Read onion address once available
        hostname_file = hs_dir / "hostname"
        if hostname_file.exists():
            state.onion_address = hostname_file.read_text().strip()
            state.status = PrivacyStatus.RUNNING
        else:
            state.status = PrivacyStatus.ERROR

        self._save_state(state)
        return state

    def _generate_hs_torrc(
        self, config: HiddenServiceConfig, hs_dir: Path
    ) -> str:
        """Generate torrc for hidden service.

        Args:
            config: Hidden service config.
            hs_dir: Hidden service directory.

        Returns:
            torrc content.
        """
        lines = [
            f"# Hidden service: {config.name}",
            f"HiddenServiceDir {hs_dir}",
            f"HiddenServicePort {config.hidden_service_port} 127.0.0.1:{config.container_port}",
            f"HiddenServiceVersion {config.version}",
        ]
        return "\n".join(lines)

    def _start_hidden_service_tor(
        self,
        config: HiddenServiceConfig,
        hs_dir: Path,
        state: HiddenServiceState,
    ) -> None:
        """Start Tor with hidden service configuration.

        Args:
            config: Hidden service config.
            hs_dir: Hidden service directory.
            state: State to update.
        """
        import subprocess
        import time

        # Generate full torrc
        data_dir = hs_dir / "tor_data"
        data_dir.mkdir(parents=True, exist_ok=True)

        torrc = f"""
DataDirectory {data_dir}
SocksPort 0
ControlPort 0
HiddenServiceDir {hs_dir}
HiddenServicePort {config.hidden_service_port} 127.0.0.1:{config.container_port}
HiddenServiceVersion {config.version}
"""
        torrc_file = hs_dir / "torrc_full"
        torrc_file.write_text(torrc)

        # Start Tor
        try:
            proc = subprocess.Popen(
                [str(self._backend.binary), "-f", str(torrc_file)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )

            # Wait for hostname file
            hostname_file = hs_dir / "hostname"
            for _ in range(60):  # Wait up to 60 seconds
                if hostname_file.exists():
                    state.onion_address = hostname_file.read_text().strip()
                    state.status = PrivacyStatus.RUNNING
                    break
                if proc.poll() is not None:
                    stderr = proc.stderr.read() if proc.stderr else ""
                    state.status = PrivacyStatus.ERROR
                    break
                time.sleep(1)
            else:
                state.status = PrivacyStatus.ERROR

            # Save PID
            pid_file = hs_dir / "tor.pid"
            pid_file.write_text(str(proc.pid))

        except Exception:
            state.status = PrivacyStatus.ERROR

    def delete(self, name: str) -> None:
        """Delete a hidden service.

        Args:
            name: Name of the service to delete.

        Raises:
            HiddenServiceNotFoundError: If service is not found.
        """
        state = self.get(name)
        if not state:
            raise HiddenServiceNotFoundError(name)

        # Stop Tor process
        pid_file = state.data_dir / "tor.pid"
        if pid_file.exists():
            try:
                pid = int(pid_file.read_text().strip())
                os.kill(pid, 15)  # SIGTERM
            except (ValueError, ProcessLookupError, PermissionError):
                pass

        # Clean up directory
        import shutil
        if state.data_dir.exists():
            shutil.rmtree(state.data_dir)

        # Remove state
        self._remove_state(name)

    def get(self, name: str) -> HiddenServiceState | None:
        """Get a hidden service by name.

        Args:
            name: Service name.

        Returns:
            HiddenServiceState if found, None otherwise.
        """
        states = self._load_states()
        for state in states:
            if state.name == name:
                # Update status
                pid_file = state.data_dir / "tor.pid"
                if pid_file.exists():
                    try:
                        pid = int(pid_file.read_text().strip())
                        os.kill(pid, 0)
                        state.status = PrivacyStatus.RUNNING
                    except (ValueError, ProcessLookupError, PermissionError):
                        state.status = PrivacyStatus.STOPPED
                else:
                    state.status = PrivacyStatus.STOPPED
                return state
        return None

    def list(self, container_name: str | None = None) -> list[HiddenServiceState]:
        """List all hidden services.

        Args:
            container_name: If specified, filter by container.

        Returns:
            List of hidden service states.
        """
        states = self._load_states()

        # Update statuses
        for state in states:
            self.get(state.name)  # Updates status

        if container_name:
            states = [s for s in states if s.container_name == container_name]

        return states

    def get_onion_address(self, name: str) -> str | None:
        """Get the .onion address for a hidden service.

        Args:
            name: Service name.

        Returns:
            .onion address if available, None otherwise.
        """
        state = self.get(name)
        if state and state.onion_address:
            return state.onion_address
        return None

    def _save_state(self, state: HiddenServiceState) -> None:
        """Save hidden service state.

        Args:
            state: State to save.
        """
        states = self._load_states()
        states = [s for s in states if s.name != state.name]
        states.append(state)
        self._write_store(states)

    def _remove_state(self, name: str) -> None:
        """Remove hidden service state.

        Args:
            name: Name of service to remove.
        """
        states = self._load_states()
        states = [s for s in states if s.name != name]
        self._write_store(states)

    def _load_states(self) -> list[HiddenServiceState]:
        """Load hidden service states from store.

        Returns:
            List of states.
        """
        if not self._store_path.exists():
            return []

        try:
            with open(self._store_path) as f:
                data = yaml.safe_load(f)
            if not data or "services" not in data:
                return []
            return [HiddenServiceState.model_validate(s) for s in data["services"]]
        except Exception:
            return []

    def _write_store(self, states: list[HiddenServiceState]) -> None:
        """Write hidden service states to store.

        Args:
            states: List of states to write.
        """
        self._store_path.parent.mkdir(parents=True, exist_ok=True)
        data = {"services": [s.model_dump(mode="json") for s in states]}
        with open(self._store_path, "w") as f:
            yaml.safe_dump(data, f, default_flow_style=False)
