"""xray subprocess management and PAC file generation."""

import json
import logging
import platform
import shutil
import socket
import stat
import subprocess
import time
import zipfile
from pathlib import Path

import httpx

from .config import settings

logger = logging.getLogger(__name__)

XRAY_VERSION = "v25.3.6"
GITHUB_RELEASE_URL = f"https://github.com/XTLS/Xray-core/releases/download/{XRAY_VERSION}"

PLATFORM_MAP = {
    ("Linux", "x86_64"): "Xray-linux-64.zip",
    ("Linux", "aarch64"): "Xray-linux-arm64-v8a.zip",
    ("Darwin", "x86_64"): "Xray-macos-64.zip",
    ("Darwin", "arm64"): "Xray-macos-arm64-v8a.zip",
    ("Windows", "AMD64"): "Xray-windows-64.zip",
    ("Windows", "ARM64"): "Xray-windows-arm64-v8a.zip",
}


def get_binary_name() -> str:
    return "xray.exe" if platform.system() == "Windows" else "xray"


def get_xray_binary() -> Path:
    """Find or download xray binary."""
    if settings.xray_binary:
        p = Path(settings.xray_binary)
        if p.exists():
            return p

    cached = settings.bin_dir / get_binary_name()
    if cached.exists():
        return cached

    return download_xray()


def download_xray() -> Path:
    """Download xray from GitHub releases for the current platform."""
    key = (platform.system(), platform.machine())
    asset = PLATFORM_MAP.get(key)
    if not asset:
        raise RuntimeError(f"Unsupported platform: {platform.system()} {platform.machine()}")

    settings.bin_dir.mkdir(parents=True, exist_ok=True)
    url = f"{GITHUB_RELEASE_URL}/{asset}"

    logger.info("Downloading xray %s from %s", XRAY_VERSION, url)
    with httpx.stream("GET", url, follow_redirects=True, timeout=120.0) as resp:
        resp.raise_for_status()
        tmp = settings.bin_dir / f"{asset}.tmp"
        with open(tmp, "wb") as f:
            for chunk in resp.iter_bytes(8192):
                f.write(chunk)

    # Extract xray binary from zip.
    binary_name = get_binary_name()
    dest = settings.bin_dir / binary_name
    with zipfile.ZipFile(tmp) as zf:
        for name in zf.namelist():
            if name.lower() == binary_name.lower():
                with zf.open(name) as src, open(dest, "wb") as dst:
                    shutil.copyfileobj(src, dst)
                break
        else:
            raise RuntimeError(f"{binary_name} not found in {asset}")

    tmp.unlink(missing_ok=True)

    if platform.system() != "Windows":
        dest.chmod(dest.stat().st_mode | stat.S_IEXEC)

    logger.info("Installed xray %s at %s", XRAY_VERSION, dest)
    return dest


def generate_xray_config(
    node_ip: str, node_port: int, uuid: str, flow: str, reality_public_key: str, reality_short_id: str, reality_sni: str
) -> dict:
    """Generate xray JSON config with HTTP + SOCKS5 inbounds and VLESS+Reality outbound."""
    return {
        "log": {"loglevel": "warning"},
        "inbounds": [
            {
                "tag": "http-in",
                "port": settings.http_proxy_port,
                "listen": "127.0.0.1",
                "protocol": "http",
                "settings": {},
            },
            {
                "tag": "socks-in",
                "port": settings.socks_proxy_port,
                "listen": "127.0.0.1",
                "protocol": "socks",
                "settings": {"udp": True},
            },
        ],
        "outbounds": [
            {
                "tag": "proxy",
                "protocol": "vless",
                "settings": {
                    "vnext": [
                        {
                            "address": node_ip,
                            "port": node_port,
                            "users": [{"id": uuid, "flow": flow, "encryption": "none"}],
                        }
                    ]
                },
                "streamSettings": {
                    "network": "tcp",
                    "security": "reality",
                    "realitySettings": {
                        "serverName": reality_sni,
                        "publicKey": reality_public_key,
                        "shortId": reality_short_id,
                        "fingerprint": "chrome",
                    },
                },
            }
        ],
    }


def generate_pac_file() -> Path:
    """Generate proxy auto-config file for browser use."""
    pac_content = f"""function FindProxyForURL(url, host) {{
    if (isPlainHostName(host) || host === "localhost"
        || host.startsWith("127.") || host.startsWith("192.168.")
        || host.startsWith("10.") || host.endsWith(".local")) {{
        return "DIRECT";
    }}
    return "PROXY 127.0.0.1:{settings.http_proxy_port}; DIRECT";
}}
"""
    settings.pac_file_path.parent.mkdir(parents=True, exist_ok=True)
    settings.pac_file_path.write_text(pac_content)
    return settings.pac_file_path


class XrayProxy:
    """Manages a local xray subprocess that provides HTTP + SOCKS5 proxy."""

    def __init__(self):
        self.process: subprocess.Popen | None = None
        self.config_path: Path | None = None
        self.current_node: str | None = None

    @property
    def running(self) -> bool:
        return self.process is not None and self.process.poll() is None

    def start(self, config: dict, node_id: str) -> None:
        """Start xray with the given config."""
        if self.running:
            self.stop()

        xray_binary = get_xray_binary()

        # Write config to temp file.
        config_dir = Path.home() / ".vpn-mcp"
        config_dir.mkdir(parents=True, exist_ok=True)
        self.config_path = config_dir / "xray-config.json"
        self.config_path.write_text(json.dumps(config, indent=2))

        # Start xray subprocess.
        creation_flags = 0
        if platform.system() == "Windows":
            creation_flags = subprocess.CREATE_NO_WINDOW

        self.process = subprocess.Popen(
            [str(xray_binary), "run", "-c", str(self.config_path)],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            creationflags=creation_flags,
        )
        self.current_node = node_id

        # Wait for proxy to be ready.
        self._wait_for_proxy()
        logger.info("xray started (pid=%d, node=%s)", self.process.pid, node_id)

    def stop(self) -> None:
        """Stop the xray subprocess."""
        if self.process:
            self.process.terminate()
            try:
                self.process.wait(timeout=5)
            except subprocess.TimeoutExpired:
                self.process.kill()
                self.process.wait(timeout=3)
            logger.info("xray stopped")
            self.process = None
            self.current_node = None

        if self.config_path and self.config_path.exists():
            self.config_path.unlink(missing_ok=True)

    def _wait_for_proxy(self, timeout: float = 10.0) -> None:
        """Wait for the HTTP proxy port to accept connections."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            # Check if process died.
            if self.process and self.process.poll() is not None:
                stderr = self.process.stderr.read().decode() if self.process.stderr else ""
                raise RuntimeError(f"xray exited with code {self.process.returncode}: {stderr[:500]}")
            try:
                with socket.create_connection(("127.0.0.1", settings.http_proxy_port), timeout=0.5):
                    return
            except (ConnectionRefusedError, OSError):
                time.sleep(0.3)
        raise TimeoutError(f"xray HTTP proxy not ready after {timeout}s")
