"""Tor daemon backend.

This module provides the TorBackend class for managing Tor daemon
lifecycle - starting, stopping, and monitoring Tor instances.
"""

from __future__ import annotations

import os
import re
import shutil
import signal
import socket
import subprocess
import time
from datetime import datetime
from pathlib import Path

from gaol.privacy.exceptions import (
    TorBootstrapError,
    TorNotFoundError,
)
from gaol.privacy.models import PrivacyStatus, TorProxyConfig, TorProxyState


class TorBackend:
    """Tor daemon backend.

    Manages Tor daemon instances for proxy and hidden service functionality.
    Each instance runs with its own data directory and configuration.
    """

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

        Args:
            state_dir: Directory for Tor 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._binary: Path | None = None

    @property
    def binary(self) -> Path:
        """Get the tor binary path.

        Returns:
            Path to tor binary.

        Raises:
            TorNotFoundError: If tor is not installed.
        """
        if self._binary is None:
            self._binary = self._find_binary()
        return self._binary

    def _find_binary(self) -> Path:
        """Find the tor binary.

        Returns:
            Path to tor binary.

        Raises:
            TorNotFoundError: If tor is not found.
        """
        path = shutil.which("tor")
        if not path:
            raise TorNotFoundError()
        return Path(path)

    def is_available(self) -> bool:
        """Check if tor is installed.

        Returns:
            True if tor is available.
        """
        return shutil.which("tor") is not None

    def start(
        self,
        name: str,
        container_name: str,
        config: TorProxyConfig,
        timeout: int = 120,
    ) -> TorProxyState:
        """Start a Tor daemon with given configuration.

        Args:
            name: Unique name for this Tor instance.
            container_name: Associated container name.
            config: Tor configuration.
            timeout: Timeout in seconds for bootstrap.

        Returns:
            TorProxyState with daemon information.

        Raises:
            TorBootstrapError: If Tor fails to bootstrap.
        """
        # Create data directory
        data_dir = self.state_dir / name
        data_dir.mkdir(parents=True, exist_ok=True)

        # Generate torrc
        torrc_path = data_dir / "torrc"
        torrc_content = self._generate_torrc(config, data_dir)
        torrc_path.write_text(torrc_content)

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

        # Create initial state
        state = TorProxyState(
            name=name,
            container_name=container_name,
            pid=proc.pid,
            socks_port=config.socks_port,
            control_port=config.control_port,
            dns_port=config.dns_port,
            data_dir=data_dir,
            config_file=torrc_path,
            status=PrivacyStatus.STARTING,
            started_at=datetime.now(),
            bootstrap_progress=0,
        )

        # Wait for bootstrap
        try:
            self._wait_for_bootstrap(
                proc, config.control_port, timeout, state
            )
            state.status = PrivacyStatus.RUNNING
        except TorBootstrapError:
            # Clean up on failure
            self.stop(name)
            raise

        return state

    def _generate_torrc(self, config: TorProxyConfig, data_dir: Path) -> str:
        """Generate torrc configuration file.

        Args:
            config: Tor configuration.
            data_dir: Data directory path.

        Returns:
            torrc file content.
        """
        lines = [
            f"# Gaol Tor configuration",
            f"DataDirectory {data_dir}",
            f"SocksPort {config.socks_port}",
            f"ControlPort {config.control_port}",
            f"DNSPort {config.dns_port}",
            "",
        ]

        # Transparent proxy mode
        if config.transparent:
            lines.extend([
                f"TransPort {config.trans_port}",
                "AutomapHostsOnResolve 1",
                "VirtualAddrNetworkIPv4 10.192.0.0/10",
                "",
            ])

        # Exit node preferences
        if config.exit_nodes:
            nodes = ",".join(f"{{{n}}}" for n in config.exit_nodes)
            lines.append(f"ExitNodes {nodes}")
            if config.strict_nodes:
                lines.append("StrictNodes 1")

        if config.exclude_nodes:
            nodes = ",".join(f"{{{n}}}" for n in config.exclude_nodes)
            lines.append(f"ExcludeNodes {nodes}")

        # Bridge mode for censorship circumvention
        if config.bridge_enabled and config.bridges:
            lines.extend([
                "",
                "UseBridges 1",
            ])
            for bridge in config.bridges:
                lines.append(f"Bridge {bridge}")

        # Safety settings
        lines.extend([
            "",
            "# Safety settings",
            "SafeLogging 1",
            "AvoidDiskWrites 1",
        ])

        return "\n".join(lines)

    def _wait_for_bootstrap(
        self,
        proc: subprocess.Popen,
        control_port: int,
        timeout: int,
        state: TorProxyState,
    ) -> None:
        """Wait for Tor to bootstrap.

        Args:
            proc: The Tor process.
            control_port: Control port to check status.
            timeout: Maximum time to wait in seconds.
            state: State object to update with progress.

        Raises:
            TorBootstrapError: If bootstrap fails or times out.
        """
        start = time.time()

        # First wait for control port to be available
        while time.time() - start < timeout:
            if proc.poll() is not None:
                stderr = proc.stderr.read() if proc.stderr else ""
                raise TorBootstrapError(0, f"Tor process died: {stderr}")

            if self._is_port_open(control_port):
                break
            time.sleep(0.5)
        else:
            raise TorBootstrapError(0, "Control port not available")

        # Now monitor bootstrap progress
        while time.time() - start < timeout:
            if proc.poll() is not None:
                stderr = proc.stderr.read() if proc.stderr else ""
                raise TorBootstrapError(
                    state.bootstrap_progress,
                    f"Tor process died: {stderr}"
                )

            progress = self._get_bootstrap_progress(control_port)
            state.bootstrap_progress = progress
            state.status = PrivacyStatus.BOOTSTRAPPING

            if progress >= 100:
                return

            time.sleep(1)

        raise TorBootstrapError(
            state.bootstrap_progress,
            f"Bootstrap timed out at {state.bootstrap_progress}%"
        )

    def _is_port_open(self, port: int, host: str = "127.0.0.1") -> bool:
        """Check if a port is open.

        Args:
            port: Port to check.
            host: Host to check.

        Returns:
            True if port is open.
        """
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(1)
                s.connect((host, port))
                return True
        except (socket.timeout, socket.error):
            return False

    def _get_bootstrap_progress(self, control_port: int) -> int:
        """Get Tor bootstrap progress via control port.

        Args:
            control_port: Control port to query.

        Returns:
            Bootstrap progress percentage (0-100).
        """
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(5)
                s.connect(("127.0.0.1", control_port))

                # Authenticate (no password for local)
                s.sendall(b"AUTHENTICATE\r\n")
                response = s.recv(1024).decode()
                if "250 OK" not in response:
                    return 0

                # Get bootstrap status
                s.sendall(b"GETINFO status/bootstrap-phase\r\n")
                response = s.recv(1024).decode()

                # Parse progress from response like:
                # 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=90 ...
                match = re.search(r"PROGRESS=(\d+)", response)
                if match:
                    return int(match.group(1))
                return 0
        except (socket.timeout, socket.error):
            return 0

    def stop(self, name: str) -> None:
        """Stop a Tor daemon by name.

        Args:
            name: Name of the Tor instance to stop.
        """
        # Read PID from state if exists
        pid_file = self.state_dir / name / "tor.pid"
        if pid_file.exists():
            try:
                pid = int(pid_file.read_text().strip())
                os.kill(pid, signal.SIGTERM)
                # Wait for graceful shutdown
                for _ in range(10):
                    try:
                        os.kill(pid, 0)
                        time.sleep(0.5)
                    except ProcessLookupError:
                        break
                else:
                    # Force kill if still running
                    try:
                        os.kill(pid, signal.SIGKILL)
                    except ProcessLookupError:
                        pass
            except (ValueError, ProcessLookupError, PermissionError):
                pass

    def cleanup(self, name: str) -> None:
        """Clean up Tor instance data directory.

        Args:
            name: Name of the Tor instance.
        """
        data_dir = self.state_dir / name
        if data_dir.exists():
            import shutil as sh
            sh.rmtree(data_dir)

    def get_status(self, name: str) -> PrivacyStatus:
        """Get status of a Tor instance.

        Args:
            name: Name of the Tor instance.

        Returns:
            Current status.
        """
        pid_file = self.state_dir / name / "tor.pid"
        if not pid_file.exists():
            return PrivacyStatus.STOPPED

        try:
            pid = int(pid_file.read_text().strip())
            os.kill(pid, 0)  # Check if process exists
            return PrivacyStatus.RUNNING
        except (ValueError, ProcessLookupError):
            return PrivacyStatus.STOPPED
        except PermissionError:
            return PrivacyStatus.RUNNING  # Assume running if we can't check

    def get_circuit_info(self, control_port: int) -> list[dict]:
        """Get information about current Tor circuits.

        Args:
            control_port: Control port to query.

        Returns:
            List of circuit information dictionaries.
        """
        circuits = []
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(5)
                s.connect(("127.0.0.1", control_port))

                s.sendall(b"AUTHENTICATE\r\n")
                s.recv(1024)

                s.sendall(b"GETINFO circuit-status\r\n")
                response = s.recv(8192).decode()

                # Parse circuit status
                for line in response.split("\n"):
                    if line.startswith("250-circuit-status="):
                        continue
                    if line.startswith("250 OK"):
                        break
                    parts = line.split()
                    if len(parts) >= 3:
                        circuits.append({
                            "id": parts[0],
                            "status": parts[1],
                            "path": parts[2] if len(parts) > 2 else "",
                        })
        except (socket.timeout, socket.error):
            pass

        return circuits
