from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path

from home_agent.contracts.types import Alert, Item, ItemKind, LLMResult, RetrievedMatch
from home_agent.core.config import (
    AlertsConfig,
    AppConfig,
    BudgetConfig,
    EmbeddingsConfig,
    LLMConfig,
    MemoryConfig,
    PollConfig,
    RetentionConfig,
    RetrievalConfig,
)
from home_agent.llm.local_fallback import DeterministicFallbackRunner
from home_agent.memory.engine import ScopedMemoryEngine
from home_agent.orchestrator.service import Orchestrator
from home_agent.retrieval.text_renderer import DefaultItemTextRenderer
from home_agent.storage.sqlite_store import SqliteStore


@dataclass
class FakeMailReader:
    def list_recent(self) -> list[Item]:
        return [
            Item(
                provider="fake-mail",
                external_id="m1",
                kind=ItemKind.EMAIL,
                received_at=datetime.now(tz=UTC),
                subject="urgent notice",
                sender="ops@example.com",
                metadata={"snippet": "Please review this urgent notice"},
            )
        ]

    def search(self, query: str) -> list[Item]:
        _ = query
        return []

    def get_message(self, external_id: str) -> Item | None:
        _ = external_id
        return None


@dataclass
class FakePlugin:
    id: str = "fake"
    version: str = "1.0"

    def get_mail_reader(self) -> FakeMailReader:
        return FakeMailReader()

    def get_calendar_reader(self) -> None:
        return None


@dataclass
class FakeNotifier:
    def notify(self, alert: Alert) -> None:
        _ = alert


class StubEmbeddingProvider:
    @property
    def provider_name(self) -> str:
        return "stub"

    @property
    def model_name(self) -> str:
        return "stub-v1"

    def embed_texts(self, texts: list[str]) -> list[list[float]]:
        return [[0.25, 0.5, 0.75] for _ in texts]


class ExplodingEmbeddingProvider:
    @property
    def provider_name(self) -> str:
        return "stub"

    @property
    def model_name(self) -> str:
        return "explode"

    def embed_texts(self, texts: list[str]) -> list[list[float]]:
        _ = texts
        msg = "boom"
        raise RuntimeError(msg)


class StubRetriever:
    def find_similar(self, query: object) -> list[RetrievedMatch]:
        _ = query
        return [
            RetrievedMatch(
                provider="fake-mail",
                external_id="old-1",
                kind=ItemKind.EMAIL,
                summary_text="old urgent notice",
                received_at=datetime.now(tz=UTC),
                similarity=0.91,
                urgency_label=None,
            )
        ]


class CapturingLLMRunner:
    def __init__(self) -> None:
        self.calls: list[list[RetrievedMatch]] = []

    def classify(
        self,
        item: Item,
        retrieved_matches: list[RetrievedMatch] | None = None,
    ) -> LLMResult:
        _ = item
        self.calls.append(retrieved_matches or [])
        return DeterministicFallbackRunner().classify(item)


class FlakyLLMRunner:
    def __init__(self) -> None:
        self.calls: int = 0

    def classify(
        self,
        item: Item,
        retrieved_matches: list[RetrievedMatch] | None = None,
    ) -> LLMResult:
        _ = item, retrieved_matches
        self.calls += 1
        if self.calls == 1:
            msg = "command timed out after 120 seconds: claude -p ..."
            raise RuntimeError(msg)
        return DeterministicFallbackRunner().classify(item)


@dataclass
class TwoItemMailReader:
    def list_recent(self) -> list[Item]:
        return [
            Item(
                provider="fake-mail",
                external_id="m1",
                kind=ItemKind.EMAIL,
                received_at=datetime.now(tz=UTC),
                subject="urgent notice",
                sender="ops@example.com",
                metadata={},
            ),
            Item(
                provider="fake-mail",
                external_id="m2",
                kind=ItemKind.EMAIL,
                received_at=datetime.now(tz=UTC),
                subject="another urgent notice",
                sender="ops@example.com",
                metadata={},
            ),
        ]

    def search(self, query: str) -> list[Item]:
        _ = query
        return []

    def get_message(self, external_id: str) -> Item | None:
        _ = external_id
        return None


