"""Phase-6 station, void-of-course, and rise-set routes."""

from __future__ import annotations

from fastapi import APIRouter, Depends, Query, Request, Response

from moira import Moira

from ..cache import ResponseLRUCache, cached_response
from ..dependencies import get_engine
from ..models.phenomena import (
    AllLunarOccultationsRequest,
    CloseApproachRequest,
    CloseApproachSearchResponse,
    EclipseEventResponse,
    EclipseSearchRequest,
    EventInstantResponse,
    GeneralVisibilityEventRequest,
    GeneralVisibilityEventResponse,
    HeliacalPhasisRequest,
    HeliacalPhasisResponse,
    HeliacalPlanetEventRequest,
    LunarEclipseLocalCircumstancesResponse,
    LunarEclipseLocationRequest,
    LunarEclipseGlobalCircumstancesRequest,
    LunarEclipseGlobalCircumstancesResponse,
    LunarEclipseVisibilityMapResponse,
    LunarEclipseVisibilityRequest,
    LunarOccultationRequest,
    LunarOccultationPathAtRequest,
    LunarOccultationPathRequest,
    LunarOccultationPathTopologyAtRequest,
    LunarOccultationPathTopologyRequest,
    LunarOccultationSearchResponse,
    LunarStarOccultationRequest,
    LunarStarOccultationPathAtRequest,
    LunarStarOccultationPathRequest,
    LunarStarOccultationPathTopologyAtRequest,
    LunarStarOccultationPathTopologyRequest,
    NextStationRequest,
    NatalAngularContactsRequest,
    NatalAngularContactsResponse,
    NatalParanSearchRequest,
    OccultationPathGeometryResponse,
    OccultationPathSearchResponse,
    OccultationPathTopologyResponse,
    OccultationPathTopologySearchResponse,
    ParanContourExtractionResponse,
    ParanContourPathSetResponse,
    ParanFieldAnalysisResponse,
    ParanFieldGridRequest,
    ParanFieldMetricRequest,
    ParanFieldSampleSearchResponse,
    ParanFieldStructureResponse,
    ParanSearchRequest,
    ParanSearchResponse,
    ParanStarCanonResponse,
    ParanSiteRequest,
    ParanSiteResultResponse,
    PlanetHeliacalEventResponse,
    RetrogradePeriodSearchRequest,
    RetrogradePeriodSearchResponse,
    RiseSetPhenomenaRequest,
    RiseSetPhenomenaResponse,
    RiseSetTransitRequest,
    SolarEclipseFootprintRequest,
    SolarEclipseCartographyRequest,
    SolarEclipseCartographyResponse,
    SolarEclipseGlobalCircumstancesRequest,
    SolarEclipseGlobalCircumstancesResponse,
    SolarEclipseLocalCircumstancesResponse,
    SolarEclipseLocationRequest,
    SolarEclipsePathRequest,
    SolarEclipsePathResponse,
    SolarEclipseVisibilityFootprintResponse,
    StationEventResponse,
    StationSearchRequest,
    StationSearchResponse,
    StationStateRequest,
    StationStateResponse,
    TwilightRequest,
    TwilightTimesResponse,
    VoidOfCourseRangeRequest,
    VoidOfCourseRangeResponse,
    VoidOfCourseRequest,
    VoidOfCourseStateResponse,
    VoidOfCourseWindowResponse,
)
from ..serializers.phenomena import (
    serialize_close_approach,
    serialize_eclipse_event,
    serialize_event_instant,
    serialize_general_visibility_event,
    serialize_heliacal_phasis,
    serialize_lunar_eclipse_local,
    serialize_lunar_eclipse_global_circumstances,
    serialize_lunar_eclipse_visibility_map,
    serialize_lunar_occultation,
    serialize_natal_angular_contact,
    serialize_occultation_path_geometry,
    serialize_occultation_path_topology,
    serialize_paran,
    serialize_paran_body_crossing_inventory,
    serialize_paran_field_analysis,
    serialize_paran_field_sample,
    serialize_paran_field_structure,
    serialize_paran_contour_extraction,
    serialize_paran_contour_path_set,
    serialize_paran_site_result,
    serialize_planet_heliacal_event,
    serialize_retrograde_period,
    serialize_rise_set_phenomena,
    serialize_solar_eclipse_footprint,
    serialize_solar_eclipse_cartography,
    serialize_solar_eclipse_global_circumstances,
    serialize_solar_eclipse_path,
    serialize_solar_eclipse_local,
    serialize_station_event,
    serialize_station_search_truth,
    serialize_station_state_truth,
    serialize_twilight_times,
    serialize_void_of_course_range_truth,
    serialize_void_of_course_window,
    serialize_void_of_course_window_truth,
)
from ..services.phenomena import (
    compute_all_lunar_occultations,
    compute_close_approaches,
    compute_general_visibility_event,
    compute_heliacal_phasis,
    compute_lunar_eclipse_local,
    compute_lunar_eclipse_global_circumstances,
    compute_lunar_eclipse_visibility,
    compute_lunar_occultations,
    compute_lunar_occultation_path_at,
    compute_lunar_occultation_paths,
    compute_lunar_occultation_path_topologies,
    compute_lunar_occultation_path_topology_at,
    compute_lunar_star_occultations,
    compute_lunar_star_occultation_path_at,
    compute_lunar_star_occultation_paths,
    compute_lunar_star_occultation_path_topologies,
    compute_lunar_star_occultation_path_topology_at,
    compute_natal_parans,
    compute_natal_parans_with_inventory,
    compute_natal_angular_contacts,
    compute_next_station_with_truth,
    compute_next_lunar_eclipse,
    compute_next_solar_eclipse,
    compute_previous_lunar_eclipse,
    compute_previous_solar_eclipse,
    compute_next_visible_solar_eclipse,
    compute_next_void_of_course_with_truth,
    compute_parans,
    compute_parans_with_inventory,
    compute_paran_field_analysis,
    compute_paran_field_contours,
    compute_paran_field_path_set,
    compute_paran_field_samples,
    compute_paran_field_structure,
    compute_paran_site,
    compute_planet_heliacal_event,
    compute_retrograde_periods,
    compute_rise_set_phenomena,
    compute_rise_set_transit,
    compute_solar_eclipse_footprint,
    compute_solar_eclipse_cartography,
    compute_solar_eclipse_global_circumstances,
    compute_station_state_with_truth,
    compute_stations_with_truth,
    compute_solar_eclipse_path,
    compute_twilight_times,
    compute_void_of_course_state_with_truth,
    compute_void_of_course_window_with_truth,
    compute_void_periods_with_truth,
    get_paran_star_canon,
)


