from __future__ import annotations

import json
import logging
from datetime import datetime
from typing import Any
from typing import NamedTuple
from typing import cast

import requests
from requests.exceptions import HTTPError

from py_nextbus.models import AgencyInfo
from py_nextbus.models import RouteDetails
from py_nextbus.models import RouteInfo
from py_nextbus.models import StopPrediction

LOG = logging.getLogger()


class NextBusError(Exception):
    pass


class NextBusHTTPError(NextBusError):
    def __init__(self, message: str, http_err: HTTPError):
        super().__init__()
        self.__dict__.update(http_err.__dict__)
        self.message: str = message


class NextBusValidationError(ValueError, NextBusError):
    """Error with missing fields for a NextBus request."""


class NextBusFormatError(ValueError, NextBusError):
    """Error with parsing a NextBus response."""


class NextBusAuthError(NextBusError):
    """Error with authentication to the NextBus API."""


class RouteStop(NamedTuple):
    route_tag: str
    stop_tag: str | int

    def __str__(self) -> str:
        return f"{self.route_tag}|{self.stop_tag}"

    @classmethod
    def from_dict(cls, legacy_dict: dict[str, str]) -> RouteStop:
        return cls(legacy_dict["route_tag"], legacy_dict["stop_tag"])


class NextBusClient:
    base_url: str = "https://api.prd-1.iq.live.umoiq.com/v2.0/riders"

    def __init__(
        self,
        agency_id: str | None = None,
    ) -> None:
        self.agency_id: str | None = agency_id

        self._session: requests.Session = requests.Session()
        self._session.headers.update(
            {
                "Accept": "application/json",
                "Accept-Encoding": "gzip, deflate, br, zstd",
                "Accept-Language": "en-US,en;q=0.5",
                "Compress": "true",
                "Connection": "keep-alive",
                "DNT": "1",
                "Origin": "https://rider.umoiq.com",
                "Referer": "https://rider.umoiq.com/",
                "Sec-Fetch-Dest": "empty",
                "Sec-Fetch-Mode": "cors",
                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0",
            }
        )

        self._rate_limit: int = 0
        self._rate_limit_remaining: int = 0
        self._rate_limit_reset: datetime | None = None

    @property
    def rate_limit(self) -> int:
        """Returns the rate limit for the API."""
        return self._rate_limit

    @property
    def rate_limit_remaining(self) -> int:
        """Returns the remaining rate limit for the API."""
        return self._rate_limit_remaining

    @property
    def rate_limit_reset(self) -> datetime | None:
        """Returns the time when the rate limit will reset."""
        return self._rate_limit_reset

    @property
    def rate_limit_percent(self) -> float:
        """Returns the percentage of the rate limit remaining."""
        if self.rate_limit == 0:
            return 0.0

        return self.rate_limit_remaining / self.rate_limit * 100

    def agencies(self) -> list[AgencyInfo]:
        return cast(list[AgencyInfo], self._get("agencies"))

    def routes(self, agency_id: str | None = None) -> list[RouteInfo]:
        if not agency_id:
            agency_id = self.agency_id

        return cast(list[RouteInfo], self._get(f"agencies/{agency_id}/routes"))

    def route_details(
        self, route_id: str, agency_id: str | None = None
    ) -> RouteDetails:
        """Includes stops and directions."""
        agency_id = agency_id or self.agency_id
        if not agency_id:
            raise NextBusValidationError("Agency ID is required")

        return cast(RouteDetails, self._get(f"agencies/{agency_id}/routes/{route_id}"))

    def predictions_for_stop(
        self,
        stop_id: str | int,
        route_id: str | None = None,
        direction_id: str | None = None,
        agency_id: str | None = None,
    ) -> list[StopPrediction]:
        """Returns predictions for a stop."""
        agency_id = agency_id or self.agency_id
        if not agency_id:
            raise NextBusValidationError("Agency ID is required")

        if direction_id:
            if not route_id:
                raise NextBusValidationError("Direction ID provided without route ID")

        if route_id:
            predictions = cast(
                list[StopPrediction],
                self._get(
                    f"agencies/{agency_id}/nstops/{route_id}:{stop_id}/predictions"
                ),
            )
        else:
            predictions = cast(
                list[StopPrediction],
                self._get(f"agencies/{agency_id}/stops/{stop_id}/predictions"),
            )

        # If route not provided, return all predictions as the API returned them
        if not route_id:
            return predictions

        # HACK: Filter predictions based on stop and route because the API seems to ignore the route
        predictions = [
            prediction_result
            for prediction_result in predictions
            if (
                prediction_result["stop"]["id"] == stop_id
                and prediction_result["route"]["id"] == route_id
            )
        ]

        # HACK: Filter predictions based on direction in case the API returns extra predictions
        if direction_id:
            for prediction_result in predictions:
                prediction_result["values"] = [
                    prediction
                    for prediction in prediction_result["values"]
                    if prediction["direction"]["id"] == direction_id
                ]

        return predictions

    def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> Any:
        if params is None:
            params = {}

        try:
            url = f"{self.base_url}/{endpoint}"
            LOG.debug("GET %s", url)
            response = self._session.get(url, params=params)
            response.raise_for_status()

            # Track rate limit information
            self._rate_limit = int(response.headers.get("X-RateLimit-Limit", 0))
            self._rate_limit_remaining = int(
                response.headers.get("X-RateLimit-Remaining", 0)
            )
            reset_time = response.headers.get("X-RateLimit-Reset")
            self._rate_limit_reset = (
                datetime.fromtimestamp(int(reset_time)) if reset_time else None
            )

            return response.json()

        except HTTPError as exc:
            raise NextBusHTTPError("Error from the NextBus API", exc) from exc
        except json.decoder.JSONDecodeError as exc:
            raise NextBusFormatError("Failed to parse JSON from request") from exc
