"""High-level tunnel management for exposing local APIs.

This module provides a clean abstraction for setting up Cloudflare tunnels
to expose local APIs to the internet.

Example:
    from synth_ai.sdk.tunnels import TunneledLocalAPI, TunnelBackend

    # Managed tunnel (stable subdomain, requires API key)
    tunnel = await TunneledLocalAPI.create(
        local_port=8001,
        backend=TunnelBackend.CloudflareManagedTunnel,
        api_key="sk_live_...",
        env_api_key="env_key_...",
    )

    # Quick tunnel (random subdomain, no API key needed)
    tunnel = await TunneledLocalAPI.create(
        local_port=8001,
        backend=TunnelBackend.CloudflareQuickTunnel,
    )

    print(f"Local API exposed at: {tunnel.url}")

    # Use the URL for remote jobs
    job = PromptLearningJob.from_dict(
        config_dict={...},
        task_app_url=tunnel.url,
    )

    # Clean up when done
    tunnel.close()

See Also:
    - `synth_ai.sdk.tunnels`: Lower-level tunnel functions
    - `synth_ai.core.integrations.cloudflare`: Core tunnel implementation
"""

from __future__ import annotations

import asyncio
import subprocess
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional

from synth_ai.core.telemetry import log_info


class TunnelBackend(str, Enum):
    """Supported tunnel backends for exposing local APIs.

    Attributes:
        CloudflareManagedTunnel: Managed tunnel via Synth backend.
            - Stable subdomains (e.g., task-1234-5678.usesynth.ai)
            - Requires Synth API key
            - Associated with your organization
            - Best for production jobs

        CloudflareQuickTunnel: Anonymous tunnel via trycloudflare.com.
            - Random subdomains that change each time
            - No API key required
            - Not associated with any organization
            - Best for quick local testing
    """

    CloudflareManagedTunnel = "cloudflare_managed"
    CloudflareQuickTunnel = "cloudflare_quick"


