"""Internal task app helpers.

Public API: Use `synth_ai.sdk.localapi` instead.
"""

from __future__ import annotations

import logging
import socket
import subprocess
import time
from collections.abc import Iterable
from dataclasses import dataclass
from urllib.parse import urlparse, urlunparse

import click
import requests

from .utils import CLIResult, http_get, run_cli


@dataclass(slots=True)
class TaskAppHealth:
    ok: bool
    health_status: int | None
    task_info_status: int | None
    detail: str | None = None


@dataclass(slots=True)
class LocalAPIHealth(TaskAppHealth):
    """Alias for TaskAppHealth with LocalAPI naming."""


def _resolve_hostname_with_explicit_resolvers(hostname: str) -> str:
    """
    Resolve hostname using explicit resolvers (1.1.1.1, 8.8.8.8) first,
    then fall back to system resolver.

    This fixes resolver path issues where system DNS is slow or blocking.
    """
    # Try Cloudflare / Google first via `dig`, then fall back to system resolver
    for resolver_ip in ("1.1.1.1", "8.8.8.8"):
        try:
            result = subprocess.run(
                ["dig", f"@{resolver_ip}", "+short", hostname],
                capture_output=True,
                text=True,
                timeout=5.0,
            )
            if result.returncode == 0 and result.stdout.strip():
                first = result.stdout.strip().splitlines()[0].strip()
                if first:
                    return first
        except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
            continue

    # Fallback: system resolver
    return socket.gethostbyname(hostname)


@dataclass(slots=True)
class CurlResponse:
    """Minimal response object from curl to match requests.Response interface."""

    status_code: int
    text: str
    _json: dict | None = None

    def json(self) -> dict:
        import json

        if self._json is None:
            self._json = json.loads(self.text) if self.text else {}
        return self._json


def _curl_with_resolve(
    url: str,
    headers: dict[str, str] | None = None,
    timeout: float = 10.0,
) -> CurlResponse | None:
    """
    Make an HTTPS request using curl with --resolve to bypass system DNS.

    This resolves the hostname using explicit DNS resolvers (1.1.1.1, 8.8.8.8)
    and then uses curl's --resolve flag to connect directly to the resolved IP
    while maintaining proper SNI for SSL/TLS.

    Args:
        url: The URL to fetch (must be HTTPS)
        headers: Optional headers to include
        timeout: Request timeout in seconds

    Returns:
        CurlResponse with status_code and text, or None if curl fails
    """
    parsed = urlparse(url)
    hostname = parsed.hostname

    if not hostname or hostname in ("localhost", "127.0.0.1"):
        return None  # Use regular requests for localhost

    # Resolve hostname using explicit resolvers
    try:
        resolved_ip = _resolve_hostname_with_explicit_resolvers(hostname)
    except (socket.gaierror, Exception):
        return None  # DNS resolution failed, caller should fall back

    # Build curl command with --resolve to bypass system DNS
    scheme = parsed.scheme or "https"
    port = parsed.port or (443 if scheme == "https" else 80)

    curl_cmd = [
        "curl",
        "-s",
        "-w",
        "\n%{http_code}",  # Append status code on new line
        "--max-time",
        str(int(timeout)),
        "-k",  # Allow self-signed certs (tunnel certs)
        "--resolve",
        f"{hostname}:{port}:{resolved_ip}",
        url,
    ]

    # Add headers
    if headers:
        for key, value in headers.items():
            curl_cmd.extend(["-H", f"{key}: {value}"])

    try:
        result = subprocess.run(
            curl_cmd,
            capture_output=True,
            text=True,
            timeout=timeout + 5,  # Allow some buffer
        )

        if result.returncode != 0:
            return None

        # Parse output: body followed by status code on last line
        output = result.stdout
        lines = output.rsplit("\n", 1)

        if len(lines) == 2:
            body = lines[0]
            status_str = lines[1].strip()
        else:
            body = ""
            status_str = lines[0].strip() if lines else "0"

        try:
            status_code = int(status_str)
        except ValueError:
            return None

        return CurlResponse(status_code=status_code, text=body)

    except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
        return None