@dataclass
class TwoItemPlugin:
    id: str = "fake"
    version: str = "1.0"

    def get_mail_reader(self) -> TwoItemMailReader:
        return TwoItemMailReader()

    def get_calendar_reader(self) -> None:
        return None


def _config(db_path: Path, enabled: bool = True) -> AppConfig:
    return AppConfig(
        poll=PollConfig(interval_minutes=15),
        budget=BudgetConfig(monthly_cap_usd=10.0),
        retention=RetentionConfig(days=30),
        alerts=AlertsConfig(auto_launch_terminal=False),
        llm=LLMConfig(primary="none"),
        memory=MemoryConfig(),
        enabled_providers=["fake"],
        sqlite_path=db_path,
        daily_digest_enabled=True,
        embeddings=EmbeddingsConfig(enabled=enabled, provider="local", model="stub-v1"),
        retrieval=RetrievalConfig(),
    )


def test_orchestrator_persists_embeddings_when_enabled(tmp_path: Path) -> None:
    cfg = _config(tmp_path / "agent.db")
    store = SqliteStore(cfg.sqlite_path)
    orchestrator = Orchestrator(
        config=cfg,
        plugins=[FakePlugin()],
        store=store,
        llm_runner=DeterministicFallbackRunner(),
        notifier=FakeNotifier(),
        memory_engine=ScopedMemoryEngine(),
        embedding_provider=StubEmbeddingProvider(),
        item_text_renderer=DefaultItemTextRenderer(),
    )

    result = orchestrator.run_once()

    assert result.processed == 1
    rendered = store.get_item_text("fake-mail", "m1")
    assert rendered is not None
    embedding = store.get_item_embedding("fake-mail", "m1", "stub", "stub-v1")
    assert embedding is not None
    assert embedding.vector == [0.25, 0.5, 0.75]


def test_orchestrator_continues_when_embedding_provider_fails(tmp_path: Path) -> None:
    cfg = _config(tmp_path / "agent.db")
    store = SqliteStore(cfg.sqlite_path)
    orchestrator = Orchestrator(
        config=cfg,
        plugins=[FakePlugin()],
        store=store,
        llm_runner=DeterministicFallbackRunner(),
        notifier=FakeNotifier(),
        memory_engine=ScopedMemoryEngine(),
        embedding_provider=ExplodingEmbeddingProvider(),
        item_text_renderer=DefaultItemTextRenderer(),
    )

    result = orchestrator.run_once()

    assert result.processed == 1
    assert result.errors == ["embedding:fake-mail:m1: boom"]
    assert store.get_item_text("fake-mail", "m1") is None


def test_orchestrator_persists_retrieval_events_when_enabled(tmp_path: Path) -> None:
    cfg = _config(tmp_path / "agent.db")
    cfg = AppConfig(
        poll=cfg.poll,
        budget=cfg.budget,
        retention=cfg.retention,
        alerts=cfg.alerts,
        llm=cfg.llm,
        memory=cfg.memory,
        enabled_providers=cfg.enabled_providers,
        sqlite_path=cfg.sqlite_path,
        daily_digest_enabled=cfg.daily_digest_enabled,
        embeddings=cfg.embeddings,
        retrieval=RetrievalConfig(enabled=True, top_k=2, min_similarity=0.5),
    )
    store = SqliteStore(cfg.sqlite_path)
    orchestrator = Orchestrator(
        config=cfg,
        plugins=[FakePlugin()],
        store=store,
        llm_runner=DeterministicFallbackRunner(),
        notifier=FakeNotifier(),
        memory_engine=ScopedMemoryEngine(),
        embedding_provider=StubEmbeddingProvider(),
        item_text_renderer=DefaultItemTextRenderer(),
        retriever=StubRetriever(),
    )

    result = orchestrator.run_once()

    assert result.processed == 1
    events = store.list_retrieval_events()
    assert len(events) == 2
    assert [event.stage for event in events] == ["retrieved", "prompt_included"]
    assert events[0].matched_external_id == "old-1"


