"""Robot Framework helper for UKO Indexer smoke tests.

Provides a CLI-style interface for Robot to invoke UKOIndexer creation,
index lifecycle operations, graceful degradation, and provenance tracking.
Exit code 0 = success, 1 = failure.

Usage:
    python robot/helper_uko_indexer.py <command>
"""

from __future__ import annotations

import sys
from collections.abc import Callable
from pathlib import Path

# Ensure the src directory is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
    sys.path.insert(0, _SRC)

from cleveragents.application.services.uko_indexer import UKOIndexer  # noqa: E402
from cleveragents.application.services.uko_indexer_protocols import (  # noqa: E402
    ContentReader,
    DefaultLifecycleHook,
    LocationContentReader,
)
from cleveragents.domain.models.acms.analyzers import (  # noqa: E402
    AnalyzerRegistry,
)
from cleveragents.domain.models.acms.index_backends import (  # noqa: E402
    IndexedDocument,
    SearchResult,
)
from cleveragents.domain.models.acms.index_stubs import (  # noqa: E402
    InMemoryGraphIndexBackend,
    InMemoryTextIndexBackend,
    InMemoryVectorIndexBackend,
)
from cleveragents.domain.models.acms.provenance import (  # noqa: E402
    IndexResult,
    ProvenanceMetadata,
)
from cleveragents.domain.models.acms.python_analyzer import (  # noqa: E402
    PythonAnalyzer,
)
from cleveragents.domain.models.core.resource import Resource  # noqa: E402

# Ensure features/ is importable for shared mocks
_FEATURES = str(Path(__file__).resolve().parents[1])
if _FEATURES not in sys.path:
    sys.path.insert(0, _FEATURES)

from features.mocks.uko_indexer_mocks import InMemoryContentReader  # noqa: E402

ULID_1 = "01HQ8ZDRX50000000000000001"
ULID_2 = "01HQ8ZDRX50000000000000002"
PROJECT = "local/test"


def _make_resource(
    resource_id: str,
    location: str = "src/example.py",
) -> Resource:
    """Create a minimal Resource for testing."""
    return Resource(
        resource_id=resource_id,
        resource_type_name="git-checkout",
        location=location,
        classification="physical",
    )


def _make_indexer(
    *,
    text: bool = True,
    vector: bool = True,
) -> tuple[
    UKOIndexer,
    InMemoryContentReader,
    InMemoryTextIndexBackend | None,
    InMemoryVectorIndexBackend | None,
    InMemoryGraphIndexBackend,
]:
    """Create a fully-wired UKOIndexer with in-memory backends."""
    registry = AnalyzerRegistry()
    registry.register(PythonAnalyzer())
    reader = InMemoryContentReader()
    graph = InMemoryGraphIndexBackend()
    text_be = InMemoryTextIndexBackend() if text else None
    vector_be = InMemoryVectorIndexBackend() if vector else None
    indexer = UKOIndexer(
        analyzer_registry=registry,
        graph_backend=graph,
        text_backend=text_be,
        vector_backend=vector_be,
        content_reader=reader,
    )
    return (
        indexer,
        reader,
        text_be,
        vector_be,
        graph,
    )


def cmd_index_resource() -> int:
    """Test basic index_resource pipeline."""
    try:
        indexer, reader, _text, _vec, _graph = _make_indexer()
        resource = _make_resource(ULID_1)
        code = '"""Module doc."""\nclass Foo:\n    """Class doc."""\n    pass\n'
        reader.set_content(ULID_1, code)
        result = indexer.index_resource(resource, project=PROJECT)

        assert result.resource_id == ULID_1
        assert result.triple_count > 0
        assert result.analyzer_domain == "python"
        assert result.text_docs_indexed == 1
        assert result.embeddings_indexed == 1
        assert len(result.errors) == 0
        assert indexer.indexed_resource_count == 1

        print("uko-indexer-index-resource-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-index-resource-fail: {exc}")
        return 1


