from datetime import datetime, timezone
from typing import Optional
from uuid import UUID

from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import and_, select, func
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.memorials.schemas.requests import UpdateMemorialRequest, UpdateTributeStatusRequest, ReplaceTimelineRequest
from src.apps.memorials.schemas.responses import MemorialResponse, MemorialSummaryResponse, TributeResponse
from src.apps.site_admin.services.audit_service import AuditService
from src.core.constants import UserRole
from src.core.dependencies import get_current_user, require_roles, require_min_role
from src.core.exceptions import NotFoundError
from src.core.schemas.response import paginated, success
from src.core.utils.sanitize import sanitize_display_name
from src.database.session import get_db

router = APIRouter(prefix="/memorials", tags=["Memorials"])


def _resolve_deceased_name(tribute: Tribute) -> Optional[str]:
    """Derive the deceased person's name for a tribute from its linked memorial.

    Prefers the memorial's ``display_name``; falls back to the record's
    ``first_name + last_name``. The result is sanitised (HTML/control chars
    stripped, capped at 200 chars) to keep server-derived text safe for both
    the API response and any audit/log usage (INDL-44 AC-10 / SEC-09).
    """
    memorial = getattr(tribute, "memorial", None)
    if memorial is None:
        return None
    name = memorial.display_name
    if not name:
        record = getattr(memorial, "record", None)
        if record is not None:
            name = f"{record.first_name} {record.last_name}".strip()
    return sanitize_display_name(name)


def _serialize_tribute(tribute: Tribute) -> dict:
    """Serialise a Tribute to the response envelope with a resolved deceased_name."""
    data = TributeResponse.model_validate(tribute).model_dump()
    data["deceased_name"] = _resolve_deceased_name(tribute)
    return data