def test_orchestrator_passes_limited_matches_into_llm_prompt(tmp_path: Path) -> None:
    cfg = _config(tmp_path / "agent.db")
    cfg = AppConfig(
        poll=cfg.poll,
        budget=cfg.budget,
        retention=cfg.retention,
        alerts=cfg.alerts,
        llm=cfg.llm,
        memory=cfg.memory,
        enabled_providers=cfg.enabled_providers,
        sqlite_path=cfg.sqlite_path,
        daily_digest_enabled=cfg.daily_digest_enabled,
        embeddings=cfg.embeddings,
        retrieval=RetrievalConfig(enabled=True, top_k=5, min_similarity=0.5, max_prompt_examples=1),
    )
    store = SqliteStore(cfg.sqlite_path)

    class MultiRetriever:
        def find_similar(self, query: object) -> list[RetrievedMatch]:
            _ = query
            return [
                RetrievedMatch(
                    provider="fake-mail",
                    external_id="old-1",
                    kind=ItemKind.EMAIL,
                    summary_text="old urgent notice",
                    received_at=datetime.now(tz=UTC),
                    similarity=0.91,
                    urgency_label=None,
                ),
                RetrievedMatch(
                    provider="fake-mail",
                    external_id="old-2",
                    kind=ItemKind.EMAIL,
                    summary_text="old second notice",
                    received_at=datetime.now(tz=UTC),
                    similarity=0.82,
                    urgency_label=None,
                ),
            ]

    llm_runner = CapturingLLMRunner()
    orchestrator = Orchestrator(
        config=cfg,
        plugins=[FakePlugin()],
        store=store,
        llm_runner=llm_runner,
        notifier=FakeNotifier(),
        memory_engine=ScopedMemoryEngine(),
        embedding_provider=StubEmbeddingProvider(),
        item_text_renderer=DefaultItemTextRenderer(),
        retriever=MultiRetriever(),
    )

    result = orchestrator.run_once()

    assert result.processed == 1
    assert len(llm_runner.calls) == 1
    assert [match.external_id for match in llm_runner.calls[0]] == ["old-1"]
    events = store.list_retrieval_events()
    prompt_events = [event for event in events if event.stage == "prompt_included"]
    assert len(prompt_events) == 1


def test_orchestrator_records_retrieval_failures_in_run_errors(tmp_path: Path) -> None:
    cfg = _config(tmp_path / "agent.db")
    cfg = AppConfig(
        poll=cfg.poll,
        budget=cfg.budget,
        retention=cfg.retention,
        alerts=cfg.alerts,
        llm=cfg.llm,
        memory=cfg.memory,
        enabled_providers=cfg.enabled_providers,
        sqlite_path=cfg.sqlite_path,
        daily_digest_enabled=cfg.daily_digest_enabled,
        embeddings=cfg.embeddings,
        retrieval=RetrievalConfig(enabled=True, top_k=2, min_similarity=0.5),
    )
    store = SqliteStore(cfg.sqlite_path)

    class ExplodingRetriever:
        def find_similar(self, query: object) -> list[RetrievedMatch]:
            _ = query
            msg = "retrieval boom"
            raise RuntimeError(msg)

    orchestrator = Orchestrator(
        config=cfg,
        plugins=[FakePlugin()],
        store=store,
        llm_runner=DeterministicFallbackRunner(),
        notifier=FakeNotifier(),
        memory_engine=ScopedMemoryEngine(),
        embedding_provider=StubEmbeddingProvider(),
        item_text_renderer=DefaultItemTextRenderer(),
        retriever=ExplodingRetriever(),
    )

    result = orchestrator.run_once()

    assert result.processed == 1
    assert result.errors == ["retrieval:fake-mail:m1: retrieval boom"]
    assert store.list_retrieval_events() == []


def test_orchestrator_continues_after_item_llm_failure(tmp_path: Path) -> None:
    cfg = _config(tmp_path / "agent.db", enabled=False)
    store = SqliteStore(cfg.sqlite_path)
    orchestrator = Orchestrator(
        config=cfg,
        plugins=[TwoItemPlugin()],
        store=store,
        llm_runner=FlakyLLMRunner(),
        notifier=FakeNotifier(),
        memory_engine=ScopedMemoryEngine(),
    )

    result = orchestrator.run_once()

    with store._connect() as conn:
        rows = conn.execute(
            "SELECT external_id, used_llm FROM items ORDER BY external_id"
        ).fetchall()

    assert result.processed == 2
    assert result.errors == ["llm:fake-mail:m1: command timed out after 120 seconds: claude -p ..."]
    assert [(row["external_id"], row["used_llm"]) for row in rows] == [("m1", 0), ("m2", 0)]