router = APIRouter(prefix="/v1", tags=["phenomena"])


def _expensive_cache(request: Request) -> ResponseLRUCache | None:
    return getattr(request.app.state, "expensive_response_cache", None)


@router.post("/stations/search", response_model=StationSearchResponse)
def station_search_route(
    request: StationSearchRequest,
    engine: Moira = Depends(get_engine),
) -> StationSearchResponse:
    result = compute_stations_with_truth(engine, request)
    return StationSearchResponse(
        events=[serialize_station_event(event) for event in result.events],
        computation_truth=serialize_station_search_truth(result.truth),
    )


@router.post(
    "/stations/next",
    response_model=StationEventResponse | None,
    responses={
        200: {
            "description": (
                "The next station, or JSON null when the bounded search has no result. "
                "A null result carries the reason and applied search controls in headers."
            ),
            "headers": {
                "X-Moira-Station-No-Result-Reason": {
                    "description": "Machine-readable reason for a null response body.",
                    "schema": {"type": "string"},
                },
                "X-Moira-Station-Search-End-JD-UT": {
                    "description": "Upper bound of the completed UT1 search.",
                    "schema": {"type": "number"},
                },
                "X-Moira-Station-Effective-Step-Days": {
                    "description": "Effective coarse scan step in days.",
                    "schema": {"type": "number"},
                },
            },
        }
    },
)
def next_station_route(
    request: NextStationRequest,
    response: Response,
    engine: Moira = Depends(get_engine),
) -> StationEventResponse | None:
    result = compute_next_station_with_truth(engine, request)
    if result.event is not None:
        return serialize_station_event(result.event)
    response.headers["X-Moira-Station-No-Result-Reason"] = (
        result.search_truth.no_result_reason or "no_station_in_search_window"
    )
    response.headers["X-Moira-Station-Search-End-JD-UT"] = str(
        result.search_truth.search_end_jd_ut
    )
    response.headers["X-Moira-Station-Effective-Step-Days"] = str(
        result.search_truth.effective_step_days
    )
    return None


