import json
import logging
import os
import shutil
import tempfile
from datetime import datetime, timezone
from typing import Any, Optional

from flask import Blueprint, jsonify, request
from werkzeug.utils import secure_filename

from semantic_models.config import get_app_config
from semantic_models.exceptions import ValidationError
from semantic_models.glossary_types import GlossaryTerm
from semantic_models.logging_utils import summarize_for_logging
from semantic_models.routes.common import get_current_user
from semantic_models.schemas import ExtractedTermData, GlossaryConflict, GlossaryTermResponse, SemanticModelSpec
from semantic_models.services.factory import get_semantic_models_service
from semantic_models.services.glossary_extraction import (
    MAX_TEXT_CHARS,
    extract_terms_from_file,
    validate_document_extension,
)
from semantic_models.services.glossary_mapping import suggest_bindings
from semantic_models.services.glossary_utils import normalize_term
from semantic_models.utils.ids import generate_uuid

logger = logging.getLogger(__name__)

bp = Blueprint("glossary", __name__, url_prefix="/api")


def _now_tag(user: Optional[str]) -> dict[str, Any]:
    tag: dict[str, Any] = {"on": datetime.now(timezone.utc).isoformat()}
    if user:
        tag["by"] = user
    return tag


def _to_response(term: dict[str, Any]) -> dict[str, Any]:
    return GlossaryTermResponse(**term).model_dump()


@bp.route("/glossary/extract", methods=["POST"])
def extract_glossary_terms():
    file = request.files.get("file")
    if not file:
        raise ValidationError("Missing file field 'file'")

    payload_raw = request.form.get("payload")
    payload = json.loads(payload_raw) if payload_raw else {}
    logger.info(
        "Glossary extract requested: file=%s content_type=%s payload_keys=%s",
        file.filename,
        file.content_type,
        sorted(payload.keys()),
    )

    spec_dict = payload.get("spec") or {}
    try:
        spec = SemanticModelSpec.model_validate(spec_dict)
    except Exception as e:
        raise ValidationError(f"Invalid spec payload: {e}")

    existing_terms_raw = payload.get("terms") or []
    if not isinstance(existing_terms_raw, list):
        raise ValidationError("'terms' must be a list")

    doc_name = file.filename or "document"
    validate_document_extension(doc_name)
    logger.info(
        "Glossary extract payload validated: doc=%s spec_entities=%d existing_terms=%d",
        doc_name,
        len(spec.entities),
        len(existing_terms_raw),
    )

    temp_dir = tempfile.mkdtemp(prefix="glossary_extract_")
    file_path = os.path.join(temp_dir, secure_filename(doc_name))

    try:
        file.save(file_path)

        llm_id = payload.get("llm_id")
        max_chunks = payload.get("max_chunks")
        chunk_size = payload.get("chunk_size")
        chunk_overlap = payload.get("chunk_overlap")
        if max_chunks is None:
            max_chunks = get_app_config().glossary_extraction_max_chunks
        if chunk_size is not None:
            chunk_size = int(chunk_size)
        if chunk_overlap is not None:
            chunk_overlap = int(chunk_overlap)

        items, sections_processed = extract_terms_from_file(
            llm_id=llm_id,
            file_path=file_path,
            file_name=doc_name,
            spec=spec,
            max_chunks=int(max_chunks),
            chunk_size=chunk_size if chunk_size is not None else MAX_TEXT_CHARS,
            chunk_overlap=chunk_overlap if chunk_overlap is not None else 0,
        )
        logger.info(
            "Glossary terms extracted from document: sections_processed=%d extracted_items=%d",
            sections_processed,
            len(items),
        )

    finally:
        shutil.rmtree(temp_dir, ignore_errors=True)

    existing_terms = [
        {
            "id": t.get("id"),
            "term": t.get("term") or "",
            "description": t.get("description"),
            "source": t.get("source") or "MANUAL",
            "userModified": bool(t.get("userModified", False)),
            "synonyms": t.get("synonyms") or [],
            "created": t.get("created"),
            "modified": t.get("modified"),
            "privateEditorData": dict(t.get("privateEditorData") or {}),
        }
        for t in existing_terms_raw
        if t.get("term")
    ]
    existing_terms_map = {normalize_term(t["term"]): t for t in existing_terms}

    new_terms: list[dict[str, Any]] = []
    updated_terms: list[dict[str, Any]] = []
    conflicts: list[dict[str, Any]] = []

    for item in items:
        normalized = normalize_term(item["term"])
        extracted_private: dict[str, Any] = {}
        extracted_term = {
            "term": item["term"],
            "description": item.get("description"),
            "synonyms": item.get("synonyms") or [],
            "privateEditorData": extracted_private,
        }

        if normalized in existing_terms_map:
            existing = dict(existing_terms_map[normalized])

            if existing.get("userModified"):
                conflicts.append(
                    GlossaryConflict(
                        existing_term=GlossaryTermResponse(**existing),
                        extracted_term=ExtractedTermData(**extracted_term),
                    ).model_dump()
                )
                continue

            update = dict(existing)
            if item.get("description"):
                update["description"] = item.get("description")

            existing_synonyms = set(existing.get("synonyms") or [])
            new_synonyms = set(item.get("synonyms") or [])
            merged_synonyms = list(existing_synonyms | new_synonyms)
            update["synonyms"] = merged_synonyms
            update["modified"] = _now_tag(get_current_user())

            updated_terms.append(update)
            existing_terms_map[normalized] = update
        else:
            new_term = {
                "id": generate_uuid(),
                "term": item["term"],
                "description": item.get("description"),
                "source": "EXTRACTED",
                "userModified": False,
                "synonyms": item.get("synonyms") or [],
                "created": _now_tag(get_current_user()),
                "privateEditorData": extracted_private,
            }
            new_terms.append(new_term)
            existing_terms_map[normalized] = new_term

    response = {
        "documentName": doc_name,
        "sectionsProcessed": sections_processed,
        "termsCreated": [_to_response(t) for t in new_terms],
        "termsUpdated": [_to_response(t) for t in updated_terms],
        "conflicts": conflicts,
    }
    logger.info(
        "Glossary extract completed: created=%d updated=%d conflicts=%d",
        len(response["termsCreated"]),
        len(response["termsUpdated"]),
        len(response["conflicts"]),
    )

    return jsonify(response)