def _resolve_url_to_ip(url: str) -> tuple[str, str]:
    """
    Resolve URL's hostname to IP using explicit resolvers, return (ip_url, hostname).

    NOTE: For HTTPS URLs, we DON'T resolve to IP because SSL/TLS requires SNI
    (Server Name Indication) with the hostname. Connecting via IP causes handshake
    failures since the server (e.g., Cloudflare) doesn't know which cert to present.

    Returns:
        Tuple of (url_with_ip, original_hostname)
    """
    parsed = urlparse(url)
    hostname = parsed.hostname

    # Skip resolution for localhost
    if not hostname or hostname in ("localhost", "127.0.0.1"):
        return url, hostname or ""

    # Skip resolution for HTTPS URLs - SSL/TLS requires hostname for SNI
    # Connecting to IP directly causes "SSLV3_ALERT_HANDSHAKE_FAILURE" because
    # the server doesn't know which certificate to present
    if parsed.scheme == "https":
        return url, hostname

    # Only resolve HTTP URLs to IP
    try:
        resolved_ip = _resolve_hostname_with_explicit_resolvers(hostname)
        # Replace hostname with IP in URL
        new_parsed = parsed._replace(
            netloc=f"{resolved_ip}:{parsed.port}" if parsed.port else resolved_ip
        )
        ip_url = urlunparse(new_parsed)
        return ip_url, hostname
    except Exception:
        # If resolution fails, return original URL
        return url, hostname or ""


def _health_response_ok(resp: requests.Response | None) -> tuple[bool, str]:
    if resp is None:
        return False, ""
    status = resp.status_code
    if status == 200:
        return True, ""
    if status in {401, 403}:
        try:
            payload = resp.json()
        except ValueError:
            payload = {}
        prefix = payload.get("expected_api_key_prefix")
        detail = str(payload.get("detail", ""))
        if prefix or "expected prefix" in detail.lower():
            note = "auth-optional"
            if prefix:
                note += f" (expected-prefix={prefix})"
            return True, note
    return False, ""


