from bisect import bisect_left
from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
    from kindle_dashboard.weather import Forecast

RAIN_THRESHOLD_MM = 0.2
RAIN_EVENT_GAP_TOLERANCE_HOURS = 1.0
NO_RAIN_LABEL = "No rain in 7d"


def build_next_rain_header_message(
    forecast: Forecast,
    now: datetime,
    *,
    rain_threshold_mm: float = RAIN_THRESHOLD_MM,
    rain_gap_tolerance_hours: float = RAIN_EVENT_GAP_TOLERANCE_HOURS,
) -> str | None:
    next_window = build_next_hourly_rain_window(
        forecast,
        now,
        rain_threshold_mm=rain_threshold_mm,
        rain_gap_tolerance_hours=rain_gap_tolerance_hours,
    )
    if next_window is not None:
        start_time, end_time = next_window
        raining_now = start_time <= now < end_time
        start_label = "now" if raining_now else start_time.strftime("%H:%M")
        last_timestamp = (
            forecast.hourly_datetimes[-1] if forecast.hourly_datetimes else None
        )
        if last_timestamp is not None and end_time >= last_timestamp:
            return (
                "Raining all day"
                if raining_now
                else f"Next Rain {start_label} onwards"
            )
        if raining_now:
            return f"Raining until {end_time.strftime('%H:%M')}"
        return f"Next Rain {start_label} to {end_time.strftime('%H:%M')}"

    next_day = build_next_rain_day(forecast, now, rain_threshold_mm=rain_threshold_mm)
    if next_day is None:
        return NO_RAIN_LABEL

    today = now.date()
    if next_day == today:
        return "Next Rain today"
    if next_day == (today + timedelta(days=1)):
        return "Next Rain tomorrow"
    return f"Next Rain {next_day.strftime('%a')}"


def build_next_hourly_rain_window(
    forecast: Forecast,
    now: datetime,
    *,
    rain_threshold_mm: float,
    rain_gap_tolerance_hours: float = RAIN_EVENT_GAP_TOLERANCE_HOURS,
) -> tuple[datetime, datetime] | None:
    window_start: datetime | None = None
    window_end: datetime | None = None
    gap_tolerance = timedelta(hours=rain_gap_tolerance_hours)

    for interval_end, precipitation_value in zip(
        forecast.hourly_datetimes,
        forecast.precipitation,
        strict=False,
    ):
        if precipitation_value < rain_threshold_mm:
            continue

        if interval_end <= now:
            continue

        interval_start = interval_end - timedelta(hours=1)
        if window_start is None:
            window_start = interval_start
            window_end = interval_end
            continue
        assert window_end is not None
        if interval_start > (window_end + gap_tolerance):
            break
        window_end = max(window_end, interval_end)

    if window_start is None or window_end is None:
        return None

    return window_start, window_end


def build_next_rain_day(
    forecast: Forecast,
    now: datetime,
    *,
    rain_threshold_mm: float,
) -> date | None:
    for day, precipitation_sum in zip(
        forecast.daily_dates,
        forecast.daily_precipitation_sum,
        strict=False,
    ):
        if precipitation_sum < rain_threshold_mm:
            continue
        if day >= now.date():
            return day

    return None


def build_sun_event_markers(
    forecast: Forecast,
    window_start: datetime,
    window_end: datetime,
) -> list[tuple[datetime, bool]]:
    markers = [
        (sunrise, True)
        for sunrise in forecast.daily_sunrise_datetimes
        if window_start <= sunrise <= window_end
    ]
    markers.extend(
        (sunset, False)
        for sunset in forecast.daily_sunset_datetimes
        if window_start <= sunset <= window_end
    )

    markers.sort(key=lambda marker: marker[0])
    return markers


def build_hourly_series(
    forecast: Forecast,
    values: list[float],
    hour_slots: list[datetime],
    *,
    hour_shift: int,
) -> list[float]:
    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
        key = shifted_timestamp.replace(minute=0, second=0, microsecond=0)
        lookup[key] = value

    assert lookup, "Forecast series did not provide any hourly values"

    missing = object()
    known_slots: list[datetime] | None = None
    known_values: list[float] | None = None
    series: list[float] = []

    for slot in hour_slots:
        value = lookup.get(slot, missing)
        if value is not missing:
            series.append(cast("float", value))
            continue

        if known_slots is None:
            known_slots = sorted(lookup)
            known_values = [lookup[known_slot] for known_slot in known_slots]
        assert known_values is not None

        series.append(_nearest_slot_value(slot, known_slots, known_values))

    return series


def _nearest_slot_value(
    slot: datetime,
    known_slots: list[datetime],
    known_values: list[float],
) -> float:
    idx = bisect_left(known_slots, slot)

    if idx == 0:
        return known_values[0]
    if idx == len(known_slots):
        return known_values[-1]

    previous_slot = known_slots[idx - 1]
    next_slot = known_slots[idx]
    previous_gap = slot - previous_slot
    next_gap = next_slot - slot

    # Keep deterministic behaviour for exact ties by preferring the earlier slot.
    if previous_gap <= next_gap:
        return known_values[idx - 1]
    return known_values[idx]


def normalise_chart_now(now: datetime, forecast: Forecast) -> datetime:
    return now.astimezone(forecast.forecast_timezone).replace(tzinfo=None)
