#!/usr/bin/env python3
"""
NIO Power Token 管理器

统一管理 NIO Power API 的 access_token / refresh_token，解决 token 过期后需手动更换的问题。

三种 token 来源（优先级从高到低）:
1. 数据库 system_config 表持久化的 token（CLI 登录或自动刷新后写入，永远最新）
2. 环境变量 NIO_AUTHORIZATION / NIO_REFRESH_TOKEN（兼容现有部署，只配环境变量也能工作）
3. 无 token 时通过 CLI 手机号登录获取

功能:
- 自动刷新: 业务请求返回 auth_failed/invalid_token 时，用 refresh_token 刷新后自动重试一次
- 手机号登录: CLI 命令发送验证码并登录，新 token 持久化到数据库
- 兼容现有: 直接配置 NIO_AUTHORIZATION 环境变量即可工作，无需数据库
- device_id 持久化: 保证多次请求使用同一设备指纹（token 与 device_id 绑定）

CLI 用法:
    python token_manager.py status                                  # 查看 token 状态
    python token_manager.py send-code <mobile>                      # 发送验证码
    python token_manager.py login <mobile> <verification_code>     # 验证码登录并持久化
    python token_manager.py refresh                                 # 手动刷新 token

示例:
    python token_manager.py send-code 13800138000
    python token_manager.py login 13800138000 123456
"""

import os
import sys
import uuid
import logging
from typing import Dict, Any, Optional

# 日志配置
logger = logging.getLogger('charger.token')
if not logger.handlers:
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter(
        '[%(asctime)s] [%(levelname)s] %(message)s',
        datefmt='%H:%M:%S'
    ))
    logger.addHandler(handler)

# 数据库 system_config 表使用的 key
KEY_ACCESS_TOKEN = "nio_access_token"
KEY_REFRESH_TOKEN = "nio_refresh_token"
KEY_DEVICE_ID = "nio_device_id"
KEY_LOGIN_MOBILE = "nio_login_mobile"

# token 失效的错误码（复刻小程序 isNotAuthedResponse 判断）
AUTH_FAILED_CODES = (
    "sign_failed",
    "auth_failed",
    "invalid_token",
    "call_account_fail",
    "user_no_login",
)