def cmd_remove_resource() -> int:
    """Test remove_resource cleanup including backend state."""
    try:
        indexer, reader, text_be, vec_be, graph_be = _make_indexer()
        resource = _make_resource(ULID_1)
        reader.set_content(ULID_1, "x = 1\n")
        indexer.index_resource(resource, project=PROJECT)
        assert indexer.indexed_resource_count == 1

        # Verify backends have data before removal
        assert graph_be.triple_count(PROJECT) > 0
        assert text_be is not None and text_be.document_count > 0
        assert vec_be is not None and vec_be.embedding_count > 0

        indexer.remove_resource(ULID_1, project=PROJECT)
        assert indexer.indexed_resource_count == 0

        # Verify backends are cleaned up after removal
        assert graph_be.triple_count(PROJECT) == 0
        assert text_be.document_count == 0
        assert vec_be.embedding_count == 0

        print("uko-indexer-remove-resource-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-remove-resource-fail: {exc}")
        return 1


def cmd_reindex_resource() -> int:
    """Test reindex_resource (remove + re-index)."""
    try:
        indexer, reader, _text, _vec, _graph = _make_indexer()
        resource = _make_resource(ULID_1)
        reader.set_content(ULID_1, "x = 1\n")
        result1 = indexer.index_resource(resource, project=PROJECT)
        assert result1.triple_count > 0

        # Update content and reindex
        reader.set_content(
            ULID_1,
            '"""Reindexed."""\nclass Bar:\n    pass\n',
        )
        result2 = indexer.reindex_resource(resource, project=PROJECT)
        assert result2.resource_id == ULID_1
        assert result2.triple_count > 0
        assert indexer.indexed_resource_count == 1

        print("uko-indexer-reindex-resource-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-reindex-resource-fail: {exc}")
        return 1


def cmd_graceful_degradation() -> int:
    """Test graceful degradation without text/vector backends."""
    try:
        indexer, reader, _, _, _graph = _make_indexer(text=False, vector=False)
        resource = _make_resource(ULID_1)
        reader.set_content(ULID_1, "y = 2\n")
        result = indexer.index_resource(resource, project=PROJECT)

        assert result.triple_count > 0
        assert result.text_docs_indexed == 0
        assert result.embeddings_indexed == 0
        assert not indexer.has_text_backend
        assert not indexer.has_vector_backend
        assert len(result.errors) == 0

        print("uko-indexer-graceful-degradation-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-graceful-degradation-fail: {exc}")
        return 1


def cmd_provenance() -> int:
    """Test provenance metadata is attached to triples."""
    try:
        # Verify ProvenanceMetadata construction
        prov = ProvenanceMetadata(
            source_resource=ULID_1,
            source_path="src/example.py",
        )
        assert prov.is_current is True
        assert prov.valid_from is not None

        # Verify provenance is attached during indexing via IndexResult
        indexer, reader, _, _, _graph = _make_indexer()
        resource = _make_resource(ULID_1)
        reader.set_content(ULID_1, "z = 3\n")
        result = indexer.index_resource(resource, project=PROJECT)
        assert result.triple_count > 0
        assert result.resource_id == ULID_1

        print("uko-indexer-provenance-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-provenance-fail: {exc}")
        return 1


