"""Step definitions for the UKO Ontology Domain Registry feature.

Covers domain lookup, DetailLevelMap resolution (all four Layer 1
domains), inheritance chain building, Turtle syntax validation, and
the Universal View Guarantee.

Steps are prefixed with ``ontology_`` where needed to avoid collision
with existing step files.
"""

from __future__ import annotations

import re
from pathlib import Path

from behave import given, then, when
from behave.runner import Context

from cleveragents.domain.models.acms.crp import DetailLevelMap
from cleveragents.domain.models.acms.ontology_registry import (
    DomainDescriptor,
    build_detail_map_chain,
    get_domain,
    get_layer1_domains,
    list_domains,
    validate_turtle,
    validate_turtle_file,
)

__all__: list[str] = []

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_PROJECT_ROOT = Path(__file__).resolve().parents[2]
"""Root of the project worktree (two levels up from features/steps/)."""


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("the UKO ontology registry module is available")
def step_ontology_module_available(context: Context) -> None:
    # Importing the module is the availability check.
    context.ontology_error = None
    context.ontology_domain = None
    context.ontology_domains_list = None
    context.ontology_layer1_list = None
    context.ontology_chain = None
    context.ontology_validation_errors = None
    context.ontology_ttl_content = None
    context.ontology_resolve_error = None
    context.ontology_file_error = None
    context.ontology_resolved_depth = None
    context.ontology_custom_map = None


@given(
    'a custom DetailLevelMap for domain "{domain}" '
    'with parent "{parent_prefix}" and levels:'
)
def step_custom_map_with_parent(
    context: Context,
    domain: str,
    parent_prefix: str,
) -> None:
    parent_desc = get_domain(parent_prefix)
    parent_map = parent_desc.detail_map

    levels: dict[str, int] = {}
    for row in context.table:
        levels[row["level"]] = int(row["depth"])

    # Determine max_depth: parent max or highest local level, whichever is bigger
    local_max = max(levels.values()) if levels else 0
    max_depth = max(parent_map.max_depth, local_max)

    context.ontology_custom_map = DetailLevelMap(
        domain=domain,
        parent=parent_map,
        levels=levels,
        max_depth=max_depth,
    )


# ---------------------------------------------------------------------------
# When steps — domain lookup
# ---------------------------------------------------------------------------


@when('I look up domain "{prefix}"')
def step_lookup_domain(context: Context, prefix: str) -> None:
    context.ontology_error = None
    try:
        context.ontology_domain = get_domain(prefix)
    except (ValueError, KeyError) as exc:
        context.ontology_error = exc


@when("I look up an empty domain prefix")
def step_lookup_empty_domain(context: Context) -> None:
    context.ontology_error = None
    try:
        context.ontology_domain = get_domain("")
    except (ValueError, KeyError) as exc:
        context.ontology_error = exc


@when("I list all domains")
def step_list_domains(context: Context) -> None:
    context.ontology_domains_list = list_domains()


@when("I get Layer 1 domains")
def step_get_layer1(context: Context) -> None:
    context.ontology_layer1_list = get_layer1_domains()


# ---------------------------------------------------------------------------
# When steps — detail map chain
# ---------------------------------------------------------------------------


@when('I build a detail map chain from "{prefix}"')
def step_build_chain_single(context: Context, prefix: str) -> None:
    context.ontology_error = None
    try:
        context.ontology_chain = build_detail_map_chain(prefix)
    except ValueError as exc:
        context.ontology_error = exc


@when("I build a detail map chain with no prefixes")
def step_build_chain_empty(context: Context) -> None:
    context.ontology_error = None
    try:
        context.ontology_chain = build_detail_map_chain()
    except ValueError as exc:
        context.ontology_error = exc


@when('I try to resolve level "{level}"')
def step_try_resolve_level(context: Context, level: str) -> None:
    context.ontology_resolve_error = None
    try:
        desc: DomainDescriptor = context.ontology_domain
        desc.detail_map.resolve(level)
    except ValueError as exc:
        context.ontology_resolve_error = exc
        # Also set the generic error so negative step works
        context.ontology_error = exc


@when('I resolve "{level}" on the custom map')
def step_resolve_custom_map(context: Context, level: str) -> None:
    context.ontology_resolved_depth = context.ontology_custom_map.resolve(level)


# ---------------------------------------------------------------------------
# When steps — Turtle validation
# ---------------------------------------------------------------------------


@when('I validate the project Turtle file "{relpath}"')
def step_validate_project_ttl(context: Context, relpath: str) -> None:
    full_path = _PROJECT_ROOT / relpath
    context.ontology_validation_errors = validate_turtle_file(full_path)


@when("I validate Turtle content:")
def step_validate_turtle_content(context: Context) -> None:
    context.ontology_validation_errors = validate_turtle(context.text)


@when("I validate empty Turtle content")
def step_validate_empty_turtle(context: Context) -> None:
    context.ontology_validation_errors = validate_turtle("")


