"""Analyzer lookup + core indexing + lifecycle + reindex steps.

All steps prefixed with ``idx`` to avoid AmbiguousStep collisions with
other feature files.
"""

from __future__ import annotations

from typing import Any

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

from cleveragents.application.services.uko_indexer import UKOIndexer
from cleveragents.domain.models.acms.markdown_analyzer import MarkdownAnalyzer
from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer
from features.mocks.uko_indexer_mocks import (
    FailingContentReader,
    FailingGraphBackend,
    FailingTextBackend,
    FailingVectorBackend,
    TrackingLifecycleHook,
)
from features.steps.uko_indexer_common import (
    DEFAULT_PROJECT,
    ULID_1,
    _make_resource,
)

__all__: list[str] = []

# AnalyzerRegistry.get_for_resource steps


@given("idx a PythonAnalyzer is registered")
def step_register_python(context: Any) -> None:
    context.idx_registry.register(PythonAnalyzer())


@given("idx a PythonAnalyzer is registered with priority {priority:d}")
def step_register_python_priority(context: Any, priority: int) -> None:
    context.idx_registry.register(PythonAnalyzer(), priority=priority)


@given(
    'idx an alternative Python analyzer "{name}" is registered with priority {priority:d}'
)
def step_register_alt_python(context: Any, name: str, priority: int) -> None:
    """Register a stub ``.py`` analyzer with custom *name* as domain."""
    from cleveragents.domain.models.acms.analyzers import UKOTriple

    class _Alt:
        supported_extensions = frozenset({".py"})
        domain = name

        def analyze(self, content: str, resource_uri: str) -> list[UKOTriple]:
            return []

    context.idx_registry.register(_Alt(), priority=priority)


@given("idx a MarkdownAnalyzer is registered")
def step_register_markdown(context: Any) -> None:
    context.idx_registry.register(MarkdownAnalyzer())


@when('idx I look up analyzer for a resource with location "{location}"')
def step_lookup_by_location(context: Any, location: str) -> None:
    resource = _make_resource(ULID_1, location=location)
    context.idx_found_analyzer = context.idx_registry.get_for_resource(resource)


@when("idx I look up analyzer for a resource with no location")
def step_lookup_no_location(context: Any) -> None:
    resource = _make_resource(ULID_1, location=None)
    context.idx_found_analyzer = context.idx_registry.get_for_resource(resource)


@then('idx the analyzer domain should be "{domain}"')
def step_check_analyzer_domain(context: Any, domain: str) -> None:
    assert context.idx_found_analyzer is not None, "Expected an analyzer"
    assert context.idx_found_analyzer.domain == domain


@then("idx no analyzer should be found")
def step_no_analyzer(context: Any) -> None:
    assert context.idx_found_analyzer is None


# UKOIndexer setup steps


@given("idx a UKOIndexer with all backends")
def step_indexer_all(context: Any) -> None:
    reader = context.idx_content_reader
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=context.idx_text_backend,
        vector_backend=context.idx_vector_backend,
        content_reader=reader,
    )


@given("idx a UKOIndexer without text backend")
def step_indexer_no_text(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=None,
        vector_backend=context.idx_vector_backend,
        content_reader=context.idx_content_reader,
    )


@given("idx a UKOIndexer without vector backend")
def step_indexer_no_vector(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=context.idx_text_backend,
        vector_backend=None,
        content_reader=context.idx_content_reader,
    )


@given("idx a UKOIndexer with only graph backend")
def step_indexer_graph_only(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=None,
        vector_backend=None,
        content_reader=context.idx_content_reader,
    )


@given("idx a UKOIndexer with a failing content reader")
def step_indexer_failing_reader(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=context.idx_text_backend,
        vector_backend=context.idx_vector_backend,
        content_reader=FailingContentReader(),
    )


@given("idx a UKOIndexer with a tracking lifecycle hook")
def step_indexer_tracking_hook(context: Any) -> None:
    context.idx_hook = TrackingLifecycleHook()
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=context.idx_text_backend,
        vector_backend=context.idx_vector_backend,
        content_reader=context.idx_content_reader,
        lifecycle_hook=context.idx_hook,
    )


@given("idx a UKOIndexer with a tracking lifecycle hook and failing reader")
def step_indexer_tracking_failing(context: Any) -> None:
    context.idx_hook = TrackingLifecycleHook()
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=context.idx_text_backend,
        vector_backend=context.idx_vector_backend,
        content_reader=FailingContentReader(),
        lifecycle_hook=context.idx_hook,
    )


@given("idx a UKOIndexer with a failing graph backend")
def step_indexer_failing_graph(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=FailingGraphBackend(),  # type: ignore[arg-type]
        text_backend=context.idx_text_backend,
        vector_backend=context.idx_vector_backend,
        content_reader=context.idx_content_reader,
    )


