"""Centralized datatype mapping helpers for ontology + web pipelines."""

from __future__ import annotations

from functools import lru_cache
import json
from pathlib import Path
from typing import Final

from rdflib import Dataset

from .namespaces import CF_BASE as _CF_BASE
from .namespaces import CFRUN_BASE as _CFRUN_BASE
from .namespaces import canonical_context

_ONTOLOGY_DIRNAME: Final = "ontology"
_DATATYPES_MAPPING_FILENAME: Final = "datatypes_mapping.nq"
_STRUCTURED_FORMAT: Final = "json" + "-ld"
_EXTRA_ALIAS_COMPACTS: Final = {
    "boolean": ("xsd:bolean",),
    "double": ("cf:float", "cf:number"),
}


def _normalize_input(value: str | None) -> str | None:
    if not value:
        return None
    text = value.strip()
    return text or None


def _prefers_iri(value: str) -> bool:
    lowered = value.lower()
    return lowered.startswith("http://") or lowered.startswith("https://")


@lru_cache(maxsize=1)
def _prefix_context() -> dict[str, str]:
    context = canonical_context()
    context["cf"] = _CF_BASE
    context["cfrun"] = _CFRUN_BASE
    return {
        key: value
        for key, value in context.items()
        if isinstance(key, str) and isinstance(value, str)
    }


def _document_from_dataset(dataset: Dataset) -> dict:
    serialized = dataset.serialize(
        format=_STRUCTURED_FORMAT,
        context=_prefix_context(),
        auto_compact=True,
    )
    if isinstance(serialized, bytes):
        serialized = serialized.decode("utf-8")
    payload = json.loads(serialized)
    if isinstance(payload, list):
        return {"@context": _prefix_context(), "@graph": payload}
    if isinstance(payload, dict):
        if "@graph" in payload:
            if "@context" not in payload:
                payload["@context"] = _prefix_context()
            return payload
        return {"@context": _prefix_context(), "@graph": [payload]}
    raise ValueError("Unsupported RDF document payload generated from N-Quads dataset.")


def _expand_identifier(value: str) -> str:
    if _prefers_iri(value):
        return value
    prefix, separator, suffix = value.partition(":")
    if not separator:
        return value
    base = _prefix_context().get(prefix)
    if not base:
        return value
    return f"{base}{suffix}"


def _identifier_variants(compact_value: str) -> set[str]:
    variants = {compact_value}
    expanded = _expand_identifier(compact_value)
    variants.add(expanded)
    return variants


def _has_type(node: dict[str, object], expected: str) -> bool:
    node_type = node.get("@type")
    if isinstance(node_type, str):
        return node_type == expected
    if isinstance(node_type, list):
        return expected in node_type
    return False


@lru_cache(maxsize=1)
def _registry() -> dict:
    path = Path(__file__).parent / _ONTOLOGY_DIRNAME / "vocab" / _DATATYPES_MAPPING_FILENAME
    dataset = Dataset()
    dataset.parse(str(path), format="nquads")
    payload = _document_from_dataset(dataset)
    graph = payload.get("@graph", [])

    parameter_aliases: dict[str, dict] = {}
    port_aliases: dict[str, dict] = {}
    supported_port_types_compact: list[str] = []
    supported_port_types_iri: list[str] = []

    parameter_rows = [node for node in graph if isinstance(node, dict) and _has_type(node, "cf:ParameterTypeDatatypeMappingShape")]
    port_rows = [node for node in graph if isinstance(node, dict) and _has_type(node, "cf:PortTypeBindingMapping")]
    port_rows_by_compact = {
        str(node["cf:mappedPortType"]["@id"]): node
        for node in port_rows
        if isinstance(node.get("cf:mappedPortType"), dict) and "@id" in node["cf:mappedPortType"]
    }

    for node in port_rows:
        mapped_port_type = node.get("cf:mappedPortType")
        if not isinstance(mapped_port_type, dict):
            continue
        compact_port_type = mapped_port_type.get("@id")
        if not isinstance(compact_port_type, str):
            continue
        supported_port_types_compact.append(compact_port_type)
        supported_port_types_iri.append(_expand_identifier(compact_port_type))

    datatypes: list[dict[str, object]] = []
    for node in parameter_rows:
        mapped_parameter_type = node.get("cf:mappedParameterType")
        mapped_literal_datatype = node.get("cf:mappedLiteralDatatype")
        if not isinstance(mapped_parameter_type, dict) or not isinstance(mapped_literal_datatype, dict):
            continue

        parameter_compact = mapped_parameter_type.get("@id")
        port_compact = mapped_literal_datatype.get("@id")
        if not isinstance(parameter_compact, str) or not isinstance(port_compact, str):
            continue

        entry_id = parameter_compact.split(":", 1)[1]
        entry = {
            "id": entry_id,
            "parameter": {
                "canonical_compact": parameter_compact,
                "canonical_iri": _expand_identifier(parameter_compact),
            },
            "port": {
                "canonical_compact": port_compact,
                "canonical_iri": _expand_identifier(port_compact),
            },
        }
        datatypes.append(entry)

        aliases = set()
        aliases.update(_identifier_variants(parameter_compact))
        aliases.update(_identifier_variants(port_compact))
        for extra_compact in _EXTRA_ALIAS_COMPACTS.get(entry_id, ()):
            aliases.update(_identifier_variants(extra_compact))

        for alias in aliases:
            parameter_aliases[alias] = entry
            port_aliases[alias] = entry

    payload["datatypes"] = datatypes
    payload["_port_binding_rows"] = tuple(sorted(port_rows_by_compact))

    payload["_parameter_aliases"] = parameter_aliases
    payload["_port_aliases"] = port_aliases
    payload["_supported_port_types_compact"] = tuple(supported_port_types_compact)
    payload["_supported_port_types_iri"] = tuple(supported_port_types_iri)
    return payload


def _canonical_value(section: dict[str, object], prefer_iri: bool) -> str:
    key = "canonical_iri" if prefer_iri else "canonical_compact"
    return str(section[key])


def normalize_parameter_type(value: str | None) -> str | None:
    text = _normalize_input(value)
    if not text:
        return None
    entry = _registry()["_parameter_aliases"].get(text)
    if not entry:
        return text
    return _canonical_value(entry["parameter"], prefer_iri=_prefers_iri(text))


def normalize_literal_datatype(value: str | None) -> str | None:
    text = _normalize_input(value)
    if not text:
        return None
    entry = _registry()["_port_aliases"].get(text)
    if not entry:
        return text
    return _canonical_value(entry["port"], prefer_iri=_prefers_iri(text))


def parameter_type_to_literal_datatype(parameter_type: str | None) -> str | None:
    text = _normalize_input(parameter_type)
    if not text:
        return None
    entry = _registry()["_parameter_aliases"].get(text)
    if not entry:
        return None
    return _canonical_value(entry["port"], prefer_iri=_prefers_iri(text))


def normalize_port_literal_datatype(port_type: str | None) -> str | None:
    text = _normalize_input(port_type)
    if not text:
        return None
    entry = _registry()["_port_aliases"].get(text)
    if entry:
        return _canonical_value(entry["port"], prefer_iri=_prefers_iri(text))
    mapped = parameter_type_to_literal_datatype(text)
    if mapped:
        return mapped
    return text


def supported_port_literal_datatypes(*, iri: bool = False) -> tuple[str, ...]:
    key = "_supported_port_types_iri" if iri else "_supported_port_types_compact"
    return _registry()[key]

