import json
from dataclasses import dataclass

from home_agent.contracts.protocols import (
    EmbeddingProvider,
    ItemTextRenderer,
    LLMRunner,
    Notifier,
    ProviderPlugin,
    Retriever,
    VectorBackend,
)
from home_agent.contracts.types import (
    Alert,
    EmbeddingRecord,
    Item,
    RetrievalDecisionTrace,
    RetrievalQuery,
    RetrievedMatch,
    RunResult,
    TodoItem,
    UrgencyDecision,
    UrgencyLabel,
    VectorDocument,
    VectorDocumentKey,
    utc_now,
)
from home_agent.core.config import AppConfig
from home_agent.core.logging import get_logger
from home_agent.core.rules import RuleUrgencyEngine
from home_agent.llm.runner import BudgetGuard, current_month_key
from home_agent.memory.engine import ScopedMemoryEngine
from home_agent.retrieval.text_renderer import DefaultItemTextRenderer
from home_agent.storage.sqlite_store import SqliteStore
from home_agent.utils.subprocess_logging import run_logged_subprocess


@dataclass
class Orchestrator:
    config: AppConfig
    plugins: list[ProviderPlugin]
    store: SqliteStore
    llm_runner: LLMRunner
    notifier: Notifier
    memory_engine: ScopedMemoryEngine
    embedding_provider: EmbeddingProvider | None = None
    item_text_renderer: ItemTextRenderer | None = None
    retriever: Retriever | None = None
    vector_backend: VectorBackend | None = None

    def run_once(self) -> RunResult:
        started_at = utc_now()
        run_id = self.store.create_run(started_at)
        logger = get_logger(__name__).bind(run_id=run_id)
        logger.info(
            "orchestrator.run.start",
            plugin_count=len(self.plugins),
            retrieval_enabled=self.config.retrieval.enabled,
            embeddings_enabled=self.config.embeddings.enabled,
        )

        processed = 0
        alerted = 0
        errors: list[str] = []
        important_items: list[Item] = []

        active_rules = {rule.rule_key for rule in self.store.list_active_memory_rules()}
        rule_engine = RuleUrgencyEngine()

        month_key = current_month_key()
        spend, _blocked = self.store.get_month_spend(month_key, self.config.budget.monthly_cap_usd)
        budget_guard = BudgetGuard(
            month_spend_usd=spend,
            month_cap_usd=self.config.budget.monthly_cap_usd,
        )

        for plugin in self.plugins:
            plugin_logger = logger.bind(plugin_id=plugin.id)
            try:
                items = list(_collect_items(plugin))
                plugin_logger.info(
                    "orchestrator.plugin.collection.completed",
                    item_count=len(items),
                )
                for item in items:
                    item_logger = plugin_logger.bind(
                        provider=item.provider,
                        external_id=item.external_id,
                        kind=item.kind.value,
                    )
                    processed += 1
                    decision = rule_engine.score(item)
                    item_logger.debug(
                        "orchestrator.item.scored",
                        score=decision.score,
                        label=decision.label.value,
                    )
                    memory_reasons = self.memory_engine.apply_active_rules(active_rules, item)
                    if memory_reasons:
                        decision = UrgencyDecision(
                            score=min(1.0, decision.score + 0.2),
                            label=decision.label,
                            reasons=decision.reasons + memory_reasons,
                            used_llm=decision.used_llm,
                        )
                        item_logger.debug(
                            "orchestrator.item.memory_adjusted",
                            memory_reason_count=len(memory_reasons),
                            score=decision.score,
                            label=decision.label.value,
                        )

                    if _is_candidate_important(decision):
                        important_items.append(item)
                        retrieval_matches, retrieval_errors = self._get_retrieval_matches(
                            run_id, item
                        )
                        item_logger.debug(
                            "orchestrator.item.retrieval.completed",
                            match_count=len(retrieval_matches),
                            error_count=len(retrieval_errors),
                        )
                        errors.extend(retrieval_errors)
                        if budget_guard.allows_call():
                            prompt_matches = retrieval_matches[
                                : self.config.retrieval.max_prompt_examples
                            ]
                            self._record_prompt_included_matches(run_id, item, prompt_matches)
                            try:
                                llm_result = self.llm_runner.classify(
                                    item,
                                    retrieved_matches=prompt_matches,
                                )
                            except Exception as exc:
                                detail = f"llm:{item.provider}:{item.external_id}: {exc}"
                                errors.append(detail)
                                item_logger.error(
                                    "orchestrator.item.classification.failed",
                                    detail=detail,
                                )
                            else:
                                decision = llm_result.decision
                                item_logger.info(
                                    "orchestrator.item.classified",
                                    label=decision.label.value,
                                    score=decision.score,
                                    used_llm=decision.used_llm,
                                )
                                self.store.save_llm_usage(run_id, llm_result.usage)
                                cost_usd = llm_result.usage.total_cost_usd
                                if cost_usd is not None:
                                    self.store.add_month_spend(
                                        month_key=month_key,
                                        delta_usd=cost_usd,
                                        hard_cap_usd=self.config.budget.monthly_cap_usd,
                                    )
                                    budget_guard = BudgetGuard(
                                        month_spend_usd=budget_guard.month_spend_usd + cost_usd,
                                        month_cap_usd=budget_guard.month_cap_usd,
                                    )

                    self.store.save_item(item, decision)
                    embedding_error = self._maybe_store_embedding(item)
                    if embedding_error is not None:
                        errors.append(embedding_error)
                        item_logger.error(
                            "orchestrator.item.embedding_failed",
                            detail=embedding_error,
                        )
                    if decision.label in {UrgencyLabel.HIGH, UrgencyLabel.CRITICAL}:
                        alerted += 1
                        item_logger.info(
                            "orchestrator.alert.created",
                            severity=decision.label.value,
                        )
                        self.notifier.notify(
                            Alert(
                                title=f"{decision.label.upper()}: {item.subject}",
                                body=f"From {item.sender} ({item.provider})",
                                severity=decision.label,
                            )
                        )
                        self.store.upsert_todo(
                            TodoItem(
                                title=item.subject,
                                priority=decision.label,
                                due_at=None,
                                source_provider=item.provider,
                                source_external_id=item.external_id,
                            )
                        )
                        if (
                            self.config.alerts.auto_launch_terminal
                            and decision.label == UrgencyLabel.CRITICAL
                        ):
                            _launch_terminal_chat(item)
                    item_logger.info(
                        "orchestrator.item.processed",
                        label=decision.label.value,
                        score=decision.score,
                    )
            except Exception as exc:
                plugin_logger.exception("orchestrator.plugin.failed")
                errors.append(f"{plugin.id}: {exc}")

        candidates = self.memory_engine.extract_candidates(important_items)
        self.store.save_memory_candidates(candidates)

        self.store.finish_run(run_id, "ok" if not errors else "partial", json.dumps(errors))
        logger.info(
            "orchestrator.run.completed",
            processed=processed,
            alerted=alerted,
            error_count=len(errors),
        )
        return RunResult(
            started_at=started_at,
            finished_at=utc_now(),
            processed=processed,
            alerted=alerted,
            errors=errors,
        )

    def _maybe_store_embedding(self, item: Item) -> str | None:
        if not self.config.embeddings.enabled or self.embedding_provider is None:
            return None

        renderer = self.item_text_renderer or DefaultItemTextRenderer()
        try:
            rendered = renderer.render(item)
            vectors = self.embedding_provider.embed_texts([rendered.embedding_text])
        except Exception as exc:
            return f"embedding:{item.provider}:{item.external_id}: {exc}"

        if not vectors:
            return f"embedding:{item.provider}:{item.external_id}: empty response"
        vector = vectors[0]
        self.store.save_item_text(rendered)
        self.store.save_item_embedding(
            EmbeddingRecord(
                provider=item.provider,
                external_id=item.external_id,
                embedding_provider=self.embedding_provider.provider_name,
                embedding_model=self.embedding_provider.model_name,
                vector=vector,
                dimension=len(vector),
                created_at=utc_now(),
            )
        )
        if self.vector_backend is not None:
            self.vector_backend.upsert_documents(
                [
                    VectorDocument(
                        key=VectorDocumentKey(
                            provider=item.provider,
                            external_id=item.external_id,
                            embedding_provider=self.embedding_provider.provider_name,
                            embedding_model=self.embedding_provider.model_name,
                        ),
                        kind=item.kind,
                        received_at=item.received_at,
                        urgency_label=None,
                        summary_text=rendered.summary_text,
                        source_version=rendered.source_version,
                        vector=vector,
                        metadata={},
                    )
                ]
            )
        return None

    def _get_retrieval_matches(
        self,
        run_id: int,
        item: Item,
    ) -> tuple[list[RetrievedMatch], list[str]]:
        if not self.config.retrieval.enabled or self.retriever is None:
            return [], []

        try:
            matches = self.retriever.find_similar(
                RetrievalQuery(
                    item=item,
                    top_k=self.config.retrieval.top_k,
                    min_similarity=self.config.retrieval.min_similarity,
                )
            )
        except Exception as exc:
            return [], [f"retrieval:{item.provider}:{item.external_id}: {exc}"]

        for match in matches:
            self.store.save_retrieval_event(
                RetrievalDecisionTrace(
                    run_id=run_id,
                    source_provider=item.provider,
                    source_external_id=item.external_id,
                    matched_provider=match.provider,
                    matched_external_id=match.external_id,
                    similarity=match.similarity,
                    stage="retrieved",
                    included_in_prompt=False,
                    created_at=utc_now(),
                )
            )
        return matches, []

    def _record_prompt_included_matches(
        self,
        run_id: int,
        item: Item,
        matches: list[RetrievedMatch],
    ) -> None:
        for match in matches:
            self.store.save_retrieval_event(
                RetrievalDecisionTrace(
                    run_id=run_id,
                    source_provider=item.provider,
                    source_external_id=item.external_id,
                    matched_provider=match.provider,
                    matched_external_id=match.external_id,
                    similarity=match.similarity,
                    stage="prompt_included",
                    included_in_prompt=True,
                    created_at=utc_now(),
                )
            )


def _collect_items(plugin: ProviderPlugin) -> list[Item]:
    items: list[Item] = []
    mail_reader = plugin.get_mail_reader()
    if mail_reader is not None:
        items.extend(mail_reader.list_recent())

    cal_reader = plugin.get_calendar_reader()
    if cal_reader is not None:
        items.extend(cal_reader.list_upcoming())
    return items


def _is_candidate_important(decision: UrgencyDecision) -> bool:
    return decision.score >= 0.4


def _launch_terminal_chat(item: Item) -> None:
    prompt = (
        "New urgent item detected. Explain what happened and next actions. "
        f"Provider={item.provider}; Subject={item.subject}; Sender={item.sender}"
    )
    run_logged_subprocess(
        ["powershell", "-NoProfile", "-Command", f'claude -p "{prompt}"'],
        logger_name=__name__,
        event="orchestrator.terminal_chat",
    )