class TokenManager:
    """NIO Power token 生命周期管理器"""

    def __init__(self, env: str = "production", authorization: Optional[str] = None):
        """
        初始化

        Args:
            env: 环境名称 (production/staging/test/dev)
            authorization: 显式传入的 Bearer token（最高优先级，用于测试）
        """
        self.env = env
        self.device_id = self._load_device_id()
        self.source = "none"

        # 加载 token（优先级：显式参数 > 数据库 > 环境变量）
        env_access = os.environ.get("NIO_AUTHORIZATION", "")
        env_refresh = os.environ.get("NIO_REFRESH_TOKEN", "")

        db_access = self._db_get(KEY_ACCESS_TOKEN)
        db_refresh = self._db_get(KEY_REFRESH_TOKEN)

        self.access_token = authorization or db_access or env_access
        self.refresh_token = db_refresh or env_refresh
        self.login_mobile = self._db_get(KEY_LOGIN_MOBILE)

        if authorization:
            self.source = "param"
        elif db_access:
            self.source = "database"
        elif env_access:
            self.source = "env"
        else:
            self.source = "none"

        # 创建 API 实例（复用固定 device_id）
        from nio_power_api_raw import NIOPowerAPIRaw
        self.api = NIOPowerAPIRaw(
            env=env,
            authorization=self.access_token or None,
            device_id=self.device_id,
        )

    # ==================== 数据库读写 ====================

    def _db_get(self, key: str) -> str:
        """从 system_config 表读取配置值（数据库不可用时降级返回空）"""
        try:
            from db import Database
            row = Database.execute(
                "SELECT value FROM system_config WHERE `key` = %s",
                (key,),
                fetch_one=True,
            )
            if row and row.get("value"):
                return row["value"]
        except Exception as e:
            logger.warning(f"读取 {key} 失败（降级为环境变量）: {e}")
        return ""

    def _db_set(self, key: str, value: str) -> None:
        """写入 system_config 表（数据库不可用时仅记录日志，不中断流程）"""
        if not value:
            return
        try:
            from db import Database
            Database.execute(
                "INSERT INTO system_config (`key`, `value`) VALUES (%s, %s) "
                "ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)",
                (key, value),
            )
            logger.info(f"已持久化 {key}")
        except Exception as e:
            logger.warning(f"持久化 {key} 失败: {e}")

    # ==================== 设备 ID ====================

    def _load_device_id(self) -> str:
        """
        加载固定 device_id（优先级：环境变量 > 数据库 > 稳定兜底 > 随机）

        NIO 将 token 绑定到 device_id，因此该值必须在多次进程重启间保持稳定。
        DB 不可用时的首启动不能退回每次随机值——否则 DB 恢复后持久化的 token
        会因 device_id 变化而静默失效。
        """
        env_device = os.environ.get("NIO_DEVICE_ID", "")
        if env_device:
            return env_device

        db_device = self._db_get(KEY_DEVICE_ID)
        if db_device:
            return db_device

        # 稳定兜底：基于 MAC 地址生成，跨重启稳定、不依赖 DB
        try:
            stable = uuid.UUID(int=uuid.getnode()).hex
        except Exception:
            stable = ""
        if stable and stable != uuid.UUID(int=0).hex:
            new_device = stable
        else:
            new_device = uuid.uuid4().hex
        self._db_set(KEY_DEVICE_ID, new_device)
        return new_device

    # ==================== token 操作 ====================

    def persist(self) -> None:
        """把当前 token 持久化到数据库"""
        self._db_set(KEY_ACCESS_TOKEN, self.access_token)
        self._db_set(KEY_REFRESH_TOKEN, self.refresh_token)
        self.source = "database"

    def send_verification_code(self, mobile: str) -> Dict[str, Any]:
        """发送手机验证码"""
        return self.api.send_verification_code(mobile, country_code="86")

    def login(self, mobile: str, verification_code: str) -> Dict[str, Any]:
        """
        手机号验证码登录，成功后持久化 token

        Returns:
            登录响应（result_code == "success" 表示成功）
        """
        result = self.api.login(mobile, verification_code, country_code="86")
        if result.get("result_code") == "success" and result.get("data"):
            data = result["data"]
            self.access_token = f"Bearer {data.get('access_token', '')}"
            self.refresh_token = data.get("refresh_token", "")
            self.login_mobile = mobile
            self.persist()
            self._db_set(KEY_LOGIN_MOBILE, mobile)
            logger.info(f"登录成功，新 token 已持久化（账号: {mobile}）")
        return result

    def refresh(self) -> bool:
        """
        用 refresh_token 刷新 access_token

        Returns:
            True 刷新成功，False 失败
        """
        if not self.refresh_token:
            logger.warning("无 refresh_token，无法自动刷新。请执行 python token_manager.py login")
            return False

        result = self.api.refresh_token(self.refresh_token)
        if result.get("result_code") == "success" and result.get("data"):
            data = result["data"]
            self.access_token = f"Bearer {data.get('access_token', '')}"
            new_refresh = data.get("refresh_token")
            if new_refresh:
                self.refresh_token = new_refresh
            self.persist()
            logger.info("token 自动刷新成功")
            return True

        logger.error(f"token 刷新失败: {result.get('result_code', result)}")
        return False

    # ==================== 业务请求（带自动刷新重试） ====================

    @staticmethod
    def _is_auth_failed(result: Any) -> bool:
        """判断响应是否为 token 失效"""
        if not isinstance(result, dict):
            return False
        return result.get("result_code", "") in AUTH_FAILED_CODES

    def get_chargers(self, group_id: str) -> Dict[str, Any]:
        """
        获取充电站数据，token 失效时自动刷新并重试一次

        Args:
            group_id: 充电站 ID

        Returns:
            API 原始响应 JSON
        """
        if not self.access_token:
            raise RuntimeError(
                "未配置 NIO token。请在 .env 设置 NIO_AUTHORIZATION，"
                "或执行 python token_manager.py login <手机号> <验证码> 获取"
            )

        result = self.api.get_chargers(group_id)

        # token 失效时自动刷新重试
        if self._is_auth_failed(result):
            logger.info("检测到 token 失效，尝试自动刷新...")
            if self.refresh():
                logger.info("token 刷新成功，使用新 token 重试请求")
                result = self.api.get_chargers(group_id)
                if self._is_auth_failed(result):
                    logger.error("刷新后重试仍返回认证错误，请检查 NIO 侧是否封禁或需重新登录")
            else:
                logger.error(
                    "token 自动刷新失败。原因见上一条日志：若 refresh_token 也已失效，"
                    "请执行 python token_manager.py login 重新登录"
                )

        return result


