"""Business logic for norm CRUD and versioning."""
from __future__ import annotations

import uuid
from typing import Any, Dict, List, Optional

from fastapi import HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from normbase.config import settings
from normbase.models.norm import Norm, NormTag, NormVersion
from normbase.schemas import NormCreate, NormUpdate
from normbase.services.cache_service import CacheService
from normbase.services.norm_vector_service import NormVectorService


class NormService:
    """Service layer for norms."""
    BUSINESS_TAG_CATEGORIES = {
        "role",
        "scenario",
        "domain",
        "tool",
        "skill",
        "capability",
    }

    def __init__(self, vector_service: NormVectorService | None = None):
        self.vector_service = vector_service or NormVectorService()

    def _as_json(self, value: Any) -> Dict[str, Any]:
        """Convert typed schema payloads to plain JSON-serializable dicts."""
        if value is None:
            return {}
        if hasattr(value, "model_dump"):
            return value.model_dump()
        if isinstance(value, dict):
            return value
        return {}

    async def _invalidate_retrieve_cache(self) -> None:
        cache = CacheService()
        try:
            await cache.delete_by_prefix(settings.retrieve_cache_prefix)
        except Exception as e:
            print(f"Retrieve cache invalidation error: {e}")

    def _build_norm_snapshot(self, norm: Norm) -> Dict[str, Any]:
        return {
            "id": str(norm.id),
            "title": norm.title,
            "field_id": str(norm.field_id) if norm.field_id else None,
            "norm_type": norm.norm_type,
            "authority_level": norm.authority_level,
            "formality_level": norm.formality_level,
            "subject": norm.subject,
            "context": norm.context,
            "condition": norm.condition,
            "action": norm.action,
            "consequence": norm.consequence,
            "source": norm.source,
            "source_details": norm.source_details,
            "case_refs": norm.case_refs,
            "created_by": norm.created_by,
            "created_at": norm.created_at.isoformat() if norm.created_at else None,
            "updated_at": norm.updated_at.isoformat() if norm.updated_at else None,
            "status": norm.status,
            "version": norm.version,
            "tags": [{"tag": t.tag, "tag_category": t.tag_category} for t in norm.tags],
        }

    def _normalize_text_list(self, values: Any) -> List[str]:
        if isinstance(values, list):
            normalized = []
            for item in values:
                text = str(item).strip()
                if text:
                    normalized.append(text)
            return normalized
        return []

    def _canonical(self, payload: Any) -> Dict[str, Any]:
        if not isinstance(payload, dict):
            return {}
        canonical = payload.get("canonical", {})
        return canonical if isinstance(canonical, dict) else {}

    def _derive_business_tags(
        self,
        *,
        subject: Dict[str, Any],
        context: Dict[str, Any],
        action: Dict[str, Any],
    ) -> List[Dict[str, str]]:
        derived: List[Dict[str, str]] = []
        subject_canonical = self._canonical(subject)
        context_canonical = self._canonical(context)
        action_canonical = self._canonical(action)

        mapping = {
            "role": self._normalize_text_list(subject_canonical.get("roles", [])),
            "scenario": self._normalize_text_list(context_canonical.get("scenarios", [])),
            "domain": self._normalize_text_list(context_canonical.get("domains", [])),
            "capability": self._normalize_text_list(
                subject_canonical.get("capabilities", [])
            ),
            "tool": self._normalize_text_list(action_canonical.get("tools", [])),
            "skill": self._normalize_text_list(action_canonical.get("skills", [])),
        }
        for category, values in mapping.items():
            for tag in values:
                derived.append({"tag_category": category, "tag": tag})
        return derived

    def _normalize_input_tags(self, tags: Any) -> List[Dict[str, str]]:
        normalized: List[Dict[str, str]] = []
        if not isinstance(tags, list):
            return normalized
        for item in tags:
            if hasattr(item, "model_dump"):
                item = item.model_dump()
            if not isinstance(item, dict):
                continue
            category = str(item.get("tag_category", "")).strip().lower()
            tag = str(item.get("tag", "")).strip()
            if not category or not tag:
                continue
            normalized.append({"tag_category": category, "tag": tag})
        return normalized

    def _dedupe_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
        deduped: List[Dict[str, str]] = []
        seen = set()
        for item in tags:
            key = (item["tag_category"], item["tag"])
            if key in seen:
                continue
            seen.add(key)
            deduped.append(item)
        return deduped

    def _merge_tags(
        self,
        *,
        derived_tags: List[Dict[str, str]],
        manual_tags: List[Dict[str, str]],
    ) -> List[Dict[str, str]]:
        filtered_manual = [
            item
            for item in manual_tags
            if item["tag_category"] not in self.BUSINESS_TAG_CATEGORIES
        ]
        return self._dedupe_tags([*derived_tags, *filtered_manual])

    async def create_norm(self, db: AsyncSession, norm_data: NormCreate) -> Norm:
        subject_payload = self._as_json(norm_data.subject)
        context_payload = self._as_json(norm_data.context)
        condition_payload = self._as_json(norm_data.condition)
        action_payload = self._as_json(norm_data.action)
        consequence_payload = self._as_json(norm_data.consequence)
        derived_tags = self._derive_business_tags(
            subject=subject_payload,
            context=context_payload,
            action=action_payload,
        )
        manual_tags = self._normalize_input_tags(norm_data.tags)
        merged_tags = self._merge_tags(derived_tags=derived_tags, manual_tags=manual_tags)

        norm = Norm(
            title=norm_data.title,
            field_id=norm_data.field_id,
            norm_type=norm_data.norm_type,
            authority_level=norm_data.authority_level,
            formality_level=norm_data.formality_level,
            subject=subject_payload,
            context=context_payload,
            condition=condition_payload,
            action=action_payload,
            consequence=consequence_payload,
            source=norm_data.source,
            source_details=norm_data.source_details,
            case_refs=norm_data.case_refs,
            created_by=norm_data.created_by,
            status=norm_data.status,
        )
        norm.tags = [
            NormTag(tag=item["tag"], tag_category=item["tag_category"])
            for item in merged_tags
        ]
        db.add(norm)
        await db.commit()
        result = await db.execute(
            select(Norm).options(selectinload(Norm.tags)).where(Norm.id == norm.id)
        )
        created = result.scalar_one()
        # Qdrant only holds active norms (NormVectorService.sync_norm also enforces this).
        if created.status == "active":
            await self.vector_service.sync_norm(created)
        await self._invalidate_retrieve_cache()
        return created

    async def list_norms(
        self,
        db: AsyncSession,
        *,
        page: int,
        page_size: int,
        norm_type: Optional[str],
        status_filter: Optional[str],
        authority_level: Optional[str],
        formality_level: Optional[str],
        field_id: Optional[uuid.UUID],
    ) -> tuple[int, List[Norm]]:
        query = select(Norm).options(selectinload(Norm.tags))
        count_query = select(func.count(Norm.id))

        if norm_type:
            query = query.where(Norm.norm_type == norm_type)
            count_query = count_query.where(Norm.norm_type == norm_type)
        if status_filter:
            query = query.where(Norm.status == status_filter)
            count_query = count_query.where(Norm.status == status_filter)
        if authority_level:
            query = query.where(Norm.authority_level == authority_level)
            count_query = count_query.where(Norm.authority_level == authority_level)
        if formality_level:
            query = query.where(Norm.formality_level == formality_level)
            count_query = count_query.where(Norm.formality_level == formality_level)
        if field_id:
            query = query.where(Norm.field_id == field_id)
            count_query = count_query.where(Norm.field_id == field_id)

        total_result = await db.execute(count_query)
        total = total_result.scalar() or 0
        query = query.offset((page - 1) * page_size).limit(page_size)
        result = await db.execute(query)
        norms = result.scalars().all()
        return total, list(norms)

    async def get_norm(self, db: AsyncSession, *, norm_id: uuid.UUID) -> Norm:
        result = await db.execute(
            select(Norm).options(selectinload(Norm.tags)).where(Norm.id == norm_id)
        )
        norm = result.scalar_one_or_none()
        if not norm:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Norm with id {norm_id} not found",
            )
        return norm

    async def update_norm(
        self,
        db: AsyncSession,
        *,
        norm_id: uuid.UUID,
        norm_data: NormUpdate,
    ) -> Norm:
        norm = await self.get_norm(db, norm_id=norm_id)

        version_snapshot = NormVersion(
            norm_id=norm.id,
            version=norm.version,
            snapshot=self._build_norm_snapshot(norm),
        )
        db.add(version_snapshot)

        update_data = norm_data.model_dump(exclude_unset=True)
        for field, value in update_data.items():
            if field == "tags":
                continue
            setattr(norm, field, value)
        should_recompute_tags = any(
            field in update_data for field in ("subject", "context", "action", "tags")
        )
        if should_recompute_tags:
            subject_payload = norm.subject if isinstance(norm.subject, dict) else {}
            context_payload = norm.context if isinstance(norm.context, dict) else {}
            action_payload = norm.action if isinstance(norm.action, dict) else {}
            derived_tags = self._derive_business_tags(
                subject=subject_payload,
                context=context_payload,
                action=action_payload,
            )
            manual_tags_source = update_data.get(
                "tags",
                [{"tag": t.tag, "tag_category": t.tag_category} for t in (norm.tags or [])],
            )
            manual_tags = self._normalize_input_tags(manual_tags_source)
            merged_tags = self._merge_tags(
                derived_tags=derived_tags,
                manual_tags=manual_tags,
            )
            norm.tags = [
                NormTag(tag=item["tag"], tag_category=item["tag_category"])
                for item in merged_tags
            ]

        norm.version += 1
        await db.commit()
        result = await db.execute(
            select(Norm).options(selectinload(Norm.tags)).where(Norm.id == norm.id)
        )
        updated = result.scalar_one()
        await self.vector_service.sync_norm(updated)
        await self._invalidate_retrieve_cache()
        return updated

    async def delete_norm(self, db: AsyncSession, *, norm_id: uuid.UUID) -> None:
        norm = await self.get_norm(db, norm_id=norm_id)
        norm.status = "archived"
        await db.commit()
        await self.vector_service.delete_norm(norm_id)
        await self._invalidate_retrieve_cache()

    async def list_norm_versions(
        self,
        db: AsyncSession,
        *,
        norm_id: uuid.UUID,
    ) -> List[NormVersion]:
        result = await db.execute(
            select(NormVersion)
            .where(NormVersion.norm_id == norm_id)
            .order_by(NormVersion.version.desc())
        )
        return list(result.scalars().all())

    async def create_norm_version(
        self,
        db: AsyncSession,
        *,
        norm_id: uuid.UUID,
        change_note: str,
    ) -> NormVersion:
        norm = await self.get_norm(db, norm_id=norm_id)
        version = NormVersion(
            norm_id=norm.id,
            version=norm.version,
            snapshot=self._build_norm_snapshot(norm),
            change_note=change_note,
        )
        db.add(version)
        await db.commit()
        await db.refresh(version)
        return version
