import io
import logging
import math
from datetime import UTC, datetime
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import TYPE_CHECKING
from urllib.parse import parse_qs, urlparse

from .config import load_server_config
from .forecast_updater import (
    ForecastFailedNoData,
    ForecastFailedStale,
    ForecastLoading,
    ForecastReady,
    clear_latest_forecast,
    get_forecast_state,
    start_forecast_updater,
    stop_forecast_updater,
)
from .logging_config import configure_logging
from .render import render_dashboard

if TYPE_CHECKING:
    from .weather import Forecast

logger = logging.getLogger(__name__)


def _render_dashboard_png_payload(
    width: int,
    height: int,
    battery_level: int | None,
    supersample_scale: int,
    forecast: Forecast | None,
    forecast_error_message: str | None,
    *,
    weather_configured: bool,
    weather_data_stale: bool,
    weather_data_stale_hours: int | None,
    next_rain_gap_tolerance_hours: float,
) -> bytes:
    image = render_dashboard(
        width,
        height,
        battery_level=battery_level,
        forecast=forecast,
        forecast_error_message=forecast_error_message,
        weather_configured=weather_configured,
        weather_data_stale=weather_data_stale,
        weather_data_stale_hours=weather_data_stale_hours,
        supersample_scale=supersample_scale,
        next_rain_gap_tolerance_hours=next_rain_gap_tolerance_hours,
    )
    # Render final image as greyscale PNG.
    buffer = io.BytesIO()
    image.save(buffer, format="PNG", optimize=False, compress_level=6)
    return buffer.getvalue()


def _normalise_forecast_state(
    forecast_state: ForecastLoading
    | ForecastReady
    | ForecastFailedNoData
    | ForecastFailedStale,
) -> tuple[Forecast | None, str | None, datetime | None]:
    if isinstance(forecast_state, ForecastReady):
        return forecast_state.forecast, None, forecast_state.updated_at
    if isinstance(forecast_state, ForecastFailedStale):
        return None, forecast_state.message, forecast_state.last_successful_update
    if isinstance(forecast_state, ForecastFailedNoData):
        return None, forecast_state.message, None
    return None, None, None


class DashboardHandler(BaseHTTPRequestHandler):
    default_width: int
    default_height: int
    render_supersample_scale: int
    weather_update_secs: int
    next_rain_gap_tolerance_hours: float
    weather_configured: bool
    _request_battery_level: int | None = None
    _response_png_bytes: int | None = None

    def do_GET(self) -> None:
        parsed = urlparse(self.path)
        if parsed.path != "/":
            logger.warning("Received request for unknown path: %s", parsed.path)
            self.send_error(HTTPStatus.NOT_FOUND, "Not found")
            return

        self._serve_dashboard_png(parsed.query)

    def _serve_dashboard_png(self, raw_query: str) -> None:
        raw_battery_level = parse_qs(raw_query).get("batteryLevel", [None])[-1]
        battery_level = int(raw_battery_level) if raw_battery_level else None
        (
            forecast,
            forecast_error_message,
            forecast_updated_at,
        ) = _normalise_forecast_state(get_forecast_state())
        weather_data_stale = False
        weather_data_stale_hours: int | None = None
        if self.weather_configured and forecast_updated_at is not None:
            weather_data_age_hours = _weather_data_age_hours(forecast_updated_at)
            weather_data_stale = _is_weather_data_stale(
                forecast_updated_at,
                self.weather_update_secs,
            )
            if weather_data_stale:
                weather_data_stale_hours = max(1, math.ceil(weather_data_age_hours))
        payload = _render_dashboard_png_payload(
            self.default_width,
            self.default_height,
            battery_level=battery_level,
            supersample_scale=self.render_supersample_scale,
            forecast=forecast,
            forecast_error_message=forecast_error_message,
            weather_configured=self.weather_configured,
            weather_data_stale=weather_data_stale,
            weather_data_stale_hours=weather_data_stale_hours,
            next_rain_gap_tolerance_hours=self.next_rain_gap_tolerance_hours,
        )
        self._request_battery_level = battery_level
        payload_size = len(payload)
        self._response_png_bytes = payload_size

        self.send_response(HTTPStatus.OK)
        self.send_header("Content-Type", "image/png")
        self.send_header("Content-Length", str(payload_size))
        self.end_headers()
        self.wfile.write(payload)

    def log_message(self, format: str, *args: object) -> None:  # noqa: A002
        message = format % args
        if self._response_png_bytes is not None:
            logger.info(
                '%s - "%s" (png_bytes=%s, battery_level=%s)',
                self.client_address[0],
                message,
                self._response_png_bytes,
                self._request_battery_level,
            )
            return

        logger.info('%s - "%s"', self.client_address[0], message)


def _is_weather_data_stale(
    forecast_updated_at: datetime,
    weather_update_secs: int,
) -> bool:
    return _weather_data_age_hours(forecast_updated_at) > (
        float(weather_update_secs) / 3600.0
    )


def _weather_data_age_hours(forecast_updated_at: datetime) -> float:
    return max(
        0.0,
        (datetime.now(tz=UTC) - forecast_updated_at).total_seconds() / 3600.0,
    )


def main() -> None:
    configure_logging()
    logger.info("Starting kindle dashboard service")

    config = load_server_config()

    logger.info("Server configuration loaded")
    clear_latest_forecast()
    logger.info("Forecast state cleared")

    if config.weather_latitude is None:
        logger.warning(
            "WEATHER_LATITUDE and WEATHER_LONGITUDE are not set; "
            "weather data will not be displayed."
        )

    server = ThreadingHTTPServer(
        server_address=(config.host, config.port),
        RequestHandlerClass=DashboardHandler,
    )
    server.daemon_threads = True
    DashboardHandler.default_width = config.default_width
    DashboardHandler.default_height = config.default_height
    DashboardHandler.render_supersample_scale = config.render_supersample_scale
    DashboardHandler.weather_update_secs = config.weather_update_secs
    DashboardHandler.next_rain_gap_tolerance_hours = (
        config.next_rain_gap_tolerance_hours
    )

    updater_thread = None
    stop_event = None
    latitude = config.weather_latitude
    longitude = config.weather_longitude
    if latitude is not None:
        assert longitude is not None
        DashboardHandler.weather_configured = True
        updater_thread, stop_event = start_forecast_updater(
            latitude,
            longitude,
            config.weather_update_secs,
        )
    else:
        DashboardHandler.weather_configured = False
        logger.info("Weather updater is disabled")

    logger.info(
        "Serving kindle dashboard on http://%s:%s/ "
        "(default: %sx%s, supersample: %sx, "
        "png mode: indexed 16-level greyscale)",
        config.host,
        config.port,
        config.default_width,
        config.default_height,
        config.render_supersample_scale,
    )
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        logger.info("Received interrupt signal, shutting down")
    finally:
        server.server_close()
        logger.info("HTTP server stopped")
        if stop_event is not None and updater_thread is not None:
            stop_forecast_updater(stop_event, updater_thread)
