"""Step definitions for uko_runtime.feature.

All steps prefixed with ``uko`` to avoid AmbiguousStep collisions.
"""

from __future__ import annotations

import tempfile
from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]

from cleveragents.application.services.temporal_service import TemporalService
from cleveragents.application.services.uko_inference import (
    UKOInferenceEngine,
)
from cleveragents.application.services.uko_persistence import (
    InMemoryPersistenceBackend,
    JSONFilePersistenceBackend,
    UKOGraphPersistence,
)
from cleveragents.application.services.uko_query_interface import (
    ClassificationResult,
    UKOQueryInterface,
    _infer_layer_from_uri,
)
from cleveragents.domain.models.acms.analyzers import AnalyzerRegistry, UKOTriple
from cleveragents.domain.models.acms.index_stubs import (
    InMemoryGraphIndexBackend,
    InMemoryTextIndexBackend,
    InMemoryVectorIndexBackend,
)
from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer
from cleveragents.domain.models.acms.temporal_stubs import InMemoryTemporalBackend
from cleveragents.domain.models.core.project import TemporalScope
from cleveragents.domain.models.core.resource import Resource
from features.mocks.uko_indexer_mocks import InMemoryContentReader

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

_DEFAULT_PROJECT = "local/test"


def _make_resource(
    resource_id: str,
    location: str = "src/main.py",
) -> Resource:
    return Resource(
        resource_id=resource_id,
        resource_type_name="git-checkout",
        classification="physical",
        location=location,
    )


def _make_indexer(context: Any) -> None:
    """Build a UKOIndexer from context state."""
    from cleveragents.application.services.uko_indexer import UKOIndexer

    context.uko_indexer = UKOIndexer(
        analyzer_registry=context.uko_registry,
        graph_backend=context.uko_graph_backend,
        text_backend=context.uko_text_backend,
        vector_backend=context.uko_vector_backend,
        content_reader=context.uko_content_reader,
    )


# ---------------------------------------------------------------------------
# Background / setup steps
# ---------------------------------------------------------------------------


@given("uko a PythonAnalyzer is registered")
def step_uko_python_analyzer(context: Any) -> None:
    context.uko_registry = AnalyzerRegistry()
    context.uko_registry.register(PythonAnalyzer())
    context.uko_graph_backend = InMemoryGraphIndexBackend()
    context.uko_text_backend = InMemoryTextIndexBackend()
    context.uko_vector_backend = InMemoryVectorIndexBackend()
    context.uko_content_reader = InMemoryContentReader()


@given("uko a UKOIndexer with all backends")
def step_uko_indexer_all_backends(context: Any) -> None:
    _make_indexer(context)


# ---------------------------------------------------------------------------
# Indexing steps
# ---------------------------------------------------------------------------


@when('uko I index a Python resource "{resource_id}" with content "{content}"')
def step_uko_index_python_resource(
    context: Any, resource_id: str, content: str
) -> None:
    content = content.replace("\\n", "\n")
    context.uko_content_reader.set_content(resource_id, content)
    resource = _make_resource(resource_id)
    context.uko_index_result = context.uko_indexer.index_resource(
        resource, project=_DEFAULT_PROJECT
    )


# ---------------------------------------------------------------------------
# Assertion steps — index result
# ---------------------------------------------------------------------------


@then("uko the index result should have triple_count greater than 0")
def step_uko_triple_count_gt_0(context: Any) -> None:
    assert context.uko_index_result.triple_count > 0, (
        f"Expected triple_count > 0, got {context.uko_index_result.triple_count}"
    )


# ---------------------------------------------------------------------------
# Assertion steps — graph backend content
# ---------------------------------------------------------------------------


