"""
认证管理模块

实现微信认证相关功能，包括获取access_token等
"""

import os
from functools import partial
from typing import Optional, Dict, Callable, Any, List
from .client import Client
from .cache import Cache

_env_loaded = False
_env_loaded_paths = set()  # 记录已加载的路径，避免重复加载


def load_env(extra_paths: Optional[List[str]] = None, force_reload: bool = False) -> None:
    """
    加载 .env 文件

    加载顺序（后面的会覆盖前面的）：
    1. 当前工作目录的 .env（用户项目根目录）
    2. xqcwechat-skill 目录的 .env（如果存在）
    3. extra_paths 中指定的路径

    :param extra_paths: 额外的 .env 文件或目录路径列表
    :param force_reload: 是否强制重新加载（忽略已加载状态）
    """
    global _env_loaded, _env_loaded_paths

    if _env_loaded and not force_reload:
        return

    try:
        from dotenv import load_dotenv
    except ImportError:
        print("[WARN] python-dotenv 未安装，无法加载 .env 文件")
        return

    paths_to_try = []

    # 1. 当前工作目录的 .env（用户项目根目录）- 最高优先级
    cwd_env = os.path.join(os.getcwd(), ".env")
    if os.path.exists(cwd_env) and cwd_env not in _env_loaded_paths:
        print(f"[LOAD] 从当前工作目录加载环境变量: {cwd_env}")
        paths_to_try.append(cwd_env)
        _env_loaded_paths.add(cwd_env)

    # 2. xqcwechat-skill 目录的 .env（如果存在）
    current_file = os.path.abspath(__file__)
    current_dir = os.path.dirname(os.path.dirname(current_file))

    # 尝试多种可能的 skill 目录位置
    possible_skill_dirs = [
        os.path.join(current_dir, "xqcwechat-skill"),  # 标准 skill 目录
        os.path.join(os.path.dirname(current_dir), "xqcwechat-skill"),  # 父目录下的 skill
    ]

    for skill_dir in possible_skill_dirs:
        skill_env = os.path.join(skill_dir, ".env")
        if os.path.exists(skill_env) and skill_env not in _env_loaded_paths:
            print(f"[LOAD] 从 skill 目录加载环境变量: {skill_env}")
            paths_to_try.append(skill_env)
            _env_loaded_paths.add(skill_env)
            break

    # 3. 额外指定的路径
    if extra_paths:
        for path in extra_paths:
            if os.path.isfile(path) and path not in _env_loaded_paths:
                print(f"[LOAD] 从额外路径加载环境变量: {path}")
                paths_to_try.append(path)
                _env_loaded_paths.add(path)
            elif os.path.isdir(path):
                env_path = os.path.join(path, ".env")
                if os.path.exists(env_path) and env_path not in _env_loaded_paths:
                    print(f"[LOAD] 从额外目录加载环境变量: {env_path}")
                    paths_to_try.append(env_path)
                    _env_loaded_paths.add(env_path)

    # 按顺序加载，后面的会覆盖前面的
    for path in paths_to_try:
        load_dotenv(path, override=True)

    _env_loaded = True


load_env()


