"""Minimal read/retrieval boundary for the local saved-Journey vertical slice."""

from __future__ import annotations

import json
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Mapping
from zoneinfo import ZoneInfo

from mountain_twin.explorer_service import ExplorerAnalysisService

from .contracts import RouteSelectionAnchor
from .projection import (
    JourneyIntelligenceProjection,
    build_journey_intelligence_projection,
    product_availability,
)
from .repository import LocalJourneyRepository
from .stages import stage_summaries_document


class JourneyReadService:
    """Resolve Journey identity before delegating analysis to existing producers."""

    def __init__(self, root: Path, repository: LocalJourneyRepository, *, producer_document: Callable[[str, datetime], Mapping[str, Any]] | None = None):
        self.root = root
        self.repository = repository
        self._producer_document = producer_document
        self._explorer: ExplorerAnalysisService | None = None

    def get_journey(self, journey_id: str):
        return self.repository.get_journey(journey_id)

    def get_plan(self, journey_id: str, journey_plan_id: str | None = None):
        journey = self.get_journey(journey_id)
        plan = self.repository.get_plan(journey_plan_id or journey.current_plan_id)
        if plan.journey_id != journey_id:
            raise ValueError("plan does not belong to journey")
        return plan

    def get_run(self, journey_id: str, analysis_run_id: str | None = None):
        plan = self.get_plan(journey_id)
        run = self.repository.get_run(analysis_run_id or self.get_journey(journey_id).current_analysis_run_id)
        if run.journey_plan_id != plan.journey_plan_id:
            raise ValueError("analysis run does not belong to selected journey plan")
        return run

    def get_scenario(self, journey_id: str, analysis_scenario_id: str):
        scenario = self.repository.get_scenario(analysis_scenario_id)
        run = self.repository.get_run(scenario.analysis_run_id)
        if self.repository.get_plan(run.journey_plan_id).journey_id != journey_id:
            raise ValueError("scenario does not belong to journey plan")
        return scenario

    def get_support(self, journey_id: str, analysis_run_id: str | None = None) -> dict[str, Any]:
        """State fixed local-slice support before producer construction begins."""
        run = self.get_run(journey_id, analysis_run_id)
        plan = self.repository.get_plan(run.journey_plan_id)
        route = self.repository.get_route_revision(run.route_revision_id)
        return {
            "route": {"state": "AVAILABLE", "route_id": route.route_id},
            "timezone": {"state": "AVAILABLE", "timezone": plan.journey_timezone},
            "terrain_horizon": {"state": "AVAILABLE", "support": "LOCAL_TMB_DAY_1_CACHE_REQUIRED"},
            "weather": {"state": "AVAILABLE", "support": "LOCAL_RESOLVER_DECIDES_AT_ANALYSIS_TIME"},
            "trail_character": {"state": "PARTIAL", "support": "FROZEN_TMB_DAY_1_FIXTURE_SECTION_ONLY"},
        }

    def get_projection(self, journey_id: str, analysis_scenario_id: str) -> JourneyIntelligenceProjection:
        journey = self.get_journey(journey_id)
        scenario = self.get_scenario(journey_id, analysis_scenario_id)
        run = self.repository.get_run(scenario.analysis_run_id)
        plan = self.repository.get_plan(run.journey_plan_id)
        route = self.repository.get_route_revision(run.route_revision_id)
        return build_journey_intelligence_projection(journey=journey, route_revision=route, plan=plan, run=run, scenario=scenario, explorer_document=self._document(route.route_id, scenario.planned_start_local, plan.journey_timezone))

    def get_product_bootstrap(self, journey_id: str, analysis_scenario_id: str) -> dict[str, Any]:
        """Return the compact product read model plus navigation-only route data.

        This deliberately does not expose the Explorer's full point matrix.  The
        navigation series is presentation geometry/timeline supplied by the
        server; point facts continue to resolve through ``get_selection_detail``.
        """
        started = time.perf_counter()
        journey = self.get_journey(journey_id)
        scenario = self.get_scenario(journey_id, analysis_scenario_id)
        run = self.repository.get_run(scenario.analysis_run_id)
        plan = self.repository.get_plan(run.journey_plan_id)
        route = self.repository.get_route_revision(run.route_revision_id)
        document = self._document(route.route_id, scenario.planned_start_local, plan.journey_timezone)
        selected = next(item for item in document["scenarios"] if item["name"] == scenario.name)
        projection = build_journey_intelligence_projection(
            journey=journey, route_revision=route, plan=plan, run=run,
            scenario=scenario, explorer_document=document,
        )
        navigation = [
            {
                "route_point_index": point["point_index"],
                "route_distance_m": point["route_distance_m"],
                "latitude": point["latitude"],
                "longitude": point["longitude"],
                "route_elevation_m": point["route_elevation_m"],
                "planned_arrival": point["planned_arrival_time"],
            }
            for point in selected["points"]
        ]
        anchor = RouteSelectionAnchor(
            route.route_revision_id, scenario.analysis_scenario_id, route_point_index=0
        )
        projection_document = projection.to_dict()
        # Real, preserved multi-day Journey context, only when this Journey's
        # route is genuinely one of the known TMB stages. This never invents
        # geometry/metrics for a stage that has none, and never claims
        # intelligence for a stage without a real AnalysisRun.
        stages_document = stage_summaries_document(self.root, route.route_id)
        return {
            "journey": {"journey_id": journey.journey_id, "title": journey.title},
            "projection": projection_document,
            "scenarios": [
                {"analysis_scenario_id": item.analysis_scenario_id, "name": item.name}
                for item in self.repository.scenarios_for_run(run.analysis_run_id)
            ],
            "navigation": navigation,
            "selection": self._selection_detail_from_document(anchor, selected, run, document),
            "performance": {
                "product_preparation_seconds": time.perf_counter() - started,
                "jip_bytes": len(json.dumps(projection_document, sort_keys=True, allow_nan=False).encode("utf-8")),
                "navigation_point_count": len(navigation),
            },
            **({"stages": stages_document} if stages_document is not None else {}),
        }

    def get_why_detail(self, journey_id: str, analysis_scenario_id: str, domain: str) -> dict[str, Any]:
        """Resolve an L3 record without exposing cache paths or raw producer payloads."""
        projection = self.get_projection(journey_id, analysis_scenario_id).to_dict()
        # JIP's Unified result currently owns the light domain's WHY record.
        why_key = "unified" if domain == "light" else domain
        if why_key not in projection["why_index"]:
            raise ValueError("unknown Journey intelligence domain")
        scenario = self.get_scenario(journey_id, analysis_scenario_id)
        run = self.repository.get_run(scenario.analysis_run_id)
        snapshots = []
        for snapshot_id in run.source_snapshot_ids:
            snapshot = self.repository.get_source_snapshot(snapshot_id)
            snapshots.append({
                "source_snapshot_id": snapshot.source_snapshot_id,
                "source_type": snapshot.source_type,
                "provider": snapshot.provider,
                "provider_run_at": snapshot.provider_run_at,
                "retrieved_at": snapshot.retrieved_at,
            })
        return {
            "domain": domain,
            "why": projection["why_index"][why_key],
            "availability": projection["availability"].get(domain),
            "limitations": projection["limitations"],
            "source_snapshots": snapshots,
        }

    def get_selection_detail(self, journey_id: str, anchor: RouteSelectionAnchor) -> dict[str, Any]:
        """Return one existing producer point; this is navigation, not client science."""
        scenario = self.get_scenario(journey_id, anchor.analysis_scenario_id)
        run = self.repository.get_run(scenario.analysis_run_id)
        if anchor.route_revision_id != run.route_revision_id:
            raise ValueError("selection route revision does not belong to journey")
        route = self.repository.get_route_revision(run.route_revision_id)
        plan = self.repository.get_plan(run.journey_plan_id)
        document = self._document(route.route_id, scenario.planned_start_local, plan.journey_timezone)
        selected = next(item for item in document["scenarios"] if item["name"] == scenario.name)
        return self._selection_detail_from_document(anchor, selected, run, document)

    @staticmethod
    def _selection_detail_from_document(anchor: RouteSelectionAnchor, selected: Mapping[str, Any], run, document: Mapping[str, Any] | None = None) -> dict[str, Any]:
        points = selected["points"]
        if anchor.route_point_index is not None:
            if anchor.route_point_index >= len(points):
                raise ValueError("selection point index outside route")
            point, resolution = points[anchor.route_point_index], "EXACT_ROUTE_POINT"
        else:
            point = min(points, key=lambda item: abs(item["route_distance_m"] - anchor.route_distance_m))
            resolution = "NEAREST_ROUTE_POINT_FOR_DISTANCE_ANCHOR"
        if anchor.planned_arrival is not None and point["planned_arrival_time"] != anchor.planned_arrival:
            raise ValueError("selection planned arrival does not match producer timeline")
        return {
            "anchor": {"route_revision_id": anchor.route_revision_id, "analysis_scenario_id": anchor.analysis_scenario_id, "route_point_index": point["point_index"], "route_distance_m": point["route_distance_m"], "planned_arrival": point["planned_arrival_time"], "resolution": resolution},
            "producer_result_reference": f"unified:{run.analysis_run_id}:{anchor.analysis_scenario_id}",
            "point": point,
            "journey_context": _journey_context(point, selected, document or {}, run, anchor.analysis_scenario_id),
        }

    def _document(self, route_id: str, start: str, journey_timezone: str) -> Mapping[str, Any]:
        instant = datetime.fromisoformat(start).astimezone(ZoneInfo(journey_timezone))
        if self._producer_document is not None:
            return self._producer_document(route_id, instant)
        if self._explorer is None:
            self._explorer = ExplorerAnalysisService(self.root)
        return self._explorer.analyze_explorer_request(route_id, instant)