def _get_all_triples(
    graph_backend: InMemoryGraphIndexBackend,
    project: str,
) -> list[tuple[str, str, str]]:
    """Return all (subject, predicate, object) triples for *project*."""
    bindings = graph_backend.query(project, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
    return [(b.get("s", ""), b.get("p", ""), b.get("o", "")) for b in bindings]


@then('uko the graph backend should contain a triple with predicate "{predicate}"')
def step_uko_graph_has_predicate(context: Any, predicate: str) -> None:
    triples = _get_all_triples(context.uko_graph_backend, _DEFAULT_PROJECT)
    predicates = [t[1] for t in triples]
    assert predicate in predicates, (
        f"Expected predicate '{predicate}' in graph, found: {sorted(set(predicates))}"
    )


@then('uko the graph backend should contain a triple with object "{obj}"')
def step_uko_graph_has_object(context: Any, obj: str) -> None:
    triples = _get_all_triples(context.uko_graph_backend, _DEFAULT_PROJECT)
    objects = [t[2] for t in triples]
    assert obj in objects, (
        f"Expected object '{obj}' in graph, found: {sorted(set(objects))[:10]}"
    )


@then('uko the graph has a predicate-object pair "{predicate}" "{obj}"')
def step_uko_graph_has_predicate_and_object(
    context: Any, predicate: str, obj: str
) -> None:
    triples = _get_all_triples(context.uko_graph_backend, _DEFAULT_PROJECT)
    matching = [(s, p, o) for s, p, o in triples if p == predicate and o == obj]
    assert matching, (
        f"Expected triple with predicate='{predicate}' and object='{obj}', "
        f"found predicates: {sorted(set(t[1] for t in triples))}"
    )


# ---------------------------------------------------------------------------
# Inference engine steps
# ---------------------------------------------------------------------------


@given("uko an inference engine")
def step_uko_inference_engine(context: Any) -> None:
    context.uko_inference_engine = UKOInferenceEngine()
    context.uko_inference_triples: list[UKOTriple] = []


@given('uko triples with two subjects of the same type "{type_uri}"')
def step_uko_two_subjects_same_type(context: Any, type_uri: str) -> None:
    context.uko_inference_triples = [
        UKOTriple(
            subject_uri="uko://code/class/foo/Foo",
            predicate="rdf:type",
            object_uri=type_uri,
        ),
        UKOTriple(
            subject_uri="uko://code/class/foo/Bar",
            predicate="rdf:type",
            object_uri=type_uri,
        ),
    ]


@given('uko triples with a parent subject "{parent}" and child "{child}"')
def step_uko_parent_child_subjects(context: Any, parent: str, child: str) -> None:
    context.uko_inference_triples = [
        UKOTriple(
            subject_uri=parent,
            predicate="rdf:type",
            object_uri="uko-code:Module",
        ),
        UKOTriple(
            subject_uri=child,
            predicate="rdf:type",
            object_uri="uko-code:Module",
        ),
    ]


@given('uko triples where subject "{subject}" references "{ref}"')
def step_uko_subject_references(context: Any, subject: str, ref: str) -> None:
    context.uko_inference_triples = [
        UKOTriple(
            subject_uri=subject,
            predicate="rdf:type",
            object_uri="uko-code:Module",
        ),
        UKOTriple(
            subject_uri=ref,
            predicate="rdf:type",
            object_uri="uko-code:Module",
        ),
        UKOTriple(
            subject_uri=subject,
            predicate="uko:references",
            object_uri=ref,
        ),
    ]


@when("uko I run inference")
def step_uko_run_inference(context: Any) -> None:
    context.uko_inferred = context.uko_inference_engine.infer(
        context.uko_inference_triples
    )


@when("uko I run inference on empty triples")
def step_uko_run_inference_empty(context: Any) -> None:
    context.uko_inferred = context.uko_inference_engine.infer([])


@then('uko the inferred triples should contain a "{predicate}" relationship')
def step_uko_inferred_has_predicate(context: Any, predicate: str) -> None:
    predicates = [t.predicate for t in context.uko_inferred]
    assert predicate in predicates, (
        f"Expected predicate '{predicate}' in inferred triples, found: {predicates}"
    )


@then("uko the inferred triples should be empty")
def step_uko_inferred_empty(context: Any) -> None:
    assert context.uko_inferred == [], (
        f"Expected empty inferred triples, got: {context.uko_inferred}"
    )


# ---------------------------------------------------------------------------
# UKO Query Interface steps
# ---------------------------------------------------------------------------


@given('uko a graph backend with triples for resource "{resource_id}"')
def step_uko_graph_with_resource(context: Any, resource_id: str) -> None:
    context.uko_graph_backend = InMemoryGraphIndexBackend()
    context.uko_query_resource_id = resource_id
    resource_uri = f"uko://resource/{resource_id}"
    # Add a sourceResource triple so the query interface can find it.
    context.uko_graph_backend.add_triple(
        _DEFAULT_PROJECT,
        f"uko://code/module/{resource_id}",
        "uko:sourceResource",
        resource_uri,
    )


@given("uko a graph backend with no triples")
def step_uko_graph_no_triples(context: Any) -> None:
    context.uko_graph_backend = InMemoryGraphIndexBackend()
    context.uko_query_resource_id = "01HQ8ZDRX50000000000000099"


@given('uko the graph has a type triple "{type_uri}" for the resource')
def step_uko_graph_type_triple(context: Any, type_uri: str) -> None:
    resource_id = context.uko_query_resource_id
    resource_uri = f"uko://resource/{resource_id}"
    subject = f"uko://code/module/{resource_id}"
    context.uko_graph_backend.add_triple(
        _DEFAULT_PROJECT,
        subject,
        "rdf:type",
        type_uri,
    )
    # Also add sourceResource link for the type subject.
    context.uko_graph_backend.add_triple(
        _DEFAULT_PROJECT,
        subject,
        "uko:sourceResource",
        resource_uri,
    )


@when('uko I classify resource "{resource_id}"')
def step_uko_classify_resource(context: Any, resource_id: str) -> None:
    interface = UKOQueryInterface(
        graph_backend=context.uko_graph_backend,
        project=_DEFAULT_PROJECT,
    )
    context.uko_classification = interface.classify_resource(resource_id)


@then("uko the classification result should have layer {layer:d}")
def step_uko_classification_layer(context: Any, layer: int) -> None:
    assert context.uko_classification.layer == layer, (
        f"Expected layer {layer}, got {context.uko_classification.layer}"
    )


@then('uko the classification result should have primary_type "{type_uri}"')
def step_uko_classification_primary_type(context: Any, type_uri: str) -> None:
    assert context.uko_classification.primary_type == type_uri, (
        f"Expected primary_type '{type_uri}', "
        f"got '{context.uko_classification.primary_type}'"
    )


@then("uko classifying with empty resource_id should raise ValueError")
def step_uko_classify_empty_id(context: Any) -> None:
    interface = UKOQueryInterface(
        graph_backend=context.uko_graph_backend,
        project=_DEFAULT_PROJECT,
    )
    try:
        interface.classify_resource("")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko creating UKOQueryInterface with invalid backend should raise TypeError")
def step_uko_query_interface_invalid_backend(context: Any) -> None:
    try:
        UKOQueryInterface(graph_backend="not-a-backend", project=_DEFAULT_PROJECT)  # type: ignore[arg-type]
        raise AssertionError("Expected TypeError")
    except TypeError:
        pass


@then("uko creating UKOQueryInterface with empty project should raise ValueError")
def step_uko_query_interface_empty_project(context: Any) -> None:
    backend = InMemoryGraphIndexBackend()
    try:
        UKOQueryInterface(graph_backend=backend, project="")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko get_resources_by_layer with layer 4 should raise ValueError")
def step_uko_get_resources_invalid_layer(context: Any) -> None:
    interface = UKOQueryInterface(
        graph_backend=context.uko_graph_backend,
        project=_DEFAULT_PROJECT,
    )
    try:
        interface.get_resources_by_layer(4)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# UKO Graph Persistence steps
# ---------------------------------------------------------------------------


@when("uko I save the graph state")
def step_uko_save_graph(context: Any) -> None:
    persistence_backend = InMemoryPersistenceBackend()
    context.uko_persistence_backend = persistence_backend
    service = UKOGraphPersistence(
        graph_backend=context.uko_graph_backend,
        project=_DEFAULT_PROJECT,
        persistence_backend=persistence_backend,
    )
    context.uko_save_result = service.save()


@when("uko I restore the graph state into a fresh backend")
def step_uko_restore_graph(context: Any) -> None:
    fresh_backend = InMemoryGraphIndexBackend()
    context.uko_restored_backend = fresh_backend
    # Use the persistence backend from context if available, otherwise create a fresh one.
    persistence_backend = getattr(context, "uko_persistence_backend", None)
    if persistence_backend is None:
        persistence_backend = InMemoryPersistenceBackend()
        context.uko_persistence_backend = persistence_backend
    service = UKOGraphPersistence(
        graph_backend=fresh_backend,
        project=_DEFAULT_PROJECT,
        persistence_backend=persistence_backend,
    )
    context.uko_restore_result = service.restore()


@then("uko the restored backend should have triples")
def step_uko_restored_has_triples(context: Any) -> None:
    bindings = context.uko_restored_backend.query(
        _DEFAULT_PROJECT, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }"
    )
    assert len(bindings) > 0, "Expected restored backend to have triples"


