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

from __future__ import annotations

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

from rdflib import Dataset, Graph, Literal, RDF, URIRef
from rdflib.namespace import SKOS

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"
_SHAPES_FILENAME: Final = "shapes.nq"
_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 _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 _compact_identifier(value: str) -> str:
    for prefix, base in sorted(
        _prefix_context().items(),
        key=lambda item: len(item[1]),
        reverse=True,
    ):
        if value.startswith(base):
            local = value[len(base) :]
            if local:
                return f"{prefix}:{local}"
    return value


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


def _load_nquads_graph(filename: str) -> Graph:
    path = Path(__file__).parent / _ONTOLOGY_DIRNAME / "vocab" / filename
    dataset = Dataset()
    dataset.parse(str(path), format="nquads")
    graph = Graph()
    for named_graph in dataset.graphs():
        for subj, pred, obj in named_graph.triples((None, None, None)):
            graph.add((subj, pred, obj))
    return graph


def _literal_text(graph: Graph, subject: URIRef, *predicates: URIRef) -> str | None:
    for predicate in predicates:
        value = graph.value(subject, predicate)
        if isinstance(value, Literal):
            text = str(value).strip()
            if text:
                return text
    return None


@lru_cache(maxsize=1)
def _registry() -> dict:
    mapping_graph = _load_nquads_graph(_DATATYPES_MAPPING_FILENAME)
    shapes_graph = _load_nquads_graph(_SHAPES_FILENAME)

    cf = URIRef(_CF_BASE)
    parameter_shape = URIRef(f"{_CF_BASE}ParameterTypeDatatypeMappingShape")
    port_shape = URIRef(f"{_CF_BASE}PortTypeBindingMapping")
    mapped_parameter_type = URIRef(f"{_CF_BASE}mappedParameterType")
    mapped_literal_datatype = URIRef(f"{_CF_BASE}mappedLiteralDatatype")
    mapped_port_type = URIRef(f"{_CF_BASE}mappedPortType")
    shape_kind_scheme = URIRef(f"{_CF_BASE}shape_kind_scheme")
    cf_description = URIRef(f"{_CF_BASE}description")

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

    datatypes: list[dict[str, object]] = []
    for node in mapping_graph.subjects(RDF.type, parameter_shape):
        if not isinstance(node, URIRef):
            continue
        parameter_type = mapping_graph.value(node, mapped_parameter_type)
        literal_datatype = mapping_graph.value(node, mapped_literal_datatype)
        if not isinstance(parameter_type, URIRef) or not isinstance(literal_datatype, URIRef):
            continue

        parameter_compact = _compact_identifier(str(parameter_type))
        literal_compact = _compact_identifier(str(literal_datatype))
        entry_id = parameter_compact.split(":", 1)[1]
        entry = {
            "id": entry_id,
            "parameter": {
                "canonical_compact": parameter_compact,
                "canonical_iri": str(parameter_type),
            },
            "port": {
                "canonical_compact": literal_compact,
                "canonical_iri": str(literal_datatype),
            },
        }
        datatypes.append(entry)

        aliases = set()
        aliases.update(_identifier_variants(parameter_compact))
        aliases.update(_identifier_variants(literal_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

    port_rows_by_compact: dict[str, dict[str, str]] = {}
    for node in mapping_graph.subjects(RDF.type, port_shape):
        if not isinstance(node, URIRef):
            continue
        port_type = mapping_graph.value(node, mapped_port_type)
        if not isinstance(port_type, URIRef):
            continue
        compact_port_type = _compact_identifier(str(port_type))
        supported_port_types_compact.append(compact_port_type)
        supported_port_types_iri.append(str(port_type))
        port_rows_by_compact[compact_port_type] = {"id": str(node)}

    shape_catalog: list[dict[str, object]] = []
    shape_aliases: dict[str, dict[str, object]] = {}
    for node in shapes_graph.subjects(SKOS.inScheme, shape_kind_scheme):
        if not isinstance(node, URIRef):
            continue
        compact_id = _compact_identifier(str(node))
        entry = {
            "id": compact_id.split(":", 1)[1] if ":" in compact_id else compact_id,
            "canonical_compact": compact_id,
            "canonical_iri": str(node),
            "label": _literal_text(shapes_graph, node, SKOS.prefLabel),
            "description": _literal_text(shapes_graph, node, cf_description, SKOS.definition),
            "notation": _literal_text(shapes_graph, node, SKOS.notation),
        }
        shape_catalog.append(entry)
        for alias in _identifier_variants(compact_id):
            shape_aliases[alias] = entry

    return {
        "datatypes": datatypes,
        "shape_catalog": shape_catalog,
        "_port_binding_rows": tuple(sorted(port_rows_by_compact)),
        "_parameter_aliases": parameter_aliases,
        "_port_aliases": port_aliases,
        "_shape_aliases": shape_aliases,
        "_supported_port_types_compact": tuple(sorted(set(supported_port_types_compact))),
        "_supported_port_types_iri": tuple(sorted(set(supported_port_types_iri))),
        "_supported_shape_kinds_compact": tuple(
            sorted({str(entry["canonical_compact"]) for entry in shape_catalog})
        ),
        "_supported_shape_kinds_iri": tuple(
            sorted({str(entry["canonical_iri"]) for entry in shape_catalog})
        ),
    }


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]


def supported_shape_kinds(*, iri: bool = False) -> tuple[str, ...]:
    key = "_supported_shape_kinds_iri" if iri else "_supported_shape_kinds_compact"
    return _registry()[key]


def shape_kind_details(value: str | None) -> dict[str, object] | None:
    text = _normalize_input(value)
    if not text:
        return None
    entry = _registry()["_shape_aliases"].get(text)
    if not entry:
        return None
    return dict(entry)
