import argparse
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import UTC, datetime
from importlib.metadata import version as pkg_version
from pathlib import Path

from home_agent.auth.google_auth import GoogleAuthManager
from home_agent.auth.token_store import EncryptedTokenStore
from home_agent.contracts.protocols import LLMRunner, ProviderPlugin
from home_agent.contracts.types import ItemKind, MemoryCandidateStatus, RunResult, UrgencyLabel
from home_agent.core.config import (
    AppConfig,
    LoggingConfig,
    load_config,
    load_env_file,
)
from home_agent.core.logging import configure_logging, get_logger
from home_agent.llm.local_fallback import DeterministicFallbackRunner
from home_agent.llm.runner import ClaudeHeadlessRunner, CodexExecRunner
from home_agent.memory.engine import ScopedMemoryEngine
from home_agent.notifications.notifier import ToastNotifier
from home_agent.orchestrator.service import Orchestrator
from home_agent.plugins_builtin.gmail import GMAIL_SCOPES
from home_agent.plugins_builtin.google_calendar import CALENDAR_SCOPES
from home_agent.plugins_builtin.registry import load_plugins
from home_agent.retrieval import (
    DefaultItemTextRenderer,
    SentenceTransformerEmbeddingProvider,
    SqliteVecBackend,
    run_embeddings_backfill,
)
from home_agent.retrieval.retriever import VectorBackendRetriever
from home_agent.storage.sqlite_store import SqliteStore


@dataclass(frozen=True)
class ProviderDiagnostic:
    provider: str
    status: str
    detail: str


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(prog="home-agent")
    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {pkg_version('home-agent')}",
    )
    parser.add_argument("--config", type=Path, default=None)
    parser.add_argument("--env-file", type=Path, default=Path(".env"))

    sub = parser.add_subparsers(dest="command", required=True)
    run = sub.add_parser("run")
    run.add_argument("--llm-provider", choices=["claude", "codex", "none"], default=None)
    run.add_argument("--log-level", choices=["info", "debug"], default=None)
    run.add_argument("--debug", action="store_true")
    run.add_argument("--log-dir", type=Path, default=None)
    run.add_argument("--raw-shell-io", action="store_true")

    auth = sub.add_parser("auth")
    auth.add_argument("provider", choices=["google", "microsoft"])
    auth.add_argument("--init", action="store_true")
    auth.add_argument("--reset", action="store_true")

    test = sub.add_parser("test")
    test.add_argument("provider", choices=["google"])

    digest = sub.add_parser("digest")
    digest.add_argument("--date", default=datetime.now(tz=UTC).date().isoformat())

    sub.add_parser("doctor")

    bootstrap = sub.add_parser("bootstrap")
    bootstrap.add_argument("--schedule", default="PT15M")

    memory = sub.add_parser("memory")
    memory_sub = memory.add_subparsers(dest="memory_command", required=True)

    memory_list = memory_sub.add_parser("list-candidates")
    memory_list.add_argument("--status", choices=["pending", "approved", "rejected"], default=None)
    memory_list.add_argument("--min-confidence", type=float, default=None)

    memory_approve = memory_sub.add_parser("approve")
    memory_approve.add_argument("--rule-key", required=True)
    memory_approve.add_argument("--reason", default=None)

    memory_reject = memory_sub.add_parser("reject")
    memory_reject.add_argument("--rule-key", required=True)
    memory_reject.add_argument("--reason", default=None)

    embeddings = sub.add_parser("embeddings")
    embeddings_sub = embeddings.add_subparsers(dest="embeddings_command", required=True)
    backfill = embeddings_sub.add_parser("backfill")
    backfill.add_argument("--provider", default=None)
    backfill.add_argument("--model", default=None)
    backfill.add_argument("--batch-size", type=int, default=None)
    backfill.add_argument("--limit", type=int, default=None)
    backfill.add_argument("--since", default=None)
    backfill.add_argument("--kind", choices=["email", "calendar_event"], default=None)
    backfill.add_argument("--dry-run", action="store_true")
    backfill.add_argument("--rebuild", action="store_true")

    retrieval = sub.add_parser("retrieval")
    retrieval_sub = retrieval.add_subparsers(dest="retrieval_command", required=True)
    retrieval_sub.add_parser("doctor")
    retrieval_sub.add_parser("stats")
    retrieval_sub.add_parser("rebuild-index")

    return parser