@router.post("/stations/is-retrograde", response_model=StationStateResponse)
def station_state_route(
    request: StationStateRequest,
    engine: Moira = Depends(get_engine),
) -> StationStateResponse:
    result = compute_station_state_with_truth(engine, request)
    return StationStateResponse(
        body=request.body,
        jd_ut=request.jd_ut,
        is_retrograde=result.is_retrograde,
        signed_longitude_speed_deg_per_day=(
            result.truth.signed_longitude_speed_deg_per_day
        ),
        computation_truth=serialize_station_state_truth(result.truth),
    )


@router.post("/stations/retrograde-periods", response_model=RetrogradePeriodSearchResponse)
def retrograde_periods_route(
    request: RetrogradePeriodSearchRequest,
    engine: Moira = Depends(get_engine),
) -> RetrogradePeriodSearchResponse:
    return RetrogradePeriodSearchResponse(
        periods=[
            serialize_retrograde_period(period)
            for period in compute_retrograde_periods(engine, request)
        ]
    )


@router.post("/void-of-course/window", response_model=VoidOfCourseWindowResponse)
def void_of_course_window_route(
    request: VoidOfCourseRequest,
    engine: Moira = Depends(get_engine),
) -> VoidOfCourseWindowResponse:
    return serialize_void_of_course_window(
        compute_void_of_course_window_with_truth(engine, request)
    )


@router.post("/void-of-course/next", response_model=VoidOfCourseWindowResponse)
def next_void_of_course_route(
    request: VoidOfCourseRequest,
    engine: Moira = Depends(get_engine),
) -> VoidOfCourseWindowResponse:
    return serialize_void_of_course_window(
        compute_next_void_of_course_with_truth(engine, request)
    )


@router.post("/void-of-course/is-active", response_model=VoidOfCourseStateResponse)
def void_of_course_state_route(
    request: VoidOfCourseRequest,
    engine: Moira = Depends(get_engine),
) -> VoidOfCourseStateResponse:
    result = compute_void_of_course_state_with_truth(engine, request)
    return VoidOfCourseStateResponse(
        jd_ut=request.jd_ut,
        modern=request.modern,
        is_void_of_course=result.is_void_of_course,
        computation_truth=serialize_void_of_course_window_truth(result.truth),
    )


@router.post("/void-of-course/range", response_model=VoidOfCourseRangeResponse)
def void_of_course_range_route(
    request: VoidOfCourseRangeRequest,
    engine: Moira = Depends(get_engine),
) -> VoidOfCourseRangeResponse:
    result = compute_void_periods_with_truth(engine, request)
    return VoidOfCourseRangeResponse(
        windows=[
            serialize_void_of_course_window(window)
            for window in result.windows
        ],
        computation_truth=serialize_void_of_course_range_truth(result.truth),
    )


@router.post("/rise-set/phenomena", response_model=RiseSetPhenomenaResponse)
def rise_set_phenomena_route(
    request: RiseSetPhenomenaRequest,
    engine: Moira = Depends(get_engine),
) -> RiseSetPhenomenaResponse:
    return serialize_rise_set_phenomena(compute_rise_set_phenomena(engine, request))


@router.post("/rise-set/transit", response_model=EventInstantResponse)
def rise_set_transit_route(
    request: RiseSetTransitRequest,
    engine: Moira = Depends(get_engine),
) -> EventInstantResponse:
    return serialize_event_instant(compute_rise_set_transit(engine, request))


