from __future__ import annotations

from dataclasses import dataclass
import os
from pathlib import Path
import platform as platform_lib
import re
import shutil
import subprocess
import sys
import tarfile
import time
from urllib.request import urlopen

from autosana_cli.models import DEFAULT_CLOUDFLARED_VERSION, TUNNEL_URL_TIMEOUT_S, bin_dir
from autosana_cli.processes import detach_kwargs, is_pid_running

# Match tunnel URLs but exclude known non-tunnel subdomains (api, www)
TRY_CLOUDFLARE_RE = re.compile(r"https://(?!api\.|www\.)[a-z0-9-]+\.trycloudflare\.com")


@dataclass(frozen=True)
class CloudflaredAsset:
    url: str
    archive: bool
    filename: str


def cloudflared_binary_path() -> Path:
    name = "cloudflared.exe" if sys.platform == "win32" else "cloudflared"
    return bin_dir() / name


def find_cloudflared_binary() -> Path | None:
    path = shutil.which("cloudflared")
    if path:
        return Path(path)
    local = cloudflared_binary_path()
    if local.exists() and os.access(local, os.X_OK):
        return local
    return None


def cloudflared_download_info(version: str = DEFAULT_CLOUDFLARED_VERSION) -> CloudflaredAsset:
    system = platform_lib.system().lower()
    machine = platform_lib.machine().lower()
    mapping = {
        ("darwin", "arm64"): ("cloudflared-darwin-arm64.tgz", True),
        ("darwin", "x86_64"): ("cloudflared-darwin-amd64.tgz", True),
        ("linux", "x86_64"): ("cloudflared-linux-amd64", False),
        ("linux", "amd64"): ("cloudflared-linux-amd64", False),
        ("linux", "aarch64"): ("cloudflared-linux-arm64", False),
        ("linux", "arm64"): ("cloudflared-linux-arm64", False),
        ("windows", "amd64"): ("cloudflared-windows-amd64.exe", False),
        ("windows", "x86_64"): ("cloudflared-windows-amd64.exe", False),
        ("windows", "arm64"): ("cloudflared-windows-arm64.exe", False),
    }
    filename, archive = mapping.get((system, machine), (None, None))
    if filename is None:
        raise RuntimeError(f"Unsupported platform for cloudflared: {system}/{machine}")
    return CloudflaredAsset(
        url=f"https://github.com/cloudflare/cloudflared/releases/download/{version}/{filename}",
        archive=bool(archive),
        filename=filename,
    )


def ensure_cloudflared(version: str = DEFAULT_CLOUDFLARED_VERSION) -> Path:
    existing = find_cloudflared_binary()
    if existing is not None:
        return existing

    asset = cloudflared_download_info(version)
    destination = cloudflared_binary_path()
    destination.parent.mkdir(parents=True, exist_ok=True)

    with urlopen(asset.url, timeout=30) as response:
        payload = response.read()

    if asset.archive:
        archive_path = destination.parent / asset.filename
        archive_path.write_bytes(payload)
        with tarfile.open(archive_path) as archive:
            members = [member for member in archive.getmembers() if member.name.endswith("cloudflared")]
            if not members:
                raise RuntimeError("cloudflared archive did not contain the binary")
            member = members[0]
            with archive.extractfile(member) as source:
                if source is None:
                    raise RuntimeError("Failed to read cloudflared binary from archive")
                destination.write_bytes(source.read())
        archive_path.unlink(missing_ok=True)
    else:
        destination.write_bytes(payload)

    if sys.platform != "win32":
        destination.chmod(0o755)
    return destination


def extract_tunnel_url(text: str) -> str | None:
    match = TRY_CLOUDFLARE_RE.search(text)
    return match.group(0) if match else None


def wait_for_tunnel_url(log_path: Path, *, pid: int, timeout: float = TUNNEL_URL_TIMEOUT_S) -> str:
    deadline = time.time() + timeout
    while time.time() < deadline:
        if log_path.exists():
            content = log_path.read_text(encoding="utf-8")
            url = extract_tunnel_url(content)
            if url:
                return url
        if not is_pid_running(pid):
            raise RuntimeError("cloudflared exited before publishing a tunnel URL")
        time.sleep(0.5)
    raise RuntimeError(f"Tunnel URL not received within {int(timeout)}s")


def start_tunnel(
    *,
    cloudflared_bin: Path,
    port: int,
    log_path: Path,
    detach: bool,
) -> tuple[subprocess.Popen[str], str]:
    log_path.parent.mkdir(parents=True, exist_ok=True)
    log_handle = log_path.open("a", encoding="utf-8")
    proc = subprocess.Popen(
        [str(cloudflared_bin), "tunnel", "--url", f"http://127.0.0.1:{port}"],
        stdout=log_handle,
        stderr=log_handle,
        text=True,
        **detach_kwargs(detach),
    )
    url = wait_for_tunnel_url(log_path, pid=proc.pid)
    return proc, url
