from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Mapping, Sequence, TypeVar, cast

import asyncio
import copy
import inspect

import tomlkit

from .config_base import AttributeData, ConfigBase, Field
from .config_utils import compare_versions, output_config_changes, recursive_parse_item_to_table
from .default_model_config import create_default_model_config
from .file_watcher import FileChange, FileWatcher
from .legacy_migration import migrate_legacy_bind_env_to_bot_config_dict, try_migrate_legacy_bot_config_dict
from .model_configs import APIProvider, ModelInfo, ModelTaskConfig
from .official_configs import (
    AMemorixConfig,
    BotConfig,
    ChatConfig,
    ChineseTypoConfig,
    DatabaseConfig,
    DebugConfig,
    EmojiConfig,
    ExpressionConfig,
    KeywordReactionConfig,
    LogConfig,
    MaimMessageConfig,
    MCPConfig,
    MemoryConfig,
    MessageReceiveConfig,
    PersonalityConfig,
    PluginRuntimeConfig,
    ResponsePostProcessConfig,
    ResponseSplitterConfig,
    TelemetryConfig,
    VisualConfig,
    VoiceConfig,
    WebUIConfig,
)
from src.common.i18n import t
from src.common.logger import get_logger

"""
如果你想要修改配置文件，请递增version的值

版本格式：主版本号.次版本号.修订号，版本号递增规则如下：
    主版本号：MMC版本更新
    次版本号：配置文件内容大更新
    修订号：配置文件内容小更新
"""

PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute().resolve()
CONFIG_DIR: Path = PROJECT_ROOT / "config"
BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute()
MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute()
LEGACY_ENV_PATH: Path = (PROJECT_ROOT / ".env").resolve().absolute()
A_MEMORIX_LEGACY_CONFIG_PATH: Path = (CONFIG_DIR / "a_memorix.toml").resolve().absolute()
MMC_VERSION: str = "1.0.0-pre.11"
CONFIG_VERSION: str = "8.10.7"
MODEL_CONFIG_VERSION: str = "1.15.3"

logger = get_logger("config")

T = TypeVar("T", bound="ConfigBase")
ConfigReloadCallback = Callable[[Sequence[str]], object] | Callable[[], object]


class Config(ConfigBase):
    """总配置类"""

    bot: BotConfig = Field(default_factory=BotConfig)
    """机器人配置类"""

    personality: PersonalityConfig = Field(default_factory=PersonalityConfig)
    """人格配置类"""

    chat: ChatConfig = Field(default_factory=ChatConfig)
    """聊天配置类"""

    visual: VisualConfig = Field(default_factory=VisualConfig)
    """视觉配置类"""

    expression: ExpressionConfig = Field(default_factory=ExpressionConfig)
    """表达配置类"""

    memory: MemoryConfig = Field(default_factory=MemoryConfig)
    """记忆配置类"""

    a_memorix: AMemorixConfig = Field(default_factory=AMemorixConfig)
    """A_Memorix 长期记忆子系统配置"""

    message_receive: MessageReceiveConfig = Field(default_factory=MessageReceiveConfig)
    """消息接收配置类"""

    voice: VoiceConfig = Field(default_factory=VoiceConfig)
    """语音配置类"""

    emoji: EmojiConfig = Field(default_factory=EmojiConfig)
    """表情包配置类"""

    keyword_reaction: KeywordReactionConfig = Field(default_factory=KeywordReactionConfig)
    """关键词反应配置类"""

    response_post_process: ResponsePostProcessConfig = Field(default_factory=ResponsePostProcessConfig)
    """回复后处理配置类"""

    chinese_typo: ChineseTypoConfig = Field(default_factory=ChineseTypoConfig)
    """中文错别字生成器配置类"""

    response_splitter: ResponseSplitterConfig = Field(default_factory=ResponseSplitterConfig)
    """回复分割器配置类"""

    telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
    """遥测配置类"""

    log: LogConfig = Field(default_factory=LogConfig)
    """日志配置类"""

    debug: DebugConfig = Field(default_factory=DebugConfig)
    """调试配置类"""

    maim_message: MaimMessageConfig = Field(default_factory=MaimMessageConfig)
    """maim_message配置类"""

    webui: WebUIConfig = Field(default_factory=WebUIConfig)
    """WebUI配置类"""

    database: DatabaseConfig = Field(default_factory=DatabaseConfig)
    """数据库配置类"""

    mcp: MCPConfig = Field(default_factory=MCPConfig)
    """MCP 配置类"""

    plugin_runtime: PluginRuntimeConfig = Field(default_factory=PluginRuntimeConfig)
    """插件运行时配置类"""