def _journey_context(point: Mapping[str, Any], selected: Mapping[str, Any], document: Mapping[str, Any], run, scenario_id: str) -> dict[str, Any]:
    """Compact L1/L2 presentation facts for one anchor; no producer science is recalculated."""
    refs = {
        "light": f"unified:{run.analysis_run_id}:{scenario_id}",
        "weather": f"unified:{run.analysis_run_id}:{scenario_id}",
        "snow": f"snow:{run.analysis_run_id}:{scenario_id}",
        "photography": f"photographer:{run.analysis_run_id}:{scenario_id}",
        "trail_character": f"trail_character:{run.route_revision_id}",
        "history": f"history:{run.analysis_run_id}:{scenario_id}",
        "conditions": f"rci:{run.analysis_run_id}:{scenario_id}",
    }
    solar = point.get("solar") or {}
    weather = point.get("weather") or {}
    values = weather.get("values") or {}
    weather_facts = [
        _fact("temperature", values.get("temperature_2m"), "weather", refs["weather"]),
        _fact("apparent_temperature", values.get("apparent_temperature"), "weather", refs["weather"]),
        _fact("wind_speed", values.get("wind_speed_10m"), "weather", refs["weather"]),
        _fact("wind_gusts", values.get("wind_gusts_10m"), "weather", refs["weather"]),
        _fact("precipitation", values.get("precipitation"), "weather", refs["weather"]),
        _fact("cloud_cover", values.get("cloud_cover"), "weather", refs["weather"]),
    ]
    weather_facts = [item for item in weather_facts if item is not None]
    snow = selected.get("snow") or {}
    snow_facts = [
        {
            "kind": (fact.get("semantics") or {}).get("fact_type"),
            "value": fact.get("value"),
            "unit": (fact.get("semantics") or {}).get("canonical_unit"),
            "availability": product_availability(fact.get("coverage"), reason_codes=fact.get("reason_codes", ()),),
            "why_reference": refs["snow"],
            "temporal_semantics": (fact.get("semantics") or {}).get("native_temporal_semantics"),
        }
        for fact in snow.get("snow_facts", ())
        if fact.get("route_point_index") == point.get("point_index")
    ]
    photographer = selected.get("photographer") or {}
    photo_facts = [
        _window_fact("golden_hour", photographer.get("golden_hour") or {}, point, refs["photography"]),
        _window_fact("blue_hour", photographer.get("blue_hour") or {}, point, refs["photography"]),
    ]
    trail = document.get("trail_terrain_character") or {}
    trail_facts = _trail_facts(trail, point, refs["trail_character"])
    availability = {
        "light": product_availability((selected.get("coverage") or {}).get("solar", {}).get("mode"), reason_codes=solar.get("reason_codes", ())),
        "weather": product_availability((selected.get("coverage") or {}).get("weather", {}).get("mode"), reason_codes=(point.get("weather_quality") or {}).get("reason_codes", ())),
        "snow": product_availability((snow.get("quality") or {}).get("coverage")),
        "photography": product_availability((photographer.get("quality") or {}).get("coverage")),
        "trail_character": product_availability(trail.get("availability")),
        "history": product_availability((selected.get("history") or {}).get("availability"), reason_codes=(selected.get("history") or {}).get("reason_codes", ())),
        "conditions": product_availability(((selected.get("rci") or {}).get("quality") or {}).get("coverage")),
    }
    timeline_items = [
        {
            "kind": "light_transition",
            "producer": "unified",
            "before_state": event.get("before_state"), "after_state": event.get("after_state"),
            "start_route_distance_m": event.get("before_route_distance_m"), "end_route_distance_m": event.get("after_route_distance_m"),
            "start_planned_time": event.get("before_planned_time"), "end_planned_time": event.get("after_planned_time"),
            "why_reference": refs["light"], "boundary_semantics": "BOUNDED_BY_ADJACENT_ROUTE_POINTS",
        }
        for event in selected.get("events", ())
    ]
    return {
        "contract_version": "journey_selection_context_v0_1",
        "position": {"route_distance_m": point.get("route_distance_m"), "planned_time": point.get("planned_arrival_time"), "elevation_m": point.get("route_elevation_m")},
        "primary": {
            "light": {"state": solar.get("terrain_solar_visibility") or solar.get("status"), "astronomical_state": solar.get("astronomical_state"), "availability": availability["light"], "why_reference": refs["light"]},
            "weather": weather_facts,
            "elevation_m": point.get("route_elevation_m"),
        },
        "secondary": {"snow": snow_facts, "photography": [item for item in photo_facts if item], "trail_character": trail_facts, "history": availability["history"], "conditions": availability["conditions"]},
        "availability": availability,
        "why_references": refs,
        "timeline_items": timeline_items,
    }


