"""When "Według planu" weather starts. The plan is a start TIME and a pace;
its date is the Journey's date. The weather never shows empty charts
because of a date (AV-048, replacing AV-035's rule).

- By default: a simulated start at the next time the start time comes round
  in the Journey's own timezone -- today if it is still ahead, else tomorrow
  (NEXT_START_TIME) -- with days 2..n from there; the reason is said
  (the saved start has passed -- also while the trip is under way --, is
  beyond the forecast, or there is no date).
- The real dates (PLAN_DATES) only when the WHOLE trip is still ahead and
  its start lies within the forecast's reach.
- On request (``prefer_saved``, the page's "Pokaż według zapisanych dat")
  the saved dates whatever they are (PLAN_DATES, ``forced``): the passed
  part of the route then says "data minęła" and the part beyond the
  forecast when it becomes available -- never a bare dash.

AV-035's rule took the real dates whenever any part of the trip was still
ahead, so a trip under way (Manaslu: start 30.09, end 04.10) had a forecast
only for its last stretch and dashes everywhere before it.

Timezone: the Journey's own (the plan's journey_timezone, resolved from the
route's first point -- route_timezone.py), never the computer's.
"""

from __future__ import annotations

from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo

PLAN_DATES = "PLAN_DATES"
NEXT_START_TIME = "NEXT_START_TIME"
PLAN_DATE_PASSED = "PLAN_DATE_PASSED"
PLAN_DATE_BEYOND_FORECAST = "PLAN_DATE_BEYOND_FORECAST"
NO_PLAN_DATE = "NO_PLAN_DATE"


def next_start(start_time: str, timezone_name: str, now: datetime) -> datetime:
    """The next moment the clock in ``timezone_name`` shows ``start_time``
    ("HH:MM"): today when still ahead of ``now``, else tomorrow."""
    zone = ZoneInfo(timezone_name)
    local_now = now.astimezone(zone)
    hours, minutes = (int(part) for part in start_time.split(":"))
    today = datetime.combine(local_now.date(), time(hours, minutes), tzinfo=zone)
    return (
        today
        if today > local_now
        else datetime.combine(
            local_now.date() + timedelta(days=1), time(hours, minutes), tzinfo=zone
        )
    )


def weather_anchor(
    *,
    planned_start_local: str | None,
    start_time: str | None,
    timezone_name: str,
    now: datetime,
    trip_end: datetime | None,
    horizon_days: int,
    prefer_saved: bool = False,
) -> dict | None:
    """Where the planned weather starts; None for a plan with no start time
    at all (then there is no "Według planu" to anchor)."""
    if start_time is None:
        return None
    plan_date = saved_start = None
    in_progress = False
    if planned_start_local is not None:
        start = datetime.fromisoformat(planned_start_local)
        plan_date, saved_start = start.date().isoformat(), start.isoformat()
        end = trip_end or start
        in_progress = start < now <= end
        reach = now + timedelta(days=horizon_days)
        saved = {
            "kind": PLAN_DATES,
            "plan_date": plan_date,
            "saved_start": saved_start,
            "start": start,
            "trip_in_progress": in_progress,
            "saved_dates_available": True,
        }
        if prefer_saved:
            reason = (
                PLAN_DATE_PASSED
                if start < now
                else PLAN_DATE_BEYOND_FORECAST
                if start > reach
                else None
            )
            return {**saved, "reason": reason, "forced": True}
        if now <= start <= reach:
            return {**saved, "reason": None, "forced": False}
        reason = PLAN_DATE_PASSED if start < now else PLAN_DATE_BEYOND_FORECAST
    else:
        reason = NO_PLAN_DATE
    return {
        "kind": NEXT_START_TIME,
        "reason": reason,
        "plan_date": plan_date,
        "saved_start": saved_start,
        "start": next_start(start_time, timezone_name, now),
        "trip_in_progress": in_progress,
        "saved_dates_available": planned_start_local is not None,
        "forced": False,
    }


def anchor_document(anchor: dict | None) -> dict | None:
    if anchor is None:
        return None
    return {**anchor, "start": anchor["start"].isoformat()}


def anchored_plan(
    plan,
    route_points,
    activity_id=None,
    now: datetime | None = None,
    horizon_days: int = 16,
    prefer_saved: bool = False,
):
    """The plan the weather is timed with, and its anchor: the plan itself
    when its dates reach into the forecast, else a copy starting at the next
    start time (its rest days before day 1 dropped -- they are calendar, the
    walk starts at the anchor). A plan with no start time comes back as it
    is, with no anchor."""
    from dataclasses import replace
    from datetime import timezone as utc_zone

    from mountain_twin.pace.pauses import generate_relative_pauses
    from mountain_twin.route_analysis import prepare_route_cached

    from .activities import pace_factor_for_activity
    from .day_derivation import DayDerivationUnavailable, derive_day_segments

    if plan is None:
        return plan, None
    start_time = plan.start_time_of_day()
    if start_time is None:
        return plan, None
    now = now or datetime.now(utc_zone.utc)
    trip_end = None
    if plan.planned_start_local is not None:
        try:
            prepared = prepare_route_cached(route_points).points
            days = derive_day_segments(
                route_points,
                pace_factor=pace_factor_for_activity(activity_id, plan.pace_policy_id),
                pauses=generate_relative_pauses(prepared[-1].cumulative_distance_m),
                start_datetime=datetime.fromisoformat(plan.planned_start_local),
                camp_markers=plan.camp_markers,
                timezone_name=plan.journey_timezone,
                rest_days_before_start=plan.rest_days_before_start,
            )
            trip_end = days[-1].arrival_time if days else None
        except DayDerivationUnavailable:
            trip_end = None
    anchor = weather_anchor(
        planned_start_local=plan.planned_start_local,
        start_time=start_time,
        timezone_name=plan.journey_timezone,
        now=now,
        trip_end=trip_end,
        horizon_days=horizon_days,
        prefer_saved=prefer_saved,
    )
    if anchor["kind"] == PLAN_DATES:
        return plan, anchor
    start = anchor["start"]
    return (
        replace(
            plan,
            planned_start_local=start.isoformat(),
            planned_start_utc=start.astimezone(utc_zone.utc).isoformat(),
            rest_days_before_start=0,
        ),
        anchor,
    )
