from __future__ import annotations

from pathlib import Path
from typing import Any, Callable

from ..auth import AuthManager
from ..config import ConfigManager, init_project_layout
from ..providers import build_provider_registry
from ..schemas import SessionState, TELEGRAM_ACTIVE_USER_ID_METADATA_KEY, ToolDefinition
from ..skills.manager import SkillManager
from ..tools.builtin import build_builtin_tools
from .agent import AgentLoop
from .session_store import SessionStore
from .shell import ShellExecutor
from .tool_registry import ToolRegistry


class OpenClawLiteRuntime:
    def __init__(self, config_path: str | Path = "config.yaml"):
        self.config_manager = ConfigManager(config_path)
        init_project_layout(self.config_manager)
        self.config = self.config_manager.data
        self.auth_manager = AuthManager(self.config)
        self.shell_executor = ShellExecutor(self.config)
        self.provider_registry = build_provider_registry(self.config)
        self.skill_manager = SkillManager(self.skill_config())
        self.skill_manager.load_all(context=self.skill_context())
        self.session_store = SessionStore(
            self.config.get("data_dir", "data"),
            default_provider=self.default_provider_name(),
            default_model=self.default_model_for_provider(self.default_provider_name()),
            history_limit=int(self.config.get("history_limit", 0)),
        )
        self.tool_registry = ToolRegistry(Path(self.config.get("logs_dir", "logs")) / "tool_audit.log")
        self._registered_dynamic_skill_tools: set[str] = set()
        self.external_tool_registrars: list[Callable[[ToolRegistry], None]] = []
        self.telegram_bot = None
        self.source_root = Path(__file__).resolve().parents[2]
        self.config["source_root"] = str(self.source_root)
        self._register_tools()
        self.agent_loop = AgentLoop(self)

    def skill_config(self) -> dict[str, Any]:
        workspace_root = Path(self.config.get("workspace_dir", "workspace")).resolve()
        skills_root = workspace_root / "skills"
        return {
            "skills_root": str(skills_root),
            "workspace": {
                "root": str(workspace_root),
                "skills_root": str(skills_root),
            },
        }

    def skill_context(self) -> dict[str, Any]:
        return {
            "runtime": self,
            "project_root": self.config.get("project_root"),
            "workspace_dir": self.config.get("workspace_dir"),
            "logs_dir": self.config.get("logs_dir"),
        }

    def add_tool_registrar(self, registrar: Callable[[ToolRegistry], None]) -> None:
        self.external_tool_registrars.append(registrar)
        registrar(self.tool_registry)

    def _register_tools(self) -> None:
        self.tool_registry = ToolRegistry(Path(self.config.get("logs_dir", "logs")) / "tool_audit.log")
        self.tool_registry.register_many(build_builtin_tools(self))
        self.sync_skill_tools()
        for registrar in self.external_tool_registrars:
            registrar(self.tool_registry)

    def sync_skill_tools(self) -> None:
        self._registered_dynamic_skill_tools = set()
        for item in self.skill_manager.get_tools():
            if not isinstance(item, dict):
                continue
            name = item.get("name")
            handler = item.get("handler")
            if not name or not callable(handler):
                continue
            definition = ToolDefinition(
                name=name,
                description=item.get("description", f"Skill tool: {name}"),
                parameters=item.get("parameters", {"type": "object", "properties": {}}),
                handler=handler,
                owner_only=bool(item.get("owner_only", True)),
                destructive=bool(item.get("destructive", False)),
            )
            self.tool_registry.register(definition)
            self._registered_dynamic_skill_tools.add(name)

    def reload(self) -> None:
        self.config = self.config_manager.reload()
        init_project_layout(self.config_manager)
        self.auth_manager = AuthManager(self.config)
        self.shell_executor = ShellExecutor(self.config)
        self.provider_registry = build_provider_registry(self.config)
        self.skill_manager = SkillManager(self.skill_config())
        self.skill_manager.load_all(context=self.skill_context())
        self.session_store.default_provider = self.default_provider_name()
        self.session_store.default_model = self.default_model_for_provider(self.default_provider_name())
        self._register_tools()

    def reload_skills(self) -> dict[str, Any]:
        summary = self.skill_manager.reload_if_needed(context=self.skill_context())
        self._register_tools()
        return {
            "loaded": summary.loaded,
            "reloaded": summary.reloaded,
            "removed": summary.removed,
            "failed": summary.failed,
        }

    def is_owner(self, user_id: int | None, chat_id: int | str | None = None) -> bool:
        resolved_chat_id = int(chat_id) if chat_id is not None and str(chat_id).lstrip("-").isdigit() else None
        return self.auth_manager.is_owner(user_id, resolved_chat_id)

    def is_authorized(self, user_id: int | None, chat_id: int | str | None = None) -> bool:
        resolved_chat_id = int(chat_id) if chat_id is not None and str(chat_id).lstrip("-").isdigit() else None
        return self.auth_manager.is_allowed(user_id, resolved_chat_id)

    def generate_pairing_code(self, *, created_by: int | None = None, note: str | None = None) -> dict[str, Any]:
        return self.auth_manager.generate_pairing_code(created_by=created_by, note=note)

    def claim_pairing_code(self, code: str, *, user_id: int | None, chat_id: int | str | None) -> dict[str, Any]:
        resolved_chat_id = int(chat_id) if chat_id is not None and str(chat_id).lstrip("-").isdigit() else None
        return self.auth_manager.claim_pairing_code(code, user_id=user_id, chat_id=resolved_chat_id)

    def auth_summary(self) -> dict[str, Any]:
        summary = self.auth_manager.summary()
        allowed_users = summary.get("allowed_user_ids", [])
        allowed_chats = summary.get("allowed_chat_ids", [])
        pairing_codes = summary.get("pairing_codes", summary.get("pending_codes", []))
        return {
            "owner_user_ids": summary.get("owner_user_ids", []),
            "owner_chat_ids": summary.get("owner_chat_ids", []),
            "allowed_user_ids": allowed_users,
            "allowed_chat_ids": allowed_chats,
            "approved_users": allowed_users,
            "approved_chats": allowed_chats,
            "pending_codes": pairing_codes,
            "pairing_codes": pairing_codes,
            "grants": summary.get("grants", []),
            "state_path": summary.get("state_path"),
            "bootstrap_code_configured": summary.get("bootstrap_code_configured", False),
        }

    def create_pairing_code(
        self,
        *,
        created_by_user_id: int | None = None,
        created_by: int | None = None,
        ttl_sec: int | None = None,
        ttl_seconds: int | None = None,
        note: str | None = None,
        max_uses: int | None = None,
    ) -> dict[str, Any]:
        creator = created_by_user_id if created_by_user_id is not None else created_by
        ttl = ttl_sec if ttl_sec is not None else ttl_seconds
        create_method = getattr(self.auth_manager, "create_pairing_code", None)
        if callable(create_method):
            result = create_method(created_by=creator, expires_in=ttl, note=note or "", max_uses=max_uses or 1)
            if hasattr(result, "__dict__"):
                return dict(result.__dict__)
        return self.auth_manager.generate_pairing_code(created_by=creator, ttl_seconds=ttl, note=note)

    def redeem_pairing_code(self, code: str, *, user_id: int | None, chat_id: int | str | None) -> dict[str, Any]:
        result = self.claim_pairing_code(code, user_id=user_id, chat_id=chat_id)
        return {"authorized": True, **result}

    def user_is_admin(self, *, user_id: int | None, chat_id: int | str | None = None) -> bool:
        return self.is_authorized(user_id, chat_id)

    def user_has_access(self, *, user_id: int | None, chat_id: int | str | None = None) -> bool:
        return self.is_authorized(user_id, chat_id)

    def auth_is_owner(self, *, user_id: int | None = None, chat_id: int | str | None = None) -> bool:
        return self.is_owner(user_id, chat_id)

    def auth_is_allowed(self, *, user_id: int | None = None, chat_id: int | str | None = None) -> bool:
        return self.is_authorized(user_id, chat_id)

    def pair_telegram_access(self, code: str, *, user_id: int | None, chat_id: int | str | None, username: str | None = None, chat_title: str | None = None) -> dict[str, Any]:
        payload = self.claim_pairing_code(code, user_id=user_id, chat_id=chat_id)
        if username is not None:
            payload["username"] = username
        if chat_title is not None:
            payload["chat_title"] = chat_title
        return payload

    def approve_telegram_access(self, *, user_id: int | None = None, chat_id: int | str | None = None, approved_by: int | None = None, username: str | None = None, chat_title: str | None = None) -> dict[str, Any]:
        resolved_chat_id = int(chat_id) if chat_id is not None and str(chat_id).lstrip("-").isdigit() else None
        return self.auth_manager.approve_identity(
            user_id=user_id,
            chat_id=resolved_chat_id,
            via="telegram_owner_approve",
            paired_by=approved_by,
            username=username,
            chat_title=chat_title,
        )

    def authorize_user(self, user_id: int, *, chat_id: int | None = None, source: str = "manual") -> dict[str, Any]:
        return self.auth_manager.approve_identity(user_id=user_id, chat_id=chat_id, via=source)

    def revoke_user(self, user_id: int) -> dict[str, Any]:
        return self.auth_manager.revoke(user_id=user_id)

    def project_agents_path(self) -> Path:
        return Path(str(self.config.get("project_root", "."))).expanduser().resolve() / "AGENTS.md"

    def read_project_agents_md(self) -> str:
        path = self.project_agents_path()
        if not path.exists():
            return ""
        return path.read_text(encoding="utf-8")

    def write_project_agents_md(self, content: str) -> dict[str, Any]:
        path = self.project_agents_path()
        path.write_text(content, encoding="utf-8")
        return {"path": str(path), "bytes": path.stat().st_size}

    def append_project_agents_md(self, content: str) -> dict[str, Any]:
        path = self.project_agents_path()
        existing = path.read_text(encoding="utf-8") if path.exists() else ""
        updated = (existing.rstrip() + "\n\n" + content.strip() + "\n").lstrip()
        path.write_text(updated, encoding="utf-8")
        return {"path": str(path), "bytes": path.stat().st_size}

    def root_agents_path(self) -> Path:
        return self.project_agents_path()

    def read_root_agents_md(self) -> str:
        return self.read_project_agents_md()

    def write_root_agents_md(self, content: str) -> dict[str, Any]:
        return self.write_project_agents_md(content)

    def append_root_agents_md(self, content: str) -> dict[str, Any]:
        return self.append_project_agents_md(content)

    def list_access_payload(self) -> dict[str, Any]:
        summary = self.auth_summary()
        return {
            "owner_user_ids": summary.get("owner_user_ids", []),
            "owner_chat_ids": summary.get("owner_chat_ids", []),
            "authorized_user_ids": summary.get("approved_users", []),
            "authorized_chat_ids": summary.get("approved_chats", []),
            "pending_codes": summary.get("pending_codes", []),
            "grants": summary.get("grants", []),
            "bootstrap_code_configured": summary.get("bootstrap_code_configured", False),
            "state_path": summary.get("state_path"),
        }

    def default_provider_name(self) -> str:
        return str(self.config.get("providers", {}).get("default", self.config_manager.default_provider()))

    def default_model_for_provider(self, provider_name: str) -> str:
        provider = (self.config.get("providers", {}).get("items", {}) or {}).get(provider_name, {})
        return str(provider.get("default_model") or self.config_manager.default_model())

    def provider_catalog(self) -> dict[str, dict[str, Any]]:
        items = (self.config.get("providers", {}) or {}).get("items", {}) or {}
        return {str(name): dict(value) for name, value in items.items() if isinstance(value, dict)}

    def provider_catalog_names(self) -> list[str]:
        return sorted(self.provider_catalog())

    def provider_summary_payload(self) -> list[dict[str, Any]]:
        default_name = str((self.config.get("providers", {}) or {}).get("default", self.default_provider_name()))
        payload = []
        for name, item in self.provider_catalog().items():
            payload.append(
                {
                    "name": name,
                    "type": item.get("type"),
                    "enabled": item.get("enabled", True),
                    "has_api_key": bool(item.get("api_key")),
                    "default_model": item.get("default_model"),
                    "is_default": name == default_name,
                }
            )
        return payload

    def configure_provider(
        self,
        provider_name: str,
        *,
        api_key: str | None = None,
        default_model: str | None = None,
        enabled: bool | None = None,
        make_default: bool | None = None,
    ) -> dict[str, Any]:
        items = self.provider_catalog()
        entry = dict(items.get(provider_name, {}))
        if not entry:
            entry = {"type": provider_name if provider_name in {"gemini", "claude", "anthropic", "openai"} else "openai_compatible"}
        if api_key is not None:
            entry["api_key"] = api_key
        if default_model is not None:
            entry["default_model"] = default_model
        if enabled is not None:
            entry["enabled"] = enabled
        elif api_key is not None:
            entry["enabled"] = True
        self.config_manager.set(f"providers.items.{provider_name}", entry, backup_reason="provider-config")
        if make_default:
            self.config_manager.set("providers.default", provider_name, backup_reason="provider-default")
        self.reload()
        return {"provider": provider_name, "config": self.provider_catalog().get(provider_name, {})}

    def set_provider_api_key(self, provider_name: str, api_key: str) -> dict[str, Any]:
        return self.configure_provider(provider_name, api_key=api_key, enabled=True)

    def set_provider_default_model(self, provider_name: str, model: str) -> dict[str, Any]:
        return self.configure_provider(provider_name, default_model=model)

    def set_default_provider_config(self, provider_name: str) -> dict[str, Any]:
        self.config_manager.set("providers.default", provider_name, backup_reason="provider-default")
        self.reload()
        return {"default_provider": provider_name}

    def list_provider_names(self) -> list[str]:
        return self.provider_registry.list_provider_names()

    async def list_models(self, provider_name: str | None = None) -> dict[str, Any] | list[str]:
        if provider_name:
            try:
                return await self.provider_registry.get(provider_name).list_models()
            except Exception:
                entry = self.provider_catalog().get(provider_name, {})
                fallback = []
                if entry.get("default_model"):
                    fallback.append(entry.get("default_model"))
                if isinstance(entry.get("models"), list):
                    fallback.extend(str(item) for item in entry.get("models") if item)
                return sorted(dict.fromkeys(fallback))
        payload = {}
        for name in self.provider_catalog_names():
            payload[name] = await self.list_models(name)
        return payload

    def get_session(self, *, session_id: str, chat_id: str, user_id: int | None) -> SessionState:
        session = self.session_store.get_or_create(session_id, chat_id=chat_id, user_id=user_id)
        if not session.provider:
            session.provider = self.default_provider_name()
        if not session.model:
            session.model = self.default_model_for_provider(session.provider)
        self.session_store.save_state(session)
        return session

    def serialise_skill_status(self, status: Any) -> dict[str, Any]:
        if isinstance(status, dict):
            return status
        return {
            "name": getattr(status, "name", str(status)),
            "enabled": getattr(status, "enabled", False),
            "fingerprint": getattr(status, "fingerprint", None),
            "error": getattr(status, "error", None),
        }

    def list_skills_payload(self) -> list[dict[str, Any]]:
        return [self.serialise_skill_status(item) for item in self.skill_manager.list_skills()]

    def get_session_status(self, session: SessionState) -> dict[str, Any]:
        return {
            "session_id": session.session_id,
            "chat_id": session.chat_id,
            "provider": session.provider,
            "model": session.model,
            "security_mode": session.security_mode,
            "history_messages": len(session.history),
            "closed": session.closed,
            "skills": self.list_skills_payload(),
            "project_root": self.config.get("project_root"),
            "workspace_dir": self.config.get("workspace_dir"),
        }

    def set_security_mode(self, session: SessionState, mode: str) -> dict[str, Any]:
        if mode not in {"safe", "yolo", "god"}:
            raise ValueError(f"Unsupported security mode: {mode}")
        session.security_mode = mode  # type: ignore[assignment]
        self.session_store.save_state(session)
        return {"security_mode": session.security_mode, "unrestricted": session.security_mode == "god"}

    def set_provider(self, session: SessionState, provider_name: str) -> dict[str, Any]:
        if provider_name not in self.list_provider_names():
            raise ValueError(f"Unknown provider: {provider_name}")
        session.provider = provider_name
        session.model = self.default_model_for_provider(provider_name)
        self.session_store.save_state(session)
        return {"provider": session.provider, "model": session.model}

    def set_model(self, session: SessionState, model: str) -> dict[str, Any]:
        session.model = model
        self.session_store.save_state(session)
        return {"provider": session.provider, "model": session.model}

    def max_agent_steps(self) -> int | None:
        raw = self.config.get("tool_loop_limit", 0)
        if raw in (None, 0, "0", False):
            return None
        return int(raw)

    def list_sessions_payload(self) -> list[dict[str, Any]]:
        return [self.get_session_status(session) for session in self.session_store.list_sessions()]

    def end_session(self, session: SessionState) -> dict[str, Any]:
        session.closed = True
        session.metadata.pop(TELEGRAM_ACTIVE_USER_ID_METADATA_KEY, None)
        self.session_store.save_state(session)
        return {"session_id": session.session_id, "closed": True}

    def reopen_session(self, session: SessionState) -> dict[str, Any]:
        session.closed = False
        self.session_store.save_state(session)
        return {"session_id": session.session_id, "closed": False}

    def reset_session(self, session_id: str) -> dict[str, Any]:
        self.session_store.reset(session_id)
        return {"session_id": session_id, "reset": True}

    async def handle_user_message(
        self,
        *,
        session_id: str,
        chat_id: str,
        user_id: int | None,
        text: str,
        transport: str = "local",
        transport_state: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        return await self.agent_loop.run_turn(
            session_id=session_id,
            chat_id=chat_id,
            user_id=user_id,
            text=text,
            transport=transport,
            transport_state=transport_state,
        )

    async def run(self) -> None:
        if not self.config.get("telegram", {}).get("enabled", True):
            raise RuntimeError("Telegram is disabled in config")
        from ..interfaces.telegram_bot import TelegramBotInterface

        interface = TelegramBotInterface(self)
        await interface.run()

    def doctor(self) -> dict[str, Any]:
        providers = {}
        for name, raw in (self.config.get("providers", {}).get("items", {}) or {}).items():
            providers[name] = {
                "type": raw.get("type"),
                "enabled": raw.get("enabled", True),
                "has_api_key": bool(raw.get("api_key")),
                "default_model": raw.get("default_model"),
            }
        return {
            "project_root": self.config.get("project_root"),
            "workspace_dir": self.config.get("workspace_dir"),
            "skills_root": self.skill_config()["skills_root"],
            "telegram_enabled": self.config.get("telegram", {}).get("enabled", True),
            "telegram_has_token": bool(self.config.get("telegram", {}).get("bot_token")),
            "provider_names": self.list_provider_names(),
            "providers": providers,
            "skills": self.list_skills_payload(),
            "auth": self.auth_summary(),
            "access": self.list_access_payload(),
            "tool_loop_limit": self.max_agent_steps(),
            "history_limit": self.session_store.history_limit,
            "shell_timeout_seconds": self.shell_executor.timeout_seconds,
            "shell_output_limit": self.shell_executor.max_output_chars,
            "liteclaw_command": "liteclaw",
        }