@when('I validate Turtle file "{path}"')
def step_validate_turtle_file_path(context: Context, path: str) -> None:
    context.ontology_file_error = None
    try:
        context.ontology_validation_errors = validate_turtle_file(path)
    except (FileNotFoundError, ValueError) as exc:
        context.ontology_file_error = exc
        context.ontology_error = exc


@when("I validate Turtle file with empty path")
def step_validate_turtle_empty_path(context: Context) -> None:
    context.ontology_file_error = None
    try:
        context.ontology_validation_errors = validate_turtle_file("")
    except (FileNotFoundError, ValueError) as exc:
        context.ontology_file_error = exc
        context.ontology_error = exc


@when('I read the project Turtle file "{relpath}"')
def step_read_project_ttl(context: Context, relpath: str) -> None:
    full_path = _PROJECT_ROOT / relpath
    context.ontology_ttl_content = full_path.read_text(encoding="utf-8")


# ---------------------------------------------------------------------------
# Then steps — domain descriptor assertions
# ---------------------------------------------------------------------------


@then('the domain IRI should be "{expected_iri}"')
def step_check_domain_iri(context: Context, expected_iri: str) -> None:
    desc: DomainDescriptor = context.ontology_domain
    assert desc.iri == expected_iri, f"Expected IRI {expected_iri!r}, got {desc.iri!r}"


@then("the domain layer should be {expected:d}")
def step_check_domain_layer(context: Context, expected: int) -> None:
    desc: DomainDescriptor = context.ontology_domain
    assert desc.layer == expected, f"Expected layer {expected}, got {desc.layer}"


@then('the domain classes should contain "{cls}"')
def step_check_domain_class(context: Context, cls: str) -> None:
    desc: DomainDescriptor = context.ontology_domain
    assert cls in desc.classes, (
        f"Class {cls!r} not found in {desc.prefix} classes: {sorted(desc.classes)}"
    )


@then("the domain should have {count:d} classes")
def step_check_domain_class_count(context: Context, count: int) -> None:
    desc: DomainDescriptor = context.ontology_domain
    assert len(desc.classes) == count, (
        f"Expected {count} classes, got {len(desc.classes)}: {sorted(desc.classes)}"
    )


# ---------------------------------------------------------------------------
# Then steps — error assertions
# ---------------------------------------------------------------------------


# REMOVED: Duplicate @then step - already defined in multi_project_service_coverage_boost_steps.py


@then("a FileNotFoundError should have been raised")
def step_check_file_not_found(context: Context) -> None:
    err = context.ontology_file_error or context.ontology_error
    assert err is not None, "Expected FileNotFoundError but none was raised"
    assert isinstance(err, FileNotFoundError), (
        f"Expected FileNotFoundError, got {type(err).__name__}"
    )


# ---------------------------------------------------------------------------
# Then steps — domain list assertions
# ---------------------------------------------------------------------------


@then("the domain list should have {count:d} entries")
def step_check_domain_list_count(context: Context, count: int) -> None:
    lst = context.ontology_domains_list
    assert len(lst) == count, f"Expected {count} domains, got {len(lst)}"


@then("the domain list should be sorted by layer then prefix")
def step_check_domain_list_sorted(context: Context) -> None:
    lst = context.ontology_domains_list
    keys = [(d.layer, d.prefix) for d in lst]
    assert keys == sorted(keys), f"Domain list not sorted: {keys}"


@then("the Layer 1 list should have {count:d} entries")
def step_check_layer1_count(context: Context, count: int) -> None:
    lst = context.ontology_layer1_list
    assert len(lst) == count, f"Expected {count} Layer 1 domains, got {len(lst)}"


@then("every domain in the Layer 1 list should have layer 1")
def step_check_all_layer1(context: Context) -> None:
    for d in context.ontology_layer1_list:
        assert d.layer == 1, f"Domain {d.prefix} has layer {d.layer}, expected 1"


# ---------------------------------------------------------------------------
# Then steps — detail map resolution
# ---------------------------------------------------------------------------


@then('the detail map should resolve "{level}" to {expected:d}')
def step_check_detail_resolve(context: Context, level: str, expected: int) -> None:
    desc: DomainDescriptor = context.ontology_domain
    actual = desc.detail_map.resolve(level)
    assert actual == expected, (
        f"{desc.prefix} map resolved {level!r} to {actual}, expected {expected}"
    )


@then("the detail map should clamp depth {raw:d} to {expected:d}")
def step_check_detail_clamp(context: Context, raw: int, expected: int) -> None:
    desc: DomainDescriptor = context.ontology_domain
    actual = desc.detail_map.resolve(raw)
    assert actual == expected, (
        f"{desc.prefix} map clamped {raw} to {actual}, expected {expected}"
    )


