# mypy: disable-error-code="arg-type,assignment,union-attr"
# SQLAlchemy 1.x Column() descriptors are Column[T] statically, T at runtime.
"""
Legal document consent service — user consents, audit log, cookie categories,
and consent statistics.

Phase 1 Step 4: extracted from ``compliance.api.legal_document_routes``.
Document/version/approval workflow lives in
``compliance.services.legal_document_service.LegalDocumentService``.
"""

import uuid as uuid_mod
from datetime import datetime, timezone
from typing import Any, Optional

from sqlalchemy import func
from sqlalchemy.orm import Session

from compliance.db.legal_document_extend_models import (
    ConsentAuditLogDB,
    CookieCategoryDB,
    UserConsentDB,
)
from compliance.db.legal_document_models import LegalDocumentDB
from compliance.domain import NotFoundError, ValidationError
from compliance.schemas.legal_document import (
    CookieCategoryCreate,
    CookieCategoryUpdate,
    UserConsentCreate,
)


# ============================================================================
# Serialisation helpers
# ============================================================================


def _log_consent_audit(
    db: Session,
    tenant_id: Any,
    action: str,
    entity_type: str,
    entity_id: Any = None,
    user_id: Optional[str] = None,
    details: Optional[dict[str, Any]] = None,
    ip_address: Optional[str] = None,
) -> ConsentAuditLogDB:
    entry = ConsentAuditLogDB(
        tenant_id=tenant_id,
        action=action,
        entity_type=entity_type,
        entity_id=entity_id,
        user_id=user_id,
        details=details or {},
        ip_address=ip_address,
    )
    db.add(entry)
    return entry


def _consent_to_dict(c: UserConsentDB) -> dict[str, Any]:
    return {
        "id": str(c.id),
        "tenant_id": str(c.tenant_id),
        "user_id": c.user_id,
        "document_id": str(c.document_id),
        "document_version_id": str(c.document_version_id) if c.document_version_id else None,
        "document_type": c.document_type,
        "consented": c.consented,
        "ip_address": c.ip_address,
        "user_agent": c.user_agent,
        "consented_at": c.consented_at.isoformat() if c.consented_at else None,
        "withdrawn_at": c.withdrawn_at.isoformat() if c.withdrawn_at else None,
        "created_at": c.created_at.isoformat() if c.created_at else None,
    }


def _cookie_cat_to_dict(c: CookieCategoryDB) -> dict[str, Any]:
    return {
        "id": str(c.id),
        "tenant_id": str(c.tenant_id),
        "name_de": c.name_de,
        "name_en": c.name_en,
        "description_de": c.description_de,
        "description_en": c.description_en,
        "is_required": c.is_required,
        "sort_order": c.sort_order,
        "is_active": c.is_active,
        "created_at": c.created_at.isoformat() if c.created_at else None,
        "updated_at": c.updated_at.isoformat() if c.updated_at else None,
    }


# ============================================================================
# Service
# ============================================================================