class ModelConfig(ConfigBase):
    """模型配置类"""

    models: list[ModelInfo] = Field(default_factory=list)
    """模型配置列表"""

    model_task_config: ModelTaskConfig = Field(default_factory=ModelTaskConfig)
    """模型任务配置"""

    api_providers: list[APIProvider] = Field(default_factory=list)
    """API提供商列表"""

    def model_post_init(self, context: Any = None):
        if not self.models:
            raise ValueError(t("config.models_empty"))
        if not self.api_providers:
            raise ValueError(t("config.api_providers_empty"))

        # 检查API提供商名称是否重复
        provider_names = [provider.name for provider in self.api_providers]
        if len(provider_names) != len(set(provider_names)):
            raise ValueError(t("config.api_provider_name_duplicate"))

        # 检查模型名称是否重复
        model_names = [model.name for model in self.models]
        if len(model_names) != len(set(model_names)):
            raise ValueError(t("config.model_name_duplicate"))

        api_providers_dict = {provider.name: provider for provider in self.api_providers}

        for model in self.models:
            if not model.model_identifier:
                raise ValueError(t("config.model_identifier_empty", model_name=model.name))
            if not model.api_provider or model.api_provider not in api_providers_dict:
                raise ValueError(
                    t(
                        "config.model_api_provider_missing",
                        api_provider=model.api_provider,
                        model_name=model.name,
                    )
                )
        return super().model_post_init(context)


def _normalize_a_memorix_legacy_config(config_data: dict[str, Any]) -> dict[str, Any]:
    normalized = copy.deepcopy(config_data)
    web_config = normalized.get("web")
    if isinstance(web_config, dict) and "import" in web_config and "import_config" not in web_config:
        web_config["import_config"] = web_config.pop("import")
    return normalized


def _migrate_legacy_a_memorix_config(config_data: dict[str, Any]) -> tuple[dict[str, Any], bool]:
    if isinstance(config_data.get("a_memorix"), dict):
        return config_data, False
    if not A_MEMORIX_LEGACY_CONFIG_PATH.exists():
        return config_data, False

    try:
        with A_MEMORIX_LEGACY_CONFIG_PATH.open("r", encoding="utf-8") as handle:
            legacy_data = tomlkit.load(handle).unwrap()
    except Exception as exc:
        logger.warning(f"读取旧版 A_Memorix 配置失败，已使用主配置默认值: {A_MEMORIX_LEGACY_CONFIG_PATH}，原因: {exc}")
        return config_data, False

    if not isinstance(legacy_data, dict):
        logger.warning(f"旧版 A_Memorix 配置内容无效，已使用主配置默认值: {A_MEMORIX_LEGACY_CONFIG_PATH}")
        return config_data, False

    migrated_data = copy.deepcopy(config_data)
    migrated_data["a_memorix"] = _normalize_a_memorix_legacy_config(legacy_data)
    logger.warning(f"检测到旧版 A_Memorix 配置，已迁移到 bot_config.toml 的 [a_memorix]: {A_MEMORIX_LEGACY_CONFIG_PATH}")
    return migrated_data, True


def _normalize_loaded_bot_config_dict(config_data: dict[str, Any]) -> dict[str, Any]:
    normalized = copy.deepcopy(config_data)
    a_memorix_config = normalized.get("a_memorix")
    if isinstance(a_memorix_config, dict):
        normalized["a_memorix"] = _normalize_a_memorix_legacy_config(a_memorix_config)
    return normalized


