"""Maeris AI client — HTTP calls to Maeris-hosted AI endpoints.

Used when ``ai_mode = "maeris"`` (Maeris AI subscription).
All requests are authenticated with the repo-scoped credentials store.

Backend API contract
--------------------
Every endpoint:
  - Requires ``Authorization: Bearer <token>`` header
  - Accepts ``Content-Type: application/json``
  - Returns JSON

POST /ai/generate-test
    Request:  {page_url, page_summary, model?}
    Response: {test_cases: [{name, steps: [{action, selector, value}],
                             assertions: [{type, selector?, value?}]}]}

POST /ai/fix-test
    Request:  {test_name, current_code, failure_logs, model?}
    Response: {fixed_code, explanation, confidence}

POST /ai/generate-suite
    Request:  {app_url, pages: [str], model?}
    Response: {test_cases: [...]}  (same shape as generate-test)

Error responses:
  401 — token invalid / expired  → re-run ``maeris login``
  402 — subscription required    → re-run ``maeris login`` to subscribe
  429 — rate limit               → retry later
  4xx/5xx — {detail: "..."}
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any

import httpx
import structlog

from maeris_mcp.auth.token_store import TokenStore

logger = structlog.get_logger(__name__)

from maeris_mcp.config import ENV as _ENV

DEFAULT_AI_BASE_URL = os.getenv("MAERIS_AI_URL", _ENV.service_url)
DEFAULT_TIMEOUT = 120.0


# ---------------------------------------------------------------------------
# Errors
# ---------------------------------------------------------------------------


class MaerisAIError(Exception):
    """Generic Maeris AI error."""


class MaerisAIAuthError(MaerisAIError):
    """Token is missing, invalid, or the subscription is inactive."""


class MaerisAIRateLimitError(MaerisAIError):
    """Rate limit exceeded."""


# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------


class MaerisAIClient:
    """Async HTTP client for Maeris-hosted AI endpoints.

    Usage::

        async with MaerisAIClient() as client:
            result = await client.generate_test(
                page_url="https://myapp.com/login",
                page_summary="Login form with email and password fields",
            )

    Token resolution order:
      1. Explicit ``token`` constructor arg.
      2. Repo credentials via ``TokenStore(config_dir=...)``.
    """

    def __init__(
        self,
        base_url: str | None = None,
        token: str | None = None,
        config_dir: Path | None = None,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        self.base_url = (base_url or DEFAULT_AI_BASE_URL).rstrip("/")
        self._explicit_token = token
        self._config_dir = config_dir
        self._timeout = timeout
        self._client: httpx.AsyncClient | None = None

    # ------------------------------------------------------------------
    # Context manager
    # ------------------------------------------------------------------

    async def __aenter__(self) -> "MaerisAIClient":
        token = self._resolve_token()
        self._client = httpx.AsyncClient(
            base_url=self.base_url,
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
            timeout=self._timeout,
        )
        return self

    async def __aexit__(self, *_: Any) -> None:
        if self._client:
            await self._client.aclose()
            self._client = None

    # ------------------------------------------------------------------
    # AI endpoints
    # ------------------------------------------------------------------

    async def generate_test(
        self,
        page_url: str,
        page_summary: str,
        model: str = "maeris-default",
    ) -> dict[str, Any]:
        """Generate test steps for a single page.

        Returns a dict with ``test_cases`` list, each item having
        ``name``, ``steps``, and ``assertions``.
        """
        return await self._post(
            "/ai/generate-test",
            {"page_url": page_url, "page_summary": page_summary, "model": model},
        )

    async def fix_test(
        self,
        test_name: str,
        current_code: str,
        failure_logs: str,
        model: str = "maeris-default",
    ) -> dict[str, Any]:
        """Request a fix for a failing Playwright test.

        Returns a dict with ``fixed_code``, ``explanation``, and ``confidence``.
        """
        return await self._post(
            "/ai/fix-test",
            {
                "test_name": test_name,
                "current_code": current_code,
                "failure_logs": failure_logs,
                "model": model,
            },
        )

    async def generate_suite(
        self,
        app_url: str,
        pages: list[str],
        model: str = "maeris-default",
    ) -> dict[str, Any]:
        """Generate a full regression suite for an application.

        Returns a dict with ``test_cases`` list (same shape as generate_test).
        """
        return await self._post(
            "/ai/generate-suite",
            {"app_url": app_url, "pages": pages, "model": model},
        )

    async def get_balance(self) -> dict[str, Any] | None:
        """Return the user's AI credit balance info, or None on any error.

        GET /ai/balance → {"balance_usd": <float>, "total_purchased_usd": <float>, ...}
        Returns None when the endpoint is unavailable or the response is unexpected.
        """
        try:
            return await self._get("/ai/balance")
        except Exception:
            return None

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _resolve_token(self) -> str:
        """Return the bearer token or raise MaerisAIAuthError."""
        if self._explicit_token:
            return self._explicit_token

        store = TokenStore(config_dir=self._config_dir)
        token = store.load_token()

        if not token or not store.is_authenticated():
            raise MaerisAIAuthError(
                "Not authenticated with Maeris.\n\nRun:\n  maeris auth login"
            )
        return token

    async def _get(self, path: str) -> dict[str, Any]:
        """HTTP GET helper — same error-handling pattern as _post."""
        if not self._client:
            raise RuntimeError("Use 'async with MaerisAIClient()' context manager")
        try:
            resp = await self._client.get(path)
        except httpx.TimeoutException as e:
            raise MaerisAIError(f"Request to Maeris AI timed out") from e
        except httpx.RequestError as e:
            raise MaerisAIError(f"Network error calling Maeris AI: {e}") from e
        if resp.status_code == 401:
            raise MaerisAIAuthError("Maeris AI token is invalid or expired.")
        if not resp.is_success:
            try:
                detail = resp.json().get("detail") or resp.text[:300]
            except Exception:
                detail = resp.text[:300]
            raise MaerisAIError(f"Maeris AI error (HTTP {resp.status_code}): {detail}")
        try:
            return resp.json()
        except Exception as e:
            raise MaerisAIError(f"Invalid JSON response from Maeris AI: {e}") from e

    async def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
        if not self._client:
            raise RuntimeError("Use 'async with MaerisAIClient()' context manager")

        try:
            resp = await self._client.post(path, json=body)
        except httpx.TimeoutException as e:
            raise MaerisAIError(
                f"Request to Maeris AI timed out after {self._timeout}s"
            ) from e
        except httpx.RequestError as e:
            raise MaerisAIError(f"Network error calling Maeris AI: {e}") from e

        if resp.status_code == 401:
            raise MaerisAIAuthError(
                "Maeris AI token is invalid or expired.\n\nRun:\n  maeris login"
            )
        if resp.status_code == 402:
            raise MaerisAIAuthError(
                "Active Maeris AI subscription required.\n\nRun:\n  maeris login"
            )
        if resp.status_code == 429:
            raise MaerisAIRateLimitError(
                "Maeris AI rate limit exceeded. Please try again later."
            )

        if not resp.is_success:
            try:
                detail = resp.json().get("detail") or resp.text[:300]
            except Exception:
                detail = resp.text[:300]
            raise MaerisAIError(
                f"Maeris AI error (HTTP {resp.status_code}): {detail}"
            )

        try:
            return resp.json()
        except Exception as e:
            raise MaerisAIError(
                f"Invalid JSON response from Maeris AI: {e}"
            ) from e
