"""Public memorial page service (INDL-46).

Assembles the full public memorial payload and handles candle increments and
public tribute listing/submission. Security posture mirrors the INDL-43
directions service: tenant scoping + publish gate + soft-delete filter enforced
here in the service, and only allowlisted fields leave this layer.
"""
from __future__ import annotations

from datetime import date
from typing import List, Optional, Tuple
from uuid import UUID

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

from src.apps.memorials.models.memorial import Memorial
from src.apps.memorials.models.tribute import Tribute
from src.apps.plots.models.plot import Plot
from src.apps.records.models.record import Record
from src.apps.settings.models.qr_code import QRCode
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
from src.apps.public.schemas.memorial import (
    CemeteryProfile,
    MemorialPhotoItem,
    PlotCoordinates,
    PublicMemorialDetail,
    PublicTributeItem,
    QuickFacts,
    TimelineEntry,
)

DEFAULT_HOURS_GROUNDS = "Daily, 8:00 AM – dusk"


def _compute_age(dob: Optional[date], dod: Optional[date]) -> Optional[int]:
    if not dob or not dod:
        return None
    age = dod.year - dob.year - ((dod.month, dod.day) < (dob.month, dob.day))
    return age if age >= 0 else None


def _image_url(s3_key: Optional[str]) -> Optional[str]:
    """Return a relative proxy path for an S3 key. The frontend prefixes the API
    origin. Returns None for an empty key so the UI can render a fallback."""
    if not s3_key:
        return None
    return f"/api/public/images/{s3_key}"


