#!/usr/bin/env python3
"""
NIO Power 充电站接口调用工具

签名算法已验证通过 ✓

环境配置:
- 各环境 secret 通过环境变量配置: NIO_API_SECRET_PRODUCTION / _STAGING / _TEST / _DEV
- 未配置时回退到空字符串（接口会返回签名错误，用于本地调试）

使用方法:
    api = NIOPowerAPI(authorization="Bearer YOUR_TOKEN")
    result = api.get_chargers("YOUR_STATION_ID")
"""

import hashlib
import time
import json
import uuid
import os
import urllib.parse
import requests
from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import List, Optional, Dict, Any


# ==================== 数据模型 ====================

from enum import Enum

class ConnectorState(Enum):
    """充电桩状态枚举"""
    FREE = "free"           # 空闲
    CHARGING = "charging"   # 充电中
    OCCUPIED = "occupied"   # 占用（插枪未充或充满未拔）

    @classmethod
    def from_desc(cls, desc: str) -> 'ConnectorState':
        """从中文描述推断状态"""
        desc_map = {
            "空闲": cls.FREE,
            "充电中": cls.CHARGING,
            "占用": cls.OCCUPIED,
        }
        return desc_map.get(desc, cls.OCCUPIED)


@dataclass
class Connector:
    """充电桩信息"""
    connector_id: str
    connector_name: str
    type: str           # AC/DC
    power: float        # kW
    state_code: str
    state_desc: str

    # 增强字段（用于前端展示）
    charging_progress: Optional[float] = None  # 充电进度百分比（仅 DC 充电中）
    estimated_charging_ready_time: Optional[int] = None  # 预计剩余充电时间（分钟，仅 DC 充电中）
    free_since: Optional[datetime] = None      # 空闲起始时间
    idle_seconds: int = 0                       # 空闲时长（秒）
    charging_since: Optional[datetime] = None   # 充电起始时间
    charging_seconds: int = 0                   # 已充电时长（秒）

    @classmethod
    def from_dict(cls, data: Dict[str, Any], state_cache: Dict[str, datetime] = None,
                  charging_cache: Dict[str, datetime] = None) -> 'Connector':
        """从 API 响应创建

        Args:
            data: API 返回的单个充电桩数据
            state_cache: 状态缓存，记录每个桩首次变为空闲的时间
            charging_cache: 充电状态缓存，记录每个桩首次变为充电的时间
        """
        state = data.get('state', {})
        state_code = state.get('type', '') or cls._infer_state_code(state.get('desc', ''))

        # 解析充电进度和预计剩余时间（直流桩充电中时）
        charging_progress = None
        estimated_charging_ready_time = None
        if data.get('type') == 'DC' and state_code == 'charging':
            # 尝试从 desc 解析进度，如 "75.0"
            try:
                desc = state.get('desc', '')
                if desc.replace('.', '').isdigit():
                    charging_progress = float(desc)
            except:
                pass
            # 预计剩余充电时间（分钟）
            estimated_charging_ready_time = data.get('estimated_charging_ready_time')

        # 计算空闲时长
        free_since = None
        idle_seconds = 0
        if state_code == 'free' and state_cache:
            connector_key = data.get('connector_id', data.get('connector_name', ''))
            if connector_key in state_cache:
                free_since = state_cache[connector_key]
                idle_seconds = int((datetime.now() - free_since).total_seconds())

        # 计算已充电时长
        charging_since = None
        charging_seconds = 0
        if state_code == 'charging' and charging_cache:
            connector_key = data.get('connector_id', data.get('connector_name', ''))
            if connector_key in charging_cache:
                charging_since = charging_cache[connector_key]
                charging_seconds = int((datetime.now() - charging_since).total_seconds())

        return cls(
            connector_id=data.get('connector_id', ''),
            connector_name=data.get('connector_name', ''),
            type=data.get('type', ''),
            power=float(data.get('power', 0)),
            state_code=state_code,
            state_desc=state.get('desc', ''),
            charging_progress=charging_progress,
            estimated_charging_ready_time=estimated_charging_ready_time,
            free_since=free_since,
            idle_seconds=idle_seconds,
            charging_since=charging_since,
            charging_seconds=charging_seconds
        )

    @staticmethod
    def _infer_state_code(desc: str) -> str:
        """从描述推断状态码"""
        if '空闲' in desc:
            return 'free'
        elif '充电' in desc:
            return 'charging'
        else:
            return 'occupied'

    def to_dict(self) -> Dict[str, Any]:
        """转换为字典（用于 API 返回）"""
        result = {
            'connector_id': self.connector_id,
            'connector_name': self.connector_name,
            'type': self.type,
            'power': self.power,
            'state': {
                'type': self.state_code,
                'desc': self.state_desc
            },
            'idle_seconds': self.idle_seconds,
            'charging_seconds': self.charging_seconds,
        }

        # 直流桩充电中时添加进度和剩余时间
        if self.type == 'DC' and self.state_code == 'charging':
            if self.charging_progress:
                result['charging_progress'] = self.charging_progress
            if self.estimated_charging_ready_time:
                result['estimated_charging_ready_time'] = self.estimated_charging_ready_time

        # 空闲时添加空闲起始时间
        if self.state_code == 'free' and self.free_since:
            result['free_since'] = int(self.free_since.timestamp())

        # 充电中时添加充电起始时间
        if self.state_code == 'charging' and self.charging_since:
            result['charging_since'] = int(self.charging_since.timestamp())

        return result

    def get_idle_display(self) -> str:
        """获取空闲时长显示文本"""
        if self.state_code != 'free' or self.idle_seconds <= 0:
            return ''

        minutes = self.idle_seconds // 60
        hours = minutes // 60

        if hours >= 24:
            return '空闲 ≥ 1 天'
        elif hours >= 1:
            return f'空闲 {hours} 小时'
        else:
            return f'空闲 {minutes} 分钟'


