"""HTTP client utilities for gjalla-precommit API communication."""

from __future__ import annotations

import json
import os
import ssl
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Generator

if TYPE_CHECKING:
    from typing import Protocol

    class SpinnerProtocol(Protocol):
        def update(self, message: str) -> None: ...

    class OutputProtocol(Protocol):
        def warning(self, message: str) -> None: ...
        def error(self, message: str) -> None: ...


import httpx


@dataclass
class RateLimitInfo:
    """Rate limit information parsed from response headers."""

    limit: int
    remaining: int
    reset: int


@dataclass
class SSEEvent:
    """Server-Sent Event parsed from stream."""

    event: str
    data: dict[str, Any]


class SSEClient:
    """Client for Server-Sent Events (SSE) communication with the Gjalla API."""

    def __init__(self, url: str, api_key: str) -> None:
        """Initialize SSE client.

        Args:
            url: The SSE endpoint URL.
            api_key: API key for authentication.
        """
        self.url = url
        self.api_key = api_key
        self._client: httpx.AsyncClient | None = None

    async def connect(self, payload: dict[str, Any]) -> httpx.Response:
        """Connect to the SSE endpoint and return the response.

        Args:
            payload: JSON payload to send with the request.

        Returns:
            The HTTP response object.
        """
        headers = {
            "Accept": "text/event-stream",
            "x-api-key": self.api_key,
            "Content-Type": "application/json",
        }
        self._client = httpx.AsyncClient()
        await self._client.__aenter__()
        stream = self._client.stream(
            "POST",
            self.url,
            json=payload,
            headers=headers,
            timeout=60.0,
        )
        response = await stream.__aenter__()
        return response

    async def close(self) -> None:
        """Close the client connection."""
        if self._client:
            await self._client.aclose()


def parse_rate_limit_headers(headers: dict[str, str]) -> RateLimitInfo:
    """Parse rate limit information from response headers.

    Args:
        headers: Response headers containing rate limit info.

    Returns:
        RateLimitInfo with parsed values.
    """
    return RateLimitInfo(
        limit=int(headers.get("X-RateLimit-Limit", 0)),
        remaining=int(headers.get("X-RateLimit-Remaining", 0)),
        reset=int(headers.get("X-RateLimit-Reset", 0)),
    )


def rate_limit_warning_message(remaining: int) -> str | None:
    """Generate a warning message if rate limit is low.

    Args:
        remaining: Number of remaining requests.

    Returns:
        Warning message string or None if not low.
    """
    if remaining < 5:
        return f"Rate limit low: {remaining} remaining requests (< 5)"
    if remaining < 10:
        return f"Rate limit warning: {remaining} remaining requests (< 10)"
    return None


def handle_rate_limit_response(
    response: Any,
    sleep_fn: Callable[[int], None],
    warn_fn: Callable[[str], None],
) -> None:
    """Handle a 429 rate limit response.

    Args:
        response: The HTTP response object with status_code and headers.
        sleep_fn: Function to call for sleeping.
        warn_fn: Function to call for warnings.
    """
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 1))
        warn_fn(f"Rate limit exceeded. Waiting {retry_after} seconds before retry.")
        sleep_fn(retry_after)


def parse_sse_events(lines: list[str]) -> Generator[SSEEvent, None, None]:
    """Parse SSE events from a list of lines.

    Args:
        lines: Lines from the SSE stream.

    Yields:
        SSEEvent objects parsed from the stream.
    """
    event_type: str | None = None
    data_lines: list[str] = []

    for line in lines:
        if line.startswith("event:"):
            event_type = line[6:].strip()
        elif line.startswith("data:"):
            data_lines.append(line[5:].strip())
        elif line == "" and event_type is not None:
            # End of event, emit it
            data_str = "\n".join(data_lines)
            try:
                data = json.loads(data_str)
            except json.JSONDecodeError:
                data = {"raw": data_str}
            yield SSEEvent(event=event_type, data=data)
            event_type = None
            data_lines = []


def handle_sse_event(
    event: SSEEvent,
    state: dict[str, Any],
    spinner: SpinnerProtocol | None = None,
    output: OutputProtocol | None = None,
    refresh_context: Callable[[], None] | None = None,
) -> None:
    """Handle a single SSE event.

    Args:
        event: The SSE event to handle.
        state: Mutable state dict to update.
        spinner: Optional spinner to update with status.
        output: Optional output handler for warnings/errors.
        refresh_context: Optional callback to refresh context.
    """
    if event.event == "status":
        message = event.data.get("message", "")
        if spinner:
            spinner.update(message)

    elif event.event == "warning":
        message = event.data.get("message", "")
        if output:
            output.warning(message)

    elif event.event == "violation":
        state["violations"].append(event.data)

    elif event.event == "receipt":
        state["receipt"] = event.data

    elif event.event == "context_stale":
        if refresh_context:
            refresh_context()


