"""Compliance check and outcome reporting APIs."""
from __future__ import annotations

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

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

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

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

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


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


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


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


def _normalize_string_list(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_required_actions(action: dict[str, Any]) -> List[str]:
    required = _normalize_string_list(action.get("required"))
    if required:
        return required
    return _normalize_string_list(action.get("required_steps"))


def _extract_prohibited_actions(action: dict[str, Any]) -> List[str]:
    prohibited = _normalize_string_list(action.get("prohibited"))
    if prohibited:
        return prohibited
    return _normalize_string_list(action.get("action_prohibited"))


@router.post("/check_action_compliance", response_model=ComplianceCheckResponse)
async def check_action_compliance(
    request: ComplianceCheckRequest,
    db: AsyncSession = Depends(get_db),
):
    requested_ids = list(dict.fromkeys(request.norm_ids))
    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 {}
        required_actions = _extract_required_actions(action)
        prohibited_actions = _extract_prohibited_actions(action)

        if norm.norm_type == "obligation":
            for required in required_actions:
                if required.lower() not in action_text:
                    violations.append(
                        ComplianceViolation(
                            norm_id=norm.id,
                            violation_type="obligation",
                            message=f"Missing required action: {required}",
                        )
                    )
        elif norm.norm_type == "prohibition":
            for prohibited in prohibited_actions:
                if prohibited.lower() in action_text:
                    violations.append(
                        ComplianceViolation(
                            norm_id=norm.id,
                            violation_type="prohibition",
                            message=f"Contains prohibited action: {prohibited}",
                        )
                    )

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


@router.post("/log-outcome", response_model=NormOutcomeReportResponse)
async def report_norm_outcome(
    request: NormOutcomeReportRequest,
    db: AsyncSession = Depends(get_db),
    x_agent_id: str | None = Header(default=None),
    x_key_id: str | None = Header(default=None),
):
    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 _to_utc(token.expires_at) < _utc_now():
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail="usage_token expired.",
        )
    if token.closed_at and _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 _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 = _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,
    )