def _fact(kind: str, value: Mapping[str, Any] | None, domain: str, why_reference: str):
    if not value:
        return None
    return {"kind": kind, "value": value.get("value"), "unit": value.get("unit"), "availability": product_availability("AVAILABLE" if value.get("value") is not None else "UNKNOWN", native_state_override=value.get("representation_state")), "why_reference": why_reference, "temporal_semantics": value.get("temporal_representation_state")}


def _window_fact(kind: str, result: Mapping[str, Any], point: Mapping[str, Any], why_reference: str):
    coverage = result.get("coverage")
    if not result:
        return None
    index = point.get("point_index")
    in_window = any((window.get("first_in_window") or {}).get("route_point_index", -1) <= index <= (window.get("last_in_window") or {}).get("route_point_index", -1) for window in result.get("windows", ()))
    return {"kind": kind, "in_window": in_window, "availability": product_availability(coverage), "why_reference": why_reference, "representation": "SAMPLED_POLICY_WINDOW"}


def _trail_facts(trail: Mapping[str, Any], point: Mapping[str, Any], why_reference: str):
    reference = trail.get("route_reference") or {}
    local_distance = point.get("route_distance_m", 0) - reference.get("section_distance_offset_m", 0)
    return [
        {"kind": segment.get("characteristic_id"), "value": segment.get("normalized_value"), "value_state": segment.get("value_state"), "availability": product_availability("AVAILABLE" if segment.get("value_state") == "KNOWN" else "UNKNOWN", native_state_override=segment.get("value_state")), "why_reference": why_reference, "start_route_distance_m": segment.get("start_route_distance_m"), "end_route_distance_m": segment.get("end_route_distance_m")}
        for segment in ((trail.get("mapped_character") or {}).get("segments") or ())
        if segment.get("start_route_distance_m", float("inf")) <= local_distance <= segment.get("end_route_distance_m", float("-inf"))
    ]