@dataclass
class ChargeStation:
    """充电站信息"""
    group_id: str
    total_ac: int
    free_ac: int
    total_dc: int
    free_dc: int
    connectors: List[Connector] = field(default_factory=list)
    fetch_time: datetime = field(default_factory=datetime.now)

    # 状态缓存（记录空闲桩的起始时间）
    _free_state_cache: Dict[str, datetime] = field(default_factory=dict)
    # 充电状态缓存（记录充电桩的起始时间）
    _charging_state_cache: Dict[str, datetime] = field(default_factory=dict)

    @classmethod
    def from_api_response(cls, group_id: str, data: Dict[str, Any],
                          prev_cache: Dict[str, datetime] = None,
                          prev_charging_cache: Dict[str, datetime] = None) -> 'ChargeStation':
        """从 API 响应创建

        Args:
            group_id: 充电站 ID
            data: API 返回的 data 字段
            prev_cache: 上一次的空闲状态缓存（用于延续空闲计时）
            prev_charging_cache: 上一次的充电状态缓存（用于延续充电计时）
        """
        # 初始化或继承状态缓存
        free_cache = prev_cache.copy() if prev_cache else {}
        charging_cache = prev_charging_cache.copy() if prev_charging_cache else {}

        # 处理每个充电桩
        connectors = []
        for c_data in data.get('connector_detail_list', []):
            state = c_data.get('state', {})
            state_code = Connector._infer_state_code(state.get('desc', ''))
            connector_key = c_data.get('connector_id', c_data.get('connector_name', ''))

            # 更新空闲状态缓存
            if state_code == 'free':
                if connector_key not in free_cache:
                    # 新变为空闲，记录当前时间
                    free_cache[connector_key] = datetime.now()
            else:
                # 非空闲状态，从缓存中移除
                free_cache.pop(connector_key, None)

            # 更新充电状态缓存
            if state_code == 'charging':
                if connector_key not in charging_cache:
                    # 新变为充电，记录当前时间
                    charging_cache[connector_key] = datetime.now()
            else:
                # 非充电状态，从缓存中移除
                charging_cache.pop(connector_key, None)

            # 创建 Connector 对象
            connector = Connector.from_dict(c_data, free_cache, charging_cache)
            connectors.append(connector)

        return cls(
            group_id=group_id,
            total_ac=data.get('total_connector_number_ac', 0),
            free_ac=data.get('free_connector_number_ac', 0),
            total_dc=data.get('total_connector_number_dc', 0),
            free_dc=data.get('free_connector_number_dc', 0),
            connectors=connectors,
            fetch_time=datetime.now(),
            _free_state_cache=free_cache,
            _charging_state_cache=charging_cache
        )

    def to_dict(self) -> Dict[str, Any]:
        """转换为字典，为 API 返回做准备"""
        return {
            'group_id': self.group_id,
            'total_ac': self.total_ac,
            'free_ac': self.free_ac,
            'total_dc': self.total_dc,
            'free_dc': self.free_dc,
            'connector_count': len(self.connectors),
            'fetch_time': self.fetch_time.strftime('%H:%M:%S'),
            'fetch_timestamp': int(self.fetch_time.timestamp()),
            'connectors': [c.to_dict() for c in self.connectors]
        }

    def get_free_cache(self) -> Dict[str, datetime]:
        """获取空闲状态缓存（用于下次请求）"""
        return self._free_state_cache.copy()

    def get_charging_cache(self) -> Dict[str, datetime]:
        """获取充电状态缓存（用于下次请求）"""
        return self._charging_state_cache.copy()

    def get_free_connectors(self, min_idle_seconds: int = 60) -> List[Connector]:
        """获取空闲超过指定时长的充电桩

        Args:
            min_idle_seconds: 最小空闲时长（秒），默认 60 秒

        Returns:
            符合条件的空闲充电桩列表
        """
        return [c for c in self.connectors
                if c.state_code == 'free' and c.idle_seconds >= min_idle_seconds]

    def to_json(self, ensure_ascii: bool = False) -> str:
        """转换为 JSON 字符串"""
        return json.dumps(self.to_dict(), ensure_ascii=ensure_ascii, indent=2)

    def print_table(self) -> None:
        """终端表格输出"""
        print(f"\n{'═' * 60}")
        print(f"充电站: {self.group_id}")
        print(f"采集时间: {self.fetch_time.strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"{'═' * 60}")
        print(f"交流桩: {self.free_ac}/{self.total_ac} 空闲")
        print(f"直流桩: {self.free_dc}/{self.total_dc} 空闲")
        print(f"{'─' * 60}")
        print("充电桩列表:")
        for i, c in enumerate(self.connectors, 1):
            idle_text = c.get_idle_display()
            progress_text = f" [{c.charging_progress:.0f}%]" if c.charging_progress else ""
            idle_display = f" ({idle_text})" if idle_text else ""
            print(f"  {i}. {c.connector_name:8} {c.type:3}-{c.power:5.1f}kW  {c.state_desc}{progress_text}{idle_display}")
        print(f"{'═' * 60}\n")


class NIOPowerAPI:
    """NIO Power API 调用类"""

    # 环境配置
    # secret 为 NIO Power 签名密钥，通过环境变量注入（NIO_API_SECRET_<ENV>），不硬编码在代码中
    ENVIRONMENTS = {
        "production": {
            "host": "https://app.niopower.com",
            "secret": os.environ.get("NIO_API_SECRET_PRODUCTION", ""),
        },
        "staging": {
            "host": "https://app-stg.niopower.com",
            "secret": os.environ.get("NIO_API_SECRET_STAGING", ""),
        },
        # 注意：测试/开发环境为 NIO 内部域名，不随开源公开；如需使用请自行配置
        "test": {
            "host": os.environ.get("NIO_API_HOST_TEST", ""),
            "secret": os.environ.get("NIO_API_SECRET_TEST", ""),
        },
        "dev": {
            "host": os.environ.get("NIO_API_HOST_DEV", ""),
            "secret": os.environ.get("NIO_API_SECRET_DEV", ""),
        },
    }

    APP_ID = "100443"
    APP_VER = "4.16.0"
    LANG = "zh-cn"
    REGION = "cn"

    def __init__(self, env="production", authorization=None, device_id=None):
        """
        初始化

        Args:
            env: 环境名称 (production/staging/test/dev)
            authorization: Bearer token (需要通过登录获取)
            device_id: 设备ID (可选，不传则自动生成)
        """
        self.env = env
        self.config = self.ENVIRONMENTS.get(env, self.ENVIRONMENTS["production"])
        self.authorization = authorization
        self.device_id = device_id or self._generate_device_id()

    def _generate_device_id(self):
        """生成设备ID (32位MD5格式)"""
        return uuid.uuid4().hex

    def _generate_sign(self, path, params, method="GET"):
        """
        生成签名

        签名算法 (已验证):
        1. 拼接: method + path + "?"
        2. 参数按字母排序，用 & 连接
        3. 去掉最后的 &，加上 secret
        4. 如果有 Bearer token，追加到末尾
        5. MD5 加密，转小写十六进制

        Args:
            path: API 路径 (不含 query string)
            params: 参数列表 (格式: ["key=value", ...])
            method: HTTP 方法

        Returns:
            32位小写十六进制签名
        """
        # 1. 拼接前缀
        a = method + path + "?"

        # 2. 参数排序并拼接
        sorted_params = sorted(params)
        for p in sorted_params:
            a += p + "&"

        # 3. 去掉最后的 &，加上 secret
        a = a[:-1] + self.config["secret"]

        # 4. 如果有 Bearer token，追加
        if self.authorization and "Bearer" in self.authorization:
            a += self.authorization

        # 5. MD5
        return hashlib.md5(a.encode()).hexdigest()

    def _build_url(self, path, extra_params=None, method="GET", body=None, headers=None):
        """
        构建完整的请求 URL 和参数

        Args:
            path: API 路径
            extra_params: 额外参数 (dict)
            method: HTTP 方法
            body: POST 请求体
            headers: 请求头

        Returns:
            (完整URL, 签名, 请求参数dict)
        """
        # 基础参数 - 用于签名 (使用原始值，不编码)
        terminal_json = json.dumps({'name': 'Samsung', 'model': 'Samsung'})
        sign_params = [
            f"app_id={self.APP_ID}",
            f"app_ver={self.APP_VER}",
            f"lang={self.LANG}",
            f"region={self.REGION}",
            f"device_id={self.device_id}",
            f"terminal={terminal_json}",  # 签名用原始 JSON
        ]

        # URL 参数 (编码后)
        url_params = [
            f"app_id={self.APP_ID}",
            f"app_ver={self.APP_VER}",
            f"lang={self.LANG}",
            f"region={self.REGION}",
            f"device_id={self.device_id}",
            f"terminal={urllib.parse.quote(terminal_json)}",  # URL 用编码后的
        ]

        # 额外参数
        if extra_params:
            for key, value in extra_params.items():
                if isinstance(value, (dict, list)):
                    value_str = json.dumps(value, ensure_ascii=False)
                else:
                    value_str = str(value)
                sign_params.append(f"{key}={value_str}")
                url_params.append(f"{key}={urllib.parse.quote(value_str)}")

        # 时间戳
        timestamp = str(int(time.time()))
        sign_params.append(f"timestamp={timestamp}")
        url_params.append(f"timestamp={timestamp}")

        # POST 请求体处理
        if method == "POST" and body:
            if headers and headers.get("Content-Type") == "application/json":
                sign_params.append(f"jsonBody={json.dumps(body, ensure_ascii=False)}")
                url_params.append(f"jsonBody={urllib.parse.quote(json.dumps(body, ensure_ascii=False))}")
            else:
                for key, value in body.items():
                    sign_params.append(f"{key}={value}")
                    url_params.append(f"{key}={urllib.parse.quote(str(value))}")

        # 生成签名 (用未编码的参数)
        sign = self._generate_sign(path, sign_params, method)

        # 构建完整 URL (用编码后的参数)
        url_params.append(f"sign={sign}")
        full_url = self.config["host"] + path + "?" + "&".join(url_params)

        return full_url, sign, sign_params

    def request(self, path, method="GET", params=None, body=None, headers=None):
        """
        发送请求

        Args:
            path: API 路径
            method: HTTP 方法
            params: URL 参数
            body: 请求体
            headers: 额外请求头

        Returns:
            响应数据
        """
        url, sign, _ = self._build_url(path, params, method, body, headers)

        # 请求头
        req_headers = {
            "User-Agent": "Mozilla/5.0 (Linux; Android 12; SM-F926U) AppleWebKit/537.36 Chrome/142.0.0.0 Safari/537.36",
            "Content-Type": "application/x-www-form-urlencoded",
            "charset": "utf-8",
        }

        if self.authorization:
            req_headers["authorization"] = self.authorization

        if headers:
            req_headers.update(headers)

        # 发送请求
        if method == "GET":
            response = requests.get(url, headers=req_headers, timeout=10)
        else:
            response = requests.post(url, headers=req_headers, json=body, timeout=10)

        return response.json()

    # ==================== 登录相关接口 ====================

    def _get_nonce(self):
        """生成随机 nonce (15位随机字符串)"""
        import random
        import string
        chars = string.ascii_lowercase + string.digits
        return ''.join(random.choice(chars) for _ in range(15))

    def _login_request(self, path, body, headers=None, need_auth=False):
        """
        构造登录/刷新类请求（复刻微信小程序请求格式）

        与普通业务请求的差异（从 NIO Power 小程序逆向得到，登录接口传 ignoreDeviceParams=true）:
        - URL 只放固定参数 app_id/app_ver/lang/region + timestamp + sign，不含任何 body 参数
        - sign 放在 URL query 中，而非 body
        - 签名参数 = 固定参数 + timestamp + body 参数（body 参数参与签名，但只出现在请求体中）
        """
        timestamp = str(int(time.time()))

        # 签名参数（登录接口 URL 不含 device_id/terminal，但它们作为 body 参数参与签名）
        sign_params = [
            f"app_id={self.APP_ID}",
            f"app_ver={self.APP_VER}",
            f"lang={self.LANG}",
            f"region={self.REGION}",
            f"timestamp={timestamp}",
        ]
        for k, v in body.items():
            sign_params.append(f"{k}={v}")

        # 生成签名（排序在 _generate_sign 内部完成）
        sign = self._generate_sign(path, sign_params, "POST")

        # 完整 URL: 固定参数 + sign（body 参数不进 URL，避免未编码 JSON 破坏签名）
        url_params = sign_params[:5] + [f"sign={sign}"]
        full_url = self.config["host"] + path + "?" + "&".join(url_params)

        req_headers = {
            "Content-Type": "application/x-www-form-urlencoded",
            "User-Agent": "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36",
            "charset": "utf-8",
        }
        if need_auth and self.authorization:
            req_headers["authorization"] = self.authorization
        if headers:
            req_headers.update(headers)

        response = requests.post(full_url, data=body, headers=req_headers, timeout=30)
        return response.json()

    def send_verification_code(self, mobile, country_code="86"):
        """
        发送验证码

        Args:
            mobile: 手机号
            country_code: 国家代码，默认 86

        Returns:
            发送结果
        """
        nonce = self._get_nonce()
        terminal = json.dumps({'name': 'Samsung', 'model': 'Samsung'})

        body = {
            "classifier": "login",
            "country_code": country_code,
            "device_id": self.device_id,
            "mobile": mobile,
            "nonce": nonce,
            "terminal": terminal
        }

        return self._login_request(
            "/pe/app/account/v1/send-verification-code",
            body
        )

    def login(self, mobile, verification_code, country_code="86", channel="default"):
        """
        手机号验证码登录

        Args:
            mobile: 手机号
            verification_code: 验证码
            country_code: 国家代码
            channel: 渠道

        Returns:
            登录结果，包含 access_token 和 refresh_token
        """
        nonce = self._get_nonce()
        terminal = json.dumps({'name': 'Samsung', 'model': 'Samsung'})

        body = {
            "classifier": "login",
            "client": "mini_program",
            "channel": channel,
            "country_code": country_code,
            "device_id": self.device_id,
            "mobile": mobile,
            "nonce": nonce,
            "terminal": terminal,
            "verification_code": verification_code
        }

        result = self._login_request("/pe/app/account/v1/auto-login", body)

        # 登录成功后自动更新 token
        if result.get("result_code") == "success" and result.get("data"):
            self.authorization = f"Bearer {result['data'].get('access_token', '')}"

        return result

    def refresh_token(self, refresh_token):
        """
        刷新 access_token

        Args:
            refresh_token: 刷新令牌

        Returns:
            新的 access_token 和 refresh_token
        """
        body = {
            "refresh_token": refresh_token,
            "device_id": self.device_id,
            "terminal": json.dumps({'name': 'Samsung', 'model': 'Samsung'})
        }

        result = self._login_request(
            "/pe/app/account/v1/refresh-access",
            body,
            need_auth=True
        )

        # 刷新成功后自动更新 token
        if result.get("result_code") == "success" and result.get("data"):
            self.authorization = f"Bearer {result['data'].get('access_token', '')}"

        return result

    # ==================== 业务接口 ====================

    def get_chargers_raw(self, group_id):
        """
        获取充电站详情 (原始 JSON)

        Args:
            group_id: 充电站ID

        Returns:
            原始 API 响应 JSON
        """
        return self.request(
            "/pe/app/charge-station/v1/resource/connectors",
            params={"group_id": group_id}
        )

    # 状态缓存（类级别，用于跨请求保持空闲计时）
    _global_free_cache: Dict[str, datetime] = {}
    # 充电状态缓存（类级别，用于跨请求保持充电计时）
    _global_charging_cache: Dict[str, datetime] = {}

    def get_chargers(self, group_id, use_cache: bool = True) -> Optional[ChargeStation]:
        """
        获取充电站详情 (结构化数据)

        Args:
            group_id: 充电站ID
            use_cache: 是否使用状态缓存（用于计算空闲时长和充电时长）

        Returns:
            ChargeStation 对象，失败返回 None
        """
        result = self.get_chargers_raw(group_id)

        if result.get("result_code") == "success" and result.get("data"):
            # 获取或创建缓存
            cache_key = f"station_{group_id}"
            prev_cache = self._global_free_cache.get(cache_key, {}) if use_cache else {}
            prev_charging_cache = self._global_charging_cache.get(cache_key, {}) if use_cache else {}

            # 解析数据
            station = ChargeStation.from_api_response(group_id, result["data"], prev_cache, prev_charging_cache)

            # 更新缓存
            if use_cache:
                self._global_free_cache[cache_key] = station.get_free_cache()
                self._global_charging_cache[cache_key] = station.get_charging_cache()

            return station

        # 请求失败，打印错误信息
        print(f"请求失败: {result.get('result_code', 'unknown')}")
        if result.get('result_msg'):
            print(f"错误信息: {result['result_msg']}")
        return None

    @classmethod
    def clear_cache(cls):
        """清除所有状态缓存"""
        cls._global_free_cache.clear()
        cls._global_charging_cache.clear()


# ==================== 使用示例 ====================

if __name__ == "__main__":
    print("NIO Power API 调用工具")
    print("=" * 50)

    # 方式1: 使用现有 token (从抓包获取)
    # 注意: token 有时效性，过期需要重新获取
    # 重要: device_id 需要使用抓包时的值，否则会报 invalid deviceId
    API = NIOPowerAPI(
        env="production",
        authorization="Bearer YOUR_TOKEN_HERE",
        device_id="YOUR_DEVICE_ID_HERE"
    )

    # 获取充电站信息
    station = API.get_chargers(os.environ.get("STATION_ID", ""))

    # 使用结构化数据输出
    if station:
        station.print_table()

    print("\n" + "=" * 50)
    print("登录流程示例:")
    print("""
    # 1. 发送验证码
    api = NIOPowerAPI(device_id="your_device_id")
    result = api.send_verification_code("13800138000")

    # 2. 验证码登录
    result = api.login("13800138000", "123456")
    # 登录成功后 api.authorization 会自动更新

    # 3. 使用 token 调用业务接口
    result = api.get_chargers(os.environ.get("STATION_ID", ""))

    # 4. 刷新 token (当 token 过期时)
    result = api.refresh_token("your_refresh_token")
    """)
