from __future__ import annotations

import json
import logging
import os
from enum import Enum

import httpx
from httpx_sse import aconnect_sse
from pydantic import ValidationError

from point_me.exceptions import AuthError, SearchError, StreamError
from point_me.models import RoundtripResults, Route

logger = logging.getLogger(__name__)

_BASE_URL = "https://api.point.me/api"
_DEFAULT_TIMEOUT = 120

_CABIN_MAP = {
    "economy": "ECONOMY",
    "premium-economy": "PREMIUM_ECONOMY",
    "business": "BUSINESS",
    "first": "FIRST",
}


class Site(str, Enum):
    AMEX = "amex"
    WWW = "www"


_SITE_DOMAINS = {
    Site.AMEX: "amex.point.me",
    Site.WWW: "www.point.me",
}


class PointMeClient:
    """Async client for the point.me award search API."""

    def __init__(self, token: str | None = None) -> None:
        resolved = token or os.environ.get("POINTME_TOKEN")
        if not resolved:
            raise AuthError(
                "No token provided. Log in to point.me in your browser, "
                "or pass token= / set POINTME_TOKEN."
            )
        self._token = resolved

    @staticmethod
    async def exchange_session_cookie(
        session_cookie: str, site: Site = Site.AMEX
    ) -> str:
        """Exchange a NextAuth session cookie for a bearer access token.

        Args:
            session_cookie: Value of the __Secure-next-auth.session-token cookie.
            site: Which point.me site the cookie belongs to.

        Returns:
            Bearer access token string.

        Raises:
            AuthError: If the exchange fails or response lacks an access token.
        """
        domain = _SITE_DOMAINS[site]
        url = f"https://{domain}/api/auth/session"
        cookies = {"__Secure-next-auth.session-token": session_cookie}
        try:
            async with httpx.AsyncClient() as client:
                resp = await client.get(url, cookies=cookies)
                if resp.status_code != 200:
                    raise AuthError(
                        f"Session cookie exchange failed (HTTP {resp.status_code}). "
                        "Log in to point.me in your browser and try again."
                    )
                data = resp.json()
                token = data.get("accessToken")
                if not token:
                    raise AuthError(
                        "Session cookie exchange returned no access token. "
                        "Your session may have expired — log in again at point.me."
                    )
                return token
        except httpx.HTTPError as exc:
            raise AuthError(
                f"Failed to exchange session cookie: {exc}"
            ) from exc

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self._token}",
            "x-cobrand": "amex",
        }

    async def search(
        self,
        origin: str,
        dest: str,
        date: str,
        cabin: str,
        *,
        timeout: float = _DEFAULT_TIMEOUT,
    ) -> list[Route]:
        """Search for one-way award flights.

        Returns:
            List of Route objects with fares and connections populated.

        Raises:
            AuthError: If the token is expired or invalid (HTTP 401).
            StreamError: If the SSE stream times out or is interrupted.
            SearchError: On network errors or unexpected HTTP status codes.
        """
        cabin_upper = _CABIN_MAP.get(cabin.lower(), cabin.upper())
        headers = self._headers()
        payload = {
            "departures": [origin.upper()],
            "arrivals": [dest.upper()],
            "departure_date": {"when": date, "flexibility": "0"},
            "return_date": None,
            "passengers": "1",
            "airlines": ["ALL"],
            "currencies": ["USD"],
            "cabins": [cabin_upper],
            "max_stops": "5",
        }

        try:
            async with httpx.AsyncClient() as client:
                search_params = await self._initiate(client, headers, payload)
                return await self._subscribe(
                    client, headers, search_params,
                    directionality="outbound", timeout=timeout,
                )
        except httpx.TimeoutException as exc:
            raise StreamError(f"Search timed out after {timeout}s") from exc
        except httpx.NetworkError as exc:
            raise SearchError(f"Network error during search: {exc}") from exc
        except httpx.HTTPStatusError as exc:
            raise SearchError(
                f"Unexpected HTTP error: {exc.response.status_code}"
            ) from exc

    async def search_roundtrip(
        self,
        origin: str,
        dest: str,
        date: str,
        return_date: str,
        cabin: str,
        *,
        timeout: float = _DEFAULT_TIMEOUT,
    ) -> RoundtripResults:
        """Search for roundtrip award flights.

        Returns:
            RoundtripResults with outbound and inbound route lists.

        Raises:
            AuthError: If the token is expired or invalid (HTTP 401).
            StreamError: If the SSE stream times out or is interrupted.
            SearchError: On network errors or unexpected HTTP status codes.
        """
        cabin_upper = _CABIN_MAP.get(cabin.lower(), cabin.upper())
        headers = self._headers()
        payload = {
            "departures": [origin.upper()],
            "arrivals": [dest.upper()],
            "departure_date": {"when": date, "flexibility": "0"},
            "return_date": {"when": return_date, "flexibility": "0"},
            "passengers": "1",
            "airlines": ["ALL"],
            "currencies": ["USD"],
            "cabins": [cabin_upper],
            "max_stops": "5",
        }

        try:
            async with httpx.AsyncClient() as client:
                search_params = await self._initiate(client, headers, payload)
                outbound = await self._subscribe(
                    client, headers, search_params,
                    directionality="outbound", timeout=timeout,
                )
                inbound = await self._subscribe(
                    client, headers, search_params,
                    directionality="inbound", timeout=timeout,
                )
                return RoundtripResults(outbound=outbound, inbound=inbound)
        except httpx.TimeoutException as exc:
            raise StreamError(f"Search timed out after {timeout}s") from exc
        except httpx.NetworkError as exc:
            raise SearchError(f"Network error during search: {exc}") from exc
        except httpx.HTTPStatusError as exc:
            raise SearchError(
                f"Unexpected HTTP error: {exc.response.status_code}"
            ) from exc

    async def _initiate(
        self,
        client: httpx.AsyncClient,
        headers: dict,
        payload: dict,
    ) -> dict:
        resp = await client.post(
            f"{_BASE_URL}/initiateSearch",
            json=payload,
            headers=headers,
        )
        if resp.status_code == 401:
            raise AuthError(
                "Token expired or invalid. Log in to point.me in your "
                "browser and try again, or provide a fresh token."
            )
        resp.raise_for_status()
        result = resp.json()
        return {
            "search_key": result["search_key"],
            "fsr_request_id": result["fsr_request_id"],
            "user_search_request_id": result["user_search_request_id"],
        }

    async def _subscribe(
        self,
        client: httpx.AsyncClient,
        headers: dict,
        search_params: dict,
        *,
        directionality: str,
        timeout: float,
    ) -> list[Route]:
        routes: list[Route] = []
        seen_ulids: set[str] = set()

        async with aconnect_sse(
            client,
            "GET",
            f"{_BASE_URL}/subscribeToSearch",
            params={
                "directionality": directionality,
                "searchKey": search_params["search_key"],
                "fsrRequestId": str(search_params["fsr_request_id"]),
                "userSearchRequestId": search_params[
                    "user_search_request_id"
                ],
            },
            headers=headers,
            timeout=httpx.Timeout(timeout=None, read=timeout),
        ) as event_source:
            async for sse in event_source.aiter_sse():
                if sse.event == "COMPLETE":
                    break
                elif sse.event == "HEARTBEAT":
                    continue
                elif sse.event == "SEARCH":
                    try:
                        raw = json.loads(sse.data)
                        for route_data in raw.get("routes", []):
                            ulid = route_data.get("ulid", "")
                            if ulid and ulid in seen_ulids:
                                continue
                            if ulid:
                                seen_ulids.add(ulid)
                            route = Route.model_validate(route_data)
                            if route.fares:
                                routes.append(route)
                    except (
                        json.JSONDecodeError,
                        ValidationError,
                    ) as exc:
                        logger.warning(
                            "Malformed SEARCH event skipped: %s", exc
                        )
                else:
                    logger.warning(
                        "Unknown SSE event type: %s", sse.event
                    )

        return routes