class ConfigManager:
    """总配置管理类"""

    VLM_NOT_CONFIGURED_WARNING: str = "未配置视觉识图模型，部分图片理解可能受限，请在webui或model_config中配置"

    def __init__(self):
        self.bot_config_path: Path = BOT_CONFIG_PATH
        self.model_config_path: Path = MODEL_CONFIG_PATH
        CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        self.global_config: Config | None = None
        self.model_config: ModelConfig | None = None
        self._reload_lock: asyncio.Lock = asyncio.Lock()
        self._reload_callbacks: list[ConfigReloadCallback] = []
        self._file_watcher: FileWatcher | None = None
        self._file_watcher_subscription_id: str | None = None
        self._hot_reload_min_interval_s: float = 1.0
        self._hot_reload_timeout_s: float = 20.0
        self._last_hot_reload_monotonic: float = 0.0

    def initialize(self):
        logger.info(t("config.current_version", version=MMC_VERSION))
        logger.info(t("config.loading"))
        self.global_config, global_updated = load_config_from_file(Config, self.bot_config_path, CONFIG_VERSION)
        self.model_config, model_updated = load_config_from_file(
            ModelConfig,
            self.model_config_path,
            MODEL_CONFIG_VERSION,
            True,
        )
        if global_updated or model_updated:
            logger.info("配置已自动升级，将继续使用更新后的配置启动")
        self._warn_if_vlm_not_configured(self.model_config)
        logger.info(t("config.loaded"))

    @classmethod
    def _warn_if_vlm_not_configured(cls, model_config: ModelConfig) -> None:
        if any(model_name.strip() for model_name in model_config.model_task_config.vlm.model_list):
            return
        logger.warning(cls.VLM_NOT_CONFIGURED_WARNING)

    def load_global_config(self) -> Config:
        config, updated = load_config_from_file(Config, self.bot_config_path, CONFIG_VERSION)
        if updated:
            logger.info("bot_config.toml 已自动升级，将继续使用更新后的配置")
        return config

    def load_model_config(self) -> ModelConfig:
        config, updated = load_config_from_file(ModelConfig, self.model_config_path, MODEL_CONFIG_VERSION, True)
        if updated:
            logger.info("model_config.toml 已自动升级，将继续使用更新后的配置")
        return config

    def get_global_config(self) -> Config:
        if self.global_config is None:
            raise RuntimeError(t("config.global_not_initialized"))
        return self.global_config

    def get_model_config(self) -> ModelConfig:
        if self.model_config is None:
            raise RuntimeError(t("config.model_not_initialized"))
        return self.model_config

    def register_reload_callback(self, callback: ConfigReloadCallback) -> None:
        """注册配置热重载回调。

        Args:
            callback: 配置热重载回调。允许无参回调，也允许接收
                ``Sequence[str]`` 类型的变更范围列表。
        """

        self._reload_callbacks.append(callback)

    def unregister_reload_callback(self, callback: ConfigReloadCallback) -> None:
        """注销配置热重载回调。

        Args:
            callback: 先前注册过的回调对象。
        """

        try:
            self._reload_callbacks.remove(callback)
        except ValueError:
            return

    @staticmethod
    def _normalize_changed_scopes(changed_scopes: Sequence[str] | None) -> tuple[str, ...]:
        """规范化配置变更范围列表。

        Args:
            changed_scopes: 原始配置变更范围。

        Returns:
            tuple[str, ...]: 去重后的配置变更范围元组。
        """

        if not changed_scopes:
            return ("bot", "model")

        normalized_scopes: list[str] = []
        for scope in changed_scopes:
            normalized_scope = str(scope or "").strip().lower()
            if normalized_scope not in {"bot", "model"}:
                continue
            if normalized_scope not in normalized_scopes:
                normalized_scopes.append(normalized_scope)
        return tuple(normalized_scopes)

    @staticmethod
    def _resolve_changed_scopes(changes: Sequence[FileChange]) -> tuple[str, ...]:
        """根据文件变更列表推断配置变更范围。

        Args:
            changes: 文件监听器返回的变更列表。

        Returns:
            tuple[str, ...]: 命中的配置变更范围元组。
        """

        changed_scopes: list[str] = []
        for change in changes:
            file_name = change.path.name
            if file_name == "bot_config.toml" and "bot" not in changed_scopes:
                changed_scopes.append("bot")
            if file_name == "model_config.toml" and "model" not in changed_scopes:
                changed_scopes.append("model")
        return tuple(changed_scopes)

    @staticmethod
    def _callback_accepts_scopes(callback: ConfigReloadCallback) -> bool:
        """判断回调是否接收配置变更范围参数。

        Args:
            callback: 待检测的回调对象。

        Returns:
            bool: 若回调可接收一个位置参数或可变位置参数，则返回 ``True``。
        """

        try:
            parameters = inspect.signature(callback).parameters.values()
        except (TypeError, ValueError):
            return False

        positional_params = {
            inspect.Parameter.POSITIONAL_ONLY,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
        }
        for parameter in parameters:
            if parameter.kind == inspect.Parameter.VAR_POSITIONAL:
                return True
            if parameter.kind in positional_params:
                return True
        return False

    async def _invoke_reload_callback(
        self,
        callback: ConfigReloadCallback,
        changed_scopes: Sequence[str],
    ) -> None:
        """执行单个配置热重载回调。

        Args:
            callback: 要执行的回调对象。
            changed_scopes: 本次热重载命中的配置范围。
        """

        if self._callback_accepts_scopes(callback):
            callback_with_scopes = cast(Callable[[Sequence[str]], object], callback)
            result = callback_with_scopes(changed_scopes)
        else:
            callback_without_scopes = cast(Callable[[], object], callback)
            result = callback_without_scopes()
        if asyncio.iscoroutine(result):
            await result

    async def reload_config(self, changed_scopes: Sequence[str] | None = None) -> bool:
        """重新加载主配置和模型配置。

        Args:
            changed_scopes: 本次触发热重载的配置范围。

        Returns:
            bool: 是否重载成功。
        """

        normalized_scopes = self._normalize_changed_scopes(changed_scopes)
        async with self._reload_lock:
            try:
                global_config_new, global_updated = load_config_from_file(
                    Config,
                    self.bot_config_path,
                    CONFIG_VERSION,
                )
                model_config_new, model_updated = load_config_from_file(
                    ModelConfig,
                    self.model_config_path,
                    MODEL_CONFIG_VERSION,
                    True,
                )
            except Exception as exc:
                logger.error(t("config.reload_failed", error=exc))
                return False

            if global_updated or model_updated:
                logger.warning(t("config.version_update_detected"))

            self.global_config = global_config_new
            self.model_config = model_config_new
            global global_config, model_config
            global_config = global_config_new
            model_config = model_config_new
            logger.info(t("config.hot_reload_completed"))

            for callback in list(self._reload_callbacks):
                try:
                    await self._invoke_reload_callback(callback, normalized_scopes)
                except Exception as exc:
                    logger.warning(t("config.reload_callback_failed", error=exc))
            return True

    async def start_file_watcher(self) -> None:
        if self._file_watcher is not None and self._file_watcher.running:
            return
        self._file_watcher = FileWatcher(
            paths=[self.bot_config_path, self.model_config_path],
            debounce_ms=600,
            callback_timeout_s=15.0,
            callback_failure_threshold=3,
            callback_cooldown_s=30.0,
        )
        self._file_watcher_subscription_id = self._file_watcher.subscribe(
            self._handle_file_changes,
            paths=[self.bot_config_path, self.model_config_path],
        )
        await self._file_watcher.start()
        logger.info(t("config.file_watcher_started"))

    async def stop_file_watcher(self) -> None:
        if self._file_watcher is None:
            return
        if self._file_watcher_subscription_id is not None:
            self._file_watcher.unsubscribe(self._file_watcher_subscription_id)
            self._file_watcher_subscription_id = None
        watcher_stats = self._file_watcher.stats
        logger.info(
            t(
                "config.file_watcher_stop_stats",
                batches=watcher_stats.batches_seen,
                changes=watcher_stats.changes_seen,
                cooldown_skip=watcher_stats.callbacks_skipped_cooldown,
                failed=watcher_stats.callbacks_failed,
                ok=watcher_stats.callbacks_succeeded,
                restart=watcher_stats.restart_count,
                timeout=watcher_stats.callbacks_timed_out,
            )
        )
        await self._file_watcher.stop()
        self._file_watcher = None

    async def _handle_file_changes(self, changes: Sequence[FileChange]) -> None:
        """处理主配置与模型配置文件变更。

        Args:
            changes: 当前批次收集到的文件变更列表。
        """

        if not changes:
            return
        now_monotonic = asyncio.get_running_loop().time()
        if now_monotonic - self._last_hot_reload_monotonic < self._hot_reload_min_interval_s:
            logger.debug(t("config.reload_skipped_too_frequent"))
            return
        self._last_hot_reload_monotonic = now_monotonic
        logger.info(t("config.file_change_detected"))
        try:
            changed_scopes = self._resolve_changed_scopes(changes)
            await asyncio.wait_for(
                self.reload_config(changed_scopes=changed_scopes),
                timeout=self._hot_reload_timeout_s,
            )
        except asyncio.TimeoutError:
            logger.error(t("config.reload_timeout", timeout_seconds=self._hot_reload_timeout_s))