# ==================== CLI ====================

def _print_result(result: Dict[str, Any]) -> None:
    """友好打印 API 响应"""
    if result.get("result_code") == "success":
        print("✅ 成功")
    else:
        print(f"❌ 失败: {result.get('result_code', 'unknown')} - {result.get('message', '')}")


def cmd_status() -> int:
    """查看 token 状态"""
    tm = TokenManager()
    print("=== NIO Power Token 状态 ===")
    print(f"环境:        {tm.env}")
    print(f"device_id:   {tm.device_id}")
    print(f"token 来源:  {tm.source}")
    print(f"登录手机号:  {tm.login_mobile or '未记录'}")
    print(f"access:      {tm.access_token[:30] + '...' if tm.access_token else '未配置'}")
    print(f"refresh:     {'已配置' if tm.refresh_token else '未配置'}")
    if not tm.access_token:
        print("\n提示: 使用 python token_manager.py login <手机号> <验证码> 获取 token")
    elif not tm.refresh_token:
        print("\n提示: 未配置 refresh_token，token 失效后无法自动刷新，需重新登录")
    return 0


def cmd_send_code(mobile: str) -> int:
    """发送验证码"""
    tm = TokenManager()
    print(f"正在向 {mobile} 发送验证码...")
    result = tm.send_verification_code(mobile)
    _print_result(result)
    if result.get("result_code") == "success":
        print("验证码已发送，请查收短信")
    return 0 if result.get("result_code") == "success" else 1


def cmd_login(mobile: str, code: str) -> int:
    """验证码登录并持久化 token"""
    tm = TokenManager()
    print(f"正在登录 {mobile} ...")
    result = tm.login(mobile, code)
    _print_result(result)
    if result.get("result_code") == "success":
        print("\n登录成功！新的 token 已持久化到数据库，scheduler 会自动使用。")
        print("如需在新环境部署，可将以下配置写入 .env:")
        print(f"  NIO_AUTHORIZATION={tm.access_token}")
        if tm.refresh_token:
            print(f"  NIO_REFRESH_TOKEN={tm.refresh_token}")
    return 0 if result.get("result_code") == "success" else 1


def cmd_refresh() -> int:
    """手动刷新 token"""
    tm = TokenManager()
    print("正在刷新 token...")
    if tm.refresh():
        print("刷新成功！新 token 已持久化。")
        return 0
    print("刷新失败，请检查 refresh_token 或重新登录。")
    return 1


def main() -> int:
    """CLI 入口"""
    if len(sys.argv) < 2:
        print(__doc__)
        return 1

    cmd = sys.argv[1]

    if cmd == "status":
        return cmd_status()
    elif cmd == "send-code":
        if len(sys.argv) < 3:
            print("用法: python token_manager.py send-code <mobile>")
            return 1
        return cmd_send_code(sys.argv[2])
    elif cmd == "login":
        if len(sys.argv) < 4:
            print("用法: python token_manager.py login <mobile> <verification_code>")
            return 1
        return cmd_login(sys.argv[2], sys.argv[3])
    elif cmd == "refresh":
        return cmd_refresh()
    else:
        print(f"未知命令: {cmd}")
        print(__doc__)
        return 1


if __name__ == "__main__":
    sys.exit(main())