class LegalDocumentConsentService:
    """Business logic for user consents, audit log, and cookie categories."""

    def __init__(self, db: Session) -> None:
        self.db = db

    # ------------------------------------------------------------------
    # User consents
    # ------------------------------------------------------------------

    def record_consent(
        self, tenant_id: str, body: UserConsentCreate
    ) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        doc_id = uuid_mod.UUID(body.document_id)

        doc = (
            self.db.query(LegalDocumentDB)
            .filter(LegalDocumentDB.id == doc_id)
            .first()
        )
        if not doc:
            raise NotFoundError("Document not found")

        consent = UserConsentDB(
            tenant_id=tid,
            user_id=body.user_id,
            document_id=doc_id,
            document_version_id=(
                uuid_mod.UUID(body.document_version_id)
                if body.document_version_id
                else None
            ),
            document_type=body.document_type,
            consented=body.consented,
            ip_address=body.ip_address,
            user_agent=body.user_agent,
        )
        self.db.add(consent)
        self.db.flush()

        _log_consent_audit(
            self.db,
            tid,
            "consent_given",
            "user_consent",
            entity_id=consent.id,
            user_id=body.user_id,
            details={
                "document_type": body.document_type,
                "document_id": body.document_id,
            },
            ip_address=body.ip_address,
        )

        self.db.commit()
        self.db.refresh(consent)
        return _consent_to_dict(consent)

    def get_my_consents(
        self, tenant_id: str, user_id: str
    ) -> list[dict[str, Any]]:
        tid = uuid_mod.UUID(tenant_id)
        consents = (
            self.db.query(UserConsentDB)
            .filter(
                UserConsentDB.tenant_id == tid,
                UserConsentDB.user_id == user_id,
                UserConsentDB.withdrawn_at.is_(None),
            )
            .order_by(UserConsentDB.consented_at.desc())
            .all()
        )
        return [_consent_to_dict(c) for c in consents]

    def check_consent(
        self, tenant_id: str, document_type: str, user_id: str
    ) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        consent = (
            self.db.query(UserConsentDB)
            .filter(
                UserConsentDB.tenant_id == tid,
                UserConsentDB.user_id == user_id,
                UserConsentDB.document_type == document_type,
                UserConsentDB.consented.is_(True),
                UserConsentDB.withdrawn_at.is_(None),
            )
            .order_by(UserConsentDB.consented_at.desc())
            .first()
        )
        return {
            "has_consent": consent is not None,
            "consent": _consent_to_dict(consent) if consent else None,
        }

    def withdraw_consent(
        self, tenant_id: str, consent_id: str
    ) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        try:
            cid = uuid_mod.UUID(consent_id)
        except ValueError:
            raise ValidationError("Invalid consent ID")

        consent = (
            self.db.query(UserConsentDB)
            .filter(
                UserConsentDB.id == cid,
                UserConsentDB.tenant_id == tid,
            )
            .first()
        )
        if not consent:
            raise NotFoundError("Consent not found")
        if consent.withdrawn_at:
            raise ValidationError("Consent already withdrawn")

        consent.withdrawn_at = datetime.now(timezone.utc)
        consent.consented = False

        _log_consent_audit(
            self.db,
            tid,
            "consent_withdrawn",
            "user_consent",
            entity_id=cid,
            user_id=consent.user_id,
            details={"document_type": consent.document_type},
        )

        self.db.commit()
        self.db.refresh(consent)
        return _consent_to_dict(consent)

    # ------------------------------------------------------------------
    # Consent statistics
    # ------------------------------------------------------------------

    def get_consent_stats(self, tenant_id: str) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        base = self.db.query(UserConsentDB).filter(
            UserConsentDB.tenant_id == tid
        )

        total = base.count()
        active = base.filter(
            UserConsentDB.consented.is_(True),
            UserConsentDB.withdrawn_at.is_(None),
        ).count()
        withdrawn = base.filter(
            UserConsentDB.withdrawn_at.isnot(None),
        ).count()

        by_type: dict[str, int] = {}
        type_counts = (
            self.db.query(UserConsentDB.document_type, func.count(UserConsentDB.id))
            .filter(UserConsentDB.tenant_id == tid)
            .group_by(UserConsentDB.document_type)
            .all()
        )
        for dtype, count in type_counts:
            by_type[dtype] = count

        unique_users = (
            self.db.query(func.count(func.distinct(UserConsentDB.user_id)))
            .filter(UserConsentDB.tenant_id == tid)
            .scalar()
        ) or 0

        return {
            "total": total,
            "active": active,
            "withdrawn": withdrawn,
            "unique_users": unique_users,
            "by_type": by_type,
        }

    # ------------------------------------------------------------------
    # Audit log
    # ------------------------------------------------------------------

    def get_audit_log(
        self,
        tenant_id: str,
        limit: int = 50,
        offset: int = 0,
        action: Optional[str] = None,
        entity_type: Optional[str] = None,
    ) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        query = self.db.query(ConsentAuditLogDB).filter(
            ConsentAuditLogDB.tenant_id == tid
        )
        if action:
            query = query.filter(ConsentAuditLogDB.action == action)
        if entity_type:
            query = query.filter(ConsentAuditLogDB.entity_type == entity_type)

        total = query.count()
        entries = (
            query.order_by(ConsentAuditLogDB.created_at.desc())
            .offset(offset)
            .limit(limit)
            .all()
        )

        return {
            "entries": [
                {
                    "id": str(e.id),
                    "action": e.action,
                    "entity_type": e.entity_type,
                    "entity_id": str(e.entity_id) if e.entity_id else None,
                    "user_id": e.user_id,
                    "details": e.details or {},
                    "ip_address": e.ip_address,
                    "created_at": (
                        e.created_at.isoformat() if e.created_at else None
                    ),
                }
                for e in entries
            ],
            "total": total,
            "limit": limit,
            "offset": offset,
        }

    # ------------------------------------------------------------------
    # Cookie categories
    # ------------------------------------------------------------------

    def list_cookie_categories(
        self, tenant_id: str
    ) -> list[dict[str, Any]]:
        tid = uuid_mod.UUID(tenant_id)
        cats = (
            self.db.query(CookieCategoryDB)
            .filter(CookieCategoryDB.tenant_id == tid)
            .order_by(CookieCategoryDB.sort_order)
            .all()
        )
        return [_cookie_cat_to_dict(c) for c in cats]

    def create_cookie_category(
        self, tenant_id: str, body: CookieCategoryCreate
    ) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        cat = CookieCategoryDB(
            tenant_id=tid,
            name_de=body.name_de,
            name_en=body.name_en,
            description_de=body.description_de,
            description_en=body.description_en,
            is_required=body.is_required,
            sort_order=body.sort_order,
        )
        self.db.add(cat)
        self.db.commit()
        self.db.refresh(cat)
        return _cookie_cat_to_dict(cat)

    def update_cookie_category(
        self, tenant_id: str, category_id: str, body: CookieCategoryUpdate
    ) -> dict[str, Any]:
        tid = uuid_mod.UUID(tenant_id)
        try:
            cid = uuid_mod.UUID(category_id)
        except ValueError:
            raise ValidationError("Invalid category ID")

        cat = (
            self.db.query(CookieCategoryDB)
            .filter(
                CookieCategoryDB.id == cid,
                CookieCategoryDB.tenant_id == tid,
            )
            .first()
        )
        if not cat:
            raise NotFoundError("Cookie category not found")

        for field in [
            "name_de", "name_en", "description_de", "description_en",
            "is_required", "sort_order", "is_active",
        ]:
            val = getattr(body, field, None)
            if val is not None:
                setattr(cat, field, val)

        cat.updated_at = datetime.now(timezone.utc)
        self.db.commit()
        self.db.refresh(cat)
        return _cookie_cat_to_dict(cat)

    def delete_cookie_category(
        self, tenant_id: str, category_id: str
    ) -> None:
        tid = uuid_mod.UUID(tenant_id)
        try:
            cid = uuid_mod.UUID(category_id)
        except ValueError:
            raise ValidationError("Invalid category ID")

        cat = (
            self.db.query(CookieCategoryDB)
            .filter(
                CookieCategoryDB.id == cid,
                CookieCategoryDB.tenant_id == tid,
            )
            .first()
        )
        if not cat:
            raise NotFoundError("Cookie category not found")

        self.db.delete(cat)
        self.db.commit()