def generate_new_config_file(
    config_class: type[T], config_path: Path, inner_config_version: str, override_repr: bool = False
) -> None:
    """生成新的配置文件

    :param config_class: 配置类
    :param config_path: 配置文件路径
    :param inner_config_version: 配置文件版本号
    """
    if config_class is ModelConfig:
        config = create_default_model_config(config_class)
    else:
        config = config_class()
    write_config_to_file(config, config_path, inner_config_version, override_repr)


def remove_legacy_env_file(env_path: Path) -> None:
    """删除已完成迁移的旧版 `.env` 文件。"""

    if not env_path.exists():
        return

    try:
        env_path.unlink()
    except OSError as exc:
        logger.warning(f"旧版 .env 配置文件删除失败，请手动删除: {env_path}，原因: {exc}")
    else:
        logger.warning(f"检测到旧版环境变量绑定配置迁移成功，已删除旧版 .env 文件: {env_path}")


def load_config_from_file(
    config_class: type[T], config_path: Path, new_ver: str, override_repr: bool = False
) -> tuple[T, bool]:
    attribute_data = AttributeData()
    if not config_path.exists():
        logger.warning(f"配置文件缺失，正在生成默认配置: {config_path}")
        generate_new_config_file(config_class, config_path, new_ver, override_repr)
    with open(config_path, "r", encoding="utf-8") as f:
        config_data = tomlkit.load(f)
    inner_table = config_data.get("inner")
    if not isinstance(inner_table, Mapping):
        raise TypeError(t("config.missing_inner_version"))
    inner_version = inner_table.get("version")
    if not isinstance(inner_version, str):
        raise TypeError(t("config.invalid_inner_version"))
    old_ver: str = inner_version
    env_migration_applied: bool = False
    a_memorix_migration_applied: bool = False
    config_data.remove("inner")  # 移除 inner 部分，避免干扰后续处理
    config_data = config_data.unwrap()  # 转换为普通字典，方便后续处理
    if config_path.name == "bot_config.toml" and config_class.__name__ == "Config":
        env_migration = migrate_legacy_bind_env_to_bot_config_dict(config_data)
        env_migration_applied = env_migration.migrated
        if env_migration.migrated:
            logger.warning(f"检测到旧版环境变量绑定配置，已迁移到主配置: {env_migration.reason}")
        config_data = env_migration.data
        legacy_migration = try_migrate_legacy_bot_config_dict(config_data)
        if legacy_migration.migrated:
            logger.warning(t("config.legacy_migrated", reason=legacy_migration.reason))
        config_data = legacy_migration.data
        config_data, a_memorix_migration_applied = _migrate_legacy_a_memorix_config(config_data)
        config_data = _normalize_loaded_bot_config_dict(config_data)
    # 保留一份“干净”的原始数据副本，避免第一次 from_dict 过程中对 dict 的就地修改
    original_data: dict[str, Any] = copy.deepcopy(config_data)
    try:
        updated: bool = False
        try:
            target_config = config_class.from_dict(attribute_data, config_data)
        except TypeError as e:
            # 可拔插的旧配置修复（仅针对 bot_config.toml 的已知结构变更）
            if config_path.name == "bot_config.toml" and config_class.__name__ == "Config":
                # 基于未被部分构造污染的 original_data 做迁移尝试
                mig = try_migrate_legacy_bot_config_dict(original_data)
                if mig.migrated:
                    logger.warning(t("config.legacy_migrated", reason=mig.reason))
                    migrated_data = mig.data
                    target_config = config_class.from_dict(attribute_data, migrated_data)
                else:
                    raise e
            else:
                raise e
        if compare_versions(old_ver, new_ver) or env_migration_applied or a_memorix_migration_applied:
            output_config_changes(attribute_data, logger, old_ver, new_ver, config_path.name)
            write_config_to_file(target_config, config_path, new_ver, override_repr)
            if env_migration_applied:
                remove_legacy_env_file(LEGACY_ENV_PATH)
            updated = True
        return target_config, updated
    except Exception as e:
        logger.critical(t("config.parse_failed", file_name=config_path.name))
        raise e