@given("idx a UKOIndexer with a failing text backend")
def step_indexer_failing_text(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=FailingTextBackend(),  # type: ignore[arg-type]
        vector_backend=context.idx_vector_backend,
        content_reader=context.idx_content_reader,
    )


@given("idx a UKOIndexer with a failing vector backend")
def step_indexer_failing_vector(context: Any) -> None:
    context.idx_indexer = UKOIndexer(
        analyzer_registry=context.idx_registry,
        graph_backend=context.idx_graph_backend,
        text_backend=context.idx_text_backend,
        vector_backend=FailingVectorBackend(),  # type: ignore[arg-type]
        content_reader=context.idx_content_reader,
    )


# UKOIndexer action steps


@when('idx I index a Python resource "{rid}" with content "{content}"')
def step_index_python(context: Any, rid: str, content: str) -> None:
    resource = _make_resource(rid, location="src/main.py")
    # Unescape literal \n from Gherkin steps into real newlines
    real_content = content.replace("\\n", "\n")
    context.idx_content_reader.set_content(rid, real_content)
    context.idx_result = context.idx_indexer.index_resource(
        resource,
        project=DEFAULT_PROJECT,
    )


@when('idx I index a Markdown resource "{rid}" with content "{content}"')
def step_index_markdown(context: Any, rid: str, content: str) -> None:
    resource = _make_resource(rid, location="README.md")
    real_content = content.replace("\\n", "\n")
    context.idx_content_reader.set_content(rid, real_content)
    context.idx_result = context.idx_indexer.index_resource(
        resource,
        project=DEFAULT_PROJECT,
    )


@when('idx I index a CSV resource "{rid}" with location "{location}"')
def step_index_csv(context: Any, rid: str, location: str) -> None:
    resource = _make_resource(rid, location=location)
    context.idx_content_reader.set_content(rid, "a,b,c")
    context.idx_result = context.idx_indexer.index_resource(
        resource,
        project=DEFAULT_PROJECT,
    )


@when('idx I remove resource "{rid}" from project "{project}"')
def step_remove_resource(context: Any, rid: str, project: str) -> None:
    context.idx_indexer.remove_resource(rid, project=project)


@when('idx I reindex resource "{rid}" with content "{content}"')
def step_reindex_resource(context: Any, rid: str, content: str) -> None:
    resource = _make_resource(rid, location="src/main.py")
    real_content = content.replace("\\n", "\n")
    context.idx_content_reader.set_content(rid, real_content)
    context.idx_result = context.idx_indexer.reindex_resource(
        resource,
        project=DEFAULT_PROJECT,
    )


@when(
    'idx I index Python resource "{rid}" under project "{project}" content "{content}"'
)
def step_index_python_in_project(
    context: Any, rid: str, project: str, content: str
) -> None:
    resource = _make_resource(rid, location="src/main.py")
    real_content = content.replace("\\n", "\n")
    context.idx_content_reader.set_content(rid, real_content)
    context.idx_result = context.idx_indexer.index_resource(
        resource,
        project=project,
    )


@when('idx I reindex resource "{rid}" under project "{project}" content "{content}"')
def step_reindex_in_project(
    context: Any,
    rid: str,
    project: str,
    content: str,
) -> None:
    resource = _make_resource(rid, location="src/main.py")
    real_content = content.replace("\\n", "\n")
    context.idx_content_reader.set_content(rid, real_content)
    context.idx_result = context.idx_indexer.reindex_resource(
        resource,
        project=project,
    )


# UKOIndexer assertion steps


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


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


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


@then("idx the index result should have embeddings_indexed {count:d}")
def step_check_embeddings(context: Any, count: int) -> None:
    assert context.idx_result.embeddings_indexed == count


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


@then("idx the index result should have {count:d} errors")
def step_check_error_count(context: Any, count: int) -> None:
    assert len(context.idx_result.errors) == count, (
        f"Expected {count} errors, got {len(context.idx_result.errors)}: "
        f"{context.idx_result.errors}"
    )


@then('idx the index result error should mention "{word}"')
def step_check_error_message(context: Any, word: str) -> None:
    assert any(word.lower() in e.lower() for e in context.idx_result.errors), (
        f"No error mentioning '{word}' in {context.idx_result.errors}"
    )


@then('idx the index result errors should mention "{word}"')
def step_check_errors_mention(context: Any, word: str) -> None:
    assert any(word.lower() in e.lower() for e in context.idx_result.errors), (
        f"No error mentioning '{word}' in {context.idx_result.errors}"
    )