def main() -> int:
    parser = _build_parser()
    args = parser.parse_args()

    load_env_file(args.env_file)
    config = load_config(args.config)
    configure_logging(_resolve_logging_config(config, args))
    logger = get_logger(__name__)
    logger.info("cli.command.start", command=args.command)

    if args.command == "doctor":
        result = _handle_doctor(config)
        logger.info("cli.command.end", command=args.command, exit_code=result)
        return result

    if args.command == "auth":
        result = _handle_auth(config, args.provider, args.init, args.reset)
        logger.info("cli.command.end", command=args.command, exit_code=result)
        return result

    if args.command == "test":
        result = _handle_test(config, args)
        logger.info("cli.command.end", command=args.command, exit_code=result)
        return result

    if args.command == "digest":
        print(f"daily digest stub for {args.date}")
        logger.info("cli.command.end", command=args.command, exit_code=0)
        return 0

    if args.command == "bootstrap":
        _write_bootstrap_script(Path("scripts/bootstrap-home-agent.ps1"), args.schedule)
        print("wrote scripts/bootstrap-home-agent.ps1")
        logger.info("cli.command.end", command=args.command, exit_code=0)
        return 0

    if args.command == "memory":
        result = _handle_memory(config, args)
        logger.info("cli.command.end", command=args.command, exit_code=result)
        return result

    if args.command == "embeddings":
        result = _handle_embeddings(config, args)
        logger.info("cli.command.end", command=args.command, exit_code=result)
        return result

    if args.command == "retrieval":
        result = _handle_retrieval(config, args)
        logger.info("cli.command.end", command=args.command, exit_code=result)
        return result

    if args.command == "run":
        store = SqliteStore(config.sqlite_path)
        plugins = load_plugins(enabled=set(config.enabled_providers))
        orchestrator = Orchestrator(
            config=config,
            plugins=plugins,
            store=store,
            llm_runner=_select_runner(config, args.llm_provider),
            notifier=ToastNotifier(Path(".data/alerts.log")),
            memory_engine=ScopedMemoryEngine(),
            embedding_provider=_build_embedding_provider(config),
            item_text_renderer=DefaultItemTextRenderer(),
            retriever=_build_retriever(config, store),
            vector_backend=_build_vector_backend(config, store),
        )
        run_result = orchestrator.run_once()
        _print_run_summary(run_result)
        logger.info("cli.command.end", command=args.command, exit_code=0)
        return 0

    parser.print_help()
    logger.info("cli.command.end", command="help", exit_code=1)
    return 1


def _resolve_logging_config(config: AppConfig, args: argparse.Namespace) -> LoggingConfig:
    log_level = config.logging.console_level
    capture_raw_payloads = config.logging.capture_raw_payloads
    if getattr(args, "log_level", None) is not None:
        log_level = str(args.log_level).upper()
    if getattr(args, "debug", False):
        log_level = "DEBUG"
        capture_raw_payloads = True
    if getattr(args, "raw_shell_io", False):
        capture_raw_payloads = True
    return LoggingConfig(
        directory=(
            args.log_dir if getattr(args, "log_dir", None) is not None else config.logging.directory
        ),
        file_name=config.logging.file_name,
        console_level=log_level,
        file_level=config.logging.file_level,
        enable_console=config.logging.enable_console,
        max_bytes=config.logging.max_bytes,
        backup_count=config.logging.backup_count,
        capture_raw_payloads=capture_raw_payloads,
        subprocess_preview_chars=config.logging.subprocess_preview_chars,
    )


