"""Terrain-only A5 composition using existing route preparation and Pace semantics."""

from __future__ import annotations

import math
from typing import Any, Mapping, Sequence

from mountain_twin.pace.contract import PaceEvaluationState, PaceResult
from mountain_twin.trail_character.contracts import (
    AscentDescentSummary,
    CharacterCoverage,
    ElevationSummary,
    EvidenceFamily,
    EvidenceOrigin,
    EvidenceReference,
    GradeReference,
    MappedCharacterExtension,
    RouteReference,
    TerrainCharacter,
    TrailTerrainCharacterResult,
)

SCHEMA_VERSION = "trail_terrain_character_v0_1"
GRADE_UNIT = "ratio"
GRADE_SIGN_SEMANTICS = "positive elevation gain along route; negative elevation loss along route"


def compose_terrain_character(
    prepared_points: Sequence[Any],
    pace_result: PaceResult,
    *,
    route_input_reference: str | None = None,
    analysis_identity: Mapping[str, Any] | None = None,
    mapped_association=None,
    mapped_features=None,
    mapped_continuity=None,
) -> TrailTerrainCharacterResult:
    """Compose factual terrain character without recalculating or affecting Pace.

    Grade and ascent/descent intentionally reference the existing Pace v0.1
    fixed-distance GPX-elevation calculation.  The result never exposes a
    grade value when that calculation is unresolved.
    """
    _validate_route(prepared_points, pace_result)
    elevations = [getattr(point, "gpx_elevation_m", None) for point in prepared_points]
    supported = [float(value) for value in elevations if _finite(value)]
    elevation_coverage = _coverage(len(supported), len(prepared_points))
    route = RouteReference(
        route_id=prepared_points[0].route_id,
        point_count=len(prepared_points),
        distance_m=float(prepared_points[-1].cumulative_distance_m),
        route_input_reference=route_input_reference,
    )
    evidence = (
        EvidenceReference(
            "route_geometry",
            EvidenceFamily.TERRAIN_GEOMETRY,
            EvidenceOrigin.ROUTE_INPUT,
            route_input_reference or "prepared_route",
            coverage=CharacterCoverage.FULL,
        ),
        EvidenceReference(
            "route_elevation_gpx",
            EvidenceFamily.TERRAIN_GEOMETRY,
            EvidenceOrigin.ROUTE_INPUT,
            route_input_reference or "prepared_route.gpx_elevation_m",
            coverage=elevation_coverage,
            limitations=("GPX_ELEVATION_IS_ROUTE_INPUT_NOT_DEM_SAMPLE",),
        ),
        EvidenceReference(
            "route_grade_pace_v0_1",
            EvidenceFamily.TERRAIN_GEOMETRY,
            EvidenceOrigin.MT_DERIVED,
            "pace_result.intervals",
            method_id=pace_result.preprocessing_policy,
            method_version=pace_result.model_version,
            coverage=_pace_coverage(pace_result),
            limitations=("GRADE_REFERENCES_PACE_V0_1_AND_DOES_NOT_MODIFY_PACE",),
        ),
    )
    elevation = ElevationSummary(
        source_evidence_id="route_elevation_gpx",
        unit="m",
        supported_point_count=len(supported),
        total_point_count=len(prepared_points),
        coverage=elevation_coverage,
        minimum_m=min(supported) if supported else None,
        maximum_m=max(supported) if supported else None,
        missing_reason_codes=("ROUTE_ELEVATION_MISSING",)
        if elevation_coverage is not CharacterCoverage.FULL
        else (),
    )
    grade_coverage = _pace_coverage(pace_result)
    grade = GradeReference(
        source_evidence_id="route_grade_pace_v0_1",
        unit=GRADE_UNIT,
        sign_semantics=GRADE_SIGN_SEMANTICS,
        definition=str(pace_result.provenance.get("grade_definition")),
        preprocessing_policy=pace_result.preprocessing_policy,
        preprocessing_distance_m=pace_result.preprocessing_distance_m,
        interval_count=len(pace_result.intervals)
        if grade_coverage is CharacterCoverage.FULL
        else 0,
        coverage=grade_coverage,
        reason_codes=pace_result.reason_codes,
    )
    ascent_descent = AscentDescentSummary(
        source_evidence_id="route_grade_pace_v0_1",
        ascent_m=pace_result.ascent_m if grade_coverage is CharacterCoverage.FULL else None,
        descent_m=pace_result.descent_m if grade_coverage is CharacterCoverage.FULL else None,
        coverage=grade_coverage,
        reason_codes=pace_result.reason_codes,
    )
    result = TrailTerrainCharacterResult(
        schema_version=SCHEMA_VERSION,
        analysis_identity={
            "analysis_type": "trail_terrain_character",
            "semantic_type": "factual_route_terrain_character",
            "version": SCHEMA_VERSION,
            "subject_id": route.route_id,
            **dict(analysis_identity or {}),
        },
        route=route,
        evidence=evidence,
        terrain_character=TerrainCharacter(elevation, grade, ascent_descent),
        mapped_character=MappedCharacterExtension(),
        limitations=(
            "TERRAIN_CHARACTER_IS_FACTUAL_CONTEXT_NOT_TRAIL_STATUS_OR_TRAVERSABILITY",
            "NO_DEM_SAMPLE_IS_REPRESENTED_UNLESS_A_FUTURE_CONTRACT_ADDS_ONE",
        ),
        data_gaps=("MAPPED_ROUTE_CHARACTER_NOT_IMPLEMENTED_WP1",),
        diagnostics=("NO_TERRAIN_SEGMENTATION_IN_WP1",),
    )
    if (mapped_association is None) != (mapped_features is None):
        raise ValueError("mapped association and mapped features must be supplied together")
    if mapped_association is not None:
        from mountain_twin.trail_character.mapped import apply_mapped_character

        return apply_mapped_character(
            result,
            mapped_association,
            mapped_features,
            continuity=mapped_continuity,
        )
    return result


def _validate_route(points: Sequence[Any], pace_result: PaceResult) -> None:
    if not points:
        raise ValueError("trail terrain character requires route points")
    route_id = getattr(points[0], "route_id", None)
    if not route_id or pace_result.route_id != route_id:
        raise ValueError("route and pace result identities must match")
    for index, point in enumerate(points):
        if (
            getattr(point, "route_id", None) != route_id
            or getattr(point, "point_index", None) != index
        ):
            raise ValueError("route points must be ordered, contiguous, and from one route")
        distance = getattr(point, "cumulative_distance_m", None)
        if not _finite(distance) or float(distance) < 0:
            raise ValueError("route distances must be finite and non-negative")
        if index and float(distance) < float(points[index - 1].cumulative_distance_m):
            raise ValueError("route distances must be monotonic")


def _finite(value: Any) -> bool:
    return value is not None and math.isfinite(float(value))


def _coverage(supported: int, total: int) -> CharacterCoverage:
    if not supported:
        return CharacterCoverage.UNAVAILABLE
    return CharacterCoverage.FULL if supported == total else CharacterCoverage.PARTIAL


def _pace_coverage(pace_result: PaceResult) -> CharacterCoverage:
    if pace_result.state is not PaceEvaluationState.COMPLETE:
        return CharacterCoverage.UNAVAILABLE
    return CharacterCoverage.FULL
