"""Extract norms from natural language via LLM; persist process JSON artifacts."""
from __future__ import annotations

import json
import uuid
from typing import Any, Dict, List, Optional, Tuple
from uuid import UUID

from fastapi import HTTPException, status
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession

from norm_extractor import (
    BadExtractionJsonError,
    LlmExtractionError,
    run_raw_extraction,
)
from normbase.config import settings
from normbase.schemas import NormCreate
from normbase.services.extraction_artifact_storage import write_process_json
from normbase.services.norm_service import NormService

_NORM_CREATE_KEYS = frozenset(NormCreate.model_fields.keys())


def _coerce_norm_dict(raw: Dict[str, Any]) -> Dict[str, Any]:
    """Normalize LLM output toward NormCreate-compatible dict."""
    item = dict(raw)
    item["status"] = "draft"
    allowed = {k: v for k, v in item.items() if k in _NORM_CREATE_KEYS}
    fid = allowed.get("field_id")
    if fid == "":
        allowed["field_id"] = None
    subj = allowed.get("subject")
    if isinstance(subj, dict):
        can = subj.get("canonical")
        if isinstance(can, dict) and "agents" in can:
            agents = can.get("agents") or []
            parsed: List[Any] = []
            if isinstance(agents, list):
                for a in agents:
                    if isinstance(a, UUID):
                        parsed.append(a)
                    elif isinstance(a, str) and a.strip():
                        try:
                            parsed.append(UUID(a.strip()))
                        except ValueError:
                            continue
            can["agents"] = parsed
    if allowed.get("tags") is None:
        allowed["tags"] = []
    return allowed


def _validation_detail(exc: ValidationError) -> str:
    try:
        return json.dumps(exc.errors(), ensure_ascii=False)[:8000]
    except Exception:
        return str(exc)[:8000]


class NormExtractionService:
    """Orchestrate LLM extraction, validation, NormCreate import, and artifact write."""

    def __init__(self, norm_service: NormService | None = None):
        self._norm_service = norm_service or NormService()

    async def extract_from_text(
        self,
        db: AsyncSession,
        *,
        text: str,
        default_lang: str,
        field_id: Optional[UUID],
        created_by: Optional[str],
        max_norms: int,
        default_authority_level: Optional[str],
        default_formality_level: Optional[str],
        submitter_user_id: Optional[str],
        llm_doc_id: Optional[str] = None,
        llm_doc_title: Optional[str] = None,
        source_extraction_extra: Optional[Dict[str, Any]] = None,
        artifact_relative_segments: Optional[tuple[str, ...]] = None,
    ) -> Tuple[UUID, str, List[UUID], List[Dict[str, Any]]]:
        session_id = uuid.uuid4()
        artifact_mode = "inline_text"
        if source_extraction_extra and source_extraction_extra.get("mode"):
            artifact_mode = str(source_extraction_extra["mode"])
        doc_id = llm_doc_id or "inline"
        doc_title = llm_doc_title or "inline-text"

        try:
            raw_out = await run_raw_extraction(
                text or "",
                default_lang=default_lang,
                field_id=field_id,
                created_by=created_by,
                max_norms=max_norms,
                default_authority_level=default_authority_level,
                default_formality_level=default_formality_level,
                doc_id=doc_id,
                doc_title=doc_title,
            )
        except LlmExtractionError as e:
            raise HTTPException(
                status_code=status.HTTP_502_BAD_GATEWAY,
                detail=f"LLM extraction failed: {e.message}",
            ) from e
        except BadExtractionJsonError as e:
            raise HTTPException(
                status_code=status.HTTP_502_BAD_GATEWAY,
                detail=e.message,
            ) from e

        norms_raw = raw_out.norms_raw
        chunk_results = raw_out.chunk_results
        chunks_len = raw_out.chunk_count
        max_chars = raw_out.max_content_chars
        overlap = raw_out.overlap_chars

        norm_ids: List[UUID] = []
        issues: List[Dict[str, Any]] = []
        artifact_norms: List[Dict[str, Any]] = []

        for idx, item in enumerate(norms_raw):
            if not isinstance(item, dict):
                issues.append({"index": idx, "detail": "Item is not an object."})
                artifact_norms.append(
                    {
                        "index": idx,
                        "status": "skipped",
                        "validation_errors": ["not an object"],
                    }
                )
                continue
            coerced = _coerce_norm_dict(item)
            if field_id is not None and coerced.get("field_id") is None:
                coerced["field_id"] = field_id
            if created_by is not None and coerced.get("created_by") is None:
                coerced["created_by"] = created_by
            if coerced.get("authority_level") is None and default_authority_level:
                coerced["authority_level"] = default_authority_level
            if coerced.get("formality_level") is None and default_formality_level:
                coerced["formality_level"] = default_formality_level

            sd = coerced.get("source_details")
            if not isinstance(sd, dict):
                sd = {}
            ext = sd.get("extraction") if isinstance(sd.get("extraction"), dict) else {}
            ext = {
                **ext,
                "mode": "inline_text",
                "session_id": str(session_id),
            }
            if chunks_len > 1:
                ext["text_chunk_count"] = chunks_len
            if submitter_user_id:
                ext["submitter_user_id"] = submitter_user_id
            if source_extraction_extra:
                ext = {**ext, **source_extraction_extra}
            sd = {**sd, "extraction": ext}
            coerced["source_details"] = sd

            try:
                norm_create = NormCreate.model_validate(coerced)
            except ValidationError as e:
                issues.append({"index": idx, "detail": _validation_detail(e)})
                artifact_norms.append(
                    {
                        "index": idx,
                        "status": "validation_failed",
                        "payload": coerced,
                        "validation_errors": e.errors(),
                    }
                )
                continue

            try:
                created = await self._norm_service.create_norm(db, norm_create)
            except Exception as e:
                issues.append({"index": idx, "detail": str(e)[:8000]})
                artifact_norms.append(
                    {
                        "index": idx,
                        "status": "import_failed",
                        "payload": coerced,
                        "detail": str(e),
                    }
                )
                continue

            norm_ids.append(created.id)
            artifact_norms.append(
                {
                    "index": idx,
                    "status": "imported",
                    "norm_id": str(created.id),
                }
            )

        if chunks_len <= 1:
            single_raw = chunk_results[0]["raw_llm"] if chunk_results else ""
            artifact_body: Dict[str, Any] = {
                "schema_version": 1,
                "session_id": str(session_id),
                "mode": artifact_mode,
                "raw_llm": single_raw,
                "norms": artifact_norms,
            }
        else:
            artifact_body = {
                "schema_version": 2,
                "session_id": str(session_id),
                "mode": artifact_mode,
                "chunking": {
                    "enabled": True,
                    "max_content_chars": max_chars,
                    "overlap_chars": overlap,
                    "chunk_count": chunks_len,
                },
                "chunk_results": chunk_results,
                "norms": artifact_norms,
            }
        rel_segments: tuple[str, ...] = artifact_relative_segments or (
            "inline",
            str(session_id),
            "process.json",
        )
        rel = write_process_json(
            artifact_root=settings.extraction_artifact_dir,
            relative_segments=rel_segments,
            data=artifact_body,
            max_bytes=settings.extraction_artifact_max_bytes,
        )

        return session_id, rel, norm_ids, issues
