"""Plugin base classes and context."""

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import aiosqlite
    from fastapi import APIRouter

    from agntspace.activity.decisions import DecisionLogger
    from agntspace.activity.logger import ActivityLogger
    from agntspace.agent.registry import ToolRegistry
    from agntspace.infra.config import Settings
    from agntspace.llm.base import LLMProvider
    from agntspace.memory.conversation import ConversationMemory
    from agntspace.plugins.hooks import HookBus
    from agntspace.skills.loader import SkillLoader
    from agntspace.vault.reader import VaultReader
    from agntspace.vault.writer import VaultWriter


@dataclass
class PluginContext:
    """Everything a plugin needs. Passed to init()."""

    registry: ToolRegistry
    vault: VaultReader
    vault_writer: VaultWriter
    llm: LLMProvider
    settings: Settings
    db: aiosqlite.Connection
    skill_loader: SkillLoader
    plugin_data_dir: Path
    log: logging.Logger
    hooks: HookBus | None = None
    activity_logger: ActivityLogger | None = None
    decisions: DecisionLogger | None = None
    # Optional: lets plugins post follow-up messages into chat conversations
    # (e.g. fire-and-forget jobs reporting completion into the originating
    # chat thread). Agent-origin callers are expected to look up the active
    # conversation id via ``agent.agent.current_conversation_id()``.
    conversation_memory: ConversationMemory | None = None


@dataclass
class PluginMeta:
    """Parsed from plugin.yaml."""

    name: str
    version: str
    title: str
    description: str
    author: str = ""
    license: str = "MIT"
    provides: dict = field(default_factory=lambda: {"tools": [], "skills": [], "integrations": [], "routes": []})
    contract_actions: list[str] = field(default_factory=list)
    enabled: bool = True
    path: Path = field(default_factory=Path)
    error: str = ""
    # Optional declarative UI panel descriptor — see plugins/UI_PANEL_SCHEMA.md.
    # When present, the /plugins page renders a generic <PluginPanel> using
    # the spec instead of (just) showing the enable/disable toggle. Format
    # is opaque to the loader: validated at render time on the frontend so
    # plugin authors get fail-soft behavior on schema drift.
    ui: dict = field(default_factory=dict)

    def to_dict(self) -> dict:
        return {
            "name": self.name,
            "version": self.version,
            "title": self.title,
            "description": self.description,
            "author": self.author,
            "license": self.license,
            "provides": self.provides,
            "contract_actions": self.contract_actions,
            "enabled": self.enabled,
            "path": str(self.path),
            "error": self.error,
            "ui": self.ui,
        }


class PluginBase(ABC):
    """Interface every plugin.py must implement."""

    @abstractmethod
    def init(self, ctx: PluginContext) -> None:
        """Initialize the plugin. Register tools, hooks, routes, etc.

        Sync — use this for tool/hook registration and lightweight setup.
        For async setup (DB connections, API calls, cache warming), override
        ``async_init`` instead.
        """

    async def async_init(self, ctx: PluginContext) -> None:
        """Optional async initialization, called after init().

        Override this when your plugin needs to do async work during setup —
        open aiosqlite connections, call external APIs, warm caches, etc.
        The default implementation is a no-op.
        """

    def get_router(self) -> APIRouter | None:
        """Optional: return a FastAPI APIRouter for custom endpoints."""
        return None

    def shutdown(self) -> None:
        """Optional cleanup on server shutdown."""
