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

from .weather import Forecast, ForecastError, fetch_forecast_data


@dataclass(slots=True)
class ForecastState:
    value: Forecast | ForecastError | None = None
    updated_at: datetime | None = None


_forecast_lock = Lock()
_latest_forecast_state = ForecastState()
logger = logging.getLogger(__name__)


def get_latest_forecast() -> Forecast | ForecastError | None:
    with _forecast_lock:
        return _latest_forecast_state.value


def get_latest_forecast_updated_at() -> datetime | None:
    with _forecast_lock:
        return _latest_forecast_state.updated_at


def _set_latest_forecast(forecast: Forecast | ForecastError | None) -> None:
    with _forecast_lock:
        _latest_forecast_state.value = forecast


def _set_latest_forecast_and_updated_at(
    forecast: Forecast | ForecastError | None,
    updated_at: datetime | None,
) -> None:
    with _forecast_lock:
        _latest_forecast_state.value = forecast
        _latest_forecast_state.updated_at = updated_at


def clear_latest_forecast() -> None:
    _set_latest_forecast_and_updated_at(None, None)


def update_forecast(latitude: float, longitude: float) -> None:
    try:
        forecast = fetch_forecast_data(latitude, longitude)
    except Exception as exc:
        logger.exception(
            "Unexpected error while fetching weather (latitude=%s, longitude=%s)",
            latitude,
            longitude,
        )
        forecast = ForecastError(message=f"Unable to fetch weather:\n{exc}")

    if isinstance(forecast, ForecastError):
        logger.warning(
            "Forecast update failed (latitude=%s, longitude=%s): %s",
            latitude,
            longitude,
            forecast.message,
        )
    else:
        logger.info(
            "Forecast updated (latitude=%s, longitude=%s, points=%s)",
            latitude,
            longitude,
            len(forecast.time),
        )

    if isinstance(forecast, Forecast):
        _set_latest_forecast_and_updated_at(
            forecast,
            updated_at=datetime.now(tz=UTC),
        )
    else:
        _set_latest_forecast(forecast)


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")