@then("the resolved depth should be {expected:d}")
def step_check_resolved_depth(context: Context, expected: int) -> None:
    actual = context.ontology_resolved_depth
    assert actual == expected, f"Expected resolved depth {expected}, got {actual}"


# ---------------------------------------------------------------------------
# Then steps — chain assertions
# ---------------------------------------------------------------------------


@then('the chain should resolve "{level}" to {expected:d}')
def step_check_chain_resolve(context: Context, level: str, expected: int) -> None:
    chain: DetailLevelMap = context.ontology_chain
    actual = chain.resolve(level)
    assert actual == expected, (
        f"Chain resolved {level!r} to {actual}, expected {expected}"
    )


@then("the chain max depth should be {expected:d}")
def step_check_chain_max_depth(context: Context, expected: int) -> None:
    chain: DetailLevelMap = context.ontology_chain
    assert chain.max_depth == expected, (
        f"Chain max_depth is {chain.max_depth}, expected {expected}"
    )


# ---------------------------------------------------------------------------
# Then steps — Turtle validation assertions
# ---------------------------------------------------------------------------


@then("the validation should produce {count:d} errors")
def step_check_validation_count(context: Context, count: int) -> None:
    errs = context.ontology_validation_errors
    assert len(errs) == count, (
        f"Expected {count} validation error(s), got {len(errs)}: {errs}"
    )


@then("the validation should produce at least {count:d} error")
def step_check_validation_min(context: Context, count: int) -> None:
    errs = context.ontology_validation_errors
    assert len(errs) >= count, (
        f"Expected at least {count} error(s), got {len(errs)}: {errs}"
    )


@then('the first error should contain "{fragment}"')
def step_check_first_error(context: Context, fragment: str) -> None:
    errs = context.ontology_validation_errors
    assert errs, "No validation errors to check"
    assert fragment in errs[0], (
        f"Expected first error to contain {fragment!r}, got: {errs[0]}"
    )


# ---------------------------------------------------------------------------
# Then steps — Turtle content assertions
# ---------------------------------------------------------------------------


@then('the content should contain prefix "{prefix}"')
def step_check_content_prefix(context: Context, prefix: str) -> None:
    content = context.ontology_ttl_content
    # Match @prefix <name>: <iri> .
    name = prefix.rstrip(":")
    pattern = rf"@prefix\s+{re.escape(name)}\s*:"
    assert re.search(pattern, content), (
        f"Prefix {prefix!r} not declared in Turtle content"
    )


@then('the content should contain "{text}"')
def step_check_content_text(context: Context, text: str) -> None:
    content = context.ontology_ttl_content
    assert text in content, f"Text {text!r} not found in Turtle content"


# ---------------------------------------------------------------------------
# Then steps — Universal View Guarantee
# ---------------------------------------------------------------------------

# Build a map of class → superclass(es) from the TTL for ancestry checks
_SUBCLASS_RE = re.compile(
    r"(\S+:\S+)\s+a\s+owl:Class\s*;\s*\n\s*rdfs:subClassOf\s+([^;.]+)",
    re.MULTILINE,
)


@then("every Layer 1 class should have a subClassOf chain to uko:InformationUnit")
def step_check_universal_view(context: Context) -> None:
    content = context.ontology_ttl_content

    # Parse subClassOf relationships
    parent_map: dict[str, set[str]] = {}
    for m in re.finditer(
        r"(\S+:\S+)\s+a\s+owl:Class\s*;[^.]*?"
        r"rdfs:subClassOf\s+([^;.]+)",
        content,
        re.DOTALL,
    ):
        cls = m.group(1)
        parents_str = m.group(2)
        parents = {
            p.strip().rstrip(";").strip() for p in parents_str.split(",") if p.strip()
        }
        parent_map[cls] = parents

    # Layer 1 prefixes to check
    layer1_prefixes = ("uko-code:", "uko-doc:", "uko-data:", "uko-infra:")
    layer1_classes = [
        cls for cls in parent_map if any(cls.startswith(p) for p in layer1_prefixes)
    ]

    assert layer1_classes, "No Layer 1 classes found in TTL"

    # Check each Layer 1 class can reach uko:InformationUnit
    for cls in layer1_classes:
        visited: set[str] = set()
        queue = [cls]
        found = False
        while queue:
            current = queue.pop()
            if current == "uko:InformationUnit":
                found = True
                break
            if current in visited:
                continue
            visited.add(current)
            for parent in parent_map.get(current, set()):
                queue.append(parent)
        assert found, (
            f"Class {cls} does not have a subClassOf chain to "
            f"uko:InformationUnit (visited: {visited})"
        )


@then("every domain detail map should resolve integer depth 0 to 0")
def step_check_all_maps_depth_zero(context: Context) -> None:
    for d in context.ontology_layer1_list:
        actual = d.detail_map.resolve(0)
        assert actual == 0, (
            f"Domain {d.prefix} resolved depth 0 to {actual}, expected 0"
        )
