"""Text/Vector/Graph backend steps + Provenance + IndexResult steps."""

from __future__ import annotations

from typing import Any

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

from cleveragents.domain.models.acms.analyzers import UKOTriple
from cleveragents.domain.models.acms.index_backends import (
    IndexedDocument,
    SearchResult,
)
from cleveragents.domain.models.acms.provenance import (
    IndexResult,
    ProvenancedTriple,
    ProvenanceMetadata,
)
from features.steps.uko_indexer_common import ULID_1

__all__: list[str] = []


@when(
    'idx I index a text document "{doc_id}" with content "{content}" in project "{project}"'
)
def step_index_text_doc(context: Any, doc_id: str, content: str, project: str) -> None:
    context.idx_text_backend.index_document(project, doc_id, content, {})


@then('idx searching "{query}" in project "{project}" should return {count:d} result')
def step_search_text_singular(
    context: Any, query: str, project: str, count: int
) -> None:
    results = context.idx_text_backend.search(project, query)
    assert len(results) == count, f"Expected {count} result, got {len(results)}"
    context.idx_search_results = results


@then('idx searching "{query}" in project "{project}" should return {count:d} results')
def step_search_text_plural(context: Any, query: str, project: str, count: int) -> None:
    results = context.idx_text_backend.search(project, query)
    assert len(results) == count, f"Expected {count} results, got {len(results)}"
    context.idx_search_results = results


@then('idx the search result doc_id should be "{doc_id}"')
def step_check_search_doc_id(context: Any, doc_id: str) -> None:
    assert context.idx_search_results[0].doc_id == doc_id


@when('idx I remove text document "{doc_id}" from project "{project}"')
def step_remove_text_doc(context: Any, doc_id: str, project: str) -> None:
    context.idx_text_backend.remove_document(project, doc_id)


@when('idx I rebuild text index for project "{project}"')
def step_rebuild_text_index(context: Any, project: str) -> None:
    context.idx_text_backend.rebuild_index(project)


@then("idx indexing a text document with empty project should raise ValueError")
def step_text_empty_project(context: Any) -> None:
    try:
        context.idx_text_backend.index_document("", "doc", "content", {})
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx indexing a text document with empty content should raise ValueError")
def step_text_empty_content(context: Any) -> None:
    try:
        context.idx_text_backend.index_document("proj", "doc", "", {})
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx searching with empty query should raise ValueError")
def step_text_empty_query(context: Any) -> None:
    try:
        context.idx_text_backend.search("proj", "")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx searching with limit 0 should raise ValueError")
def step_text_zero_limit(context: Any) -> None:
    try:
        context.idx_text_backend.search("proj", "q", limit=0)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@when(
    'idx I index an embedding "{doc_id}" with vector "{vector_str}" in project "{project}"'
)
def step_index_embedding(
    context: Any, doc_id: str, vector_str: str, project: str
) -> None:
    embedding = [float(v) for v in vector_str.split(",")]
    context.idx_vector_backend.index_embedding(project, doc_id, embedding, {})


@then('idx searching similar in project "{project}" should return {count:d} result')
def step_search_similar_singular(context: Any, project: str, count: int) -> None:
    results = context.idx_vector_backend.search_similar(project, [1.0])
    assert len(results) == count, f"Expected {count}, got {len(results)}"


@then('idx searching similar in project "{project}" should return {count:d} results')
def step_search_similar_plural(context: Any, project: str, count: int) -> None:
    results = context.idx_vector_backend.search_similar(project, [1.0])
    assert len(results) == count, f"Expected {count}, got {len(results)}"


@then(
    "idx searching similar in project"
    ' "{project}" with min_relevance {threshold} should return {count:d} results'
)
def step_search_similar_min_relevance(
    context: Any,
    project: str,
    threshold: str,
    count: int,
) -> None:
    results = context.idx_vector_backend.search_similar(
        project,
        [1.0],
        min_relevance=float(threshold),
    )
    assert len(results) == count, f"Expected {count}, got {len(results)}"


@then("idx searching similar with min_relevance {threshold} should raise ValueError")
def step_search_similar_invalid_min_relevance(
    context: Any,
    threshold: str,
) -> None:
    try:
        context.idx_vector_backend.search_similar(
            "local/app",
            [1.0],
            min_relevance=float(threshold),
        )
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@when('idx I remove embedding "{doc_id}" from project "{project}"')
def step_remove_embedding(context: Any, doc_id: str, project: str) -> None:
    context.idx_vector_backend.remove_embedding(project, doc_id)


@then("idx indexing an embedding with empty vector should raise ValueError")
def step_vector_empty_embedding(context: Any) -> None:
    try:
        context.idx_vector_backend.index_embedding("proj", "doc", [], {})
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@when('idx I add a triple "{subj}" "{pred}" "{obj}" in project "{project}"')
def step_add_triple(context: Any, subj: str, pred: str, obj: str, project: str) -> None:
    context.idx_graph_backend.add_triple(project, subj, pred, obj)


