"""
suxitunnel
A simple Python library to create Cloudflare Tunnels programmatically
Similar to localtunnel-py but using Cloudflare's infrastructure
"""

import subprocess
import re
import time
import threading
import requests
import platform
import os
import stat
from pathlib import Path
from typing import Optional, Callable
import logging

__version__ = "0.1.0"

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class TunnelError(Exception):
    """Base exception for tunnel errors"""
    pass


class Tunnel:
    """
    Create a Cloudflare tunnel to expose a local port.
    
    Usage:
        tunnel = Tunnel(8000)
        print(tunnel.url)  # https://random-name.trycloudflare.com
        
        # Keep your app running
        # ...
        
        tunnel.close()
    
    Or use as context manager:
        with Tunnel(8000) as tunnel:
            print(tunnel.url)
            # Do work...
    """
    
    def __init__(
        self,
        port: int,
        host: str = "localhost",
        cloudflared_path: Optional[str] = None,
        auto_download: bool = True
    ):
        """
        Create a tunnel to expose a local port.
        
        Args:
            port: Local port to expose (e.g., 8000)
            host: Local host (default: "localhost")
            cloudflared_path: Path to cloudflared binary (auto-detected if None)
            auto_download: Automatically download cloudflared if not found
        """
        self.port = port
        self.host = host
        self.url: Optional[str] = None
        self._process: Optional[subprocess.Popen] = None
        self._ready = threading.Event()
        
        # Find or download cloudflared
        if cloudflared_path:
            self.cloudflared_path = cloudflared_path
        else:
            self.cloudflared_path = self._find_cloudflared()
            if not self.cloudflared_path and auto_download:
                logger.info("cloudflared not found, downloading...")
                self.cloudflared_path = self._download_cloudflared()
            elif not self.cloudflared_path:
                raise TunnelError(
                    "cloudflared not found. Install from: "
                    "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/ "
                    "or set auto_download=True"
                )
        
        # Start the tunnel
        self._start()
    
    def _get_library_dir(self) -> Path:
        """Get the directory where the library is located"""
        return Path(__file__).parent

    def _find_cloudflared(self) -> Optional[str]:
        """Try to find cloudflared in common locations"""
        system = platform.system().lower()
        binary_name = "cloudflared.exe" if system == "windows" else "cloudflared"

        # First check the library directory (where downloaded binaries are stored)
        lib_dir_path = self._get_library_dir() / binary_name
        if lib_dir_path.exists():
            return str(lib_dir_path)

        # Check if in PATH
        try:
            result = subprocess.run(
                ["cloudflared", "--version"],
                capture_output=True,
                timeout=5
            )
            if result.returncode == 0:
                return "cloudflared"
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass

        # Check common install locations
        possible_paths = [
            "/usr/local/bin/cloudflared",
            "/usr/bin/cloudflared",
            str(Path.home() / "bin" / "cloudflared"),
            str(Path.home() / ".local" / "bin" / "cloudflared"),
            # Legacy download location
            str(Path.home() / ".cloudflare-tunnel" / binary_name),
        ]

        for path in possible_paths:
            if Path(path).exists():
                return path

        return None
    
    def _download_cloudflared(self) -> str:
        """Download cloudflared binary for the current platform"""
        system = platform.system().lower()
        machine = platform.machine().lower()

        # Determine download URL
        base_url = "https://github.com/cloudflare/cloudflared/releases/latest/download"

        if system == "darwin":  # macOS
            if "arm" in machine or "aarch64" in machine:
                filename = "cloudflared-darwin-arm64.tgz"
            else:
                filename = "cloudflared-darwin-amd64.tgz"
        elif system == "linux":
            if "arm" in machine or "aarch64" in machine:
                filename = "cloudflared-linux-arm64"
            else:
                filename = "cloudflared-linux-amd64"
        elif system == "windows":
            filename = "cloudflared-windows-amd64.exe"
        else:
            raise TunnelError(f"Unsupported platform: {system} {machine}")

        download_url = f"{base_url}/{filename}"

        # Download to the library directory
        download_dir = self._get_library_dir()

        if filename.endswith(".tgz"):
            archive_path = download_dir / filename
            binary_path = download_dir / "cloudflared"
        elif filename.endswith(".exe"):
            binary_path = download_dir / "cloudflared.exe"
        else:
            binary_path = download_dir / "cloudflared"

        # Check if already downloaded
        if binary_path.exists():
            return str(binary_path)
        
        logger.info(f"Downloading cloudflared from {download_url}...")
        
        try:
            response = requests.get(download_url, stream=True, timeout=60)
            response.raise_for_status()
            
            if filename.endswith(".tgz"):
                # Download and extract tar.gz
                import tarfile
                with open(archive_path, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
                
                with tarfile.open(archive_path, 'r:gz') as tar:
                    tar.extractall(download_dir)
                
                archive_path.unlink()  # Remove archive
            else:
                # Direct binary download
                with open(binary_path, 'wb') as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        f.write(chunk)
            
            # Make executable
            if system != "windows":
                binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC)
            
            logger.info(f"Downloaded cloudflared to {binary_path}")
            return str(binary_path)
            
        except Exception as e:
            raise TunnelError(f"Failed to download cloudflared: {e}")
    
    def _start(self):
        """Start the cloudflared tunnel"""
        local_url = f"http://{self.host}:{self.port}"
        
        # Use Cloudflare Quick Tunnels (no authentication required!)
        cmd = [
            self.cloudflared_path,
            "tunnel",
            "--url", local_url
        ]
        
        try:
            self._process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1
            )
            
            # Start thread to read output and extract URL
            thread = threading.Thread(target=self._read_output, daemon=True)
            thread.start()
            
            # Wait for URL to be ready (max 30 seconds)
            if not self._ready.wait(timeout=30):
                self.close()
                raise TunnelError("Tunnel failed to start within 30 seconds")
            
        except Exception as e:
            self.close()
            raise TunnelError(f"Failed to start tunnel: {e}")
    
    def _read_output(self):
        """Read cloudflared output to extract the public URL"""
        url_pattern = re.compile(r'https://[a-zA-Z0-9-]+\.trycloudflare\.com')
        
        try:
            for line in self._process.stdout:
                logger.debug(line.strip())
                
                # Look for the public URL
                match = url_pattern.search(line)
                if match and not self.url:
                    self.url = match.group(0)
                    logger.info(f"Tunnel URL: {self.url}")
                    self._ready.set()
                
                # Check for errors
                if "error" in line.lower() and not self.url:
                    logger.error(f"Tunnel error: {line.strip()}")
        except Exception as e:
            logger.error(f"Error reading tunnel output: {e}")
    
    def close(self):
        """Close the tunnel"""
        if self._process:
            logger.info("Closing tunnel...")
            self._process.terminate()
            try:
                self._process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                self._process.kill()
                self._process.wait()
            self._process = None
            self.url = None
    
    def is_alive(self) -> bool:
        """Check if tunnel is still running"""
        if not self._process:
            return False
        return self._process.poll() is None
    
    def __enter__(self):
        """Context manager entry"""
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit"""
        self.close()
    
    def __del__(self):
        """Cleanup on deletion"""
        self.close()
    
    def __repr__(self):
        status = "active" if self.is_alive() else "closed"
        return f"<Tunnel port={self.port} url={self.url} status={status}>"


def open_tunnel(port: int, host: str = "localhost") -> Tunnel:
    """
    Convenience function to create a tunnel.
    
    Args:
        port: Local port to expose
        host: Local host (default: "localhost")
    
    Returns:
        Tunnel instance
    
    Example:
        tunnel = open_tunnel(8000)
        print(f"Public URL: {tunnel.url}")
    """
    return Tunnel(port=port, host=host)
