"""Business logic for compliance checks and outcome logging."""
from __future__ import annotations

from datetime import datetime, timezone
import os
import uuid
from typing import Any, List

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

from normbase.models.norm import Norm, NormUsageLog, NormUsageToken
from normbase.schemas import (
    ComplianceCheckRequest,
    ComplianceCheckResponse,
    ComplianceViolation,
    NormOutcomeReportRequest,
    NormOutcomeReportResponse,
)

ALLOWED_OUTCOMES = {"complied", "violated", "exception", "inapplicable"}


class ComplianceService:
    """Service layer for compliance workflows."""

    def _utc_now(self) -> datetime:
        return datetime.now(timezone.utc).replace(tzinfo=None)

    def _to_utc(self, dt: datetime) -> datetime:
        if dt.tzinfo is None:
            return dt
        return dt.astimezone(timezone.utc).replace(tzinfo=None)

    def _strict_token_replay_enabled(self) -> bool:
        raw = os.getenv("NORMBASE_USAGE_TOKEN_STRICT_REPLAY", "1").strip().lower()
        return raw not in {"0", "false", "no", "off"}

    def _normalize_string_list(self, value: Any) -> List[str]:
        if isinstance(value, list):
            return [str(item).strip() for item in value if str(item).strip()]
        if isinstance(value, str):
            text = value.strip()
            return [text] if text else []
        return []

    def _extract_action_modality(self, action: dict[str, Any]) -> str:
        canonical = action.get("canonical", {}) if isinstance(action, dict) else {}
        if not isinstance(canonical, dict):
            return ""
        return str(canonical.get("modality", "")).strip().lower()

    def _extract_action_operations(self, action: dict[str, Any]) -> List[str]:
        canonical = action.get("canonical", {}) if isinstance(action, dict) else {}
        if not isinstance(canonical, dict):
            return []
        return self._normalize_string_list(canonical.get("operations"))

    async def check_action_compliance(
        self,
        db: AsyncSession,
        *,
        request: ComplianceCheckRequest,
        x_agent_id: str | None,
        x_key_id: str | None,
    ) -> ComplianceCheckResponse:
        token = (
            await db.execute(
                select(NormUsageToken).where(NormUsageToken.usage_token == request.usage_token)
            )
        ).scalar_one_or_none()
        if token is None:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="Unknown usage_token.",
            )
        if self._to_utc(token.expires_at) < self._utc_now():
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="usage_token expired.",
            )
        if token.closed_at and self._strict_token_replay_enabled():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="usage_token already closed.",
            )

        incoming_agent_id = (x_agent_id or "").strip() or None
        incoming_key_id = (x_key_id or "").strip() or None
        if token.agent_id and incoming_agent_id and token.agent_id != incoming_agent_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="usage_token does not belong to this agent_id.",
            )
        if token.key_id and incoming_key_id and token.key_id != incoming_key_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="usage_token does not belong to this key_id.",
            )

        token_norm_id_texts = [str(item).strip() for item in (token.norm_ids or []) if str(item).strip()]
        token_norm_id_set = set(token_norm_id_texts)
        requested_id_texts = [str(item).strip() for item in (request.norm_ids or []) if str(item).strip()]
        if requested_id_texts:
            out_of_token = sorted({item for item in requested_id_texts if item not in token_norm_id_set})
            if out_of_token:
                raise HTTPException(
                    status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                    detail=f"norm_ids not bound to usage_token: {out_of_token}",
                )
            requested_ids = list(dict.fromkeys(request.norm_ids or []))
        else:
            requested_ids = []
            for item in token_norm_id_texts:
                try:
                    requested_ids.append(uuid.UUID(item))
                except ValueError as exc:
                    raise HTTPException(
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                        detail="Invalid norm_id stored in usage_token.",
                    ) from exc

        if not requested_ids:
            return ComplianceCheckResponse(compliant=True, violations=[])

        rows = (
            await db.execute(select(Norm).where(Norm.id.in_(requested_ids)))
        ).scalars().all()
        norm_map = {row.id: row for row in rows}
        missing = [str(norm_id) for norm_id in requested_ids if norm_id not in norm_map]
        if missing:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Unknown norm_ids: {missing}",
            )

        action_text = request.action_description.lower().strip()
        violations: List[ComplianceViolation] = []
        for norm_id in requested_ids:
            norm = norm_map[norm_id]
            action = norm.action or {}
            modality = self._extract_action_modality(action)
            operations = self._extract_action_operations(action)

            if norm.norm_type == "obligation":
                if modality and modality != "must":
                    violations.append(
                        ComplianceViolation(
                            norm_id=norm.id,
                            violation_type="obligation",
                            message=f"Invalid action modality for obligation: {modality}",
                        )
                    )
                for operation in operations:
                    if operation.lower() not in action_text:
                        violations.append(
                            ComplianceViolation(
                                norm_id=norm.id,
                                violation_type="obligation",
                                message=f"Missing required operation: {operation}",
                            )
                        )
            elif norm.norm_type == "prohibition":
                if modality and modality != "must_not":
                    violations.append(
                        ComplianceViolation(
                            norm_id=norm.id,
                            violation_type="prohibition",
                            message=f"Invalid action modality for prohibition: {modality}",
                        )
                    )
                for operation in operations:
                    if operation.lower() in action_text:
                        violations.append(
                            ComplianceViolation(
                                norm_id=norm.id,
                                violation_type="prohibition",
                                message=f"Contains prohibited operation: {operation}",
                            )
                        )

        return ComplianceCheckResponse(
            compliant=len(violations) == 0,
            violations=violations,
        )

    async def report_norm_outcome(
        self,
        db: AsyncSession,
        *,
        request: NormOutcomeReportRequest,
        x_agent_id: str | None,
        x_key_id: str | None,
    ) -> NormOutcomeReportResponse:
        outcome = request.outcome.strip().lower()
        if outcome not in ALLOWED_OUTCOMES:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"Invalid outcome '{request.outcome}'. Allowed: {sorted(ALLOWED_OUTCOMES)}",
            )

        norm_exists = (
            await db.execute(select(Norm.id).where(Norm.id == request.norm_id))
        ).scalar_one_or_none()
        if norm_exists is None:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"Norm with id {request.norm_id} not found",
            )

        token = (
            await db.execute(
                select(NormUsageToken).where(NormUsageToken.usage_token == request.usage_token)
            )
        ).scalar_one_or_none()
        if token is None:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="Unknown usage_token.",
            )
        if self._to_utc(token.expires_at) < self._utc_now():
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="usage_token expired.",
            )
        if token.closed_at and self._strict_token_replay_enabled():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="usage_token already closed.",
            )
        if str(request.norm_id) not in (token.norm_ids or []):
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail=f"norm_id {request.norm_id} is not bound to usage_token.",
            )

        incoming_agent_id = (x_agent_id or request.agent_id or "").strip() or None
        incoming_key_id = (x_key_id or "").strip() or None
        if token.agent_id and incoming_agent_id and token.agent_id != incoming_agent_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="usage_token does not belong to this agent_id.",
            )
        if token.key_id and incoming_key_id and token.key_id != incoming_key_id:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="usage_token does not belong to this key_id.",
            )

        reported_norm_ids = list(token.reported_norm_ids or [])
        norm_id_str = str(request.norm_id)
        if norm_id_str in reported_norm_ids and self._strict_token_replay_enabled():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail=f"norm_id {request.norm_id} already reported for usage_token.",
            )

        row = NormUsageLog(
            usage_token=request.usage_token,
            norm_id=request.norm_id,
            agent_id=incoming_agent_id or token.agent_id,
            project_id=request.project_id,
            task_context=request.task_context,
            outcome=outcome,
            details=request.details or {},
        )
        db.add(row)
        if norm_id_str not in reported_norm_ids:
            reported_norm_ids.append(norm_id_str)
        token.reported_norm_ids = reported_norm_ids
        expected_norm_ids = {str(item) for item in (token.norm_ids or [])}
        if expected_norm_ids and expected_norm_ids.issubset(set(reported_norm_ids)):
            token.closed_at = self._utc_now()
        await db.commit()
        await db.refresh(row)

        return NormOutcomeReportResponse(
            id=row.id,
            usage_token=row.usage_token,
            norm_id=row.norm_id,
            outcome=row.outcome,
            created_at=row.created_at,
        )