@router.get("", response_model=dict)
async def list_memorials(
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    is_published: Optional[bool] = Query(
        None,
        description="Filter by publication state. true → published only, "
        "false → unpublished only. Used by the moderation stat card "
        "(GET ?is_published=true&limit=1 reads `total`).",
    ),
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    """List memorials for the current tenant.

    INDL-44: supports an optional ``is_published`` filter so the moderation page
    can render the "Published Memorials" stat card without downloading rows.
    Tenant-scoped and authenticated-only — the filter is never exposed on
    ``/api/public/`` (INDL-44 SEC-09/SEC-10).
    """
    conditions = [Memorial.tenant_id == current_user.tenant_id]
    if is_published is not None:
        conditions.append(Memorial.is_published == is_published)

    count_result = await db.execute(
        select(func.count(Memorial.id)).where(and_(*conditions))
    )
    total = count_result.scalar_one()

    result = await db.execute(
        select(Memorial)
        .where(and_(*conditions))
        .order_by(Memorial.created_at.desc())
        .offset(skip)
        .limit(limit)
    )
    memorials = result.scalars().all()
    return paginated(
        items=[MemorialSummaryResponse.model_validate(m).model_dump() for m in memorials],
        total=total,
        page=(skip // limit) + 1,
        page_size=limit,
    )


@router.get("/tributes", response_model=dict)
async def list_tributes(
    skip: int = Query(0, ge=0),
    limit: int = Query(20, ge=1, le=100),
    status: Optional[str] = Query(None, description="Filter by status: pending | approved | rejected"),
    memorial_id: Optional[UUID] = Query(None, description="Filter to a specific memorial"),
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    """List tributes with optional filtering by status and memorial.

    INDL-44: eager-loads the linked memorial and record so each tribute row
    carries ``deceased_name`` (memorial.display_name, falling back to the
    record's first + last name). The ``limit`` query param is capped at 100 to
    prevent resource exhaustion (INDL-44 SEC-07 / OWASP API4).
    """
    conditions = [
        Tribute.tenant_id == current_user.tenant_id,
    ]
    if status:
        conditions.append(Tribute.status == status)
    elif memorial_id is None:
        # When querying the global moderation queue (no specific memorial),
        # default to pending-only to avoid returning all tributes across the tenant.
        conditions.append(Tribute.status == "pending")
    # When memorial_id is supplied without a status filter, return all statuses for that memorial.

    if memorial_id:
        conditions.append(Tribute.memorial_id == memorial_id)

    count_result = await db.execute(
        select(func.count(Tribute.id)).where(and_(*conditions))
    )
    total = count_result.scalar_one()

    result = await db.execute(
        select(Tribute)
        .options(selectinload(Tribute.memorial).selectinload(Memorial.record))
        .where(and_(*conditions))
        .order_by(Tribute.submitted_at.asc())
        .offset(skip)
        .limit(limit)
    )
    tributes = result.scalars().all()
    return paginated(
        items=[_serialize_tribute(t) for t in tributes],
        total=total,
        page=(skip // limit) + 1,
        page_size=limit,
    )


@router.get("/{memorial_id}", response_model=dict)
async def get_memorial(
    memorial_id: UUID,
    current_user=Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(Memorial)
        .options(
            selectinload(Memorial.tributes),
            selectinload(Memorial.timeline_events),
        )
        .where(
            and_(
                Memorial.id == memorial_id,
                Memorial.tenant_id == current_user.tenant_id,
            )
        )
    )
    memorial = result.scalar_one_or_none()
    if not memorial:
        raise NotFoundError("Memorial not found")
    return success(data=MemorialResponse.model_validate(memorial).model_dump())


@router.patch("/{memorial_id}", response_model=dict)
async def update_memorial(
    memorial_id: UUID,
    body: UpdateMemorialRequest,
    request: Request,
    current_user=Depends(
        require_roles(UserRole.ADMINISTRATOR, UserRole.MANAGER, UserRole.STAFF)
    ),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(Memorial).where(
            and_(
                Memorial.id == memorial_id,
                Memorial.tenant_id == current_user.tenant_id,
            )
        )
    )
    memorial = result.scalar_one_or_none()
    if not memorial:
        raise NotFoundError("Memorial not found")

    old_values = {
        "biography_text": memorial.biography_text,
        "is_published": memorial.is_published,
        "display_name": memorial.display_name,
        "headstone_inscription": memorial.headstone_inscription,
    }

    update_data = body.model_dump(exclude_none=True)
    for field, value in update_data.items():
        if hasattr(memorial, field):
            setattr(memorial, field, value)

    await db.flush()
    await db.refresh(memorial)

    await AuditService.log(
        db,
        "memorial",
        memorial.id,
        "updated",
        current_user,
        request,
        old_value=old_values,
        new_value=update_data,
    )

    return success(
        data=MemorialSummaryResponse.model_validate(memorial).model_dump(),
        message="Memorial updated",
    )


@router.patch("/{memorial_id}/publish", response_model=dict)
async def toggle_publish(
    memorial_id: UUID,
    request: Request,
    current_user=Depends(
        require_roles(UserRole.ADMINISTRATOR, UserRole.MANAGER, UserRole.STAFF)
    ),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(Memorial).where(
            and_(
                Memorial.id == memorial_id,
                Memorial.tenant_id == current_user.tenant_id,
            )
        )
    )
    memorial = result.scalar_one_or_none()
    if not memorial:
        raise NotFoundError("Memorial not found")

    old_published = memorial.is_published
    memorial.is_published = not memorial.is_published
    memorial.published_at = datetime.now(timezone.utc) if memorial.is_published else None

    await db.flush()
    await db.refresh(memorial)

    action = "published" if memorial.is_published else "unpublished"
    await AuditService.log(
        db,
        "memorial",
        memorial.id,
        action,
        current_user,
        request,
        old_value={"is_published": old_published},
        new_value={"is_published": memorial.is_published},
    )

    return success(
        data=MemorialSummaryResponse.model_validate(memorial).model_dump(),
        message=f"Memorial {action}",
    )


@router.patch("/tributes/{tribute_id}/status", response_model=dict)
async def update_tribute_status(
    tribute_id: UUID,
    body: UpdateTributeStatusRequest,
    request: Request,
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    """Approve or reject a tribute, optionally editing its message first.

    INDL-44: the tribute is fetched tenant-scoped (returns 404 on a cross-tenant
    id — INDL-44 SEC-01); when ``body.message`` is supplied it is persisted
    (already sanitised by the schema) before the status change; the tribute
    update and audit-log write share one transaction (INDL-44 SEC-04).
    """
    result = await db.execute(
        select(Tribute)
        .options(selectinload(Tribute.memorial).selectinload(Memorial.record))
        .where(
            and_(
                Tribute.id == tribute_id,
                Tribute.tenant_id == current_user.tenant_id,
            )
        )
    )
    tribute = result.scalar_one_or_none()
    if not tribute:
        raise NotFoundError("Tribute not found")

    old_status = tribute.status
    old_message = tribute.message
    message_edited = body.message is not None and body.message != old_message

    if body.message is not None:
        tribute.message = body.message
    tribute.status = body.status
    tribute.moderated_by = current_user.id
    tribute.moderated_at = datetime.now(timezone.utc)

    await db.flush()
    await db.refresh(tribute)

    # Structured audit entry: records status transition and, when the message was
    # edited, the old/new message lengths (not the full text) to avoid PII in logs
    # while keeping a forensic trail (INDL-44 SEC-04 / SEC-06 / ASVS V7.2.1).
    action = f"status_{body.status}"
    if message_edited:
        action = f"status_{body.status}_edited"
    old_value = {"status": old_status}
    new_value = {"status": tribute.status}
    if message_edited:
        old_value["message_length"] = len(old_message or "")
        new_value["message_length"] = len(tribute.message or "")
    await AuditService.log(
        db,
        "tribute",
        tribute.id,
        action,
        current_user,
        request,
        old_value=old_value,
        new_value=new_value,
        # SEC-04: the audit entry and the status change must commit atomically —
        # if the audit write fails the whole request rolls back.
        raise_on_error=True,
    )

    return success(
        data=_serialize_tribute(tribute),
        message=f"Tribute {body.status}",
    )


@router.post("/{memorial_id}/biography", response_model=dict)
async def generate_biography_for_memorial(
    memorial_id: UUID,
    current_user=Depends(
        require_roles(UserRole.ADMINISTRATOR, UserRole.MANAGER, UserRole.STAFF)
    ),
    db: AsyncSession = Depends(get_db),
):
    """
    Generate an AI biography draft for a memorial, using data from the linked
    Record. Stores the result in memorial.ai_biography_text (separate from the
    user-editable biography_text column). Returns the generated text.
    """
    from fastapi import HTTPException, status as http_status
    from src.core.config import settings
    from src.apps.records.models.record import Record

    # 1. Load the memorial (tenant-scoped)
    result = await db.execute(
        select(Memorial).where(
            and_(
                Memorial.id == memorial_id,
                Memorial.tenant_id == current_user.tenant_id,
            )
        )
    )
    memorial = result.scalar_one_or_none()
    if not memorial:
        raise NotFoundError("Memorial not found")

    # 2. Load the linked record
    record_result = await db.execute(
        select(Record).where(
            Record.id == memorial.record_id,
            Record.tenant_id == current_user.tenant_id,
            Record.deleted_at.is_(None),
        )
    )
    record = record_result.scalar_one_or_none()
    if not record:
        raise NotFoundError("Associated record not found")

    if not settings.ANTHROPIC_API_KEY:
        raise HTTPException(
            status_code=http_status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="AI service is not configured. Set ANTHROPIC_API_KEY.",
        )

    # 3. Build the prompt from record data
    life_span = ""
    if record.date_of_birth and record.date_of_death:
        life_span = f" ({record.date_of_birth.year}–{record.date_of_death.year})"
    elif record.date_of_birth:
        life_span = f" (born {record.date_of_birth.year})"

    display_name = memorial.display_name or f"{record.first_name} {record.last_name}"
    prompt = (
        f"You are a compassionate writer helping a cemetery staff member create a "
        f"memorial biography. Write a biography for {display_name}{life_span} "
        f"in a warm and respectful tone.\n\n"
        f"Write 2–4 paragraphs. Focus on celebrating their life. "
        f"Do not add unverified facts. Keep it dignified and heartfelt."
    )

    # 4. Call Claude
    try:
        import anthropic
        client = anthropic.AsyncAnthropic(api_key=settings.ANTHROPIC_API_KEY)
        message = await client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            messages=[{"role": "user", "content": prompt}],
        )
        biography_text = message.content[0].text.strip()
    except Exception as exc:  # pragma: no cover
        raise HTTPException(
            status_code=http_status.HTTP_502_BAD_GATEWAY,
            detail=f"AI service error: {str(exc)}",
        )

    # 5. Persist the AI draft in ai_biography_text (does NOT touch biography_text)
    memorial.ai_biography_text = biography_text
    await db.flush()
    await db.refresh(memorial)

    return success(
        data={"ai_biography_text": biography_text},
        message="AI biography draft generated",
    )


@router.get("/{memorial_id}/timeline", response_model=dict)
async def get_timeline(
    memorial_id: UUID,
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    # Verify memorial belongs to tenant
    result = await db.execute(
        select(Memorial).where(
            and_(
                Memorial.id == memorial_id,
                Memorial.tenant_id == current_user.tenant_id,
            )
        )
    )
    memorial = result.scalar_one_or_none()
    if not memorial:
        raise NotFoundError("Memorial not found")

    # Load timeline events ordered by sort_order
    from src.apps.memorials.models.timeline_event import MemorialTimelineEvent
    from src.apps.memorials.schemas.responses import TimelineEventResponse
    result = await db.execute(
        select(MemorialTimelineEvent)
        .where(
            and_(
                MemorialTimelineEvent.memorial_id == memorial_id,
                MemorialTimelineEvent.tenant_id == current_user.tenant_id,
            )
        )
        .order_by(MemorialTimelineEvent.sort_order)
    )
    events = result.scalars().all()
    return success(
        data=[TimelineEventResponse.model_validate(e).model_dump() for e in events]
    )


@router.put("/{memorial_id}/timeline", response_model=dict)
async def replace_timeline(
    memorial_id: UUID,
    body: ReplaceTimelineRequest,
    request: Request,
    current_user=Depends(require_min_role(UserRole.STAFF)),
    db: AsyncSession = Depends(get_db),
):
    from src.apps.memorials.services.memorial_service import MemorialService
    from src.apps.memorials.schemas.responses import TimelineEventResponse
    events = await MemorialService.replace_timeline(
        db=db,
        memorial_id=memorial_id,
        tenant_id=current_user.tenant_id,
        events=body.events,
        current_user=current_user,
        request=request,
    )
    return success(
        data=[TimelineEventResponse.model_validate(e).model_dump() for e in events],
        message="Timeline updated",
    )