@router.post("/rise-set/twilight", response_model=TwilightTimesResponse)
def twilight_times_route(
    request: TwilightRequest,
    engine: Moira = Depends(get_engine),
) -> TwilightTimesResponse:
    return serialize_twilight_times(compute_twilight_times(engine, request))


@router.post("/eclipses/solar/next", response_model=EclipseEventResponse)
def next_solar_eclipse_route(
    request: EclipseSearchRequest,
    engine: Moira = Depends(get_engine),
) -> EclipseEventResponse:
    return serialize_eclipse_event(compute_next_solar_eclipse(engine, request))


@router.post("/eclipses/lunar/next", response_model=EclipseEventResponse)
def next_lunar_eclipse_route(
    request: EclipseSearchRequest,
    engine: Moira = Depends(get_engine),
) -> EclipseEventResponse:
    return serialize_eclipse_event(compute_next_lunar_eclipse(engine, request))


@router.post("/eclipses/solar/previous", response_model=EclipseEventResponse)
def previous_solar_eclipse_route(
    request: EclipseSearchRequest,
    engine: Moira = Depends(get_engine),
) -> EclipseEventResponse:
    return serialize_eclipse_event(compute_previous_solar_eclipse(engine, request))


@router.post("/eclipses/lunar/previous", response_model=EclipseEventResponse)
def previous_lunar_eclipse_route(
    request: EclipseSearchRequest,
    engine: Moira = Depends(get_engine),
) -> EclipseEventResponse:
    return serialize_eclipse_event(compute_previous_lunar_eclipse(engine, request))


@router.post("/eclipses/solar/local-visible", response_model=SolarEclipseLocalCircumstancesResponse)
def next_visible_solar_eclipse_route(
    request: SolarEclipseLocationRequest,
    engine: Moira = Depends(get_engine),
) -> SolarEclipseLocalCircumstancesResponse:
    return serialize_solar_eclipse_local(compute_next_visible_solar_eclipse(engine, request))


@router.post("/eclipses/lunar/local", response_model=LunarEclipseLocalCircumstancesResponse)
def lunar_eclipse_local_route(
    request: LunarEclipseLocationRequest,
    engine: Moira = Depends(get_engine),
) -> LunarEclipseLocalCircumstancesResponse:
    return serialize_lunar_eclipse_local(compute_lunar_eclipse_local(engine, request))


@router.post(
    "/eclipses/lunar/visibility",
    response_model=LunarEclipseVisibilityMapResponse,
)
def lunar_eclipse_visibility_route(
    request: LunarEclipseVisibilityRequest,
    engine: Moira = Depends(get_engine),
) -> LunarEclipseVisibilityMapResponse:
    return serialize_lunar_eclipse_visibility_map(
        compute_lunar_eclipse_visibility(engine, request)
    )


@router.post(
    "/eclipses/lunar/global-circumstances",
    response_model=LunarEclipseGlobalCircumstancesResponse,
)
def lunar_eclipse_global_circumstances_route(
    request: LunarEclipseGlobalCircumstancesRequest,
    engine: Moira = Depends(get_engine),
) -> LunarEclipseGlobalCircumstancesResponse:
    return serialize_lunar_eclipse_global_circumstances(
        compute_lunar_eclipse_global_circumstances(engine, request)
    )


@router.post("/eclipses/solar/path", response_model=SolarEclipsePathResponse)
def solar_eclipse_path_route(
    request: SolarEclipsePathRequest,
    engine: Moira = Depends(get_engine),
) -> SolarEclipsePathResponse:
    return serialize_solar_eclipse_path(compute_solar_eclipse_path(engine, request))


@router.post(
    "/eclipses/solar/footprint",
    response_model=SolarEclipseVisibilityFootprintResponse,
)
def solar_eclipse_footprint_route(
    request: SolarEclipseFootprintRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> SolarEclipseVisibilityFootprintResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:eclipses:solar:footprint",
        request,
        engine,
        lambda: serialize_solar_eclipse_footprint(
            compute_solar_eclipse_footprint(engine, request)
        ),
    )