def _handle_auth(config: AppConfig, provider: str, init: bool, reset: bool = False) -> int:
    if reset:
        return _reset_auth(provider)

    if not init:
        print(f"auth configured for provider={provider}; use --init to start consent flow")
        return 0

    plugins = load_plugins(enabled=set(config.enabled_providers))
    if provider == "google":
        return _handle_google_auth_init(plugins)

    for plugin in plugins:
        if provider == "microsoft" and plugin.id == "graph_mail":
            init_fn = getattr(plugin, "init_auth", None)
            if callable(init_fn):
                try:
                    init_fn()
                except RuntimeError as exc:
                    print(_format_auth_init_error(provider, exc))
                    return 1
                print("initialized auth for graph_mail")
                return 0

    print(f"provider not enabled or no auth initializer: {provider}")
    return 1


def _reset_auth(provider: str) -> int:
    token_name_by_provider = {
        "google": "google",
        "microsoft": "microsoft",
    }
    token_name = token_name_by_provider.get(provider)
    if token_name is None:
        print(f"provider not enabled or no auth initializer: {provider}")
        return 1

    EncryptedTokenStore(Path(".data/tokens")).delete(token_name)
    print(f"reset auth for {provider}")
    return 0


def _handle_google_auth_init(plugins: Sequence[ProviderPlugin]) -> int:
    scopes: list[str] = []
    plugin_ids = {getattr(plugin, "id", None) for plugin in plugins}
    if "gmail" in plugin_ids:
        scopes.extend(GMAIL_SCOPES)
    if "google_calendar" in plugin_ids:
        scopes.extend(CALENDAR_SCOPES)
    if not scopes:
        print("provider not enabled or no auth initializer: google")
        return 1

    try:
        GoogleAuthManager(
            EncryptedTokenStore(Path(".data/tokens")), token_name="google"
        ).init_desktop_auth(tuple(dict.fromkeys(scopes)))
    except RuntimeError as exc:
        print(_format_auth_init_error("google", exc))
        return 1

    print("initialized auth for google")
    return 0


def _format_auth_init_error(provider: str, exc: RuntimeError) -> str:
    message = str(exc)
    if provider == "google" and "GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET" in message:
        return (
            "google auth setup incomplete: missing GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET. "
            "Add them to .env or your shell environment, then rerun "
            "`home-agent auth google --init`."
        )
    if provider == "microsoft" and "MICROSOFT_CLIENT_ID" in message:
        return (
            "microsoft auth setup incomplete: missing MICROSOFT_CLIENT_ID. "
            "Set MICROSOFT_TENANT_ID as needed or rely on the default `consumers` tenant. "
            "Add them to .env or your shell environment, then rerun "
            "`home-agent auth microsoft --init`."
        )
    return f"{provider} auth initialization failed: {message}"


def _handle_doctor(config: AppConfig) -> int:
    diagnostics = _collect_provider_diagnostics(config)
    ok_count = sum(1 for diagnostic in diagnostics if diagnostic.status == "ok")
    error_count = len(diagnostics) - ok_count

    print("home-agent doctor")
    for diagnostic in diagnostics:
        print(f"- {diagnostic.provider}: {diagnostic.status} ({diagnostic.detail})")
    print(f"doctor completed: providers={len(diagnostics)} ok={ok_count} errors={error_count}")
    return 1 if error_count else 0


def _handle_test(config: AppConfig, args: argparse.Namespace) -> int:
    if args.provider == "google":
        return _handle_google_test(config)
    print(f"unsupported test provider: {args.provider}")
    return 1


def _handle_google_test(config: AppConfig) -> int:
    plugins = load_plugins(enabled=set(config.enabled_providers))
    errors: list[str] = []
    summaries: list[str] = []

    for plugin in plugins:
        if plugin.id == "gmail":
            try:
                mail_reader = plugin.get_mail_reader()
                items = list(mail_reader.list_recent()) if mail_reader is not None else []
                summaries.append(_format_test_items("gmail", items))
            except Exception as exc:
                errors.append(f"gmail: {exc}")
        if plugin.id == "google_calendar":
            try:
                calendar_reader = plugin.get_calendar_reader()
                items = list(calendar_reader.list_upcoming()) if calendar_reader is not None else []
                summaries.append(_format_test_items("google_calendar", items))
            except Exception as exc:
                errors.append(f"google_calendar: {exc}")

    for summary in summaries:
        print(summary)
    for error in errors:
        print(error)
    return 1 if errors else 0


