import logging
from dataclasses import dataclass
from datetime import UTC, datetime
from threading import Event, Lock, Thread

from .weather import Forecast, WeatherFetchError, fetch_forecast_data


@dataclass(frozen=True, slots=True)
class ForecastLoading:
    message: str = "Loading weather data"


@dataclass(frozen=True, slots=True)
class ForecastReady:
    forecast: Forecast
    updated_at: datetime


@dataclass(frozen=True, slots=True)
class ForecastFailedNoData:
    message: str


@dataclass(frozen=True, slots=True)
class ForecastFailedStale:
    message: str
    last_successful_update: datetime


type ForecastState = (
    ForecastLoading | ForecastReady | ForecastFailedNoData | ForecastFailedStale
)


_forecast_lock = Lock()
_latest_forecast_state: list[ForecastState] = [ForecastLoading()]
logger = logging.getLogger(__name__)


def get_forecast_state() -> ForecastState:
    with _forecast_lock:
        return _latest_forecast_state[0]


def clear_latest_forecast() -> None:
    with _forecast_lock:
        _latest_forecast_state[0] = ForecastLoading()


def _last_successful_update(state: ForecastState) -> datetime | None:
    if isinstance(state, ForecastReady):
        return state.updated_at
    if isinstance(state, ForecastFailedStale):
        return state.last_successful_update
    return None


def update_forecast(latitude: float, longitude: float) -> None:
    try:
        forecast = fetch_forecast_data(latitude, longitude)
    except WeatherFetchError as exc:
        with _forecast_lock:
            previous_state = _latest_forecast_state[0]
            last_successful_update = _last_successful_update(previous_state)
            if last_successful_update is not None:
                _latest_forecast_state[0] = ForecastFailedStale(
                    message=str(exc),
                    last_successful_update=last_successful_update,
                )
            else:
                _latest_forecast_state[0] = ForecastFailedNoData(message=str(exc))
        logger.warning(
            "Forecast update failed (latitude=%s, longitude=%s): %s",
            latitude,
            longitude,
            exc,
        )
        return

    updated_at = datetime.now(tz=UTC)
    with _forecast_lock:
        _latest_forecast_state[0] = ForecastReady(
            forecast=forecast, updated_at=updated_at
        )

    logger.info(
        "Forecast updated (latitude=%s, longitude=%s, points=%s)",
        latitude,
        longitude,
        len(forecast.time),
    )


def run_forecast_updater(
    latitude: float,
    longitude: float,
    weather_update_secs: int,
    stop_event: Event,
) -> None:
    logger.info("Forecast updater loop started")
    while not stop_event.is_set():
        update_forecast(latitude, longitude)
        if stop_event.wait(weather_update_secs):
            break
    logger.info("Forecast updater loop stopped")


def start_forecast_updater(
    latitude: float,
    longitude: float,
    weather_update_secs: int,
) -> tuple[Thread, Event]:
    logger.info(
        "Starting forecast updater (interval=%ss, latitude=%s, longitude=%s)",
        weather_update_secs,
        latitude,
        longitude,
    )
    stop_event = Event()
    updater_thread = Thread(
        target=run_forecast_updater,
        args=(latitude, longitude, weather_update_secs, stop_event),
        daemon=True,
    )
    updater_thread.start()
    return updater_thread, stop_event


def stop_forecast_updater(stop_event: Event, _updater_thread: Thread) -> None:
    stop_event.set()
    # We deliberately do not care about joining the daemon updater thread.
    logger.info("Signalled forecast updater to stop")
