from __future__ import annotations

import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml


ENV_PATTERN = re.compile(r"\$\{([A-Z0-9_]+)(?::-([^}]+))?\}")


def _expand_env(value: str) -> str:
    def replace(match: re.Match[str]) -> str:
        key = match.group(1)
        default = match.group(2) or ""
        return os.environ.get(key, default)

    return ENV_PATTERN.sub(replace, value)


def _resolve_env_recursive(value: Any) -> Any:
    if isinstance(value, str):
        return _expand_env(value)
    if isinstance(value, list):
        return [_resolve_env_recursive(item) for item in value]
    if isinstance(value, dict):
        return {key: _resolve_env_recursive(item) for key, item in value.items()}
    return value


@dataclass(slots=True)
class ProviderConfig:
    name: str
    type: str
    api_key: str = ""
    base_url: str | None = None
    default_model: str | None = None
    timeout_sec: float = 90.0
    extra_headers: dict[str, str] = field(default_factory=dict)
    extra_params: dict[str, Any] = field(default_factory=dict)


@dataclass(slots=True)
class PairingConfig:
    enabled: bool = True
    code_ttl_sec: int = 900
    bootstrap_code: str = ""
    bootstrap_one_time: bool = True


@dataclass(slots=True)
class TelegramConfig:
    bot_token: str = ""
    owner_user_ids: list[int] = field(default_factory=list)
    owner_chat_ids: list[int] = field(default_factory=list)
    allowed_user_ids: list[int] = field(default_factory=list)
    allowed_chat_ids: list[int] = field(default_factory=list)
    parse_mode: str = "HTML"
    pairing: PairingConfig = field(default_factory=PairingConfig)


@dataclass(slots=True)
class ShellConfig:
    default_mode: str = "safe"
    timeout_sec: int = 45
    max_output_chars: int = 12000
    safe_allowlist: list[str] = field(
        default_factory=lambda: [
            "ls",
            "pwd",
            "cat",
            "echo",
            "whoami",
            "id",
            "uname",
            "df",
            "du",
            "ps",
            "top",
            "free",
            "ip",
            "ss",
            "ping",
            "python",
            "python3",
            "pip",
            "pip3",
            "git",
            "rg",
            "find",
            "sed",
            "awk",
            "grep",
            "curl",
            "wget",
        ]
    )
    safe_blocklist: list[str] = field(
        default_factory=lambda: ["rm -rf /", "shutdown", "reboot", "mkfs", ":(){:|:&};:"]
    )


@dataclass(slots=True)
class RuntimeConfig:
    default_provider: str = "openai"
    default_model: str = "gpt-4.1-mini"
    max_tool_rounds: int = 0
    history_limit: int = 0
    poll_interval_sec: float = 2.0
    web_search_engine: str = "duckduckgo"


@dataclass(slots=True)
class PathsConfig:
    root: Path
    data: Path
    logs: Path
    workspace: Path
    skills: Path
    sessions: Path
    config_file: Path


@dataclass(slots=True)
class AppConfig:
    name: str
    paths: PathsConfig
    telegram: TelegramConfig = field(default_factory=TelegramConfig)
    shell: ShellConfig = field(default_factory=ShellConfig)
    runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
    providers: dict[str, ProviderConfig] = field(default_factory=dict)
    default_provider_name: str = "openai"

    def default_model_for(self, provider_name: str | None = None) -> str | None:
        name = provider_name or self.default_provider_name
        provider = self.providers.get(name)
        if not provider:
            return self.runtime.default_model
        return provider.default_model or self.runtime.default_model


def default_config(root: str | Path) -> AppConfig:
    root_path = Path(root).resolve()
    paths = PathsConfig(
        root=root_path,
        data=root_path / "data",
        logs=root_path / "logs",
        workspace=root_path / "workspace",
        skills=root_path / "workspace" / "skills",
        sessions=root_path / "data" / "sessions",
        config_file=root_path / "config.yaml",
    )
    providers = {
        "openai": ProviderConfig(
            name="openai",
            type="openai",
            api_key=os.environ.get("OPENAI_API_KEY", ""),
            default_model="gpt-4.1-mini",
        ),
        "gemini": ProviderConfig(
            name="gemini",
            type="gemini",
            api_key=os.environ.get("GEMINI_API_KEY", ""),
            default_model="gemini-2.5-flash",
        ),
        "claude": ProviderConfig(
            name="claude",
            type="claude",
            api_key=os.environ.get("ANTHROPIC_API_KEY", ""),
            default_model="claude-3-7-sonnet-latest",
        ),
    }
    return AppConfig(
        name="openclaw-lite",
        paths=paths,
        providers=providers,
        default_provider_name="openai",
    )


