from datetime import timedelta
from typing import TYPE_CHECKING, cast

from .chart_context import CHART_HOURS
from .common import COLOUR_BLACK, COLOUR_WHITE, ChartBounds, build_vertical_band

if TYPE_CHECKING:
    from datetime import datetime

    from PIL import ImageDraw

    from kindle_dashboard.weather import Forecast

    from .chart_context import ChartRenderContext

CLOUD_COVER_PLOT_HEIGHT_RATIO = 0.05


def clamp_percentage(value: float) -> float:
    return max(0.0, min(100.0, value))


def combine_cloud_cover(low_cloud_cover: float, mid_cloud_cover: float) -> float:
    return clamp_percentage(max(low_cloud_cover, mid_cloud_cover / 2.0))


def cloud_cover_to_greyscale(cloud_cover: float) -> int:
    clamped_cloud_cover = clamp_percentage(cloud_cover)
    return round(
        COLOUR_WHITE - ((clamped_cloud_cover / 100.0) * (COLOUR_WHITE - COLOUR_BLACK))
    )


def _build_strict_hourly_series(
    forecast: Forecast,
    values: list[float],
    hour_slots: list[datetime],
    *,
    hour_shift: int,
    series_name: str,
) -> list[float]:
    if len(hour_slots) != CHART_HOURS:
        msg = "Cloud cover renderer requires a 24-hour display window"
        raise ValueError(msg)

    lookup: dict[datetime, float] = {}
    shift_delta = timedelta(hours=hour_shift)
    for timestamp, value in zip(forecast.hourly_datetimes, values, strict=False):
        shifted_timestamp = timestamp + shift_delta
        lookup[shifted_timestamp.replace(minute=0, second=0, microsecond=0)] = value

    missing = object()
    series: list[float] = []
    missing_slots: list[datetime] = []
    for slot in hour_slots:
        value = lookup.get(slot, missing)
        if value is missing:
            missing_slots.append(slot)
            continue
        series.append(cast("float", value))

    if missing_slots:
        first_missing_slot = missing_slots[0].isoformat(timespec="minutes")
        msg = (
            f"Cloud cover series '{series_name}' did not provide {CHART_HOURS} points "
            "for the display window; "
            f"missing {len(missing_slots)} point(s) "
            f"(first missing slot: {first_missing_slot})"
        )
        raise ValueError(msg)

    return series


def build_cloud_cover_series(
    forecast: Forecast,
    hour_slots: list[datetime],
) -> list[float]:
    # Open-Meteo documents low/mid cloud cover as instantaneous hourly values,
    # so we draw each timestamp's value from that hour onwards.
    slot_low_cloud_cover = _build_strict_hourly_series(
        forecast,
        forecast.cloudcover_low,
        hour_slots,
        hour_shift=0,
        series_name="cloudcover_low",
    )
    slot_mid_cloud_cover = _build_strict_hourly_series(
        forecast,
        forecast.cloudcover_mid,
        hour_slots,
        hour_shift=0,
        series_name="cloudcover_mid",
    )
    return [
        combine_cloud_cover(low_cloud_cover, mid_cloud_cover)
        for low_cloud_cover, mid_cloud_cover in zip(
            slot_low_cloud_cover, slot_mid_cloud_cover, strict=False
        )
    ]


def draw_cloud_cover_chart(
    draw: ImageDraw.ImageDraw,
    chart_bounds: ChartBounds,
    chart_context: ChartRenderContext,
    forecast: Forecast,
) -> None:
    slot_cloud_cover = build_cloud_cover_series(forecast, chart_context.hour_slots)
    cloud_cover_top, cloud_cover_bottom = build_vertical_band(
        chart_bounds.top,
        chart_bounds.bottom,
        top_ratio=0.0,
        bottom_ratio=CLOUD_COVER_PLOT_HEIGHT_RATIO,
    )

    for index, cloud_cover in enumerate(slot_cloud_cover[:-1]):
        interval_left = chart_context.x_positions[index]
        interval_right = chart_context.x_positions[index + 1]
        fill_colour = cloud_cover_to_greyscale(cloud_cover)
        draw.rectangle(
            (interval_left, cloud_cover_top, interval_right, cloud_cover_bottom),
            fill=fill_colour,
            outline=fill_colour,
        )