def _format_test_items(provider: str, items: Sequence[object]) -> str:
    lines = [f"{provider}: {len(items)} item(s)"]
    for item in items[:5]:
        subject = getattr(item, "subject", "(no subject)")
        sender = getattr(item, "sender", "unknown")
        lines.append(f"- {subject} [{sender}]")
    return "\n".join(lines)


def _collect_provider_diagnostics(config: AppConfig) -> list[ProviderDiagnostic]:
    plugins = load_plugins(enabled=set(config.enabled_providers))
    return [_diagnose_plugin(plugin) for plugin in plugins]


def _diagnose_plugin(plugin: ProviderPlugin) -> ProviderDiagnostic:
    provider = plugin.id
    try:
        mail_reader = plugin.get_mail_reader()
        if mail_reader is not None:
            items = list(mail_reader.list_recent())
            return ProviderDiagnostic(provider, "ok", f"read {len(items)} recent email(s)")

        calendar_reader = plugin.get_calendar_reader()
        if calendar_reader is not None:
            items = list(calendar_reader.list_upcoming())
            return ProviderDiagnostic(provider, "ok", f"read {len(items)} upcoming event(s)")

        return ProviderDiagnostic(provider, "error", "no diagnostic reader available")
    except Exception as exc:
        return ProviderDiagnostic(provider, "error", _format_provider_error(exc))


def _format_provider_error(exc: Exception) -> str:
    message = " ".join(str(exc).strip().split())
    return message or exc.__class__.__name__


def _print_run_summary(result: RunResult) -> None:
    error_count = len(result.errors)
    print(
        f"run completed: processed={result.processed} alerted={result.alerted} errors={error_count}"
    )
    if result.errors:
        print("provider errors:")
        for error in result.errors:
            print(f"- {error}")


def _handle_memory(config: AppConfig, args: argparse.Namespace) -> int:
    store = SqliteStore(config.sqlite_path)
    if args.memory_command == "list-candidates":
        status = args.status
        typed_status: MemoryCandidateStatus | None = status
        candidates = store.list_memory_candidates(
            status=typed_status,
            min_confidence=args.min_confidence,
        )
        for candidate in candidates:
            print(
                f"{candidate.rule_key} status={candidate.status} evidence={candidate.evidence_count} confidence={candidate.confidence:.2f}"
            )
            if candidate.rejected_reason:
                print(f"  rejected_reason={candidate.rejected_reason}")
        return 0

    if args.memory_command == "approve":
        candidates = store.list_memory_candidates(status="pending")
        maybe_candidate = next((c for c in candidates if c.rule_key == args.rule_key), None)
        if maybe_candidate is None:
            print(f"pending candidate not found: {args.rule_key}")
            return 1
        candidate = maybe_candidate
        if candidate.evidence_count < config.memory.min_evidence:
            print(f"cannot approve: evidence {candidate.evidence_count} < {config.memory.min_evidence}")
            return 1
        if candidate.confidence < config.memory.min_confidence:
            print(f"cannot approve: confidence {candidate.confidence:.2f} < {config.memory.min_confidence:.2f}")
            return 1
        store.approve_memory_candidate(args.rule_key, reason=args.reason)
        print(f"approved {args.rule_key}")
        return 0

    if args.memory_command == "reject":
        store.reject_memory_candidate(args.rule_key, reason=args.reason)
        print(f"rejected {args.rule_key}")
        return 0

    return 1


def _select_runner(config: AppConfig, override: str | None) -> LLMRunner:
    selected = override or config.llm.primary
    if selected == "none":
        return DeterministicFallbackRunner()
    if selected == "codex":
        if override is None and not config.llm.codex_enabled_for_scheduler:
            return ClaudeHeadlessRunner(timeout_seconds=config.llm.timeout_seconds)
        return CodexExecRunner(timeout_seconds=config.llm.timeout_seconds)
    if selected == "claude":
        return ClaudeHeadlessRunner(timeout_seconds=config.llm.timeout_seconds)
    if selected == "claude-sdk":
        from home_agent.llm.sdk_runner import ClaudeSDKRunner

        return ClaudeSDKRunner(timeout_seconds=config.llm.timeout_seconds)
    return DeterministicFallbackRunner()