def cmd_index_backends() -> int:
    """Test in-memory index backend operations."""
    try:
        # Text backend
        text_be = InMemoryTextIndexBackend()
        text_be.index_document(PROJECT, "doc1", "hello world", {"key": "val"})
        results = text_be.search(PROJECT, "hello", limit=5)
        assert len(results) == 1
        assert results[0].doc_id == "doc1"
        text_be.remove_document(PROJECT, "doc1")
        results = text_be.search(PROJECT, "hello", limit=5)
        assert len(results) == 0

        # Vector backend
        vec_be = InMemoryVectorIndexBackend()
        vec_be.index_embedding(PROJECT, "emb1", [1.0, 2.0, 3.0], {"key": "val"})
        results = vec_be.search_similar(PROJECT, [1.0, 2.0, 3.0], limit=5)
        assert len(results) == 1
        assert results[0].doc_id == "emb1"
        vec_be.remove_embedding(PROJECT, "emb1")
        results = vec_be.search_similar(PROJECT, [1.0, 2.0, 3.0], limit=5)
        assert len(results) == 0

        # Graph backend
        graph_be = InMemoryGraphIndexBackend()
        graph_be.add_triple(PROJECT, "s1", "p1", "o1")
        bindings = graph_be.query(PROJECT, "SELECT * WHERE { ?s ?p ?o }")
        assert len(bindings) == 1
        assert bindings[0]["s"] == "s1"
        assert bindings[0]["p"] == "p1"
        assert bindings[0]["o"] == "o1"
        graph_be.remove_triples(PROJECT, subject="s1", predicate=None, obj=None)
        bindings = graph_be.query(PROJECT, "SELECT * WHERE { ?s ?p ?o }")
        assert len(bindings) == 0

        # Validation
        doc = IndexedDocument(project=PROJECT, doc_id="d1", char_count=10)
        assert doc.char_count == 10

        sr = SearchResult(doc_id="d1", score=0.5, content="test")
        assert sr.score == 0.5

        print("uko-indexer-index-backends-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-index-backends-fail: {exc}")
        return 1


def cmd_validation() -> int:
    """Test input validation guards."""
    try:
        indexer, reader, _, _, _ = _make_indexer()
        resource = _make_resource(ULID_1)
        reader.set_content(ULID_1, "a = 1\n")

        # Empty project
        try:
            indexer.index_resource(resource, project="")
            print("uko-indexer-validation-fail: no ValueError for empty project")
            return 1
        except ValueError:
            pass

        # Empty resource_id on remove
        try:
            indexer.remove_resource("", project=PROJECT)
            print("uko-indexer-validation-fail: no ValueError for empty resource_id")
            return 1
        except ValueError:
            pass

        # Invalid analyzer_registry type
        try:
            UKOIndexer(
                analyzer_registry="not_a_registry",  # type: ignore[arg-type]
                graph_backend=InMemoryGraphIndexBackend(),
            )
            print("uko-indexer-validation-fail: no TypeError for invalid registry")
            return 1
        except TypeError:
            pass

        # SearchResult score out of range
        try:
            SearchResult(doc_id="x", score=1.5, content="test")
            print("uko-indexer-validation-fail: no ValueError for score > 1.0")
            return 1
        except ValueError:
            pass

        print("uko-indexer-validation-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-validation-fail: {exc}")
        return 1


def cmd_protocol_compliance() -> int:
    """Verify protocol compliance for key types."""
    try:
        reader = InMemoryContentReader()
        assert isinstance(reader, ContentReader)

        hook = DefaultLifecycleHook()
        # DefaultLifecycleHook logs but doesn't fail
        result = IndexResult(
            resource_id=ULID_1,
            analyzer_domain="python",
            triple_count=5,
        )
        hook.on_indexed(result)
        hook.on_removed(ULID_1, PROJECT)
        hook.on_error(ULID_1, "test error")

        # LocationContentReader protocol compliance
        loc_reader = LocationContentReader()
        assert isinstance(loc_reader, ContentReader)

        print("uko-indexer-protocol-compliance-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-protocol-compliance-fail: {exc}")
        return 1