class Auth:
    """
    认证管理类

    用于获取和管理微信access_token
    """

    def __init__(
        self,
        app_id: Optional[str] = None,
        app_secret: Optional[str] = None,
        proxy: Optional[str] = None,
        callback_first: bool = True,
        expire_offset: int = 100,
        access_token_getter: Optional[Callable[[], Dict[str, Any]]] = None,
    ):
        """
        初始化认证管理

        :param app_id: 微信公众号AppID
        :param app_secret: 微信公众号AppSecret
        :param proxy: 代理服务器地址，格式为 "http://ip:port" 或 "https://ip:port"
        :param callback_first: 获取access_token的策略，True（默认，先调用回调函数）或 False（先调用官方接口）
        :param expire_offset: access_token提前过期时间，单位为秒，默认值为100
                - 用于在access_token过期前提前刷新，避免在过期边缘使用
                - 必须为非负整数
        :param access_token_getter: 获取access_token的无参回调函数
                - 返回格式为包含 'access_token' 和 'expires_in' 的字典，或包含 'access_token' 和 'expires_at' 的字典
                - expires_in：access_token的有效期，单位为秒
                - expires_at：access_token的过期时间戳，单位为秒
                - 当未提供此参数但环境变量存在 WECHAT_GET_WECHAT_TOKEN_SECRET 时，
                    将自动使用内置的 get_wechat_token 函数（传入 secret 和 proxy）

                示例1 - 回调函数本身无参数：
                    def my_getter():
                        return {"access_token": "xxx", "expires_in": 7200}

                    auth = Auth(access_token_getter=my_getter)

                示例2 - 回调函数有参数，使用 lambda 绑定：
                    def my_getter(api_url, api_key):
                        resp = requests.get(api_url, headers={"X-Key": api_key})
                        return resp.json()  # {"access_token": "xxx", "expires_in": 7200}

                    auth = Auth(
                        access_token_getter=lambda: my_getter("https://example.com/token", "my_key")
                    )

                示例3 - 回调函数有参数，使用 functools.partial 绑定：
                    from functools import partial

                    def my_getter(secret, proxy=None):
                        # 通过自定义服务获取token
                        return {"access_token": "xxx", "expires_in": 7200}

                    auth = Auth(
                        access_token_getter=partial(my_getter, secret="my_secret", proxy="http://127.0.0.1:7890")
                    )
        """
        # 从环境变量获取配置（如果参数未提供）
        self.app_id = app_id or os.environ.get("WECHAT_APP_ID")
        self.app_secret = app_secret or os.environ.get("WECHAT_APP_SECRET")
        self.proxy = proxy or os.environ.get("WECHAT_PROXY")
        self.callback_first = callback_first

        # 验证并设置提前过期时间
        if not isinstance(expire_offset, int) or expire_offset < 0:
            raise ValueError("[ERROR] expire_offset必须为非负整数")
        self.expire_offset = expire_offset

        self.client = Client(proxy=self.proxy)
        self.cache = Cache()
        self.access_token_getter = access_token_getter

        # 用户未提供回调函数时，自动检测环境变量并选择 Token 获取方式：
        # - 优先使用 WECHAT_TOKEN_SECRET（WECHAT_GET_WECHAT_TOKEN_SECRET）配置默认回调函数
        # - 如未配置，则使用 WECHAT_APP_ID + WECHAT_APP_SECRET 调用官方 API
        if not self.access_token_getter:
            wechat_token_secret = os.environ.get(
                "WECHAT_GET_WECHAT_TOKEN_SECRET"
            ) or os.environ.get("WECHAT_TOKEN_SECRET")
            if wechat_token_secret:
                try:
                    from .token_getters import get_wechat_token

                    wechat_token_app_id = os.environ.get(
                        "WECHAT_GET_WECHAT_TOKEN_APP_ID"
                    ) or os.environ.get("WECHAT_TOKEN_APP_ID")
                    wechat_token_url = os.environ.get(
                        "WECHAT_GET_WECHAT_TOKEN_URL"
                    ) or os.environ.get("WECHAT_TOKEN_URL")

                    kwargs = {
                        "secret": wechat_token_secret,
                        "proxy": self.proxy,
                    }
                    if wechat_token_app_id:
                        kwargs["app_id"] = wechat_token_app_id
                    if wechat_token_url:
                        kwargs["url"] = wechat_token_url

                    self.access_token_getter = partial(
                        get_wechat_token,
                        **kwargs,
                    )
                except ImportError:
                    pass

        # 默认缓存键
        self.cache_key = (
            f"access_token_{self.app_id}" if self.app_id else "access_token_default"
        )

    def set_access_token_getter(self, getter: Callable[[], Dict[str, Any]]) -> None:
        """
        设置access_token获取函数

        :param getter: 获取access_token的无参回调函数
                      - 返回格式为包含 'access_token' 和 'expires_in' 的字典，或包含 'access_token' 和 'expires_at' 的字典
                      - 如果函数本身需要参数，请在外部用 functools.partial 或 lambda 绑定好
                      - 示例1：def my_getter(): return {"access_token": "xxx", "expires_in": 7200}
                      - 示例2：def my_getter(): return {"access_token": "xxx", "expires_at": 1680000000}
        """
        self.access_token_getter = getter

    @staticmethod
    def from_env(account: Optional[str] = None) -> "Auth":
        """
        从环境变量智能加载配置

        自动检测环境变量并创建 Auth 实例，支持多种配置方式：
        1. 微信官方 API（需要 WECHAT_ACCOUNT_N_APP_ID + WECHAT_ACCOUNT_N_APP_SECRET）
        2. 默认回调函数（只需要 WECHAT_TOKEN_SECRET + WECHAT_ACCOUNT_N_TOKEN_APP_ID）
        3. 自定义回调函数（使用 WECHAT_ACCOUNT_N_TOKEN_GETTER_MODULE）

        :param account: 账号标识，如 "1", "2" 或 "default"，默认为 None（使用账号1配置）
        :return: Auth 实例
        """
        # 确定账号前缀，默认使用账号1
        account_num = account if account else "1"
        account_prefix = f"WECHAT_ACCOUNT_{account_num}_"

        proxy = os.environ.get("WECHAT_PROXY")
        token_secret = os.environ.get("WECHAT_TOKEN_SECRET") or os.environ.get(
            "WECHAT_GET_WECHAT_TOKEN_SECRET"
        )

        # 1. 检查自定义回调函数配置
        token_getter_module = os.environ.get(f"{account_prefix}TOKEN_GETTER_MODULE")
        if token_getter_module:
            try:
                import importlib
                from functools import partial
                module = importlib.import_module(token_getter_module)
                token_func = getattr(module, "get_token")
                # 使用 partial 绑定账号索引
                access_token_getter = partial(token_func, account_index=int(account_num))
                return Auth(
                    app_id=os.environ.get(f"{account_prefix}TOKEN_APP_ID"),
                    proxy=proxy,
                    access_token_getter=access_token_getter
                )
            except Exception as e:
                print(f"[WARN] 加载自定义 Token 获取器失败: {e}")

        # 2. 检查账号级配置
        account_app_id = os.environ.get(f"{account_prefix}APP_ID")
        account_app_secret = os.environ.get(f"{account_prefix}APP_SECRET")
        account_token_app_id = os.environ.get(f"{account_prefix}TOKEN_APP_ID")

        # 3. 方式二：使用默认回调函数（TOKEN_SECRET）
        if token_secret:
            token_app_id = account_token_app_id or os.environ.get("WECHAT_TOKEN_APP_ID")
            kwargs = {"proxy": proxy} if proxy else {}
            return Auth(
                app_id=token_app_id,
                access_token_getter=lambda s=token_secret,
                aid=token_app_id,
                p=proxy: Auth._default_token_getter(s, aid, p),
                **kwargs,
            )

        # 4. 方式一：使用微信官方 API
        # 优先使用账号级配置，再使用全局配置
        app_id = account_app_id or os.environ.get("WECHAT_APP_ID")
        app_secret = account_app_secret or os.environ.get("WECHAT_APP_SECRET")

        return Auth(app_id=app_id, app_secret=app_secret, proxy=proxy)

    @staticmethod
    def _default_token_getter(
        secret: str, app_id: Optional[str] = None, proxy: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        默认的 token 获取回调函数

        :param secret: 密钥
        :param app_id: 微信公众号AppID（用于区分不同账号）
        :param proxy: 代理
        :return: 包含 access_token 和 expires_in 的字典
        """
        from .token_getters import get_wechat_token

        token_url = os.environ.get("WECHAT_TOKEN_URL") or os.environ.get(
            "WECHAT_GET_WECHAT_TOKEN_URL"
        )

        use_app_id = (
            app_id
            or os.environ.get("WECHAT_TOKEN_APP_ID")
            or os.environ.get("WECHAT_GET_WECHAT_TOKEN_APP_ID")
        )

        kwargs = {"secret": secret, "proxy": proxy}
        if token_url:
            kwargs["url"] = token_url
        if use_app_id:
            kwargs["app_id"] = use_app_id

        result = get_wechat_token(**kwargs)
        if result:
            return result
        raise Exception("默认回调函数获取 token 失败")

    def clear_cache(self) -> None:
        """
        清理本地缓存

        清空所有缓存的access_token等数据
        """
        self.cache.clear()
        print("[OK] 本地缓存已清空")

    def _call_access_token_getter(self) -> Optional[Dict[str, Any]]:
        """
        调用回调函数获取access_token并验证返回格式

        :return: 包含access_token和expires_in/expires_at的字典，失败返回None
        """
        if not self.access_token_getter:
            return None

        try:
            token_data = self.access_token_getter()
        except Exception as e:
            print(f"[ERROR] 自定义接口获取access_token失败：{str(e)}")
            return None

        if (
            not isinstance(token_data, dict)
            or "access_token" not in token_data
            or ("expires_in" not in token_data and "expires_at" not in token_data)
        ):
            print(
                "[ERROR] 自定义接口返回格式不正确，需包含 'access_token' 和 'expires_in' 或 'expires_at'"
            )
            return None

        # 如果返回的是 expires_at，则转换为 expires_in
        if "expires_at" in token_data and "expires_in" not in token_data:
            import time

            current_time = int(time.time())
            expires_at = int(token_data["expires_at"])
            expires_in = expires_at - current_time
            token_data["expires_in"] = expires_in

        print("[OK] 自定义接口获取access_token成功")
        return token_data

    def get_access_token(self) -> str:
        """
        获取access_token

        优先从缓存获取，如果缓存不存在或已过期，则通过配置的方式获取
        根据callback_first参数决定调用顺序：
        - True：先调用回调函数获取，如果失败或未设置，则调用官方接口
        - False：先调用官方接口获取，如果失败或未配置，则调用回调函数

        :return: access_token
        """
        # 尝试从缓存获取
        cached_token = self.cache.get(self.cache_key)
        if cached_token:
            print("✅ 从缓存获取access_token成功")
            return cached_token

        # 缓存不存在或已过期，通过配置的方式获取
        token_data = None

        if self.callback_first:
            # ---------- 优先使用回调函数 ----------
            if self.access_token_getter:
                print("[INFO] 通过自定义接口获取access_token")
                token_data = self._call_access_token_getter()

            # 回调失败或未设置，退回到官方接口
            if token_data is None:
                if not self.app_id or not self.app_secret:
                    raise ValueError(
                        "[ERROR] 无法获取access_token：未提供app_id和app_secret，且自定义接口获取失败"
                    )
                print("[INFO] 通过官方接口获取access_token")
                token_data = self._get_access_token_from_official()
        else:
            # ---------- 优先使用官方接口 ----------
            if self.app_id and self.app_secret:
                print("[INFO] 通过官方接口获取access_token")
                try:
                    token_data = self._get_access_token_from_official()
                    print("[OK] 官方接口获取access_token成功")
                except Exception as e:
                    print(
                        f"[ERROR] 官方接口获取access_token失败：{str(e)}，尝试使用自定义接口"
                    )
                    token_data = None

            # 官方接口失败或未配置，退回到回调函数
            if token_data is None:
                if not self.access_token_getter:
                    raise ValueError(
                        "[ERROR] 无法获取access_token：未设置自定义接口，且官方接口获取失败"
                    )
                print("[INFO] 通过自定义接口获取access_token")
                token_data = self._call_access_token_getter()

        # 最终校验
        if (
            not isinstance(token_data, dict)
            or "access_token" not in token_data
            or ("expires_in" not in token_data and "expires_at" not in token_data)
        ):
            raise ValueError("[ERROR] 获取access_token失败：返回格式不正确")

        access_token = token_data["access_token"]

        # 处理过期时间
        if "expires_in" in token_data:
            expires_in = int(token_data["expires_in"])
        else:
            import time

            current_time = int(time.time())
            expires_at = int(token_data["expires_at"])
            expires_in = expires_at - current_time

        # 缓存access_token，提前expire_offset秒过期，避免在过期边缘使用
        cache_expire_time = max(
            0, expires_in - self.expire_offset
        )  # 确保缓存时间不为负
        self.cache.set(self.cache_key, access_token, cache_expire_time)
        print(
            f"[OK] 获取access_token成功，有效期：{expires_in}秒，缓存时间：{cache_expire_time}秒"
        )

        return access_token

    def _get_access_token_from_official(self) -> Dict[str, Any]:
        """
        从官方接口获取access_token

        :return: 包含access_token和expires_in的字典
        """
        if not self.app_id or not self.app_secret:
            raise ValueError(
                "[ERROR] 无法从官方接口获取access_token：未提供app_id和app_secret"
            )

        url = "https://api.weixin.qq.com/cgi-bin/token"
        params = {
            "grant_type": "client_credential",
            "appid": self.app_id,
            "secret": self.app_secret,
        }

        response = self.client.get(url, params)

        if "errcode" in response and response["errcode"] != 0:
            raise ValueError(f"[ERROR] 从官方接口获取access_token失败：{response['errmsg']}")

        return response

    def clear_access_token_cache(self) -> None:
        """
        清除access_token缓存
        """
        self.cache.delete(self.cache_key)
        print("[OK] 清除access_token缓存成功")