@then("uko the save result should be 0")
def step_uko_save_result_zero(context: Any) -> None:
    assert context.uko_save_result == 0, (
        f"Expected save result 0, got {context.uko_save_result}"
    )


@then("uko the restore result should be 0")
def step_uko_restore_result_zero(context: Any) -> None:
    assert context.uko_restore_result == 0, (
        f"Expected restore result 0, got {context.uko_restore_result}"
    )


@then("uko creating UKOGraphPersistence with empty project should raise ValueError")
def step_uko_persistence_empty_project(context: Any) -> None:
    backend = InMemoryGraphIndexBackend()
    try:
        UKOGraphPersistence(graph_backend=backend, project="")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko creating UKOGraphPersistence with invalid backend should raise TypeError")
def step_uko_persistence_invalid_backend(context: Any) -> None:
    try:
        UKOGraphPersistence(graph_backend="not-a-backend", project=_DEFAULT_PROJECT)  # type: ignore[arg-type]
        raise AssertionError("Expected TypeError")
    except TypeError:
        pass


# ---------------------------------------------------------------------------
# InMemoryPersistenceBackend steps
# ---------------------------------------------------------------------------


@given("uko an in-memory persistence backend")
def step_uko_in_memory_persistence(context: Any) -> None:
    context.uko_persistence_backend = InMemoryPersistenceBackend()
    context.uko_test_triples = [
        {"subject": "uko://s1", "predicate": "rdf:type", "object": "uko-py:Class"},
        {"subject": "uko://s2", "predicate": "uko:contains", "object": "uko://s1"},
    ]