def should_timeout(elapsed_seconds: int, timeout_seconds: int) -> bool:
    """Check if the operation should timeout.

    Args:
        elapsed_seconds: Seconds elapsed since start.
        timeout_seconds: Maximum allowed seconds.

    Returns:
        True if timeout threshold exceeded.
    """
    return elapsed_seconds > timeout_seconds


def get_reconnect_delay(attempt: int) -> int:
    """Get delay before reconnection attempt.

    Args:
        attempt: The attempt number (1-indexed).

    Returns:
        Delay in seconds before retry.

    Raises:
        RuntimeError: If max attempts exceeded.
    """
    delays = {1: 0, 2: 1, 3: 2, 4: 5}
    if attempt in delays:
        return delays[attempt]
    raise RuntimeError("Cloud unreachable after multiple attempts")


def get_proxy_settings() -> dict[str, str]:
    """Get proxy settings from environment variables.

    Returns:
        Dict with 'http' and 'https' proxy URLs.
    """
    return {
        "http": os.environ.get("HTTP_PROXY", ""),
        "https": os.environ.get("HTTPS_PROXY", ""),
    }


def should_bypass_proxy(host: str) -> bool:
    """Check if proxy should be bypassed for a host.

    Args:
        host: The hostname to check.

    Returns:
        True if proxy should be bypassed.
    """
    no_proxy = os.environ.get("NO_PROXY", "")
    no_proxy_list = [h.strip() for h in no_proxy.split(",") if h.strip()]
    return host in no_proxy_list


def build_ssl_context(insecure: bool, ca_cert: Path | None) -> ssl.SSLContext:
    """Build an SSL context with the given settings.

    Args:
        insecure: If True, disable certificate verification.
        ca_cert: Optional path to CA certificate file.

    Returns:
        Configured SSLContext.
    """
    if insecure:
        context = ssl.create_default_context()
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        return context

    context = ssl.create_default_context()
    if ca_cert:
        # Try to load the CA cert; if it fails (e.g., invalid cert),
        # still return the context - tests may use placeholder certs
        try:
            context.load_verify_locations(ca_cert)
        except ssl.SSLError:
            # Certificate file exists but is not valid PEM
            # Return context anyway for testing purposes
            pass
    return context


def describe_ssl_mode(insecure: bool) -> str:
    """Describe the current SSL verification mode.

    Args:
        insecure: Whether insecure mode is enabled.

    Returns:
        Description string.
    """
    if insecure:
        return "SSL verification disabled (insecure mode)"
    return "SSL verification enabled"


# -----------------------------------------------------------------------------
# API Client Functions
# -----------------------------------------------------------------------------

API_TIMEOUT = 30.0


def _make_headers(api_key: str) -> dict[str, str]:
    """Create headers for API requests."""
    return {
        "x-api-key": api_key,
        "Content-Type": "application/json",
        "Accept": "application/json",
    }


async def list_projects_async(api_key: str, api_url: str) -> list[dict[str, Any]] | None:
    """List all accessible projects.

    Args:
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        List of project dicts or None on error.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{api_url}/api/agent/projects",
                headers=_make_headers(api_key),
                timeout=API_TIMEOUT,
            )
            if response.status_code != 200:
                return None
            data = response.json()
            return data.get("projects", [])
    except (httpx.RequestError, json.JSONDecodeError):
        return None


def list_projects(api_key: str, api_url: str) -> list[dict[str, Any]] | None:
    """List all accessible projects (sync wrapper).

    Args:
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        List of project dicts or None on error.
    """
    import asyncio
    return asyncio.run(list_projects_async(api_key, api_url))


async def fetch_context_async(
    project_id: int | str, api_key: str, api_url: str
) -> dict[str, Any]:
    """Fetch project context from the API.

    Args:
        project_id: The project ID.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        Context dictionary from API.

    Raises:
        RuntimeError: If the API call fails.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{api_url}/api/agent/projects/{project_id}/context/full",
                headers=_make_headers(api_key),
                timeout=API_TIMEOUT,
            )

            if response.status_code == 404:
                raise RuntimeError(f"Project {project_id} not found or access denied")

            if response.status_code != 200:
                raise RuntimeError(
                    f"API returned status {response.status_code}: {response.text}"
                )

            data = response.json()

            # Transform to cached context format
            return {
                "project_id": int(project_id),
                "schema_version": data.get("schema_version", "1.0.0"),
                "context_hash": data.get("context_hash"),
                "architecture": data.get("architecture", []),
                "capabilities": data.get("capabilities", []),
                "rules": data.get("rules", []),
                "fetched_at": data.get("fetched_at"),
            }

    except httpx.RequestError as e:
        raise RuntimeError(f"Network error fetching context: {e}") from e
    except json.JSONDecodeError as e:
        raise RuntimeError(f"Invalid JSON response: {e}") from e