def _build_embedding_provider(
    config: AppConfig,
) -> SentenceTransformerEmbeddingProvider | None:
    if not config.embeddings.enabled:
        return None
    return SentenceTransformerEmbeddingProvider(model_name=config.embeddings.model)


def _build_vector_backend(config: AppConfig, store: SqliteStore) -> SqliteVecBackend | None:
    if not config.retrieval.enabled:
        return None
    return SqliteVecBackend(store=store, config=config.sqlite_vec)


def _build_retriever(config: AppConfig, store: SqliteStore) -> VectorBackendRetriever | None:
    provider = _build_embedding_provider(config)
    backend = _build_vector_backend(config, store)
    if provider is None or backend is None or not config.retrieval.enabled:
        return None
    return VectorBackendRetriever(
        embedding_provider=provider,
        item_text_renderer=DefaultItemTextRenderer(),
        vector_backend=backend,
        include_item_kinds=set(config.retrieval.include_item_kinds),
        prioritize_labels={UrgencyLabel(label) for label in config.retrieval.prioritize_labels},
    )


def _handle_embeddings(config: AppConfig, args: argparse.Namespace) -> int:
    if args.embeddings_command != "backfill":
        return 1

    store = SqliteStore(config.sqlite_path)
    model_name = str(args.model or config.embeddings.model)
    batch_size = int(args.batch_size or config.embeddings.batch_size)
    provider = SentenceTransformerEmbeddingProvider(model_name=model_name)
    backend = SqliteVecBackend(store=store, config=config.sqlite_vec)

    kind = ItemKind(args.kind) if args.kind is not None else None
    since = datetime.fromisoformat(args.since) if args.since is not None else None
    result = run_embeddings_backfill(
        store,
        provider,
        DefaultItemTextRenderer(),
        vector_backend=backend,
        batch_size=batch_size,
        limit=args.limit,
        since=since,
        kind=kind,
        dry_run=bool(args.dry_run),
        rebuild=bool(args.rebuild),
    )
    print(f"embeddings backfill: processed={result.processed} skipped={result.skipped} failed={result.failed}")
    return 0


def _handle_retrieval(config: AppConfig, args: argparse.Namespace) -> int:
    store = SqliteStore(config.sqlite_path)
    backend = _build_vector_backend(config, store)
    if backend is None:
        print("retrieval backend not configured")
        return 1
    if args.retrieval_command == "doctor":
        try:
            backend.healthcheck()
        except Exception as exc:
            print(f"retrieval doctor: error backend={backend.backend_name} detail={exc}")
            return 1
        print(f"retrieval doctor: ok backend={backend.backend_name}")
        return 0
    if args.retrieval_command == "stats":
        print(f"retrieval stats: backend={backend.backend_name} indexed={backend.indexed_count()}")
        return 0
    if args.retrieval_command == "rebuild-index":
        provider = _build_embedding_provider(config)
        if provider is None:
            print("embedding provider not configured")
            return 1
        result = run_embeddings_backfill(
            store,
            provider,
            DefaultItemTextRenderer(),
            vector_backend=backend,
            batch_size=config.embeddings.batch_size,
            limit=None,
            since=None,
            kind=None,
            dry_run=False,
            rebuild=True,
        )
        print(f"retrieval rebuild-index: backend={backend.backend_name} processed={result.processed} failed={result.failed}")
        return 0
    return 1


def _write_bootstrap_script(path: Path, schedule: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    content = f"""$Action = New-ScheduledTaskAction `
  -Execute "powershell.exe" `
  -Argument "-NoProfile -Command 'uv run home-agent run'"
$Trigger = New-ScheduledTaskTrigger `
  -Once -At (Get-Date) `
  -RepetitionInterval (New-TimeSpan -Minutes 15)
Register-ScheduledTask `
  -TaskName "home-agent" `
  -Action $Action `
  -Trigger $Trigger `
  -Description "home-agent poller" `
  -Force
# Requested schedule profile: {schedule}
"""
    path.write_text(content, encoding="utf-8")


if __name__ == "__main__":
    raise SystemExit(main())