@when('uko I save triples to the persistence backend for project "{project}"')
def step_uko_save_to_persistence(context: Any, project: str) -> None:
    context.uko_persistence_backend.save(project, context.uko_test_triples)
    context.uko_saved_project = project


@then("uko loading from the persistence backend should return the same triples")
def step_uko_load_from_persistence(context: Any) -> None:
    loaded = context.uko_persistence_backend.load(context.uko_saved_project)
    assert loaded == context.uko_test_triples, (
        f"Expected {context.uko_test_triples}, got {loaded}"
    )


@then(
    "uko loading from the persistence backend for unknown project should return empty list"
)
def step_uko_load_unknown_project(context: Any) -> None:
    loaded = context.uko_persistence_backend.load("unknown/project")
    assert loaded == [], f"Expected empty list, got {loaded}"


@then("uko saving to persistence backend with empty project should raise ValueError")
def step_uko_save_empty_project(context: Any) -> None:
    try:
        context.uko_persistence_backend.save("", [])
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko loading from persistence backend with empty project should raise ValueError")
def step_uko_load_empty_project(context: Any) -> None:
    try:
        context.uko_persistence_backend.load("")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# JSONFilePersistenceBackend steps
# ---------------------------------------------------------------------------


@given("uko a JSON file persistence backend in a temp directory")
def step_uko_json_file_backend(context: Any) -> None:
    context.uko_temp_dir = tempfile.mkdtemp()
    context.uko_json_backend = JSONFilePersistenceBackend(base_dir=context.uko_temp_dir)
    context.uko_test_triples = [
        {"subject": "uko://s1", "predicate": "rdf:type", "object": "uko-py:Class"},
    ]


