"""Provider registry and orchestration for tasks + notes integrations."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from sqlalchemy.orm import Session

from app.services import runtime_config
from app.services.integrations.errors import ProviderDisabledError, ProviderNotConfiguredError
from app.services.integrations.providers import (
    GitHubIssuesProvider,
    GoogleTasksProvider,
    MicrosoftTodoProvider,
    NotionNotesProvider,
    TodoistTaskProvider,
    TrelloTaskProvider,
    VaultLocalNotesProvider,
)
from app.services.integrations.types import NotesHit, ProviderConnectionResult, UnifiedTask


@dataclass(frozen=True, slots=True)
class ProviderMeta:
    """Static metadata used by UI and route validation."""

    provider_id: str
    label: str
    kind: str  # tasks | notes | both
    auth_type: str  # token | oauth | local
    supports_disconnect: bool = True


PROVIDER_META: dict[str, ProviderMeta] = {
    "todoist": ProviderMeta("todoist", "Todoist", "tasks", "token"),
    "google_tasks": ProviderMeta("google_tasks", "Google Tasks", "tasks", "oauth"),
    "microsoft_todo": ProviderMeta("microsoft_todo", "Microsoft To Do", "tasks", "oauth"),
    "trello": ProviderMeta("trello", "Trello", "tasks", "token"),
    "github_issues": ProviderMeta("github_issues", "GitHub Issues", "both", "token"),
    "notion": ProviderMeta("notion", "Notion", "notes", "token"),
    "vault_local": ProviderMeta("vault_local", "Vault (Local)", "notes", "local", supports_disconnect=False),
}

TASK_PROVIDER_IDS = tuple(provider_id for provider_id, meta in PROVIDER_META.items() if meta.kind in {"tasks", "both"})
NOTES_PROVIDER_IDS = tuple(provider_id for provider_id, meta in PROVIDER_META.items() if meta.kind in {"notes", "both"})


def _safe_dict(value: Any) -> dict[str, Any]:
    if isinstance(value, dict):
        return value
    return {}


class ProviderRegistry:
    """Build and query provider adapters from runtime config."""

    def __init__(self):
        self._config = runtime_config.get_runtime_config()
        self._providers_map = runtime_config.get_providers_snapshot()

    def provider_meta(self, provider_id: str) -> ProviderMeta:
        meta = PROVIDER_META.get(provider_id)
        if meta is None:
            raise LookupError(f"Unknown provider: {provider_id}")
        return meta

    def all_provider_meta(self) -> list[ProviderMeta]:
        return [PROVIDER_META[item] for item in PROVIDER_META]

    def provider_settings(self, provider_id: str) -> dict[str, Any]:
        if provider_id not in PROVIDER_META:
            raise LookupError(f"Unknown provider: {provider_id}")
        return _safe_dict(self._providers_map.get(provider_id))

    def _credentials(self, provider_id: str) -> dict[str, str]:
        settings = self.provider_settings(provider_id)
        raw = settings.get("credentials")
        if not isinstance(raw, dict):
            return {}
        result: dict[str, str] = {}
        for key, value in raw.items():
            key_text = str(key or "").strip()
            value_text = str(value or "").strip()
            if key_text and value_text:
                result[key_text] = value_text
        return result

    def _provider_options(self, provider_id: str) -> dict[str, Any]:
        settings = self.provider_settings(provider_id)
        raw = settings.get("settings")
        if not isinstance(raw, dict):
            return {}
        return {str(key): value for key, value in raw.items() if str(key).strip()}

    def is_enabled(self, provider_id: str) -> bool:
        return bool(self.provider_settings(provider_id).get("enabled"))

    def is_configured(self, provider_id: str) -> bool:
        creds = self._credentials(provider_id)
        options = self._provider_options(provider_id)
        if provider_id == "todoist":
            return bool(creds.get("token") or (self._config.todoist_api_token if self._config else ""))
        if provider_id == "google_tasks":
            return bool(creds.get("access_token") or creds.get("refresh_token"))
        if provider_id == "microsoft_todo":
            return bool(creds.get("access_token") or creds.get("refresh_token"))
        if provider_id == "trello":
            return bool(creds.get("api_key") and creds.get("token") and (options.get("board_id") or options.get("list_id")))
        if provider_id == "github_issues":
            return bool(creds.get("token") and options.get("repo"))
        if provider_id == "notion":
            return bool(creds.get("token") and (options.get("page_id") or options.get("database_id")))
        if provider_id == "vault_local":
            paths = options.get("paths")
            if isinstance(paths, list):
                return bool(paths)
            return bool(self._config and self._config.vault_paths)
        return False

    def _assert_enabled(self, provider_id: str) -> None:
        if not self.is_enabled(provider_id):
            raise ProviderDisabledError(f"Provider {provider_id} is disabled")
        if not self.is_configured(provider_id):
            raise ProviderNotConfiguredError(f"Provider {provider_id} is not configured")

    def default_task_provider(self) -> str:
        value = runtime_config.get_default_task_provider("todoist")
        return value if value in TASK_PROVIDER_IDS else "todoist"

    def default_notes_provider(self) -> str:
        value = runtime_config.get_default_notes_provider("vault_local")
        return value if value in NOTES_PROVIDER_IDS else "vault_local"

    def enabled_task_provider_ids(self) -> list[str]:
        return [provider_id for provider_id in TASK_PROVIDER_IDS if self.is_enabled(provider_id) and self.is_configured(provider_id)]

    def enabled_notes_provider_ids(self) -> list[str]:
        return [provider_id for provider_id in NOTES_PROVIDER_IDS if self.is_enabled(provider_id) and self.is_configured(provider_id)]

    def build_task_provider(self, provider_id: str):
        self._assert_enabled(provider_id)
        creds = self._credentials(provider_id)
        options = self._provider_options(provider_id)
        if provider_id == "todoist":
            return TodoistTaskProvider()
        if provider_id == "google_tasks":
            return GoogleTasksProvider(
                access_token=creds.get("access_token", ""),
                refresh_token=creds.get("refresh_token"),
                task_list_id=str(options.get("task_list_id", "")).strip() or None,
            )
        if provider_id == "microsoft_todo":
            return MicrosoftTodoProvider(
                access_token=creds.get("access_token", ""),
                refresh_token=creds.get("refresh_token"),
                list_id=str(options.get("list_id", "")).strip() or None,
            )
        if provider_id == "trello":
            return TrelloTaskProvider(
                api_key=creds.get("api_key", ""),
                token=creds.get("token", ""),
                board_id=str(options.get("board_id", "")).strip() or None,
                list_id=str(options.get("list_id", "")).strip() or None,
                done_list_id=str(options.get("done_list_id", "")).strip() or None,
            )
        if provider_id == "github_issues":
            return GitHubIssuesProvider(
                token=creds.get("token", ""),
                repo=str(options.get("repo", "")).strip(),
            )
        raise LookupError(f"Unknown task provider: {provider_id}")

    def build_notes_provider(self, provider_id: str):
        self._assert_enabled(provider_id)
        creds = self._credentials(provider_id)
        options = self._provider_options(provider_id)
        if provider_id == "vault_local":
            paths = options.get("paths")
            if not isinstance(paths, list):
                paths = self._config.vault_paths if self._config else []
            return VaultLocalNotesProvider(paths=[str(item) for item in paths])
        if provider_id == "notion":
            return NotionNotesProvider(
                token=creds.get("token", ""),
                page_id=str(options.get("page_id", "")).strip() or None,
                database_id=str(options.get("database_id", "")).strip() or None,
            )
        if provider_id == "github_issues":
            return GitHubIssuesProvider(
                token=creds.get("token", ""),
                repo=str(options.get("repo", "")).strip(),
            )
        raise LookupError(f"Unknown notes provider: {provider_id}")

    def provider_statuses(self) -> dict[str, dict[str, Any]]:
        statuses: dict[str, dict[str, Any]] = {}
        for provider_id, meta in PROVIDER_META.items():
            settings = self.provider_settings(provider_id)
            configured = self.is_configured(provider_id)
            enabled = bool(settings.get("enabled"))
            raw_last_error = settings.get("last_error")
            last_error = raw_last_error.strip() if isinstance(raw_last_error, str) and raw_last_error.strip() else None
            if last_error:
                state = "Error"
            elif not configured:
                state = "Not configured"
            elif enabled:
                state = "Enabled"
            else:
                state = "Disabled"

            statuses[provider_id] = {
                "provider": provider_id,
                "label": meta.label,
                "kind": meta.kind,
                "auth_type": meta.auth_type,
                "configured": configured,
                "enabled": enabled,
                "state": state,
                "last_error": last_error,
            }
        return statuses

    def list_tasks(
        self,
        *,
        provider_id: str | None = None,
        across_all: bool = False,
        include_completed: bool = False,
    ) -> list[UnifiedTask]:
        selected: list[str]
        if provider_id:
            selected = [provider_id]
        elif across_all:
            selected = self.enabled_task_provider_ids()
        else:
            default_provider = self.default_task_provider()
            selected = [default_provider] if default_provider in self.enabled_task_provider_ids() else self.enabled_task_provider_ids()

        tasks: list[UnifiedTask] = []
        for item in selected:
            provider = self.build_task_provider(item)
            tasks.extend(provider.list_tasks(include_completed=include_completed))
        return tasks

    def create_task(
        self,
        *,
        content: str,
        description: str | None = None,
        due_string: str | None = None,
        provider_id: str | None = None,
    ) -> UnifiedTask:
        target_provider_id = provider_id or self.default_task_provider()
        provider = self.build_task_provider(target_provider_id)
        return provider.create_task(content=content, description=description, due_string=due_string)

    def complete_task(self, *, task_id: str, provider_id: str | None = None) -> None:
        target_provider_id = provider_id or self.default_task_provider()
        provider = self.build_task_provider(target_provider_id)
        provider.complete_task(task_id=task_id)

    def update_due(self, *, task_id: str, due_string: str, provider_id: str | None = None) -> UnifiedTask | None:
        target_provider_id = provider_id or self.default_task_provider()
        provider = self.build_task_provider(target_provider_id)
        return provider.update_due(task_id=task_id, due_string=due_string)

    def search_notes(
        self,
        *,
        query: str,
        provider_ids: list[str] | None = None,
        limit: int = 5,
        db: Session | None = None,
    ) -> list[NotesHit]:
        targets: list[str]
        if provider_ids:
            targets = provider_ids
        else:
            default_provider = self.default_notes_provider()
            targets = [default_provider] if default_provider in self.enabled_notes_provider_ids() else self.enabled_notes_provider_ids()

        results: list[NotesHit] = []
        for provider_id in targets:
            provider = self.build_notes_provider(provider_id)
            results.extend(provider.search(query=query, limit=limit, db=db))
        return results

    def append_note_to_targets(
        self,
        *,
        title: str,
        content: str,
        provider_ids: list[str],
        db: Session | None = None,
    ) -> dict[str, str]:
        target_ids: dict[str, str] = {}
        for provider_id in provider_ids:
            provider = self.build_notes_provider(provider_id)
            created_id = provider.create_note(title=title, content=content, db=db)
            target_ids[provider_id] = created_id
        return target_ids

    def test_provider(self, provider_id: str, *, db: Session | None = None) -> ProviderConnectionResult:
        meta = self.provider_meta(provider_id)
        if meta.kind in {"tasks", "both"}:
            provider = self.build_task_provider(provider_id)
            return provider.test_connection()
        provider = self.build_notes_provider(provider_id)
        return provider.test_connection(db=db)