def cmd_file_watching() -> int:
    """Test ResourceFileWatcher lifecycle, change detection, and EventBus."""
    import threading
    import time

    from watchdog.events import FileDeletedEvent, FileModifiedEvent

    from cleveragents.application.services.resource_file_watcher import (
        FileChangeType,
        ResourceFileWatcher,
    )
    from cleveragents.infrastructure.events.models import DomainEvent
    from cleveragents.infrastructure.events.types import EventType

    class _MiniEventBus:
        def __init__(self) -> None:
            self.events: list[DomainEvent] = []
            self._ev = threading.Event()

        def emit(self, event: DomainEvent) -> None:
            self.events.append(event)
            self._ev.set()

        def subscribe(self, event_type: EventType, handler: object) -> None:
            _ = event_type, handler

    try:
        import tempfile as _tf
        from pathlib import Path as _P

        tmpdir = _tf.mkdtemp()
        fpath = _P(tmpdir) / "watched.py"
        fpath.write_text("x = 1\n")

        changes: list[tuple[str, str, FileChangeType]] = []
        cb_event = threading.Event()

        def _on_change(rid: str, proj: str, ct: FileChangeType) -> None:
            changes.append((rid, proj, ct))
            cb_event.set()

        bus = _MiniEventBus()
        watcher = ResourceFileWatcher(
            on_change=_on_change,
            event_bus=bus,
            debounce_seconds=0.05,
        )

        # 1. Lifecycle
        # Avoid creating real OS-level inotify watches in CI/parallel runs,
        # which can fail with ENOSPC when global watch limits are exhausted.
        # We only need the watcher in a "running" state to exercise internal
        # debounce/callback/event-bus behavior via synthetic watchdog events.
        assert not watcher.is_running
        watcher._running = True
        assert watcher.is_running

        # 2. Watch a file
        watcher.watch(fpath, resource_id=ULID_1, project=PROJECT)
        assert len(watcher.watched_paths) == 1

        # 3. Simulate modification
        ev = FileModifiedEvent(str(fpath.resolve()))
        watcher._handle_fs_event(ev)
        assert cb_event.wait(timeout=2.0), "Callback did not fire"
        assert changes[-1][0] == ULID_1
        assert changes[-1][2] == FileChangeType.MODIFIED

        # 4. EventBus received event
        time.sleep(0.1)
        assert len(bus.events) > 0
        assert bus.events[-1].event_type == EventType.RESOURCE_MODIFIED
        assert bus.events[-1].details["resource_id"] == ULID_1

        # 5. Simulate deletion
        cb_event.clear()
        ev2 = FileDeletedEvent(str(fpath.resolve()))
        watcher._handle_fs_event(ev2)
        assert cb_event.wait(timeout=2.0), "Callback did not fire for delete"
        assert changes[-1][2] == FileChangeType.DELETED

        # 6. Unwatch
        watcher.unwatch(fpath)
        assert len(watcher.watched_paths) == 0

        # 7. Stop
        watcher.stop()
        assert not watcher.is_running

        # Cleanup
        fpath.unlink(missing_ok=True)
        _P(tmpdir).rmdir()

        print("uko-indexer-file-watching-ok")
        return 0
    except Exception as exc:
        print(f"uko-indexer-file-watching-fail: {exc}")
        import traceback

        traceback.print_exc()
        return 1


COMMANDS: dict[str, Callable[[], int]] = {
    "index-resource": cmd_index_resource,
    "remove-resource": cmd_remove_resource,
    "reindex-resource": cmd_reindex_resource,
    "graceful-degradation": cmd_graceful_degradation,
    "provenance": cmd_provenance,
    "index-backends": cmd_index_backends,
    "validation": cmd_validation,
    "protocol-compliance": cmd_protocol_compliance,
    "file-watching": cmd_file_watching,
}


def main() -> int:
    """Entry point called by Robot Framework ``Run Process``."""
    if len(sys.argv) < 2:
        print("Usage: helper_uko_indexer.py <command>")
        print(f"Commands: {', '.join(COMMANDS)}")
        return 1

    command: str = sys.argv[1]
    handler = COMMANDS.get(command)
    if handler is None:
        print(f"Unknown command: {command}")
        print(f"Commands: {', '.join(COMMANDS)}")
        return 1

    return handler()


if __name__ == "__main__":
    sys.exit(main())