def _provider_configs(raw: dict[str, Any]) -> tuple[str, dict[str, ProviderConfig]]:
    default_name = raw.get("default", "openai")
    items = raw.get("items", {})
    providers: dict[str, ProviderConfig] = {}
    for name, item in items.items():
        providers[name] = ProviderConfig(
            name=name,
            type=item.get("type", "openai_compatible"),
            api_key=item.get("api_key", ""),
            base_url=item.get("base_url"),
            default_model=item.get("default_model"),
            timeout_sec=float(item.get("timeout_sec", 90.0)),
            extra_headers=dict(item.get("extra_headers", {})),
            extra_params=dict(item.get("extra_params", {})),
        )
    return default_name, providers


def load_config(path: str | Path) -> AppConfig:
    path = Path(path).resolve()
    base = default_config(path.parent)
    if not path.exists():
        base.paths.config_file = path
        return base

    raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
    raw = _resolve_env_recursive(raw)
    paths = base.paths
    telegram_raw = raw.get("telegram", {})
    runtime_raw = raw.get("runtime", {})
    shell_raw = raw.get("shell", {})
    providers_default, providers = _provider_configs(raw.get("providers", {}))
    if not providers:
        providers = base.providers
        providers_default = base.default_provider_name

    config = AppConfig(
        name=raw.get("name", base.name),
        paths=PathsConfig(
            root=Path(raw.get("paths", {}).get("root", paths.root)).resolve(),
            data=Path(raw.get("paths", {}).get("data", paths.data)).resolve(),
            logs=Path(raw.get("paths", {}).get("logs", paths.logs)).resolve(),
            workspace=Path(raw.get("paths", {}).get("workspace", paths.workspace)).resolve(),
            skills=Path(raw.get("paths", {}).get("skills", paths.skills)).resolve(),
            sessions=Path(raw.get("paths", {}).get("sessions", paths.sessions)).resolve(),
            config_file=path,
        ),
        telegram=TelegramConfig(
            bot_token=telegram_raw.get("bot_token", ""),
            owner_user_ids=[int(item) for item in telegram_raw.get("owner_user_ids", [])],
            owner_chat_ids=[int(item) for item in telegram_raw.get("owner_chat_ids", [])],
            allowed_user_ids=[int(item) for item in telegram_raw.get("allowed_user_ids", [])],
            allowed_chat_ids=[int(item) for item in telegram_raw.get("allowed_chat_ids", [])],
            parse_mode=telegram_raw.get("parse_mode", "HTML"),
            pairing=PairingConfig(
                enabled=bool((telegram_raw.get("pairing", {}) or {}).get("enabled", True)),
                code_ttl_sec=int((telegram_raw.get("pairing", {}) or {}).get("code_ttl_sec", 900)),
                bootstrap_code=str((telegram_raw.get("pairing", {}) or {}).get("bootstrap_code", os.environ.get("LITECLAW_PAIRING_CODE", ""))),
                bootstrap_one_time=bool((telegram_raw.get("pairing", {}) or {}).get("bootstrap_one_time", True)),
            ),
        ),
        shell=ShellConfig(
            default_mode=shell_raw.get("default_mode", base.shell.default_mode),
            timeout_sec=int(shell_raw.get("timeout_sec", base.shell.timeout_sec)),
            max_output_chars=int(shell_raw.get("max_output_chars", base.shell.max_output_chars)),
            safe_allowlist=list(shell_raw.get("safe_allowlist", base.shell.safe_allowlist)),
            safe_blocklist=list(shell_raw.get("safe_blocklist", base.shell.safe_blocklist)),
        ),
        runtime=RuntimeConfig(
            default_provider=runtime_raw.get("default_provider", providers_default),
            default_model=runtime_raw.get("default_model", base.runtime.default_model),
            max_tool_rounds=int(runtime_raw.get("max_tool_rounds", base.runtime.max_tool_rounds)),
            history_limit=int(runtime_raw.get("history_limit", base.runtime.history_limit)),
            poll_interval_sec=float(runtime_raw.get("poll_interval_sec", base.runtime.poll_interval_sec)),
            web_search_engine=runtime_raw.get("web_search_engine", base.runtime.web_search_engine),
        ),
        providers=providers,
        default_provider_name=providers_default,
    )
    if config.runtime.default_provider not in config.providers and config.providers:
        config.runtime.default_provider = next(iter(config.providers))
    if config.runtime.default_provider:
        config.default_provider_name = config.runtime.default_provider
    return config