def write_config_to_file(
    config: ConfigBase, config_path: Path, inner_config_version: str, override_repr: bool = False
) -> None:
    """将配置写入文件

    :param config: 配置对象
    :param config_path: 配置文件路径
    """
    # 创建空TOMLDocument
    full_config_data = tomlkit.document()

    # 首先写入配置文件版本信息
    version_table = tomlkit.table()
    version_table.add("version", inner_config_version)
    full_config_data.add("inner", version_table)

    # 递归解析配置项为表格
    for config_item_name, config_item in type(config).model_fields.items():
        if not config_item.repr and not override_repr:
            continue
        if config_item_name in ["field_docs", "_validate_any", "suppress_any_warning"]:
            continue
        config_field = getattr(config, config_item_name)
        if isinstance(config_field, ConfigBase):
            full_config_data.add(
                config_item_name, recursive_parse_item_to_table(config_field, override_repr=override_repr)
            )
        elif isinstance(config_field, list):
            aot = tomlkit.aot()
            for item in config_field:
                if not isinstance(item, ConfigBase):
                    raise TypeError(t("config.write_unsupported_type"))
                aot.append(recursive_parse_item_to_table(item, override_repr=override_repr))
            full_config_data.add(config_item_name, aot)
        else:
            raise TypeError(t("config.write_unsupported_type"))

    if isinstance(config, Config):
        try:
            a_memorix_web = full_config_data["a_memorix"]["web"]
            if "import_config" in a_memorix_web and "import" not in a_memorix_web:
                a_memorix_web["import"] = a_memorix_web.pop("import_config")
        except Exception:
            logger.debug("A_Memorix 配置写出时转换 web.import_config 失败", exc_info=True)

    # 备份旧文件
    if config_path.exists():
        backup_root = config_path.parent / "old"
        backup_root.mkdir(parents=True, exist_ok=True)
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_path = backup_root / f"{config_path.stem}_{timestamp}.toml"
        config_path.replace(backup_path)

    # 写入文件
    with open(config_path, "w", encoding="utf-8") as f:
        tomlkit.dump(full_config_data, f)


# generate_new_config_file(Config, BOT_CONFIG_PATH, CONFIG_VERSION)
config_manager = ConfigManager()
config_manager.initialize()
global_config = config_manager.get_global_config()
model_config = config_manager.get_model_config()