@then('idx querying project "{project}" should return {count:d} binding')
def step_query_graph_singular(context: Any, project: str, count: int) -> None:
    results = context.idx_graph_backend.query(project, "SELECT * WHERE { ?s ?p ?o }")
    assert len(results) == count, f"Expected {count}, got {len(results)}"
    context.idx_graph_results = results


@then('idx querying project "{project}" should return {count:d} bindings')
def step_query_graph_plural(context: Any, project: str, count: int) -> None:
    results = context.idx_graph_backend.query(project, "SELECT * WHERE { ?s ?p ?o }")
    assert len(results) == count, f"Expected {count}, got {len(results)}"
    context.idx_graph_results = results


@then('idx the binding should have subject "{subject}"')
def step_check_binding_subject(context: Any, subject: str) -> None:
    assert context.idx_graph_results[0]["s"] == subject


@when('idx I remove triples with subject "{subject}" from project "{project}"')
def step_remove_triples(context: Any, subject: str, project: str) -> None:
    context.idx_graph_backend.remove_triples(
        project, subject=subject, predicate=None, obj=None
    )


@then("idx removing triples with all-None pattern should raise ValueError")
def step_graph_all_none(context: Any) -> None:
    try:
        context.idx_graph_backend.remove_triples("proj", None, None, None)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx adding a triple with empty project should raise ValueError")
def step_graph_empty_project(context: Any) -> None:
    try:
        context.idx_graph_backend.add_triple("", "s", "p", "o")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@when('idx I create provenance for resource "{resource_id}"')
def step_create_provenance(context: Any, resource_id: str) -> None:
    context.idx_provenance = ProvenanceMetadata(source_resource=resource_id)


@then('idx the provenance should have source_resource "{resource_id}"')
def step_check_provenance_resource(context: Any, resource_id: str) -> None:
    assert context.idx_provenance.source_resource == resource_id


@then("idx the provenance should be current")
def step_check_provenance_current(context: Any) -> None:
    assert context.idx_provenance.is_current is True


@then("idx the provenance should have a valid_from timestamp")
def step_check_provenance_timestamp(context: Any) -> None:
    assert context.idx_provenance.valid_from is not None


@then("idx creating provenance with empty source_resource should raise ValidationError")
def step_provenance_empty_resource(context: Any) -> None:
    try:
        ProvenanceMetadata(source_resource="")
        assert False, "Expected ValidationError"  # noqa: B011
    except ValidationError:
        pass


@when('idx I create a provenanced triple for resource "{resource_id}"')
def step_create_provenanced_triple(context: Any, resource_id: str) -> None:
    triple = UKOTriple(
        subject_uri="uko://s", predicate="uko:type", object_uri="uko://C"
    )
    provenance = ProvenanceMetadata(source_resource=resource_id)
    context.idx_pt = ProvenancedTriple(triple=triple, provenance=provenance)


@then("idx the provenanced triple should have the triple")
def step_check_pt_triple(context: Any) -> None:
    assert context.idx_pt.triple.subject_uri == "uko://s"


@then("idx the provenanced triple should have the provenance")
def step_check_pt_provenance(context: Any) -> None:
    assert context.idx_pt.provenance.source_resource == ULID_1


@then("idx mutating the provenance should raise ValidationError")
def step_provenance_frozen(context: Any) -> None:
    try:
        context.idx_provenance.is_current = False  # type: ignore[misc]
        assert False, "Expected error"  # noqa: B011
    except ValidationError:
        # Pydantic frozen models raise ValidationError on mutation
        pass
    # Verify the object wasn't actually mutated
    assert context.idx_provenance.is_current is True


@when("idx I create an IndexResult with {triples:d} triples and {text:d} text doc")
def step_create_index_result(context: Any, triples: int, text: int) -> None:
    context.idx_result = IndexResult(
        resource_id=ULID_1,
        triple_count=triples,
        text_docs_indexed=text,
        analyzer_domain="python",
    )


@then("idx the IndexResult should have triple_count {count:d}")
def step_check_ir_triples(context: Any, count: int) -> None:
    assert context.idx_result.triple_count == count


@then("idx the IndexResult should have text_docs_indexed {count:d}")
def step_check_ir_text(context: Any, count: int) -> None:
    assert context.idx_result.text_docs_indexed == count


@then('idx the IndexResult should have analyzer_domain "{domain}"')
def step_check_ir_domain(context: Any, domain: str) -> None:
    assert context.idx_result.analyzer_domain == domain


@then(
    "idx creating IndexResult with negative triple_count should raise ValidationError"
)
def step_ir_negative(context: Any) -> None:
    try:
        IndexResult(resource_id=ULID_1, triple_count=-1)
        assert False, "Expected ValidationError"  # noqa: B011
    except ValidationError:
        pass