@router.post(
    "/eclipses/solar/global-circumstances",
    response_model=SolarEclipseGlobalCircumstancesResponse,
)
def solar_eclipse_global_circumstances_route(
    request: SolarEclipseGlobalCircumstancesRequest,
    engine: Moira = Depends(get_engine),
) -> SolarEclipseGlobalCircumstancesResponse:
    return serialize_solar_eclipse_global_circumstances(
        compute_solar_eclipse_global_circumstances(engine, request)
    )


@router.post(
    "/eclipses/solar/cartography",
    response_model=SolarEclipseCartographyResponse,
)
def solar_eclipse_cartography_route(
    request: SolarEclipseCartographyRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> SolarEclipseCartographyResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:eclipses:solar:cartography",
        request,
        engine,
        lambda: serialize_solar_eclipse_cartography(
            compute_solar_eclipse_cartography(engine, request)
        ),
    )


@router.post("/occultations/close-approaches", response_model=CloseApproachSearchResponse)
def close_approaches_route(
    request: CloseApproachRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> CloseApproachSearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:close-approaches",
        request,
        engine,
        lambda: CloseApproachSearchResponse(
            events=[
                serialize_close_approach(event)
                for event in compute_close_approaches(engine, request)
            ]
        ),
    )


@router.post("/occultations/lunar", response_model=LunarOccultationSearchResponse)
def lunar_occultations_route(
    request: LunarOccultationRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> LunarOccultationSearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar",
        request,
        engine,
        lambda: LunarOccultationSearchResponse(
            events=[
                serialize_lunar_occultation(event)
                for event in compute_lunar_occultations(engine, request)
            ]
        ),
    )


@router.post("/occultations/lunar-star", response_model=LunarOccultationSearchResponse)
def lunar_star_occultations_route(
    request: LunarStarOccultationRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> LunarOccultationSearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-star",
        request,
        engine,
        lambda: LunarOccultationSearchResponse(
            events=[
                serialize_lunar_occultation(event)
                for event in compute_lunar_star_occultations(engine, request)
            ]
        ),
    )


@router.post("/occultations/all-lunar", response_model=LunarOccultationSearchResponse)
def all_lunar_occultations_route(
    request: AllLunarOccultationsRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> LunarOccultationSearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:all-lunar",
        request,
        engine,
        lambda: LunarOccultationSearchResponse(
            events=[
                serialize_lunar_occultation(event)
                for event in compute_all_lunar_occultations(engine, request)
            ]
        ),
    )


@router.post("/occultations/lunar-path", response_model=OccultationPathSearchResponse)
def lunar_occultation_path_route(
    request: LunarOccultationPathRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathSearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-path",
        request,
        engine,
        lambda: OccultationPathSearchResponse(
            events=[
                serialize_occultation_path_geometry(event)
                for event in compute_lunar_occultation_paths(engine, request)
            ]
        ),
    )


@router.post("/occultations/lunar-path-at", response_model=OccultationPathGeometryResponse)
def lunar_occultation_path_at_route(
    request: LunarOccultationPathAtRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathGeometryResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-path-at",
        request,
        engine,
        lambda: serialize_occultation_path_geometry(
            compute_lunar_occultation_path_at(engine, request)
        ),
    )


@router.post("/occultations/lunar-star-path", response_model=OccultationPathSearchResponse)
def lunar_star_occultation_path_route(
    request: LunarStarOccultationPathRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathSearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-star-path",
        request,
        engine,
        lambda: OccultationPathSearchResponse(
            events=[
                serialize_occultation_path_geometry(event)
                for event in compute_lunar_star_occultation_paths(engine, request)
            ]
        ),
    )


@router.post("/occultations/lunar-star-path-at", response_model=OccultationPathGeometryResponse)
def lunar_star_occultation_path_at_route(
    request: LunarStarOccultationPathAtRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathGeometryResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-star-path-at",
        request,
        engine,
        lambda: serialize_occultation_path_geometry(
            compute_lunar_star_occultation_path_at(engine, request)
        ),
    )