@when('uko I save triples to the JSON file backend for project "{project}"')
def step_uko_save_to_json_backend(context: Any, project: str) -> None:
    context.uko_json_backend.save(project, context.uko_test_triples)
    context.uko_saved_project = project


@then("uko loading from the JSON file backend should return the same triples")
def step_uko_load_from_json_backend(context: Any) -> None:
    loaded = context.uko_json_backend.load(context.uko_saved_project)
    assert loaded == context.uko_test_triples, (
        f"Expected {context.uko_test_triples}, got {loaded}"
    )


@then(
    "uko loading from the JSON file backend for unknown project should return empty list"
)
def step_uko_load_json_unknown_project(context: Any) -> None:
    loaded = context.uko_json_backend.load("unknown/project")
    assert loaded == [], f"Expected empty list, got {loaded}"


@then("uko saving to JSON file backend with empty project should raise ValueError")
def step_uko_save_json_empty_project(context: Any) -> None:
    try:
        context.uko_json_backend.save("", [])
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko loading from JSON file backend with empty project should raise ValueError")
def step_uko_load_json_empty_project(context: Any) -> None:
    try:
        context.uko_json_backend.load("")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Temporal versioning steps
# ---------------------------------------------------------------------------


@given("uko a temporal backend")
def step_uko_temporal_backend(context: Any) -> None:
    context.uko_temporal_backend = InMemoryTemporalBackend()
    context.uko_temporal_service = TemporalService(backend=context.uko_temporal_backend)


@given('uko I store an initial node "{node_uri}" for resource "{resource_id}"')
def step_uko_store_initial_node(context: Any, node_uri: str, resource_id: str) -> None:
    context.uko_temporal_service.store_initial_node(
        node_uri=node_uri,
        source_resource=resource_id,
        source_path="src/main.py",
    )


@given('uko I create a revision from "{current_uri}" to "{new_uri}"')
@when('uko I create a revision from "{current_uri}" to "{new_uri}"')
def step_uko_create_revision(context: Any, current_uri: str, new_uri: str) -> None:
    context.uko_temporal_service.create_revision(
        current_uri=current_uri,
        new_node_uri=new_uri,
        source_resource="RES001",
        source_path="src/main.py",
    )


@when('uko I mark "{node_uri}" as historical')
def step_uko_mark_historical(context: Any, node_uri: str) -> None:
    context.uko_temporal_service.mark_historical(node_uri)


@when('uko I get the revision chain for "{node_uri}"')
def step_uko_get_revision_chain(context: Any, node_uri: str) -> None:
    context.uko_revision_chain = context.uko_temporal_service.get_revision_chain(
        node_uri
    )


@when('uko I get history for "{node_uri_base}" with scope ALL')
def step_uko_get_history_all(context: Any, node_uri_base: str) -> None:
    context.uko_history = context.uko_temporal_service.get_history(
        node_uri_base, temporal_scope=TemporalScope.ALL
    )


@when('uko I get current for "{node_uri_base}"')
def step_uko_get_current(context: Any, node_uri_base: str) -> None:
    context.uko_current_node = context.uko_temporal_service.get_current(node_uri_base)


