from datetime import datetime, date, timezone
from http import HTTPStatus
from typing import List, Type, TypeVar, Dict, Callable
from typing import Optional

import requests
import urllib3
from pydantic import BaseModel, Field, ConfigDict
from pydantic import model_validator

from vito.sas.air import logger
from vito.sas.air.api_client import RESTclient

T = TypeVar('T')

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class StationListItem(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    nsit: int
    isit: Optional[str]
    name: Optional[str]

    l72_x: Optional[float]  # X coordinate in EPSG:31370
    l72_y: Optional[float]  # Y coordinate in EPSG:31370

    wgs84_lon: Optional[float] # Longitude in WGS84 (EPSG:4326)
    wgs84_lat: Optional[float] # Latitude in WGS84 (EPSG:4326)

    @model_validator(mode="after")
    def validate_after(self) -> 'StationListItem':
        # compute latitude and longitude from x and y if not provided or vice versa
        if self.wgs84_lon is None or self.wgs84_lat is None:
            if self.l72_x is not None and self.l72_y is not None:
                # Convert EPSG:31370 to EPSG:4326
                from pyproj import Transformer
                transformer = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
                self.longitude, self.latitude = transformer.transform(self.l72_x, self.l72_y)
            else:
                raise Exception("Either latitude and longitude or x and y coordinates must be provided")
        if self.l72_x is None or self.l72_y is None:
            if self.wgs84_lon is not None and self.wgs84_lat is not None:
                # Convert EPSG:4326 to EPSG:31370
                from pyproj import Transformer
                transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
                self.l72_x, self.l72_y = transformer.transform(self.wgs84_lon, self.wgs84_lat)
            else:
                raise Exception("Either latitude and longitude or x and y coordinates must be provided")
        return self

    def __repr__(self) -> str:
        return self.__str__()

    def __str__(self) -> str:
        return f"StationListItem (nsit: {self.nsit}, isit: {self.isit}, name: {self.name}, l72_x: {self.l72_x}, l72_y: {self.l72_y},  wgs84_lon: {self.wgs84_lon}, wgs84_lat: {self.wgs84_lat})"


class MesureHoraire(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    m_unit: Optional[str]
    location_code: str
    param_name: Optional[str]
    param_code: str
    m_date: Optional[date]
    m_datetime_utc: datetime
    m_value: Optional[float]
    m_height: Optional[float]
    l72_x: Optional[float]
    l72_y: Optional[float]
    wgs84_lon: Optional[float]
    wgs84_lat: Optional[float]

    @model_validator(mode="after")
    def validate_after(self) -> 'MesureHoraire':
        if isinstance(self.m_datetime_utc, datetime):
            # just ensure it's timezone-aware in UTC
            self.m_datetime_utc = self.m_datetime_utc.replace(tzinfo=timezone.utc)

        return self

    def __repr__(self) -> str:
        return self.__str__()

    def __str__(self) -> str:
        return f"MesureHoraire (location_code: {self.location_code}, param_name: {self.param_name}, param_code: {self.param_code}, m_date: {self.m_date}, m_datetime_utc: {self.m_datetime_utc}, m_value: {self.m_value}, m_height: {self.m_height}, l72_x: {self.l72_x}, l72_y: {self.l72_y},  wgs84_lon: {self.wgs84_lon}, wgs84_lat: {self.wgs84_lat})"


class StationParam(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    param_name: Optional[str]
    label: Optional[str]
    param_code: str
    saroad: Optional[int]
    unit: str
    height: Optional[float]

    def __repr__(self) -> str:
        return self.__str__()

    def __str__(self) -> str:
        return f"StationParam (param_name: {self.param_name}, label: {self.label}, param_code: {self.param_code}, saroad: {self.saroad}, unit: {self.unit}, height: {self.height})"


class ForecastHourly(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

# {"id":980260,"stationId":268,"stationCode":"FLM3M6"

    id: int
    station_id: int = Field(alias="stationId")
    station_code: str = Field(alias="stationCode")
    run_time: datetime = Field(alias="runTime")
    forecast_time: datetime = Field(alias="forecastTime")
    no2: Optional[float]
    o3: Optional[float]
    pm10: Optional[float]
    pm25: Optional[float]
    lon: Optional[float]
    lat: Optional[float]
    model_type: Optional[str] = Field(alias="modelType", default=None)
    model_version: Optional[str]  = Field(alias="modelVersion", default=None)

    @model_validator(mode="after")
    def validate_after(self) -> 'ForecastHourly':
        self.run_time = self.run_time.replace(tzinfo=timezone.utc)
        self.forecast_time = self.forecast_time.replace(tzinfo=timezone.utc)
        return self

    def __repr__(self) -> str:
        return self.__str__()

    def __str__(self) -> str:
        return f"ForecastHourly (id: {self.id}, station_code: {self.station_code}, run_time: {self.run_time}, forecast_time: {self.forecast_time}, no2: {self.no2}, o3: {self.o3}, pm10: {self.pm10}, pm25: {self.pm25}, lon: {self.lon}, lat: {self.lat}, model_type: {self.model_type}, model_version: {self.model_version})"


class IssepAirClient(RESTclient):
    """
    Client for the ISSEP AIR API:   https://opendata.issep.be/env/air/api/rm_camsncpbe
    Swagger: https://opendata.issep.be/env/air/api/rm_camsncpbe/index.html
    """

    def __init__(self, base_url: Optional[str] = None, session: Optional[requests.Session] = None, token: Optional[str | Callable] = None):
        """
        Initialize the client.

        Args:
            base_url: The base URL of the SOS API. Defaults to "https://opendata.issep.be/env/air/api/rm_camsncpbe"
            session: The requests session to use. If None, a new session will be created.
        """
        super().__init__(base_url or "https://opendata.issep.be/env/air/api/rm_camsncpbe", session)
        self.token = token
        logger.info(f"Initialized IssepAirClient with base URL: {self.base_url}")

    # -------------------------------------------------------------------------
    # MEASUREMENTS API
    # -------------------------------------------------------------------------

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/locations
    def get_locations(self) -> List[StationListItem]:
        logger.info("Getting available locations")
        full_url = self._build_url("locations")
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=StationListItem)

    #  https://opendata.issep.be/env/air/api/rm_camsncpbe/locations/{location_code}/params
    def get_params(self, location_code: str) -> List[StationParam]:
        logger.info("Getting available parameters for a location")
        full_url = self._build_url("locations", location_code, "params")
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=StationParam)

    # 'https://opendata.issep.be/env/air/api/rm_camsncpbe/lastdata
    def get_lastdata(self,) -> List[MesureHoraire]:
        logger.info("Getting last available measurements")
        full_url = self._build_url("lastdata")
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=MesureHoraire)

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/locations/RMCH32_LITE1/params/PM10/lastdata
    def get_lastdata_for_locations_and_param(self, location_code: str, param_name: str) -> List[MesureHoraire]:
        logger.info("Getting last available measurements")
        full_url = self._build_url("locations", location_code, "params", param_name, "lastdata")
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=MesureHoraire)

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/locations/RMCH32_LITE1/params/PM10/start/2026-01-01/end/2026-01-30'
    def get_data_for_locations_and_param(self, location_code: str, param_name: str, start_date: date, end_date: date) -> List[MesureHoraire]:
        logger.info("Getting available measurements for a location and parameter in a date range")
        start_date_str: str = start_date.isoformat()
        end_date_str: str = end_date.isoformat()
        full_url = self._build_url("locations", location_code, "params", param_name, "start", start_date_str, "end", end_date_str)
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=MesureHoraire)

    # -------------------------------------------------------------------------
    # FORECAST API
    # -------------------------------------------------------------------------

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/forecast/ms
    def get_last_forecasts(self) -> List[ForecastHourly]:
        logger.info("Getting last available forecasts.")
        full_url = self._build_url("forecast", "ms")
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=ForecastHourly)

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/forecast/ms/station/10324
    def get_last_forecasts_for_station(self, station_id: int) -> List[ForecastHourly]:
        logger.info("Getting last available forecasts for a station.")
        full_url = self._build_url("forecast", "ms", "station", str(station_id))
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=ForecastHourly)

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/forecast/ms/from/2026-01-10/to/2026-01-12
    def get_forecasts(self,  start_date: date, end_date: date) -> List[ForecastHourly]:
        logger.info("Getting the forecasts for a date range.")
        full_url = self._build_url("forecast", "ms", "from", start_date.isoformat(), "to", end_date.isoformat())
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=ForecastHourly)

    # https://opendata.issep.be/env/air/api/rm_camsncpbe/forecast/ms/from/2026-01-10/to/2026-01-12/station/40AL01
    def get_forecasts_for_station(self,  start_date: date, end_date: date, station_code: str) -> List[ForecastHourly]:
        logger.info("Getting the forecasts for a date range for a station.")
        full_url = self._build_url("forecast", "ms", "from", start_date.isoformat(), "to", end_date.isoformat(), "station", station_code)
        response = self._exec_get(full_url)
        return self._parse_list_response(response, model=ForecastHourly)

    def _parse_list_response(self, response: requests.Response, model: Type[T]) -> List[T]:
        """
        Parses a list response into a List of the specified model.
        """
        try:
            if response.status_code >= HTTPStatus.BAD_REQUEST:
                logger.exception(response.text)
            response.raise_for_status()
            return [model(**item) for item in response.json()]
        except requests.RequestException as e:
            logger.error(f"HTTP error: {e} | URL: {response.url}")
            raise
        except Exception as e:
            logger.error(f"List response parsing failed: {e} | Response: {response.text}")
            raise

    def _parse_typed_response(self, response: requests.Response, model: Type[T]) -> T:
        """
        Parses a single response into the specified model.
        """
        try:
            if response.status_code >= HTTPStatus.BAD_REQUEST:
                logger.exception(response.text)
            response.raise_for_status()
            response_object = response.json()

            if isinstance(response_object, model):
                return response_object
            return model(**response_object)
        except requests.RequestException as e:
            logger.error(f"HTTP error: {e} | URL: {response.url}")
            raise
        except Exception as e:
            logger.error(f"Response parsing failed: {e} | Response: {response.text}")
            raise

    def _base_headers(self) -> Dict[str, str]:
        """Get base headers for API requests"""
        token_str = None
        if isinstance(self.token, Callable):
            token_str = self.token()
        elif isinstance(self.token, str):
            token_str = self.token
        if token_str is not None:
            return {
                "Accept": "application/json",
                "Authorization": f"Bearer {token_str}"
            }
        else:
            return {
                "Accept": "application/json"
            }