@router.post(
    "/occultations/lunar-path-topology",
    response_model=OccultationPathTopologySearchResponse,
)
def lunar_occultation_path_topology_route(
    request: LunarOccultationPathTopologyRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathTopologySearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-path-topology",
        request,
        engine,
        lambda: OccultationPathTopologySearchResponse(
            events=[
                serialize_occultation_path_topology(event)
                for event in compute_lunar_occultation_path_topologies(engine, request)
            ]
        ),
    )


@router.post(
    "/occultations/lunar-path-topology-at",
    response_model=OccultationPathTopologyResponse,
)
def lunar_occultation_path_topology_at_route(
    request: LunarOccultationPathTopologyAtRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathTopologyResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-path-topology-at",
        request,
        engine,
        lambda: serialize_occultation_path_topology(
            compute_lunar_occultation_path_topology_at(engine, request)
        ),
    )


@router.post(
    "/occultations/lunar-star-path-topology",
    response_model=OccultationPathTopologySearchResponse,
)
def lunar_star_occultation_path_topology_route(
    request: LunarStarOccultationPathTopologyRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathTopologySearchResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-star-path-topology",
        request,
        engine,
        lambda: OccultationPathTopologySearchResponse(
            events=[
                serialize_occultation_path_topology(event)
                for event in compute_lunar_star_occultation_path_topologies(
                    engine,
                    request,
                )
            ]
        ),
    )


@router.post(
    "/occultations/lunar-star-path-topology-at",
    response_model=OccultationPathTopologyResponse,
)
def lunar_star_occultation_path_topology_at_route(
    request: LunarStarOccultationPathTopologyAtRequest,
    http_request: Request,
    engine: Moira = Depends(get_engine),
) -> OccultationPathTopologyResponse:
    return cached_response(
        _expensive_cache(http_request),
        "v1:occultations:lunar-star-path-topology-at",
        request,
        engine,
        lambda: serialize_occultation_path_topology(
            compute_lunar_star_occultation_path_topology_at(engine, request)
        ),
    )


@router.post("/heliacal/planet", response_model=PlanetHeliacalEventResponse | None)
def planet_heliacal_event_route(
    request: HeliacalPlanetEventRequest,
    engine: Moira = Depends(get_engine),
) -> PlanetHeliacalEventResponse | None:
    event = compute_planet_heliacal_event(engine, request)
    return serialize_planet_heliacal_event(event) if event is not None else None


@router.post("/heliacal/visibility-event", response_model=GeneralVisibilityEventResponse | None)
def general_visibility_event_route(
    request: GeneralVisibilityEventRequest,
    engine: Moira = Depends(get_engine),
) -> GeneralVisibilityEventResponse | None:
    event = compute_general_visibility_event(engine, request)
    return serialize_general_visibility_event(event) if event is not None else None


@router.post("/heliacal/phasis", response_model=HeliacalPhasisResponse)
def heliacal_phasis_route(
    request: HeliacalPhasisRequest,
    engine: Moira = Depends(get_engine),
) -> HeliacalPhasisResponse:
    """Nearest occurrence of every standard heliacal phenomenon within jd_ut +/- window_days."""
    return serialize_heliacal_phasis(compute_heliacal_phasis(engine, request))


@router.post("/parans/search", response_model=ParanSearchResponse)
def paran_search_route(
    request: ParanSearchRequest,
    engine: Moira = Depends(get_engine),
) -> ParanSearchResponse:
    if request.include_crossing_inventory:
        result = compute_parans_with_inventory(engine, request)
        return ParanSearchResponse(
            events=[serialize_paran(event) for event in result.events],
            crossing_inventory=[
                serialize_paran_body_crossing_inventory(inventory)
                for inventory in result.crossing_inventory
            ],
            effective_policy_preset=request.policy_preset,
        )
    return ParanSearchResponse(
        events=[serialize_paran(event) for event in compute_parans(engine, request)],
        effective_policy_preset=request.policy_preset,
    )


