"""Cloudflared tunnel backend.

This module provides integration with cloudflared for creating
quick tunnels that expose local ports to the internet.
"""

from __future__ import annotations

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

from gaol.tunnel.exceptions import (
    CloudflaredNotFoundError,
    TunnelError,
    TunnelStartError,
)


class CloudflaredBackend:
    """Cloudflared tunnel backend.

    Manages cloudflared quick tunnels for exposing local ports to the internet.
    Quick tunnels require no Cloudflare account and provide a random subdomain
    like https://random-words.trycloudflare.com.
    """

    def __init__(self) -> None:
        """Initialize the cloudflared backend."""
        self._binary: Path | None = None

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

        Returns:
            Path to cloudflared binary.

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

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

        Returns:
            Path to cloudflared binary.

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

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

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

    def start_quick_tunnel(
        self, host: str, port: int, timeout: float = 30
    ) -> tuple[subprocess.Popen, str]:
        """Start a quick tunnel (no account needed).

        Quick tunnels provide a random subdomain like:
        https://random-words.trycloudflare.com

        Args:
            host: Host to tunnel (usually "localhost").
            port: Port to expose.
            timeout: Timeout in seconds waiting for URL.

        Returns:
            Tuple of (process, public_url).

        Raises:
            TunnelStartError: If tunnel fails to start.
        """
        try:
            proc = subprocess.Popen(
                [str(self.binary), "tunnel", "--url", f"http://{host}:{port}"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
        except OSError as e:
            raise TunnelStartError(f"Failed to execute cloudflared: {e}") from e

        try:
            public_url = self._wait_for_url(proc, timeout)
            return proc, public_url
        except TunnelError:
            proc.terminate()
            raise

    def _wait_for_url(self, proc: subprocess.Popen, timeout: float = 30) -> str:
        """Wait for cloudflared to print the public URL.

        Cloudflared prints the tunnel URL to stderr when the tunnel is ready.

        Args:
            proc: The cloudflared process.
            timeout: Maximum time to wait in seconds.

        Returns:
            The public URL.

        Raises:
            TunnelStartError: If URL is not found within timeout.
        """
        start = time.time()
        output = ""

        while time.time() - start < timeout:
            # Check if process died
            if proc.poll() is not None:
                stderr = proc.stderr.read() if proc.stderr else ""
                raise TunnelStartError(f"cloudflared exited unexpectedly: {stderr}")

            if proc.stderr:
                line = proc.stderr.readline()
                if line:
                    output += line
                    # Look for URL pattern: https://xxx.trycloudflare.com
                    match = re.search(
                        r"https://[a-z0-9-]+\.trycloudflare\.com", line, re.IGNORECASE
                    )
                    if match:
                        return match.group(0)

            time.sleep(0.1)

        raise TunnelStartError(
            f"Timeout waiting for tunnel URL after {timeout}s. Output: {output}"
        )

    def stop(self, pid: int) -> None:
        """Stop a tunnel by PID.

        Args:
            pid: Process ID of the cloudflared process.
        """
        try:
            os.kill(pid, signal.SIGTERM)
        except ProcessLookupError:
            pass  # Already dead
        except PermissionError:
            pass  # Can't kill it