@then("idx creating SearchResult with score 1.5 should raise ValueError")
def step_search_result_bad_score(context: Any) -> None:
    try:
        SearchResult(doc_id="d", content="c", score=1.5)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx creating SearchResult with empty doc_id should raise ValueError")
def step_search_result_empty_id(context: Any) -> None:
    try:
        SearchResult(doc_id="", content="c", score=0.5)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx creating IndexedDocument with negative char_count should raise ValueError")
def step_indexed_doc_negative(context: Any) -> None:
    try:
        IndexedDocument(project="p", doc_id="d", char_count=-1)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx creating IndexedDocument with empty project should raise ValueError")
def step_indexed_doc_empty_project(context: Any) -> None:
    try:
        IndexedDocument(project="", doc_id="d")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then(
    "idx the graph backend should contain triple"
    ' with predicate "{predicate}" and object "{obj}"'
)
def step_graph_contains_triple(context: Any, predicate: str, obj: str) -> None:
    backend = context.idx_graph_backend
    project = getattr(context, "idx_project", "local/test")
    bindings = backend.query(project, "SELECT *")
    matched = [b for b in bindings if b["p"] == predicate and b["o"] == obj]
    assert len(matched) > 0, (
        f"No triple with predicate={predicate!r} object={obj!r} found in {bindings}"
    )


@then('idx the graph backend should not contain triple with object "{obj}"')
def step_graph_not_contains_object(context: Any, obj: str) -> None:
    backend = context.idx_graph_backend
    project = getattr(context, "idx_project", "local/test")
    bindings = backend.query(project, "SELECT *")
    matched = [b for b in bindings if b["o"] == obj]
    assert len(matched) == 0, (
        f"Found unexpected triple(s) with object={obj!r}: {matched}"
    )


@then('idx the provenance source_resource should be "{expected}"')
def step_provenance_source_resource(context: Any, expected: str) -> None:
    pt = context.idx_pt
    assert pt.provenance.source_resource == expected, (
        f"Expected source_resource={expected!r}, got {pt.provenance.source_resource!r}"
    )


@then('idx the provenance source_path should be "{expected}"')
def step_provenance_source_path(context: Any, expected: str) -> None:
    pt = context.idx_pt
    assert pt.provenance.source_path == expected, (
        f"Expected source_path={expected!r}, got {pt.provenance.source_path!r}"
    )


@then(
    "idx indexing a text document with whitespace-only project should raise ValueError"
)
def step_text_whitespace_project(context: Any) -> None:
    try:
        context.idx_text_backend.index_document("   ", "doc", "content", {})
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then(
    "idx indexing a text document with whitespace-only content should raise ValueError"
)
def step_text_whitespace_content(context: Any) -> None:
    try:
        context.idx_text_backend.index_document("proj", "doc", "   ", {})
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx adding a triple with whitespace-only subject should raise ValueError")
def step_graph_whitespace_subject(context: Any) -> None:
    try:
        context.idx_graph_backend.add_triple("proj", "   ", "p", "o")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then('idx the text backend should contain doc_id "{doc_id}" in project "{project}"')
def step_text_contains_doc(context: Any, doc_id: str, project: str) -> None:
    key = (project, doc_id)
    assert key in context.idx_text_backend._docs, (
        f"doc_id {doc_id!r} not found in text backend for project {project!r}"
    )


@then('idx the vector backend should contain doc_id "{doc_id}" in project "{project}"')
def step_vector_contains_doc(context: Any, doc_id: str, project: str) -> None:
    key = (project, doc_id)
    assert key in context.idx_vector_backend._embeddings, (
        f"doc_id {doc_id!r} not found in vector backend for project {project!r}"
    )


@then("idx creating IndexResult with empty resource_id should raise ValidationError")
def step_ir_empty_resource_id(context: Any) -> None:
    try:
        IndexResult(resource_id="")
        assert False, "Expected ValidationError"  # noqa: B011
    except ValidationError:
        pass


@then("idx creating SearchResult with score -0.1 should raise ValueError")
def step_search_result_negative_score(context: Any) -> None:
    try:
        SearchResult(doc_id="d", content="c", score=-0.1)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx creating SearchResult with NaN score should raise ValueError")
def step_search_result_nan_score(context: Any) -> None:
    try:
        SearchResult(doc_id="d", content="c", score=float("nan"))
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx removing triples with empty-string subject should raise ValueError")
def step_graph_empty_string_subject(context: Any) -> None:
    try:
        context.idx_graph_backend.remove_triples(
            "proj", subject="", predicate=None, obj=None
        )
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx removing triples with empty-string predicate should raise ValueError")
def step_graph_empty_string_predicate(context: Any) -> None:
    try:
        context.idx_graph_backend.remove_triples(
            "proj", subject="s", predicate="", obj=None
        )
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx removing triples with empty-string obj should raise ValueError")
def step_graph_empty_string_obj(context: Any) -> None:
    try:
        context.idx_graph_backend.remove_triples(
            "proj", subject="s", predicate=None, obj=""
        )
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass
