from dataclasses import dataclass
from typing import List, Optional, Tuple
from uuid import UUID

from sqlalchemy import and_, outerjoin, select
from sqlalchemy.ext.asyncio import AsyncSession

from src.apps.records.models.burial_info import BurialInfo
from src.apps.records.models.family_contact import FamilyContact
from src.apps.site_admin.models.audit_log import AuditLog

# Upper bound on the number of related entity IDs collected for a single record's
# audit query. Prevents an unbounded IN-clause (and slow query / DoS) on records
# that accumulate a very large number of documents. See INDL-39 security notes
# (OWASP API4 — Unrestricted Resource Consumption, AS-10).
MAX_RELATED_IDS = 500


@dataclass
class AuditLogWithUser:
    log: AuditLog
    user_first_name: Optional[str]
    user_last_name: Optional[str]


class AuditService:
    @staticmethod
    async def log(
        db: AsyncSession,
        entity_type: str,
        entity_id: UUID,
        action: str,
        user,
        request,
        old_value: Optional[dict] = None,
        new_value: Optional[dict] = None,
        raise_on_error: bool = False,
    ) -> None:
        """Write a structured audit entry.

        By default audit failures are swallowed so a logging hiccup never breaks
        the primary request. Pass ``raise_on_error=True`` for high-value actions
        (e.g. tribute moderation, INDL-44 SEC-04) where the audit entry and the
        state change must be atomic — the exception then propagates and the
        request transaction rolls back so neither is persisted.
        """
        try:
            # Extract IP: prefer X-Forwarded-For, fall back to direct client host
            ip_address = None
            if request is not None:
                forwarded_for = request.headers.get("x-forwarded-for")
                if forwarded_for:
                    ip_address = forwarded_for.split(",")[0].strip()
                elif request.client:
                    ip_address = str(request.client.host)

            entry = AuditLog(
                tenant_id=getattr(user, "tenant_id", None),
                user_id=user.id,
                entity_type=entity_type,
                entity_id=entity_id,
                action=action,
                old_value=old_value,
                new_value=new_value,
                ip_address=ip_address,
                user_agent=request.headers.get("user-agent") if request is not None else None,
            )
            db.add(entry)
            await db.flush()
        except Exception:
            if raise_on_error:
                raise
            pass

    @staticmethod
    async def get_record_audit(
        db: AsyncSession,
        tenant_id: UUID,
        record_id: UUID,
        page: int = 1,
        page_size: int = 20,
    ) -> Tuple[List[AuditLogWithUser], int]:
        from sqlalchemy import func
        from src.apps.auth.models.user import User
        from src.apps.documents.models.document import Document
        from src.apps.memorials.models.memorial import Memorial

        # Collect all related entity IDs: the record itself + burial_info +
        # family_contacts + attached documents + the memorial (if any).
        related_ids: List[UUID] = [record_id]

        burial_result = await db.execute(
            select(BurialInfo.id).where(BurialInfo.record_id == record_id)
        )
        burial_ids = burial_result.scalars().all()
        related_ids.extend(burial_ids)

        contact_result = await db.execute(
            select(FamilyContact.id).where(FamilyContact.record_id == record_id)
        )
        contact_ids = contact_result.scalars().all()
        related_ids.extend(contact_ids)

        # Documents attached to this record. The tenant_id filter is applied
        # INDEPENDENTLY here (not inferred from the parent record) so a document
        # from another tenant that happens to share an entity_id can never leak
        # its audit entries into this response (INDL-39 AS-02 / SEC-04).
        # Cap the collected IDs to keep the downstream IN-clause bounded.
        remaining = MAX_RELATED_IDS - len(related_ids)
        if remaining > 0:
            doc_result = await db.execute(
                select(Document.id)
                .where(
                    Document.entity_type == "record",
                    Document.entity_id == record_id,
                    Document.tenant_id == tenant_id,
                )
                .limit(remaining)
            )
            related_ids.extend(doc_result.scalars().all())

        # The memorial for this record (one-to-one). Tenant filter applied
        # independently for the same cross-tenant isolation reason.
        memorial_result = await db.execute(
            select(Memorial.id).where(
                Memorial.record_id == record_id,
                Memorial.tenant_id == tenant_id,
            )
        )
        memorial_id = memorial_result.scalar_one_or_none()
        if memorial_id is not None:
            related_ids.append(memorial_id)

        base_conditions = and_(
            AuditLog.tenant_id == tenant_id,
            AuditLog.entity_id.in_(related_ids),
            AuditLog.entity_type.in_(
                ["Record", "BurialInfo", "FamilyContact", "document", "memorial"]
            ),
        )

        count_result = await db.execute(
            select(func.count(AuditLog.id)).where(base_conditions)
        )
        total = count_result.scalar_one()

        offset = (page - 1) * page_size
        data_result = await db.execute(
            select(AuditLog, User.first_name, User.last_name)
            .select_from(
                outerjoin(AuditLog, User, AuditLog.user_id == User.id)
            )
            .where(base_conditions)
            .order_by(AuditLog.created_at.desc())
            .offset(offset)
            .limit(page_size)
        )
        rows = data_result.all()

        results = [
            AuditLogWithUser(
                log=row[0],
                user_first_name=row[1],
                user_last_name=row[2],
            )
            for row in rows
        ]

        return results, total