@dataclass
class TunneledLocalAPI:
    """A managed tunnel exposing a local API to the internet.

    This class provides a clean interface for:
    1. Provisioning a Cloudflare tunnel (managed or quick)
    2. Starting the cloudflared process
    3. Verifying DNS resolution and connectivity
    4. Tracking the process for cleanup

    Use `TunneledLocalAPI.create()` with a `TunnelBackend` to provision a tunnel.

    Attributes:
        url: Public HTTPS URL for the tunnel (e.g., "https://task-1234-5678.usesynth.ai")
        hostname: Hostname without protocol (e.g., "task-1234-5678.usesynth.ai")
        local_port: Local port being tunneled
        backend: The tunnel backend used (CloudflareManagedTunnel or CloudflareQuickTunnel)
        process: The cloudflared subprocess (for advanced use)

    Example:
        >>> from synth_ai.sdk.tunnels import TunneledLocalAPI, TunnelBackend
        >>> tunnel = await TunneledLocalAPI.create(
        ...     local_port=8001,
        ...     backend=TunnelBackend.CloudflareManagedTunnel,
        ...     api_key="sk_live_...",
        ...     env_api_key="env_key_...",
        ... )
        >>> print(tunnel.url)
        https://task-1234-5678.usesynth.ai
        >>> tunnel.close()
    """

    url: str
    hostname: str
    local_port: int
    backend: TunnelBackend
    process: Optional[subprocess.Popen] = None
    tunnel_token: Optional[str] = field(default=None, repr=False)
    _raw: dict[str, Any] = field(default_factory=dict, repr=False)

    @classmethod
    async def create(
        cls,
        local_port: int,
        backend: TunnelBackend = TunnelBackend.CloudflareManagedTunnel,
        *,
        api_key: Optional[str] = None,
        env_api_key: Optional[str] = None,
        reason: Optional[str] = None,
        backend_url: Optional[str] = None,
        verify_dns: bool = True,
        progress: bool = False,
    ) -> "TunneledLocalAPI":
        """Create a tunnel to expose a local API.

        This is the main entry point for creating tunnels. It handles:
        1. Requesting a tunnel (from Synth backend for managed, or trycloudflare for quick)
        2. Starting the cloudflared process
        3. Waiting for DNS propagation
        4. Verifying HTTP connectivity
        5. Registering for automatic cleanup

        Args:
            local_port: Local port to tunnel (e.g., 8001)
            backend: Tunnel backend to use. Defaults to CloudflareManagedTunnel.
                - CloudflareManagedTunnel: Stable subdomain, requires api_key
                - CloudflareQuickTunnel: Random subdomain, no api_key needed
            api_key: Synth API key for authentication (required for managed tunnels)
            env_api_key: API key for the local task app (for health checks).
                Defaults to ENVIRONMENT_API_KEY env var.
            reason: Optional reason for creating tunnel (for logging, managed only)
            backend_url: Optional backend URL (defaults to production, managed only)
            verify_dns: Whether to verify DNS resolution after creating tunnel.
                Set to False if you're sure DNS will work (e.g., reusing subdomain).
            progress: If True, print status updates during setup

        Returns:
            TunneledLocalAPI instance with .url, .hostname, .close(), etc.

        Raises:
            ValueError: If api_key is missing for managed tunnels
            RuntimeError: If tunnel creation or verification fails

        Example:
            >>> # Managed tunnel (stable subdomain)
            >>> tunnel = await TunneledLocalAPI.create(
            ...     local_port=8001,
            ...     backend=TunnelBackend.CloudflareManagedTunnel,
            ...     api_key="sk_live_...",
            ...     progress=True,
            ... )
            Provisioning managed tunnel for port 8001...
            Starting cloudflared...
            Tunnel ready: https://task-1234-5678.usesynth.ai

            >>> # Quick tunnel (random subdomain)
            >>> tunnel = await TunneledLocalAPI.create(
            ...     local_port=8001,
            ...     backend=TunnelBackend.CloudflareQuickTunnel,
            ...     progress=True,
            ... )
            Starting quick tunnel for port 8001...
            Tunnel ready: https://random-words.trycloudflare.com
        """
        import os

        from .cleanup import track_process

        # Resolve env_api_key from environment if not provided
        if env_api_key is None:
            env_api_key = os.environ.get("ENVIRONMENT_API_KEY")

        if backend == TunnelBackend.CloudflareManagedTunnel:
            return await cls._create_managed(
                local_port=local_port,
                api_key=api_key,
                env_api_key=env_api_key,
                reason=reason,
                backend_url=backend_url,
                verify_dns=verify_dns,
                progress=progress,
                track_process=track_process,
            )
        elif backend == TunnelBackend.CloudflareQuickTunnel:
            return await cls._create_quick(
                local_port=local_port,
                env_api_key=env_api_key,
                progress=progress,
                track_process=track_process,
            )
        else:
            raise ValueError(f"Unsupported tunnel backend: {backend}")

    @classmethod
    async def _create_managed(
        cls,
        local_port: int,
        api_key: Optional[str],
        env_api_key: Optional[str],
        reason: Optional[str],
        backend_url: Optional[str],
        verify_dns: bool,
        progress: bool,
        track_process,
    ) -> "TunneledLocalAPI":
        """Internal: Create a managed tunnel via Synth backend."""
        from synth_ai.core.integrations.cloudflare import (
            open_managed_tunnel_with_connection_wait,
            rotate_tunnel,
            verify_tunnel_dns_resolution,
        )

        if not api_key:
            raise ValueError(
                "api_key is required for CloudflareManagedTunnel. "
                "Use CloudflareQuickTunnel for anonymous tunnels."
            )

        # Step 1: Provision tunnel from backend
        if progress:
            print(f"Provisioning managed tunnel for port {local_port}...")

        tunnel_data = await rotate_tunnel(
            api_key,
            local_port,
            reason=reason,
            backend_url=backend_url,
        )

        hostname = tunnel_data["hostname"]
        tunnel_token = tunnel_data["tunnel_token"]
        url = f"https://{hostname}"

        log_info(
            "TunneledLocalAPI.create: managed tunnel provisioned",
            ctx={"hostname": hostname, "local_port": local_port},
        )

        # Step 2: Start cloudflared and WAIT for it to connect
        # This is critical - DNS only resolves after cloudflared connects to Cloudflare's edge
        if progress:
            print(f"Starting cloudflared for {hostname}...")
            print("Waiting for cloudflared to connect to Cloudflare edge...")

        proc = await open_managed_tunnel_with_connection_wait(
            tunnel_token,
            timeout_seconds=30.0,
        )
        track_process(proc)

        log_info(
            "TunneledLocalAPI.create: cloudflared connected",
            ctx={"hostname": hostname},
        )

        # Step 3: Verify DNS resolution and connectivity (if requested)
        # DNS should now resolve quickly since cloudflared is connected
        if verify_dns:
            dns_verified = tunnel_data.get("dns_verified", False)
            if not dns_verified:
                if progress:
                    print("Verifying DNS propagation...")

                await verify_tunnel_dns_resolution(
                    url,
                    name="tunnel",
                    timeout_seconds=60.0,  # Reduced from 90s since cloudflared is already connected
                    api_key=env_api_key,
                )

        if progress:
            print(f"Tunnel ready: {url}")

        return cls(
            url=url,
            hostname=hostname,
            local_port=local_port,
            backend=TunnelBackend.CloudflareManagedTunnel,
            process=proc,
            tunnel_token=tunnel_token,
            _raw=tunnel_data,
        )

    @classmethod
    async def _create_quick(
        cls,
        local_port: int,
        env_api_key: Optional[str],
        progress: bool,
        track_process,
    ) -> "TunneledLocalAPI":
        """Internal: Create a quick (anonymous) tunnel via trycloudflare.com."""
        from synth_ai.core.integrations.cloudflare import (
            open_quick_tunnel_with_dns_verification,
        )

        if progress:
            print(f"Starting quick tunnel for port {local_port}...")

        url, proc = await open_quick_tunnel_with_dns_verification(
            port=local_port,
            api_key=env_api_key,
        )

        track_process(proc)

        # Extract hostname from URL
        hostname = url.replace("https://", "").replace("http://", "").rstrip("/")

        log_info(
            "TunneledLocalAPI.create: quick tunnel ready",
            ctx={"hostname": hostname, "local_port": local_port},
        )

        if progress:
            print(f"Tunnel ready: {url}")

        return cls(
            url=url,
            hostname=hostname,
            local_port=local_port,
            backend=TunnelBackend.CloudflareQuickTunnel,
            process=proc,
            tunnel_token=None,
            _raw={},
        )

    def close(self) -> None:
        """Close the tunnel and terminate the cloudflared process.

        This is called automatically when the process exits (via atexit),
        but you can call it explicitly for earlier cleanup.
        """
        from synth_ai.core.integrations.cloudflare import stop_tunnel

        if self.process:
            stop_tunnel(self.process)
            self.process = None
            log_info(
                "TunneledLocalAPI.close: tunnel closed",
                ctx={"hostname": self.hostname, "backend": self.backend.value},
            )

    def __enter__(self) -> "TunneledLocalAPI":
        """Context manager entry (for sync use after async creation)."""
        return self

    def __exit__(self, *args: Any) -> None:
        """Context manager exit - closes tunnel."""
        self.close()


__all__ = ["TunneledLocalAPI", "TunnelBackend"]