@then('idx the graph backend should have triples for project "{project}"')
def step_graph_has_triples(context: Any, project: str) -> None:
    count = context.idx_graph_backend.triple_count(project)
    assert count > 0, f"Expected triples, got {count}"


@then('idx the graph backend should have {count:d} triples for project "{project}"')
def step_graph_triple_count(context: Any, count: int, project: str) -> None:
    actual = context.idx_graph_backend.triple_count(project)
    assert actual == count, f"Expected {count}, got {actual}"


@then("idx the text backend should have {count:d} document")
def step_text_doc_count_singular(context: Any, count: int) -> None:
    assert context.idx_text_backend.document_count == count


@then("idx the text backend should have {count:d} documents")
def step_text_doc_count_plural(context: Any, count: int) -> None:
    assert context.idx_text_backend.document_count == count


@then("idx the vector backend should have {count:d} embedding")
def step_vector_count_singular(context: Any, count: int) -> None:
    assert context.idx_vector_backend.embedding_count == count


@then("idx the vector backend should have {count:d} embeddings")
def step_vector_count_plural(context: Any, count: int) -> None:
    assert context.idx_vector_backend.embedding_count == count


@then("idx the indexer should track {count:d} indexed resource")
def step_indexed_count_singular(context: Any, count: int) -> None:
    assert context.idx_indexer.indexed_resource_count == count


@then("idx the indexer should track {count:d} indexed resources")
def step_indexed_count_plural(context: Any, count: int) -> None:
    assert context.idx_indexer.indexed_resource_count == count


# Validation error steps


@then("idx indexing with empty project should raise ValueError")
def step_index_empty_project(context: Any) -> None:
    resource = _make_resource(ULID_1)
    try:
        context.idx_indexer.index_resource(resource, project="")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx removing with empty resource_id should raise ValueError")
def step_remove_empty_rid(context: Any) -> None:
    try:
        context.idx_indexer.remove_resource("", project=DEFAULT_PROJECT)
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx removing with empty project should raise ValueError")
def step_remove_empty_project(context: Any) -> None:
    try:
        context.idx_indexer.remove_resource(ULID_1, project="")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


# Lifecycle hook steps


@then("idx the lifecycle hook should have received {count:d} on_indexed event")
def step_hook_indexed_singular(context: Any, count: int) -> None:
    assert len(context.idx_hook.indexed_events) == count


@then("idx the lifecycle hook should have received {count:d} on_indexed events")
def step_hook_indexed_plural(context: Any, count: int) -> None:
    actual = len(context.idx_hook.indexed_events)
    assert actual == count, f"Expected {count} on_indexed events, got {actual}"


@then("idx the lifecycle hook should have received {count:d} on_error events")
def step_hook_error(context: Any, count: int) -> None:
    assert len(context.idx_hook.error_events) == count


@then("idx the lifecycle hook should have received {count:d} on_removed event")
def step_hook_removed(context: Any, count: int) -> None:
    assert len(context.idx_hook.removed_events) == count


@then("idx the lifecycle hook should have received {count:d} on_removed events")
def step_hook_removed_plural(context: Any, count: int) -> None:
    actual = len(context.idx_hook.removed_events)
    assert actual == count, f"Expected {count} on_removed events, got {actual}"


# Constructor validation steps


@then("idx creating UKOIndexer with invalid registry should raise TypeError")
def step_invalid_registry(context: Any) -> None:
    try:
        UKOIndexer(
            analyzer_registry="not a registry",  # type: ignore[arg-type]
            graph_backend=context.idx_graph_backend,
        )
        assert False, "Expected TypeError"  # noqa: B011
    except TypeError:
        pass


@then("idx the indexer should have text backend")
def step_has_text(context: Any) -> None:
    assert context.idx_indexer.has_text_backend is True


@then("idx the indexer should have vector backend")
def step_has_vector(context: Any) -> None:
    assert context.idx_indexer.has_vector_backend is True


@then("idx the indexer should not have text backend")
def step_no_text(context: Any) -> None:
    assert context.idx_indexer.has_text_backend is False


@then("idx the indexer should not have vector backend")
def step_no_vector(context: Any) -> None:
    assert context.idx_indexer.has_vector_backend is False


# Reindex validation steps


@then("idx reindexing with empty project should raise ValueError")
def step_reindex_empty_project(context: Any) -> None:
    resource = _make_resource(ULID_1)
    try:
        context.idx_indexer.reindex_resource(resource, project="")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass


@then("idx reindexing with whitespace-only project should raise ValueError")
def step_reindex_whitespace_project(context: Any) -> None:
    resource = _make_resource(ULID_1)
    try:
        context.idx_indexer.reindex_resource(resource, project="   ")
        assert False, "Expected ValueError"  # noqa: B011
    except ValueError:
        pass