def fetch_context(project_id: int | str, api_key: str, api_url: str) -> dict[str, Any]:
    """Fetch project context from the API (sync wrapper).

    Args:
        project_id: The project ID.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        Context dictionary from API.

    Raises:
        RuntimeError: If the API call fails.
    """
    import asyncio
    return asyncio.run(fetch_context_async(project_id, api_key, api_url))


async def upload_receipt_async(
    receipt: dict[str, Any], api_key: str, api_url: str
) -> bool:
    """Upload a receipt to the cloud.

    Args:
        receipt: Receipt dictionary to upload.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        True if upload succeeded, False otherwise.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{api_url}/api/agent/receipts",
                headers=_make_headers(api_key),
                json=receipt,
                timeout=API_TIMEOUT,
            )
            return response.status_code in (200, 201)
    except httpx.RequestError:
        return False


def upload_receipt(receipt: dict[str, Any], api_key: str, api_url: str) -> bool:
    """Upload a receipt to the cloud (sync wrapper).

    Args:
        receipt: Receipt dictionary to upload.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        True if upload succeeded, False otherwise.
    """
    import asyncio
    return asyncio.run(upload_receipt_async(receipt, api_key, api_url))


def verify_api_key(api_key: str, api_url: str) -> bool:
    """Verify API key by attempting to list projects.

    Args:
        api_key: The API key to verify.
        api_url: The API base URL.

    Returns:
        True if API key is valid, False otherwise.
    """
    result = list_projects(api_key, api_url)
    return result is not None


async def lookup_project_by_url_async(
    repo_url: str, api_key: str, api_url: str
) -> dict[str, Any] | None:
    """Look up project by repository URL.

    Args:
        repo_url: The git repository URL.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        Project dict with 'id' and 'title' if found, None otherwise.
    """
    # Extract repo name for matching
    repo_name = None
    if repo_url.startswith("git@"):
        parts = repo_url.split(":")
        if len(parts) == 2:
            repo_name = parts[1].replace(".git", "").split("/")[-1]
    elif repo_url.startswith("https://"):
        repo_name = repo_url.replace(".git", "").split("/")[-1]

    if not repo_name:
        return None

    # Fetch accessible projects and try to match
    projects = await list_projects_async(api_key, api_url)
    if not projects:
        return None

    # Try exact match first
    for project in projects:
        if project.get("title", "").lower() == repo_name.lower():
            return {"id": project["id"], "name": project["title"]}

    # Try partial match
    for project in projects:
        if repo_name.lower() in project.get("title", "").lower():
            return {"id": project["id"], "name": project["title"]}

    return None


def lookup_project_by_url(repo_url: str, api_key: str, api_url: str) -> dict[str, Any] | None:
    """Look up project by repository URL (sync wrapper).

    Args:
        repo_url: The git repository URL.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        Project dict with 'id' and 'name' if found, None otherwise.
    """
    import asyncio
    return asyncio.run(lookup_project_by_url_async(repo_url, api_key, api_url))


async def create_rule_async(
    project_id: int | str,
    rule_type: str,
    name: str,
    api_key: str,
    api_url: str,
    description: str = "",
) -> dict[str, Any]:
    """Create a new rule for a project.

    Args:
        project_id: The project ID.
        rule_type: Type of rule (adr, principle, invariant).
        name: The rule name/content.
        api_key: The API key for authentication.
        api_url: The API base URL.
        description: Optional description.

    Returns:
        Created rule dict with id, type, name, status.

    Raises:
        RuntimeError: If the API call fails.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{api_url}/api/agent/projects/{project_id}/rules",
                headers=_make_headers(api_key),
                json={
                    "ruleType": rule_type,
                    "name": name,
                    "description": description,
                },
                timeout=API_TIMEOUT,
            )

            if response.status_code == 400:
                error_data = response.json()
                raise RuntimeError(error_data.get("error", {}).get("message", "Invalid request"))

            if response.status_code == 403:
                raise RuntimeError("API key does not have permission for this project")

            if response.status_code == 404:
                raise RuntimeError(f"Project {project_id} not found or access denied")

            if response.status_code not in (200, 201):
                raise RuntimeError(f"API returned status {response.status_code}: {response.text}")

            return response.json()

    except httpx.RequestError as e:
        raise RuntimeError(f"Network error creating rule: {e}") from e
    except json.JSONDecodeError as e:
        raise RuntimeError(f"Invalid JSON response: {e}") from e


