"""Small application read model joining the TMB reference and durable containers."""

from __future__ import annotations

from contextlib import contextmanager
from dataclasses import dataclass
from datetime import date
from typing import Iterator

from .contracts import Journey, JourneyActivityIntent, JourneyDateIntent, JourneyDateIntentKind
from .creator import CreateJourney, JourneyCreatorService
from .persistence import JourneySqliteDatabase, JourneySqliteRepository
from .repository import TMB_DAY_1_JOURNEY_ID, tmb_day_1_repository
from .route_persistence import JourneyRouteRepository
from .route_region import resolve_route_region

# The Aventurro start screen's Upcoming/Completed split. This is the ONLY
# place the rule is expressed -- JourneyCatalogService._summary() calls it,
# nothing else (least of all the frontend) re-implements it.
JOURNEY_TEMPORAL_STATUS_UPCOMING = "UPCOMING"
JOURNEY_TEMPORAL_STATUS_COMPLETED = "COMPLETED"


def classify_journey_temporal_status(journey: Journey) -> str:
    """COMPLETED only when the owner marked the Journey as done
    (Journey.completed_at, AV-027); every other Journey is UPCOMING. Never
    derived from dates: a trip whose dates have passed may not have
    happened, and the owner decides (the earlier date rule was removed in
    AV-027 at the owner's request)."""
    if journey.completed_at is not None:
        return JOURNEY_TEMPORAL_STATUS_COMPLETED
    return JOURNEY_TEMPORAL_STATUS_UPCOMING


@dataclass(frozen=True)
class JourneyCapabilities:
    has_route: bool
    has_plan: bool
    has_analysis: bool

    def to_dict(self) -> dict[str, bool]:
        return {
            "has_route": self.has_route,
            "has_plan": self.has_plan,
            "has_analysis": self.has_analysis,
            "can_show_map_route": self.has_route,
            "can_show_telemetry": self.has_plan and self.has_analysis,
            "can_show_intelligence": self.has_analysis,
        }