@bp.route(
    "/semantic-models/<model_id>/versions/<version_id>/glossary/map",
    methods=["POST"],
)
def map_glossary(model_id: str, version_id: str):
    data = request.json or {}
    spec_dict = data.get("spec") or {}
    terms_raw = data.get("terms") or []
    logger.info(
        "Glossary map requested: model_id=%s version_id=%s payload=%s",
        model_id,
        version_id,
        summarize_for_logging(
            {
                "keys": sorted(data.keys()),
                "terms_count": len(terms_raw) if isinstance(terms_raw, list) else "invalid",
                "llm_id": data.get("llm_id"),
            },
            max_chars=300,
            max_items=20,
        ),
    )

    try:
        spec = SemanticModelSpec.model_validate(spec_dict)
    except Exception as e:
        raise ValidationError(f"Invalid spec payload: {e}")

    if not isinstance(terms_raw, list):
        raise ValidationError("'terms' must be a list")

    terms: list[GlossaryTerm] = []
    for t in terms_raw:
        if not isinstance(t, dict):
            continue
        term_value = (t.get("term") or "").strip()
        if not term_value:
            continue
        terms.append(
            GlossaryTerm(
                id=t.get("id") or "",
                term=term_value,
                description=t.get("description"),
                source=t.get("source") or "MANUAL",
                userModified=bool(t.get("userModified", False)),
                synonyms=t.get("synonyms") or [],
                created=t.get("created"),
                modified=t.get("modified"),
                privateEditorData=dict(t.get("privateEditorData") or {}),
            )
        )

    service = get_semantic_models_service()
    version = service.get_version(model_id, version_id)
    bound_term_ids = {b.termId for b in version.glossaryBindings}
    terms = [t for t in terms if t.id not in bound_term_ids]
    logger.info(
        "Glossary map pre-filtered terms: model_id=%s version_id=%s input_terms=%d bound_terms=%d eligible_terms=%d",
        model_id,
        version_id,
        len(terms_raw) if isinstance(terms_raw, list) else 0,
        len(bound_term_ids),
        len(terms),
    )
    llm_id = data.get("llm_id")
    suggestions = suggest_bindings(
        terms,
        spec,
        llm_id,
        semantic_model_id=model_id,
        version_id=version_id,
    )
    logger.info(
        "Glossary map completed: model_id=%s version_id=%s suggestions=%s",
        model_id,
        version_id,
        summarize_for_logging(suggestions, max_chars=300, max_items=30, max_depth=3),
    )
    return jsonify(suggestions)