def check_task_app_health(
    base_url: str, api_key: str, *, timeout: float = 10.0, max_retries: int = 5
) -> TaskAppHealth:
    # Send ALL known environment keys so the server can authorize any valid one
    import os

    headers = {"X-API-Key": api_key}
    aliases = (os.getenv("ENVIRONMENT_API_KEY_ALIASES") or "").strip()
    keys: list[str] = [api_key]
    if aliases:
        keys.extend([p.strip() for p in aliases.split(",") if p.strip()])
    if keys:
        headers["X-API-Keys"] = ",".join(keys)
        headers.setdefault("Authorization", f"Bearer {api_key}")
    base = base_url.rstrip("/")
    detail_parts: list[str] = []

    def _is_dns_error(exc: requests.RequestException) -> bool:
        """Check if exception is a DNS resolution error."""
        exc_str = str(exc).lower()
        return any(
            phrase in exc_str
            for phrase in [
                "failed to resolve",
                "name resolution",
                "nodename nor servname",
                "name or service not known",
                "[errno 8]",
            ]
        )

    health_resp: requests.Response | CurlResponse | None = None
    health_ok = False

    # Resolve hostname to IP using explicit resolvers to avoid system DNS issues
    health_url = f"{base}/health"
    is_https = health_url.startswith("https://")

    # Retry health check with exponential backoff for DNS errors
    # Re-resolve DNS on each retry attempt to handle DNS propagation delays
    for attempt in range(max_retries):
        # For HTTPS URLs, try curl with --resolve first to bypass system DNS
        # This handles cases where explicit resolvers (1.1.1.1) work but system DNS doesn't
        if is_https:
            curl_resp = _curl_with_resolve(health_url, headers=headers, timeout=timeout)
            if curl_resp is not None:
                health_resp = curl_resp
                health_ok, note = _health_response_ok(health_resp)
                suffix = f" ({note})" if note else ""
                if not health_ok and health_resp is not None:
                    try:
                        hjs = health_resp.json()
                        expected = hjs.get("expected_api_key_prefix")
                        authorized = hjs.get("authorized")
                        detail = hjs.get("detail")
                        extras = []
                        if authorized is not None:
                            extras.append(f"authorized={authorized}")
                        if expected:
                            extras.append(f"expected_prefix={expected}")
                        if detail:
                            extras.append(f"detail={str(detail)[:80]}")
                        if extras:
                            suffix += " [" + ", ".join(extras) + "]"
                    except Exception:
                        pass
                detail_parts.append(f"/health={health_resp.status_code}{suffix}")
                break  # Success via curl, exit retry loop

        # Fall back to requests-based approach (works for HTTP, or HTTPS if curl failed)
        ip_health_url, original_hostname = _resolve_url_to_ip(health_url)
        use_ip_directly = ip_health_url != health_url  # True if we resolved to IP

        # Ensure Host header is set if we resolved to IP
        if (
            use_ip_directly
            and original_hostname
            and original_hostname not in ("localhost", "127.0.0.1")
        ):
            headers["Host"] = original_hostname

        try:
            # If using IP directly, disable SSL verification (cert is for hostname, not IP)
            if use_ip_directly:
                health_resp = requests.get(
                    ip_health_url, headers=headers, timeout=timeout, verify=False
                )
            else:
                health_resp = http_get(ip_health_url, headers=headers, timeout=timeout)
            health_ok, note = _health_response_ok(health_resp)
            suffix = f" ({note})" if note else ""
            # On non-200, include brief JSON detail if present
            if not health_ok and health_resp is not None:
                try:
                    hjs = health_resp.json()
                    # pull a few helpful fields without dumping everything
                    expected = hjs.get("expected_api_key_prefix")
                    authorized = hjs.get("authorized")
                    detail = hjs.get("detail")
                    extras = []
                    if authorized is not None:
                        extras.append(f"authorized={authorized}")
                    if expected:
                        extras.append(f"expected_prefix={expected}")
                    if detail:
                        extras.append(f"detail={str(detail)[:80]}")
                    if extras:
                        suffix += " [" + ", ".join(extras) + "]"
                except Exception:
                    pass
            detail_parts.append(f"/health={health_resp.status_code}{suffix}")
            break  # Success, exit retry loop
        except requests.RequestException as exc:
            if _is_dns_error(exc) and attempt < max_retries - 1:
                # DNS error, retry with exponential backoff
                delay = 2**attempt  # 1s, 2s, 4s, 8s, 16s
                logger = logging.getLogger(__name__)
                logger.warning(
                    "DNS resolution failed (attempt %s/%s), retrying in %ss...",
                    attempt + 1,
                    max_retries,
                    delay,
                )
                time.sleep(delay)
                continue
            # Not a DNS error or final attempt, record and break
            detail_parts.append(f"/health_error={exc}")
            break

    task_resp: requests.Response | CurlResponse | None = None
    task_ok = False

    # Resolve hostname to IP using explicit resolvers to avoid system DNS issues
    task_info_url = f"{base}/task_info"
    # Host header already set from health check above

    # Retry task_info check with exponential backoff for DNS errors
    # Re-resolve DNS on each retry attempt to handle DNS propagation delays
    for attempt in range(max_retries):
        # For HTTPS URLs, try curl with --resolve first to bypass system DNS
        if is_https:
            curl_resp = _curl_with_resolve(task_info_url, headers=headers, timeout=timeout)
            if curl_resp is not None:
                task_resp = curl_resp
                task_ok = bool(task_resp.status_code == 200)
                if not task_ok and task_resp is not None:
                    try:
                        tjs = task_resp.json()
                        msg = tjs.get("detail") or tjs.get("status")
                        detail_parts.append(f"/task_info={task_resp.status_code} ({str(msg)[:80]})")
                    except Exception:
                        detail_parts.append(f"/task_info={task_resp.status_code}")
                else:
                    detail_parts.append(f"/task_info={task_resp.status_code}")
                break  # Success via curl, exit retry loop

        # Fall back to requests-based approach
        ip_task_info_url, task_info_hostname = _resolve_url_to_ip(task_info_url)
        use_ip_directly_task = ip_task_info_url != task_info_url  # True if we resolved to IP

        # Ensure Host header is set if we resolved to IP
        if (
            use_ip_directly_task
            and task_info_hostname
            and task_info_hostname not in ("localhost", "127.0.0.1")
        ):
            headers["Host"] = task_info_hostname

        try:
            # If using IP directly, disable SSL verification (cert is for hostname, not IP)
            if use_ip_directly_task:
                task_resp = requests.get(
                    ip_task_info_url, headers=headers, timeout=timeout, verify=False
                )
            else:
                task_resp = http_get(ip_task_info_url, headers=headers, timeout=timeout)
            task_ok = bool(task_resp.status_code == 200)
            if not task_ok and task_resp is not None:
                try:
                    tjs = task_resp.json()
                    msg = tjs.get("detail") or tjs.get("status")
                    detail_parts.append(f"/task_info={task_resp.status_code} ({str(msg)[:80]})")
                except Exception:
                    detail_parts.append(f"/task_info={task_resp.status_code}")
            else:
                detail_parts.append(f"/task_info={task_resp.status_code}")
            break  # Success, exit retry loop
        except requests.RequestException as exc:
            if _is_dns_error(exc) and attempt < max_retries - 1:
                # DNS error, retry with exponential backoff
                # DNS will be re-resolved on next iteration
                delay = 2**attempt  # 1s, 2s, 4s, 8s, 16s
                logger = logging.getLogger(__name__)
                logger.warning(
                    "DNS resolution failed (attempt %s/%s), retrying in %ss...",
                    attempt + 1,
                    max_retries,
                    delay,
                )
                time.sleep(delay)
                continue
            # Not a DNS error or final attempt, record and break
            detail_parts.append(f"/task_info_error={exc}")
            break

    ok = bool(health_ok and task_ok)
    detail = ", ".join(detail_parts)
    return TaskAppHealth(
        ok=ok,
        health_status=None if health_resp is None else health_resp.status_code,
        task_info_status=None if task_resp is None else task_resp.status_code,
        detail=detail,
    )