def ensure_directories(config: AppConfig) -> None:
    for path in [
        config.paths.data,
        config.paths.logs,
        config.paths.workspace,
        config.paths.skills,
        config.paths.sessions,
    ]:
        path.mkdir(parents=True, exist_ok=True)


def to_dict(config: AppConfig) -> dict[str, Any]:
    return {
        "name": config.name,
        "paths": {
            "root": str(config.paths.root),
            "data": str(config.paths.data),
            "logs": str(config.paths.logs),
            "workspace": str(config.paths.workspace),
            "skills": str(config.paths.skills),
            "sessions": str(config.paths.sessions),
        },
        "telegram": {
            "bot_token": config.telegram.bot_token,
            "owner_user_ids": config.telegram.owner_user_ids,
            "owner_chat_ids": config.telegram.owner_chat_ids,
            "allowed_user_ids": config.telegram.allowed_user_ids,
            "allowed_chat_ids": config.telegram.allowed_chat_ids,
            "parse_mode": config.telegram.parse_mode,
            "pairing": {
                "enabled": config.telegram.pairing.enabled,
                "code_ttl_sec": config.telegram.pairing.code_ttl_sec,
                "bootstrap_code": config.telegram.pairing.bootstrap_code,
                "bootstrap_one_time": config.telegram.pairing.bootstrap_one_time,
            },
        },
        "runtime": {
            "default_provider": config.runtime.default_provider,
            "default_model": config.runtime.default_model,
            "max_tool_rounds": config.runtime.max_tool_rounds,
            "history_limit": config.runtime.history_limit,
            "poll_interval_sec": config.runtime.poll_interval_sec,
            "web_search_engine": config.runtime.web_search_engine,
        },
        "shell": {
            "default_mode": config.shell.default_mode,
            "timeout_sec": config.shell.timeout_sec,
            "max_output_chars": config.shell.max_output_chars,
            "safe_allowlist": config.shell.safe_allowlist,
            "safe_blocklist": config.shell.safe_blocklist,
        },
        "providers": {
            "default": config.default_provider_name,
            "items": {
                name: {
                    "type": provider.type,
                    "api_key": provider.api_key,
                    "base_url": provider.base_url,
                    "default_model": provider.default_model,
                    "timeout_sec": provider.timeout_sec,
                    "extra_headers": provider.extra_headers,
                    "extra_params": provider.extra_params,
                }
                for name, provider in config.providers.items()
            },
        },
    }


def save_config(config: AppConfig, path: str | Path | None = None) -> Path:
    target = Path(path or config.paths.config_file).resolve()
    target.write_text(yaml.safe_dump(to_dict(config), allow_unicode=True, sort_keys=False), encoding="utf-8")
    return target

from copy import deepcopy
import shutil

from .util import deep_get as _compat_deep_get, deep_set as _compat_deep_set, ensure_dir, utcnow_iso


