# -*- coding: utf-8 -*-
"""
阿越云验证 - Python SDK

支持：卡密登录、心跳保活、公告获取、版本检查（含强制更新）、
      卡密解绑、获取应用配置、获取外链文件、应用账号登录

依赖：Python 3.7+，requests
    pip install requests

@version 1.0.0
"""

import hashlib
import time
import json
import random
import string
import urllib.parse
import urllib.request
import urllib.error
import ssl


class AyueVerifyError(Exception):
    """SDK 异常"""
    pass


class AyueVerify(object):
    """阿越云验证客户端"""

    def __init__(self, base_url, app_id, app_key='', rc4_key='',
                 mi_type=0, sign=False, timeout=10, mi_time=10):
        """
        :param base_url: 验证服务器地址，如 https://yz.example.com
        :param app_id:   应用 ID
        :param app_key:  应用密钥
        :param rc4_key:  通信密钥（后台"加密密钥"）
        :param mi_type:  加密类型 0=明文 1=RC4 2=Base64 11=AES-CBC 12=AES-ECB
        :param sign:     是否签名
        :param timeout:  超时秒数
        """
        self.base_url = base_url.rstrip('/') + '/'
        self.app_id = int(app_id)
        self.app_key = app_key or ''
        self.rc4_key = rc4_key or ''
        self.mi_type = int(mi_type)
        self.mi_time = int(mi_time)
        self.sign_enabled = bool(sign)
        self.timeout = int(timeout)

        self.last_error = ''
        self.last_code = 0

    # ==================== 业务接口 ====================

    def login(self, kami, markcode):
        """
        卡密登录

        :param kami:     卡密
        :param markcode: 设备码
        :return: dict(ok, code, msg, data, expires_ts)
        """
        res = self._request('kmlogon', {'kami': kami, 'markcode': markcode})
        return self._normalize(res, 'expires_ts')

    def heartbeat(self, kami, markcode):
        """心跳保活"""
        res = self._request('heartbeat', {'kami': kami, 'markcode': markcode})
        return self._normalize(res, 'expires_ts')

    def heartbeat_quit(self, kami):
        """主动退出心跳"""
        return self.heartbeat(kami, 'quit')

    def notice(self):
        """
        获取应用公告

        :return: dict(ok, notice, raw)
        """
        res = self._request('notice', {})
        ok = int(res.get('code', -1)) == 200
        msg = res.get('msg')
        notice = ''
        if ok and isinstance(msg, dict):
            notice = msg.get('app_gg', '') or ''
        return {'ok': ok, 'notice': notice, 'raw': res,
                'msg': self.last_error, 'code': self.last_code}

    def config(self, with_pay=False):
        """
        获取应用配置

        :param with_pay: 是否同时返回支付配置
        :return: dict(ok, version, version_info, update_url, update_show,
                      must_update, pay, raw)
        """
        extra = {'pay': 1} if with_pay else {}
        res = self._request('ini', extra)
        ok = int(res.get('code', -1)) == 200
        d = res.get('msg') if (ok and isinstance(res.get('msg'), dict)) else {}

        return {
            'ok': ok,
            'version': d.get('version', ''),
            'version_info': d.get('version_info', ''),
            'update_url': d.get('app_update_url', ''),
            'update_show': d.get('app_update_show', ''),
            'must_update': d.get('app_update_must', 'n') == 'y',
            'pay': d.get('pay'),
            'raw': res,
            'msg': self.last_error,
            'code': self.last_code,
        }

    def check_update(self, current_version):
        """
        检查更新

        :param current_version: 当前客户端版本
        :return: dict(ok, has_update, must_update, latest, url, desc)
        """
        cfg = self.config()
        if not cfg['ok']:
            return {'ok': False, 'has_update': False, 'must_update': False,
                    'latest': '', 'url': '', 'desc': '', 'msg': cfg['msg']}

        has_update = self.version_compare(cfg['version'], current_version) > 0
        return {
            'ok': True,
            'has_update': has_update,
            'must_update': has_update and cfg['must_update'],
            'latest': cfg['version'],
            'url': cfg['update_url'],
            'desc': cfg['update_show'] or cfg['version_info'],
            'msg': '',
        }

    def unbind(self, kami, markcode):
        """卡密解绑"""
        res = self._request('kmunmachine', {'kami': kami, 'markcode': markcode})
        return self._normalize(res)

    def get_files(self, file_id=0):
        """
        获取外链文件列表

        :return: dict(ok, files)
        """
        p = {}
        if int(file_id) > 0:
            p['id'] = int(file_id)
        res = self._request('getfile', p)
        ok = int(res.get('code', -1)) == 200
        msg = res.get('msg')
        files = msg if (ok and isinstance(msg, list)) else []
        return {'ok': ok, 'files': files, 'msg': self.last_error,
                'code': self.last_code}

    def user_login(self, username, password):
        """应用账号登录"""
        res = self._request('userlogon', {'user': username, 'password': password})
        return self._normalize(res)

    def user_register(self, username, password, extra=None):
        """应用账号注册"""
        p = {'user': username, 'password': password}
        if extra:
            p.update(extra)
        res = self._request('userreg', p)
        return self._normalize(res)

    def user_heartbeat(self, username, markcode):
        """应用账号心跳"""
        res = self._request('userheartbeat',
                            {'user': username, 'markcode': markcode})
        return self._normalize(res)

    # ==================== 底层通信 ====================

    def _request(self, api, fields):
        self.last_error = ''
        self.last_code = 0

        value = self._random_str(16)
        fields['value'] = value

        # 关键：服务端 mi_time > 0 时强制要求 t 参数（时间戳），缺失返回 108
        if self.mi_time > 0 and 't' not in fields:
            fields['t'] = int(time.time())

        url = '%sapi.php?api=%s&app=%d' % (
            self.base_url, urllib.parse.quote(api), self.app_id)

        post = {}
        if self.mi_type == 0:
            post = dict(fields)
        else:
            plain = self._build_query(fields)
            post['data'] = self._encrypt(plain)
            post['value'] = value

        if self.sign_enabled:
            src = dict(fields)
            if self.mi_type != 0:
                src['data'] = post['data']
            post['sign'] = self._make_sign(src)

        body = urllib.parse.urlencode(post).encode('utf-8')

        try:
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
            req = urllib.request.Request(
                url, data=body,
                headers={'User-Agent': 'AyueVerify-Python-SDK/1.0.0',
                         'Content-Type': 'application/x-www-form-urlencoded'})
            with urllib.request.urlopen(req, timeout=self.timeout, context=ctx) as r:
                raw = r.read().decode('utf-8', 'replace')
        except Exception as e:
            self.last_error = '网络请求失败: %s' % e
            self.last_code = -1
            return {'code': -1, 'msg': self.last_error}

        try:
            raw = raw.strip().lstrip('\ufeff')
            data = json.loads(raw)
        except Exception:
            self.last_error = '返回格式错误: %s' % raw[:120]
            self.last_code = -1
            return {'code': -1, 'msg': self.last_error}

        self.last_code = int(data.get('code', -1))
        if self.last_code != 200:
            m = data.get('msg')
            self.last_error = m if isinstance(m, str) else json.dumps(m, ensure_ascii=False)
        return data

    def _normalize(self, res, expires_key=''):
        code = int(res.get('code', -1))
        ok = (code == 200)
        msg = res.get('msg')
        out = {
            'ok': ok,
            'code': code,
            'msg': msg,
            'data': msg if (ok and isinstance(msg, dict)) else {},
        }
        if expires_key and ok and isinstance(msg, dict) and expires_key in msg:
            try:
                out['expires_ts'] = int(msg[expires_key])
            except Exception:
                out['expires_ts'] = 0
        return out

    def _build_query(self, fields):
        return '&'.join('%s=%s' % (k, v) for k, v in fields.items())

    def _make_sign(self, fields):
        """
        签名：字段按 k=v& 拼接（排除 sign/app/api/value），末尾拼 appkey，取 md5
        """
        ignore = ('sign', 'app', 'api', 'value', 'PHPSESSID',
                  'sec_defend', 'sidenav-state')
        s = ''
        for k, v in fields.items():
            if k in ignore:
                continue
            s += '%s=%s&' % (k, v)
        s += self.app_key
        return hashlib.md5(s.encode('utf-8')).hexdigest()

    def _encrypt(self, plain):
        import base64
        if self.mi_type == 1:
            # 关键：服务端 mi_rc4 会先做 UTF-8 -> GBK 转换，客户端必须保持一致，
            # 否则含中文的参数会解密失败。
            try:
                plain_bytes = plain.encode('gbk')
            except Exception:
                plain_bytes = plain.encode('utf-8', 'replace')
            try:
                key_bytes = self.rc4_key.encode('gbk')
            except Exception:
                key_bytes = self.rc4_key.encode('utf-8', 'replace')
            return binascii_hexlify_safe(self._rc4(plain_bytes, key_bytes))
        if self.mi_type in (2, 5):
            return base64.b64encode(plain.encode('utf-8')).decode('ascii')
        # 其他类型回退 base64，避免静默失败
        return base64.b64encode(plain.encode('utf-8')).decode('ascii')

    @staticmethod
    def _rc4(data, key):
        if not key:
            return b''
        S = list(range(256))
        j = 0
        kl = len(key)
        for i in range(256):
            j = (j + S[i] + key[i % kl]) % 256
            S[i], S[j] = S[j], S[i]
        i = j = 0
        out = bytearray()
        for ch in data:
            i = (i + 1) % 256
            j = (j + S[i]) % 256
            S[i], S[j] = S[j], S[i]
            out.append(ch ^ S[(S[i] + S[j]) % 256])
        return bytes(out)

    # ==================== 工具 ====================

    @staticmethod
    def version_compare(v1, v2):
        """版本比较：v1 > v2 返回 1"""
        import re
        a = re.split(r'[.\-+]', str(v1).strip())
        b = re.split(r'[.\-+]', str(v2).strip())
        n = max(len(a), len(b))
        for i in range(n):
            x = a[i] if i < len(a) else '0'
            y = b[i] if i < len(b) else '0'
            try:
                xi, yi = int(x), int(y)
                if xi > yi:
                    return 1
                if xi < yi:
                    return -1
            except ValueError:
                if x > y:
                    return 1
                if x < y:
                    return -1
        return 0

    @staticmethod
    def _random_str(n=16):
        chars = string.ascii_letters + string.digits
        return ''.join(random.choice(chars) for _ in range(n))

    @staticmethod
    def generate_markcode():
        """
        生成设备码（客户端首次运行应持久化到本地配置文件）
        """
        import platform
        import uuid
        seed = '%s|%s|%s' % (platform.node(), platform.machine(), uuid.getnode())
        return hashlib.md5(seed.encode('utf-8')).hexdigest()[:24].upper()


def binascii_hexlify_safe(b):
    import binascii
    return binascii.hexlify(b).decode('ascii')
