"""
Directions service for INDL-43 — resolves the memorial → record → plot chain for a
published memorial and computes a straight-line walking-time/distance approximation.

Security posture (unauthenticated public surface):
- tenant scoping asserted on the memorial lookup (API1 / SAC-01)
- is_published gate + record deleted_at IS NULL filter (API2 / SAC-02)
- returns a plain dataclass of allowlisted fields only — no ORM model leaves this
  layer, so internal record/plot columns cannot be serialised (API3 / SAC-03)
- access-control decisions are enforced here in the service, not only the router
  (CIS 6 / V4.1.1), so a NotFoundError is raised for any gate failure — the router
  maps every failure mode to an identical 404 to avoid an enumeration oracle.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Optional
from uuid import UUID

from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from src.apps.memorials.models.memorial import Memorial
from src.apps.plots.models.plot import Plot
from src.apps.records.models.record import Record
from src.apps.tenants.models.account import Account
from src.core.exceptions import NotFoundError
from src.core.geo import estimate_walking_minutes, haversine_distance_m

DEFAULT_VISITING_HOURS = "Daily, 8:00 AM – 6:00 PM (grounds); office hours vary."


@dataclass(frozen=True)
class MemorialDirections:
    """Resolved, allowlisted directions data for a single published memorial."""

    slug: str
    section_code: Optional[str]
    section_name: Optional[str]
    plot_ref: str
    latitude: Optional[float]
    longitude: Optional[float]
    walking_minutes: Optional[int]
    distance_m: Optional[int]
    cemetery_name: Optional[str]
    cemetery_address: Optional[str]
    visiting_hours: Optional[str]

    @property
    def has_gps(self) -> bool:
        return self.latitude is not None and self.longitude is not None


def _entrance_coords(account: Account) -> tuple[Optional[float], Optional[float]]:
    """
    Read optional entrance coordinates.

    INDL-22 (cemetery_map_configs.entrance_latitude/longitude) is not yet built, so
    we fall back to an optional ``config_json`` block. When absent, walking-time and
    distance stay hidden (graceful degradation per AC-03 / Technical Decision).
    """
    cfg = getattr(account, "config_json", None) or {}
    try:
        lat = cfg.get("entrance_latitude")
        lng = cfg.get("entrance_longitude")
    except AttributeError:
        return None, None
    if lat is None or lng is None:
        return None, None
    try:
        return float(lat), float(lng)
    except (TypeError, ValueError):
        return None, None


def _visiting_hours(account: Account) -> str:
    cfg = getattr(account, "config_json", None) or {}
    hours = cfg.get("visiting_hours") if hasattr(cfg, "get") else None
    if isinstance(hours, str) and hours.strip():
        return hours.strip()
    return DEFAULT_VISITING_HOURS


async def get_memorial_directions(
    db: AsyncSession,
    slug: str,
    tenant_id: UUID | str,
) -> MemorialDirections:
    """
    Resolve directions for a published memorial.

    Raises NotFoundError for: missing tenant, unknown/cross-tenant slug, unpublished
    memorial, soft-deleted record, or a record with no linked plot. Callers must map
    NotFoundError to HTTP 404 (never 403) to avoid leaking memorial existence.
    """
    if not tenant_id:
        # Defensive: middleware should always populate tenant_id for /api/public.
        raise NotFoundError("Directions not found")

    result = await db.execute(
        select(Memorial)
        .options(
            selectinload(Memorial.record)
            .selectinload(Record.plot)
            .selectinload(Plot.section)
        )
        .where(
            and_(
                Memorial.tenant_id == tenant_id,
                Memorial.slug == slug,
                Memorial.is_published.is_(True),
            )
        )
    )
    memorial = result.scalar_one_or_none()
    if memorial is None:
        raise NotFoundError("Directions not found")

    record = memorial.record
    # Record is soft-deletable; never expose directions for a deleted record.
    if record is None or getattr(record, "deleted_at", None) is not None:
        raise NotFoundError("Directions not found")

    plot = record.plot
    if plot is None:
        # No linked plot → no location to give directions to.
        raise NotFoundError("Directions not found")

    section = plot.section
    section_code = section.code if section else None
    section_name = section.name if section else None

    # Coordinates: validate numeric + in-range via the geo helper (None if invalid).
    lat = plot.latitude
    lng = plot.longitude
    validated_distance = None
    walking_minutes = None
    distance_m = None

    # Only treat coordinates as present if they pass validation.
    if haversine_distance_m(lat, lng, lat, lng) is None:
        lat = None
        lng = None
    else:
        lat = float(lat)
        lng = float(lng)

    account = (
        await db.execute(select(Account).where(Account.id == tenant_id))
    ).scalar_one_or_none()

    cemetery_name = account.organization_name if account else None
    cemetery_address = getattr(account, "address", None) if account else None
    visiting_hours = _visiting_hours(account) if account else DEFAULT_VISITING_HOURS

    # Walking time / distance only when both the plot GPS and the entrance coords exist.
    if lat is not None and lng is not None and account is not None:
        ent_lat, ent_lng = _entrance_coords(account)
        validated_distance = haversine_distance_m(ent_lat, ent_lng, lat, lng)
        if validated_distance is not None:
            distance_m = int(round(validated_distance))
            walking_minutes = estimate_walking_minutes(validated_distance)

    return MemorialDirections(
        slug=slug,
        section_code=section_code,
        section_name=section_name,
        plot_ref=plot.plot_ref,
        latitude=lat,
        longitude=lng,
        walking_minutes=walking_minutes,
        distance_m=distance_m,
        cemetery_name=cemetery_name,
        cemetery_address=cemetery_address,
        visiting_hours=visiting_hours,
    )