def _compat_default_flat_config(project_root: str | Path) -> dict[str, Any]:
    root = Path(project_root).resolve()
    return {
        "project_name": "openclaw-lite",
        "project_root": str(root),
        "workspace_dir": str(root / "workspace"),
        "data_dir": str(root / "data"),
        "logs_dir": str(root / "logs"),
        "backups_dir": str(root / "backups"),
        "history_limit": 0,
        "tool_loop_limit": 0,
        "system_prompt": (
            "You are OpenClaw Lite, a local AI platform inspired by OpenClaw. "
            "You can manage providers, create/reload skills, inspect and modify your own files, "
            "and work through Telegram with owner-only administration."
        ),
        "telegram": {
            "enabled": True,
            "bot_token": os.environ.get("TELEGRAM_BOT_TOKEN", ""),
            "owner_user_ids": [],
            "owner_chat_ids": [],
            "allowed_user_ids": [],
            "allowed_chat_ids": [],
            "parse_mode": "HTML",
            "pairing": {
                "enabled": True,
                "code_ttl_sec": 900,
                "bootstrap_code": os.environ.get("LITECLAW_PAIRING_CODE", ""),
                "bootstrap_one_time": True,
            },
        },
        "shell": {
            "default_mode": "safe",
            "timeout_seconds": 45,
            "max_output_chars": 0,
            "safe_allowed_commands": [
                "pwd",
                "ls",
                "echo",
                "cat",
                "head",
                "tail",
                "grep",
                "rg",
                "find",
                "python",
                "python3",
                "pip",
                "pip3",
                "which",
                "env",
                "uname",
                "id",
            ],
        },
        "providers": {
            "default": "openai",
            "items": {
                "openai": {
                    "type": "openai",
                    "enabled": True,
                    "api_key": os.environ.get("OPENAI_API_KEY", ""),
                    "base_url": "https://api.openai.com/v1",
                    "default_model": "gpt-4.1-mini",
                },
                "gemini": {
                    "type": "gemini",
                    "enabled": True,
                    "api_key": os.environ.get("GEMINI_API_KEY", ""),
                    "default_model": "gemini-2.5-flash",
                 },
             },
         },
     }