class JourneyCatalogService:
    """Application boundary: reference analyzed Journey plus durable containers."""

    def __init__(self, database: JourneySqliteDatabase):
        self._database = database
        self._reference = tmb_day_1_repository().get_journey(TMB_DAY_1_JOURNEY_ID)

    def list(self, owner_id: str) -> list[dict[str, object]]:
        values = [
            {**self._summary(self._reference, source="REFERENCE_ANALYZED"), "activity_id": "trekking"}
        ]
        routes = JourneyRouteRepository(self._database)
        try:
            starts = routes.current_route_starts(owner_id=owner_id)
            activities = routes.current_route_activities(owner_id=owner_id)
        finally:
            routes.close()
        with self._creator() as creator:
            for item in creator.list_journeys(owner_id=owner_id):
                summary = self._summary(item, source="DURABLE_CONTAINER")
                # The hero photo's region (AV-027): from the route's first
                # point, as the Journey's own bootstrap gives it, so a tile's
                # thumbnail is the photo the Journey itself shows.
                start = starts.get(item.journey_id)
                summary["route_region"] = resolve_route_region(*start) if start else None
                # AV-034: what the current route was drawn for (the left
                # panel's groups); None = no route, or a route saved before
                # AV-031 -- the page groups those with trekking.
                summary["activity_id"] = activities.get(item.journey_id)
                values.append(summary)
        return values

    def create(self, owner_id: str, payload: dict[str, object]) -> dict[str, object]:
        command = CreateJourney(
            owner_id=owner_id,
            title=_required_string(payload, "title"),
            destination_label=_optional_string(payload, "destination_label"),
            country_label=_optional_string(payload, "country_label"),
            date_intent=_date_intent(payload.get("date_intent")),
            activity_intent=_activity_intent(payload.get("activity_intent")),
            creation_request_id=_optional_string(payload, "creation_request_id"),
        )
        with self._creator() as creator:
            result = creator.create_journey(command)
        return {
            "created": result.created,
            "journey": self._summary(result.journey, source="DURABLE_CONTAINER"),
        }

    def get_journey(self, owner_id: str, journey_id: str) -> Journey:
        """Route persistence v0.1 Work Package 3: the durable-Journey read
        the bootstrap dispatch needs to build a GEOMETRY_ONLY_JOURNEY
        document once route_persistence.py shows the Journey has a route.
        Not used for the TMB reference Journey -- that path never reaches
        here (catalog.is_reference() routes it elsewhere first)."""
        with self._creator() as creator:
            return creator.get_journey(owner_id=owner_id, journey_id=journey_id)

    def empty_bootstrap(self, owner_id: str, journey_id: str) -> dict[str, object]:
        with self._creator() as creator:
            journey = creator.get_journey(owner_id=owner_id, journey_id=journey_id)
        return {
            "kind": "EMPTY_JOURNEY",
            "journey": self._summary(journey, source="DURABLE_CONTAINER"),
            "capabilities": JourneyCapabilities(False, False, False).to_dict(),
            "planning_state": {
                "code": "ROUTE_NOT_ADDED",
                "title": "Trasa nie została jeszcze dodana",
                "next_action": "ADD_ROUTE",
            },
        }

    def is_reference(self, journey_id: str) -> bool:
        return journey_id == self._reference.journey_id

    def set_completed(self, owner_id: str, journey_id: str, completed: bool) -> dict[str, object]:
        """AV-027: mark a durable Journey as done, or take it back to
        upcoming. The reference TMB Journey is immutable fixture data and
        cannot be marked; an unknown durable Journey raises KeyError."""
        if self.is_reference(journey_id):
            raise PermissionError("the reference Journey cannot be marked as done")
        if not isinstance(completed, bool):
            raise ValueError("completed must be true or false")
        repository = JourneySqliteRepository(self._database)
        try:
            journey = repository.set_completed(
                owner_id=owner_id, journey_id=journey_id, completed=completed
            )
        finally:
            repository.close()
        return self._summary(journey, source="DURABLE_CONTAINER")

    def delete(self, owner_id: str, journey_id: str) -> None:
        """docs/journey_edit_delete_v0_1_design.md section 1. The reference
        TMB Journey is immutable fixture data (never in SQL) and can never
        be deleted; an unknown durable Journey raises KeyError."""
        if self.is_reference(journey_id):
            raise PermissionError("the reference Journey cannot be deleted")
        repository = JourneySqliteRepository(self._database)
        try:
            repository.delete(owner_id=owner_id, journey_id=journey_id)
        finally:
            repository.close()

    @contextmanager
    def _creator(self) -> Iterator[JourneyCreatorService]:
        """Give each operation a connection owned by its calling request thread."""

        repository = JourneySqliteRepository(self._database)
        try:
            yield JourneyCreatorService(repository)
        finally:
            repository.close()

    @staticmethod
    def _summary(journey: Journey, *, source: str) -> dict[str, object]:
        capabilities = JourneyCapabilities(
            # Route persistence v0.1 Work Package 3: closes the gap the WP1
            # report flagged -- a durable Journey with a RouteRevision saved
            # via route_persistence.py now correctly reports has_route=true,
            # not just the TMB reference fixture.
            has_route=source == "REFERENCE_ANALYZED"
            or journey.current_route_revision_id is not None,
            has_plan=journey.current_plan_id is not None,
            has_analysis=journey.current_analysis_run_id is not None,
        )
        return {
            "journey_id": journey.journey_id,
            "title": journey.title,
            "destination_label": journey.destination_label,
            "country_label": journey.country_label,
            "date_intent": _date_intent_document(journey.date_intent),
            "temporal_status": classify_journey_temporal_status(journey),
            "completed_at": journey.completed_at,
            "activity_intent": None
            if journey.activity_intent is None
            else journey.activity_intent.value,
            "source": source,
            "capabilities": capabilities.to_dict(),
        }


def _required_string(payload: dict[str, object], key: str) -> str:
    value = payload.get(key)
    if not isinstance(value, str):
        raise ValueError(f"{key} is required")
    return value


def _optional_string(payload: dict[str, object], key: str) -> str | None:
    value = payload.get(key)
    if value is None:
        return None
    if not isinstance(value, str):
        raise ValueError(f"{key} must be text")
    return value


def _activity_intent(value: object) -> JourneyActivityIntent | None:
    if value is None or value == "":
        return None
    if not isinstance(value, str):
        raise ValueError("activity_intent is invalid")
    try:
        return JourneyActivityIntent(value)
    except ValueError as error:
        raise ValueError("activity_intent is invalid") from error


def _date_intent(value: object) -> JourneyDateIntent:
    if value is None:
        return JourneyDateIntent.unspecified()
    if not isinstance(value, dict) or not isinstance(value.get("kind"), str):
        raise ValueError("date_intent is invalid")
    kind = JourneyDateIntentKind(value["kind"])
    if kind is JourneyDateIntentKind.UNSPECIFIED:
        return JourneyDateIntent.unspecified()
    if kind is JourneyDateIntentKind.MONTH:
        return JourneyDateIntent.month_intent(value.get("year"), value.get("month"))
    return JourneyDateIntent.date_range(
        date.fromisoformat(value.get("start_date")), date.fromisoformat(value.get("end_date"))
    )


def _date_intent_document(intent: JourneyDateIntent) -> dict[str, object]:
    return {
        "kind": intent.kind.value,
        "year": intent.year,
        "month": intent.month,
        "start_date": None if intent.start_date is None else intent.start_date.isoformat(),
        "end_date": None if intent.end_date is None else intent.end_date.isoformat(),
    }