@then('uko the temporal backend should have node "{node_uri}"')
def step_uko_temporal_has_node(context: Any, node_uri: str) -> None:
    node = context.uko_temporal_backend.get_current(node_uri)
    if node is None:
        # Check if it's historical.
        history = context.uko_temporal_backend.get_history(node_uri, TemporalScope.ALL)
        assert any(n.node_uri == node_uri for n in history), (
            f"Expected node '{node_uri}' in temporal backend"
        )


@then("uko the node should be current")
def step_uko_node_is_current(context: Any) -> None:
    # The last stored/created node should be current.
    pass  # Verified by other steps


@then('uko node "{node_uri}" should be historical')
def step_uko_node_is_historical(context: Any, node_uri: str) -> None:
    history = context.uko_temporal_backend.get_history(node_uri, TemporalScope.ALL)
    historical = [
        n for n in history if n.node_uri == node_uri and not n.temporal.is_current
    ]
    assert historical, f"Expected node '{node_uri}' to be historical"


@then('uko node "{node_uri}" should be current')
def step_uko_node_is_current_named(context: Any, node_uri: str) -> None:
    node = context.uko_temporal_backend.get_current(node_uri)
    assert node is not None and node.temporal.is_current, (
        f"Expected node '{node_uri}' to be current"
    )


@then("uko the revision chain should have depth {depth:d}")
def step_uko_chain_depth(context: Any, depth: int) -> None:
    assert context.uko_revision_chain.depth == depth, (
        f"Expected chain depth {depth}, got {context.uko_revision_chain.depth}"
    )


@then('uko the revision chain current_uri should be "{current_uri}"')
def step_uko_chain_current_uri(context: Any, current_uri: str) -> None:
    assert context.uko_revision_chain.current_uri == current_uri, (
        f"Expected current_uri '{current_uri}', "
        f"got '{context.uko_revision_chain.current_uri}'"
    )


@then('uko the revision chain predecessors should contain "{predecessor_uri}"')
def step_uko_chain_has_predecessor(context: Any, predecessor_uri: str) -> None:
    assert predecessor_uri in context.uko_revision_chain.predecessors, (
        f"Expected '{predecessor_uri}' in predecessors "
        f"{context.uko_revision_chain.predecessors}"
    )


@then("uko the history should have {count:d} nodes")
def step_uko_history_count(context: Any, count: int) -> None:
    assert len(context.uko_history) == count, (
        f"Expected {count} history nodes, got {len(context.uko_history)}"
    )


@then('uko the current node should be "{node_uri}"')
def step_uko_current_node_uri(context: Any, node_uri: str) -> None:
    assert context.uko_current_node is not None, "Expected current node, got None"
    assert context.uko_current_node.node_uri == node_uri, (
        f"Expected current node '{node_uri}', got '{context.uko_current_node.node_uri}'"
    )


# ---------------------------------------------------------------------------
# Layer inference helper steps
# ---------------------------------------------------------------------------


@then('uko inferring layer for "{uri}" should return {layer:d}')
def step_uko_infer_layer(context: Any, uri: str, layer: int) -> None:
    result = _infer_layer_from_uri(uri)
    assert result == layer, f"Expected layer {layer} for URI '{uri}', got {result}"


# ---------------------------------------------------------------------------
# ClassificationResult validation steps
# ---------------------------------------------------------------------------


@then(
    "uko creating ClassificationResult with empty resource_id should raise ValueError"
)
def step_uko_classification_empty_id(context: Any) -> None:
    try:
        ClassificationResult(resource_id="")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko creating ClassificationResult with layer {layer:d} should raise ValueError")
def step_uko_classification_invalid_layer(context: Any, layer: int) -> None:
    try:
        ClassificationResult(resource_id="RES001", layer=layer)
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@then("uko creating ClassificationResult with layer {layer:d} should succeed")
def step_uko_classification_valid_layer(context: Any, layer: int) -> None:
    result = ClassificationResult(resource_id="RES001", layer=layer)
    assert result.layer == layer