def _compat_merge_dicts(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
    merged = deepcopy(base)
    for key, value in overlay.items():
        if isinstance(value, dict) and isinstance(merged.get(key), dict):
            merged[key] = _compat_merge_dicts(merged[key], value)
        else:
            merged[key] = value
    return merged


def _compat_flat_from_app_config(config: AppConfig) -> dict[str, Any]:
    raw = to_dict(config)
    return {
        "project_name": raw.get("name", "openclaw-lite"),
        "project_root": raw["paths"]["root"],
        "workspace_dir": raw["paths"]["workspace"],
        "data_dir": raw["paths"]["data"],
        "logs_dir": raw["paths"]["logs"],
        "backups_dir": str(Path(raw["paths"]["root"]).resolve() / "backups"),
        "history_limit": raw.get("runtime", {}).get("history_limit", 0),
        "tool_loop_limit": raw.get("runtime", {}).get("max_tool_rounds", 0),
        "telegram": {
            "enabled": True,
            "bot_token": raw.get("telegram", {}).get("bot_token", ""),
            "owner_user_ids": raw.get("telegram", {}).get("owner_user_ids", []),
            "owner_chat_ids": raw.get("telegram", {}).get("owner_chat_ids", []),
            "allowed_user_ids": raw.get("telegram", {}).get("allowed_user_ids", []),
            "allowed_chat_ids": raw.get("telegram", {}).get("allowed_chat_ids", []),
            "parse_mode": raw.get("telegram", {}).get("parse_mode", "HTML"),
            "pairing": raw.get("telegram", {}).get("pairing", {"enabled": True, "code_ttl_sec": 900, "bootstrap_code": os.environ.get("LITECLAW_PAIRING_CODE", ""), "bootstrap_one_time": True}),
        },
        "shell": {
            "default_mode": raw.get("shell", {}).get("default_mode", "safe"),
            "timeout_seconds": raw.get("shell", {}).get("timeout_sec", 45),
            "max_output_chars": raw.get("shell", {}).get("max_output_chars", 0),
            "safe_allowed_commands": raw.get("shell", {}).get("safe_allowlist", []),
        },
        "providers": {
            "default": raw.get("providers", {}).get("default", "openai"),
            "items": {
                name: {
                    "type": provider.get("type", "openai_compatible"),
                    "enabled": True,
                    "api_key": provider.get("api_key", ""),
                    "base_url": provider.get("base_url"),
                    "default_model": provider.get("default_model"),
                    "headers": provider.get("extra_headers", {}),
                    "timeout": provider.get("timeout_sec", 60),
                }
                for name, provider in raw.get("providers", {}).get("items", {}).items()
            },
        },
    }


class ConfigManager:
    def __init__(self, path: str | Path):
        self.path = Path(path).expanduser().resolve()
        self.data = self._load()

    def _load(self) -> dict[str, Any]:
        if not self.path.exists():
            data = _compat_default_flat_config(self.path.parent)
            self.data = data
            self.save()
            self._ensure_dirs(data)
            return data
        raw = yaml.safe_load(self.path.read_text(encoding="utf-8")) or {}
        raw = _resolve_env_recursive(raw)
        if "project_root" in raw or "workspace_dir" in raw:
            data = _compat_merge_dicts(_compat_default_flat_config(raw.get("project_root") or self.path.parent), raw)
        else:
            data = _compat_flat_from_app_config(load_config(self.path))
        self._ensure_dirs(data)
        return data

    def reload(self) -> dict[str, Any]:
        self.data = self._load()
        return self.data

    def save(self) -> None:
        self._ensure_dirs(self.data)
        self.path.write_text(yaml.safe_dump(self.data, allow_unicode=True, sort_keys=False), encoding="utf-8")

    def backup(self, reason: str = "config") -> Path:
        backups_dir = ensure_dir(self.get("backups_dir", str(self.path.parent / "backups")))
        timestamp = utcnow_iso().replace(":", "-")
        target = backups_dir / f"{reason}-{timestamp}.yaml"
        if self.path.exists():
            shutil.copy2(self.path, target)
        return target

    def get(self, dotted_path: str, default: Any = None) -> Any:
        return _compat_deep_get(self.data, dotted_path, default)

    def set(self, dotted_path: str, value: Any, backup_reason: str = "config-set") -> None:
        self.backup(backup_reason)
        _compat_deep_set(self.data, dotted_path, value)
        self.save()

    def provider_items(self) -> dict[str, dict[str, Any]]:
        items = self.get("providers.items", {})
        return items if isinstance(items, dict) else {}

    def default_provider(self) -> str:
        return str(self.get("providers.default", "openai"))

    def default_model(self) -> str:
        provider = self.provider_items().get(self.default_provider(), {})
        return str(provider.get("default_model", ""))

    def is_owner(self, user_id: int | None = None, chat_id: int | None = None) -> bool:
        owner_ids = {int(item) for item in self.get("telegram.owner_user_ids", [])}
        owner_chat_ids = {int(item) for item in self.get("telegram.owner_chat_ids", [])}
        return (
            (user_id is not None and int(user_id) in owner_ids)
            or (chat_id is not None and int(chat_id) in owner_chat_ids)
        )

    def allowed_user_ids(self) -> set[int]:
        return {int(item) for item in self.get("telegram.allowed_user_ids", [])}

    def allowed_chat_ids(self) -> set[int]:
        return {int(item) for item in self.get("telegram.allowed_chat_ids", [])}

    def is_allowed(self, user_id: int | None = None, chat_id: int | None = None) -> bool:
        return self.is_owner(user_id=user_id, chat_id=chat_id) or (
            (user_id is not None and int(user_id) in self.allowed_user_ids())
            or (chat_id is not None and int(chat_id) in self.allowed_chat_ids())
        )

    def pairing_enabled(self) -> bool:
        return bool(self.get("telegram.pairing.enabled", True))

    def pairing_code_ttl_sec(self) -> int:
        return int(self.get("telegram.pairing.code_ttl_sec", 900))

    def _ensure_dirs(self, data: dict[str, Any]) -> None:
        for key in ["workspace_dir", "data_dir", "logs_dir", "backups_dir"]:
            value = data.get(key)
            if value:
                ensure_dir(value)
        ensure_dir(Path(data.get("workspace_dir", self.path.parent / "workspace")) / "skills")


def init_project_layout(config: ConfigManager) -> None:
    config._ensure_dirs(config.data)