async def get_public_memorial(
    db: AsyncSession, slug: str, tenant_id: UUID | str
) -> PublicMemorialDetail:
    """Load a published memorial and assemble the full public page payload.

    Raises NotFoundError for a missing tenant, unknown/cross-tenant slug,
    unpublished memorial, or a soft-deleted linked record (all mapped to 404 by
    the router to avoid an enumeration oracle).
    """
    if not tenant_id:
        raise NotFoundError("Memorial not found")

    result = await db.execute(
        select(Memorial)
        .options(
            selectinload(Memorial.record).selectinload(Record.plot).selectinload(Plot.section),
            selectinload(Memorial.photos),
            selectinload(Memorial.timeline_events),
            selectinload(Memorial.tributes),
        )
        .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("Memorial not found")

    record = memorial.record
    if record is None or getattr(record, "deleted_at", None) is not None:
        raise NotFoundError("Memorial not found")

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

    plot = record.plot
    section = plot.section if plot else None

    # ── Display name ────────────────────────────────────────────────────────
    display_name = memorial.display_name or " ".join(
        p for p in [record.first_name, record.middle_name, record.last_name] if p
    ).strip()

    # ── Biography (prefer curated biography_text, fall back to AI draft) ─────
    biography = memorial.biography_text or memorial.ai_biography_text

    # ── Plot coordinates + walking estimate ─────────────────────────────────
    plot_coordinates: Optional[PlotCoordinates] = None
    if plot is not None:
        lat = plot.latitude
        lng = plot.longitude
        has_gps = haversine_distance_m(lat, lng, lat, lng) is not None
        walking_min: Optional[int] = None
        distance_m: Optional[int] = None
        if has_gps and account is not None:
            cfg = account.config_json or {}
            ent_lat = cfg.get("entrance_latitude")
            ent_lng = cfg.get("entrance_longitude")
            dist = haversine_distance_m(ent_lat, ent_lng, float(lat), float(lng))
            if dist is not None:
                distance_m = int(round(dist))
                walking_min = estimate_walking_minutes(dist)
        plot_coordinates = PlotCoordinates(
            latitude=float(lat) if has_gps else None,
            longitude=float(lng) if has_gps else None,
            has_gps=has_gps,
            walking_time_min=walking_min,
            distance_m=distance_m,
        )

    # ── Timeline (reuse existing memorial_timeline_events) ──────────────────
    timeline: List[TimelineEntry] = []
    for ev in sorted(memorial.timeline_events, key=lambda e: e.sort_order):
        year = ev.event_date.year if ev.event_date else None
        timeline.append(TimelineEntry(year=year, heading=ev.title, body=ev.description))

    # ── Photos ──────────────────────────────────────────────────────────────
    photos = [
        MemorialPhotoItem(
            id=p.id,
            url=_image_url(p.s3_key) or "",
            caption=p.caption,
            sort_order=p.sort_order,
        )
        for p in sorted(memorial.photos, key=lambda p: p.sort_order)
    ]

    approved = [t for t in memorial.tributes if t.status == "approved"]

    # ── QR code (INDL-11): only if a generated memorial QR exists ───────────
    qr_code_url: Optional[str] = None
    qr = (
        await db.execute(
            select(QRCode).where(
                QRCode.tenant_id == tenant_id,
                QRCode.qr_type == "memorial",
                QRCode.reference_id == slug,
                QRCode.is_active.is_(True),
            )
        )
    ).scalar_one_or_none()
    if qr is not None:
        qr_code_url = _image_url(qr.svg_s3_key or qr.pdf_s3_key)

    # ── Cemetery profile ────────────────────────────────────────────────────
    cfg = (account.config_json or {}) if account else {}
    cemetery = CemeteryProfile(
        name=account.organization_name if account else None,
        address=getattr(account, "address", None) if account else None,
        phone=getattr(account, "contact_phone", None) if account else None,
        email=getattr(account, "contact_email", None) if account else None,
        hours_grounds=(cfg.get("visiting_hours") or DEFAULT_HOURS_GROUNDS),
        hours_office=cfg.get("office_hours"),
    )

    return PublicMemorialDetail(
        id=memorial.id,
        slug=memorial.slug,
        is_published=memorial.is_published,
        display_name=display_name,
        maiden_name=record.maiden_name,
        date_of_birth=record.date_of_birth,
        date_of_death=record.date_of_death,
        age_at_death=_compute_age(record.date_of_birth, record.date_of_death),
        plot_ref=plot.plot_ref if plot else None,
        section_code=section.code if section else None,
        biography=biography,
        biography_ai_generated=bool(memorial.biography_ai_generated),
        candle_count=memorial.candle_count or 0,
        photo_count=len(memorial.photos),
        tribute_count=len(approved),
        qr_code_url=qr_code_url,
        quick_facts=QuickFacts(
            birthplace=record.birthplace,
            city_of_residence=record.city_of_residence,
            occupation=record.occupation,
            service_date=memorial.service_date,
            service_location=memorial.service_location,
        ),
        plot_coordinates=plot_coordinates,
        timeline=timeline,
        photos=photos,
        cemetery=cemetery,
    )


async def _resolve_published_memorial(
    db: AsyncSession, slug: str, tenant_id: UUID | str
) -> Memorial:
    """Resolve a published, tenant-scoped memorial whose linked record is not
    soft-deleted. Mirrors the gate in get_public_memorial / the directions
    service so candle + tribute endpoints stay consistent with the page."""
    if not tenant_id:
        raise NotFoundError("Memorial not found")
    memorial = (
        await db.execute(
            select(Memorial)
            .options(selectinload(Memorial.record))
            .where(
                and_(
                    Memorial.tenant_id == tenant_id,
                    Memorial.slug == slug,
                    Memorial.is_published.is_(True),
                )
            )
        )
    ).scalar_one_or_none()
    if memorial is None:
        raise NotFoundError("Memorial not found")
    record = memorial.record
    if record is None or getattr(record, "deleted_at", None) is not None:
        raise NotFoundError("Memorial not found")
    return memorial


async def increment_candle(
    db: AsyncSession, slug: str, tenant_id: UUID | str
) -> int:
    """Increment candle_count by 1 and return the new total.

    Uses an atomic ``UPDATE ... SET candle_count = candle_count + 1 RETURNING``
    so concurrent visitors on different IPs cannot lose updates (the per-IP
    rate limit does not serialise cross-IP concurrency)."""
    memorial = await _resolve_published_memorial(db, slug, tenant_id)
    new_count = (
        await db.execute(
            update(Memorial)
            .where(Memorial.id == memorial.id)
            .values(candle_count=Memorial.candle_count + 1)
            .returning(Memorial.candle_count)
        )
    ).scalar_one()
    await db.flush()
    return new_count


async def list_approved_tributes(
    db: AsyncSession,
    slug: str,
    tenant_id: UUID | str,
    page: int = 1,
    page_size: int = 6,
) -> Tuple[List[PublicTributeItem], int]:
    """Return a page of approved tributes (newest first) plus the total count."""
    memorial = await _resolve_published_memorial(db, slug, tenant_id)

    base = and_(
        Tribute.tenant_id == tenant_id,
        Tribute.memorial_id == memorial.id,
        Tribute.status == "approved",
    )
    total = (
        await db.execute(select(func.count(Tribute.id)).where(base))
    ).scalar_one()

    offset = (page - 1) * page_size
    rows = (
        await db.execute(
            select(Tribute)
            .where(base)
            .order_by(Tribute.submitted_at.desc())
            .offset(offset)
            .limit(page_size)
        )
    ).scalars().all()

    items = [
        PublicTributeItem(
            id=t.id,
            submitter_name=t.submitter_name,
            relationship=t.relationship_type,
            message=t.message,
            photo_url=_image_url(t.photo_url) if t.photo_url else None,
            created_at=t.submitted_at,
        )
        for t in rows
    ]
    return items, total