def check_local_api_health(
    base_url: str, api_key: str, *, timeout: float = 10.0, max_retries: int = 5
) -> LocalAPIHealth:
    """Alias for check_task_app_health with LocalAPI naming."""
    health = check_task_app_health(base_url, api_key, timeout=timeout, max_retries=max_retries)
    return LocalAPIHealth(
        ok=health.ok,
        health_status=health.health_status,
        task_info_status=health.task_info_status,
        detail=health.detail,
    )


@dataclass(slots=True)
class ModalSecret:
    name: str
    value: str


@dataclass(slots=True)
class ModalApp:
    app_id: str
    label: str
    url: str


def _run_modal(args: Iterable[str]) -> CLIResult:
    return run_cli(["modal", *args], timeout=30.0)


def list_modal_secrets(pattern: str | None = None) -> list[str]:
    result = _run_modal(["secret", "list"])
    if result.code != 0:
        raise click.ClickException(f"modal secret list failed: {result.stderr or result.stdout}")
    names: list[str] = []
    for line in result.stdout.splitlines():
        line = line.strip()
        if not line or line.startswith("NAME"):
            continue
        parts = line.split()
        name = parts[0]
        if pattern and pattern.lower() not in name.lower():
            continue
        names.append(name)
    return names


def get_modal_secret_value(name: str) -> str:
    result = _run_modal(["secret", "get", name])
    if result.code != 0:
        raise click.ClickException(
            f"modal secret get {name} failed: {result.stderr or result.stdout}"
        )
    value = result.stdout.strip()
    if not value:
        raise click.ClickException(f"Secret {name} is empty")
    return value


def list_modal_apps(pattern: str | None = None) -> list[ModalApp]:
    result = _run_modal(["app", "list"])
    if result.code != 0:
        raise click.ClickException(f"modal app list failed: {result.stderr or result.stdout}")
    apps: list[ModalApp] = []
    for line in result.stdout.splitlines():
        line = line.strip()
        if not line or line.startswith("APP"):
            continue
        parts = line.split()
        if len(parts) < 3:
            continue
        app_id, label, url = parts[0], parts[1], parts[-1]
        if pattern and pattern.lower() not in (label.lower() + url.lower() + app_id.lower()):
            continue
        apps.append(ModalApp(app_id=app_id, label=label, url=url))
    return apps


def format_modal_apps(apps: list[ModalApp]) -> str:
    rows = [f"{idx}) {app.label} {app.url}" for idx, app in enumerate(apps, start=1)]
    return "\n".join(rows)


def format_modal_secrets(names: list[str]) -> str:
    return "\n".join(f"{idx}) {name}" for idx, name in enumerate(names, start=1))


__all__ = [
    "ModalApp",
    "ModalSecret",
    "check_task_app_health",
    "check_local_api_health",
    "format_modal_apps",
    "format_modal_secrets",
    "get_modal_secret_value",
    "list_modal_apps",
    "list_modal_secrets",
    "LocalAPIHealth",
]