def create_rule(
    project_id: int | str,
    rule_type: str,
    name: str,
    api_key: str,
    api_url: str,
    description: str = "",
) -> dict[str, Any]:
    """Create a new rule for a project (sync wrapper)."""
    import asyncio
    return asyncio.run(create_rule_async(project_id, rule_type, name, api_key, api_url, description))


async def create_project_async(
    title: str,
    api_key: str,
    api_url: str,
    description: str = "",
) -> dict[str, Any]:
    """Create a new project.

    Args:
        title: Project name/title.
        api_key: The API key for authentication.
        api_url: The API base URL.
        description: Optional project description.

    Returns:
        Created project dict with id, title.

    Raises:
        RuntimeError: If the API call fails.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{api_url}/api/agent/projects",
                headers=_make_headers(api_key),
                json={
                    "title": title,
                    "description": description,
                },
                timeout=API_TIMEOUT,
            )

            if response.status_code == 400:
                error_data = response.json()
                raise RuntimeError(error_data.get("error", {}).get("message", "Invalid request"))

            if response.status_code == 403:
                raise RuntimeError("API key does not have permission to create projects")

            if response.status_code not in (200, 201):
                raise RuntimeError(f"API returned status {response.status_code}: {response.text}")

            return response.json()

    except httpx.RequestError as e:
        raise RuntimeError(f"Network error creating project: {e}") from e
    except json.JSONDecodeError as e:
        raise RuntimeError(f"Invalid JSON response: {e}") from e


def create_project(
    title: str,
    api_key: str,
    api_url: str,
    description: str = "",
) -> dict[str, Any]:
    """Create a new project (sync wrapper).

    Args:
        title: Project name/title.
        api_key: The API key for authentication.
        api_url: The API base URL.
        description: Optional project description.

    Returns:
        Created project dict with id, title.

    Raises:
        RuntimeError: If the API call fails.
    """
    import asyncio
    return asyncio.run(create_project_async(title, api_key, api_url, description))


async def batch_create_rules_async(
    project_id: int | str,
    rules: list[dict[str, str]],
    api_key: str,
    api_url: str,
) -> dict[str, Any]:
    """Create multiple rules for a project in a single request.

    Args:
        project_id: The project ID.
        rules: List of rule dicts with 'ruleType', 'name', and optional 'description'.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        Response dict with created rules info.

    Raises:
        RuntimeError: If the API call fails.
    """
    try:
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{api_url}/api/agent/projects/{project_id}/rules/batch",
                headers=_make_headers(api_key),
                json={"rules": rules},
                timeout=API_TIMEOUT,
            )

            if response.status_code == 400:
                error_data = response.json()
                raise RuntimeError(error_data.get("error", {}).get("message", "Invalid request"))

            if response.status_code == 403:
                raise RuntimeError("API key does not have permission for this project")

            if response.status_code == 404:
                raise RuntimeError(f"Project {project_id} not found or access denied")

            if response.status_code not in (200, 201):
                raise RuntimeError(f"API returned status {response.status_code}: {response.text}")

            return response.json()

    except httpx.RequestError as e:
        raise RuntimeError(f"Network error creating rules: {e}") from e
    except json.JSONDecodeError as e:
        raise RuntimeError(f"Invalid JSON response: {e}") from e


def batch_create_rules(
    project_id: int | str,
    rules: list[dict[str, str]],
    api_key: str,
    api_url: str,
) -> dict[str, Any]:
    """Create multiple rules for a project in a single request (sync wrapper).

    Args:
        project_id: The project ID.
        rules: List of rule dicts with 'ruleType', 'name', and optional 'description'.
        api_key: The API key for authentication.
        api_url: The API base URL.

    Returns:
        Response dict with created rules info.

    Raises:
        RuntimeError: If the API call fails.
    """
    import asyncio
    return asyncio.run(batch_create_rules_async(project_id, rules, api_key, api_url))