@router.get("/parans/star-canon", response_model=ParanStarCanonResponse)
def paran_star_canon_route(
    tiers: list[str] | None = Query(default=None),
    available_only: bool = True,
) -> ParanStarCanonResponse:
    """Return the engine-owned fixed-star selection profile for paran clients."""

    return get_paran_star_canon(
        tiers=tiers,
        available_only=available_only,
    )


@router.post("/parans/natal", response_model=ParanSearchResponse)
def natal_paran_search_route(
    request: NatalParanSearchRequest,
    engine: Moira = Depends(get_engine),
) -> ParanSearchResponse:
    if request.include_crossing_inventory:
        result = compute_natal_parans_with_inventory(engine, request)
        return ParanSearchResponse(
            events=[serialize_paran(event) for event in result.events],
            crossing_inventory=[
                serialize_paran_body_crossing_inventory(inventory)
                for inventory in result.crossing_inventory
            ],
            effective_policy_preset=request.policy_preset,
        )
    return ParanSearchResponse(
        events=[serialize_paran(event) for event in compute_natal_parans(engine, request)],
        effective_policy_preset=request.policy_preset,
    )


@router.post(
    "/parans/natal-angular-contacts",
    response_model=NatalAngularContactsResponse,
)
def natal_angular_contacts_route(
    request: NatalAngularContactsRequest,
    engine: Moira = Depends(get_engine),
) -> NatalAngularContactsResponse:
    """Return individual mundane-circle contacts near the natal moment."""

    return NatalAngularContactsResponse(
        contacts=[
            serialize_natal_angular_contact(contact)
            for contact in compute_natal_angular_contacts(engine, request)
        ]
    )


@router.post("/parans/site", response_model=ParanSiteResultResponse)
def paran_site_route(
    request: ParanSiteRequest,
    engine: Moira = Depends(get_engine),
) -> ParanSiteResultResponse:
    return serialize_paran_site_result(compute_paran_site(engine, request)).model_copy(
        update={"effective_policy_preset": request.policy_preset}
    )


@router.post("/parans/field/samples", response_model=ParanFieldSampleSearchResponse)
def paran_field_samples_route(
    request: ParanFieldGridRequest,
    engine: Moira = Depends(get_engine),
) -> ParanFieldSampleSearchResponse:
    return ParanFieldSampleSearchResponse(
        samples=[serialize_paran_field_sample(sample) for sample in compute_paran_field_samples(engine, request)],
        effective_policy_preset=request.policy_preset,
    )


@router.post("/parans/field/analysis", response_model=ParanFieldAnalysisResponse)
def paran_field_analysis_route(
    request: ParanFieldMetricRequest,
    engine: Moira = Depends(get_engine),
) -> ParanFieldAnalysisResponse:
    return serialize_paran_field_analysis(compute_paran_field_analysis(engine, request)).model_copy(
        update={"effective_policy_preset": request.policy_preset}
    )


@router.post("/parans/field/contours", response_model=ParanContourExtractionResponse)
def paran_field_contours_route(
    request: ParanFieldMetricRequest,
    engine: Moira = Depends(get_engine),
) -> ParanContourExtractionResponse:
    return serialize_paran_contour_extraction(compute_paran_field_contours(engine, request)).model_copy(
        update={"effective_policy_preset": request.policy_preset}
    )


@router.post("/parans/field/paths", response_model=ParanContourPathSetResponse)
def paran_field_paths_route(
    request: ParanFieldMetricRequest,
    engine: Moira = Depends(get_engine),
) -> ParanContourPathSetResponse:
    return serialize_paran_contour_path_set(compute_paran_field_path_set(engine, request)).model_copy(
        update={"effective_policy_preset": request.policy_preset}
    )


@router.post("/parans/field/structure", response_model=ParanFieldStructureResponse)
def paran_field_structure_route(
    request: ParanFieldMetricRequest,
    engine: Moira = Depends(get_engine),
) -> ParanFieldStructureResponse:
    return serialize_paran_field_structure(compute_paran_field_structure(engine, request)).model_copy(
        update={"effective_policy_preset": request.policy_preset}
    )
