# -*- coding: utf-8 -*-
import base64
import hashlib
import hmac
import json
import os
import platform
import shlex
import subprocess
import tempfile
import sys
import urllib.error
import urllib.request
import uuid
from typing import List, Optional, Tuple, Union


PRODUCT_ID = "missing-relinker"
API_URL = "https://www.ainuke.cn/api/activate_plugin"

PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxYvUfGEzRY1BVgBaBLxV
BsjFSWd0h1i86K0bw3yKfmyLGkfVODm1CsLi0zC2RgmJgRhP2dAALrL/1Xu1VMog
0FOON0KT7apmh+8x8N2cQDeoPcgtMDpJRUlbUwU6wINECnNf7ZDXRVl46FezwnsE
+rdUZCGK61D9Ufamfyj2cvLptI/YnkwKNTsuE56hfOnc7zvLZmBQMJT5fuJ8FICZ
uldGDu84VrMvDQtEO0kJ2Ep4FilbL77STRO0EUD/VceXppEG/H5oFiyoZZ+tuXji
zrmDgxtKZLdiZXVyXg+JFa2G+UqU9zgHe+jJXpTksrRq7yjOpCc0VAE2n/0OwMpd
LwIDAQAB
-----END PUBLIC KEY-----"""

_CACHED_MACHINE_CODE = ""
_CACHE_ID = hashlib.md5(b"missing_relinker_auth").hexdigest()

LEGACY_LICENSE_FILE = os.path.join(os.path.expanduser("~"), ".nuke", ".nuke_cache_%s.dat" % _CACHE_ID[:8])
HW_CACHE_FILE = os.path.join(os.path.expanduser("~"), ".nuke", ".nuke_hw_%s.dat" % _CACHE_ID[:8])
LICENSE_DIR = os.path.join(os.path.expanduser("~"), ".nuke", "MissingRelinker_License")
ONLINE_LICENSE_FILE = os.path.join(LICENSE_DIR, "license.json")


def _get_dynamic_salt() -> bytes:
    return b"Nuke_Relinker_By_NukeStudy_Anti_Crack_2026_!"


def _run_command(cmd: Union[str, List[str]], timeout: int = 5) -> str:
    try:
        startupinfo = None
        if platform.system() == "Windows":
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

        cmd_args = shlex.split(cmd) if isinstance(cmd, str) else cmd
        output = subprocess.check_output(
            cmd_args,
            shell=False,
            startupinfo=startupinfo,
            stderr=subprocess.STDOUT,
            timeout=timeout,
        )
        return output.decode(errors="ignore").strip()
    except Exception:
        return ""


def _get_fast_machine_id() -> str:
    try:
        return hashlib.md5(("%s_%s" % (platform.node(), uuid.getnode())).encode("utf-8")).hexdigest()
    except Exception:
        return hashlib.md5(platform.node().encode("utf-8")).hexdigest()


def _get_motherboard_serial() -> str:
    system = platform.system()
    if system == "Windows":
        res = _run_command(["powershell", "-NoProfile", "-Command", "(Get-CimInstance -Class Win32_BaseBoard).SerialNumber"])
        return res if res and "SerialNumber" not in res else ""
    if system == "Darwin":
        res = _run_command(["ioreg", "-l"])
        for line in res.splitlines():
            if "IOPlatformSerialNumber" in line:
                parts = line.split("=")
                if len(parts) > 1:
                    return parts[1].strip().replace('"', "")
    if system == "Linux":
        for path in ("/sys/class/dmi/id/board_serial", "/sys/class/dmi/id/product_uuid"):
            try:
                with open(path, "r") as handle:
                    value = handle.read().strip()
                    if value:
                        return value
            except Exception:
                pass
    return ""


def _get_cpu_id() -> str:
    system = platform.system()
    if system == "Windows":
        res = _run_command(["powershell", "-NoProfile", "-Command", "(Get-CimInstance -Class Win32_Processor)[0].ProcessorId"])
        return res if res and "ProcessorId" not in res else ""
    if system == "Darwin":
        return _run_command(["sysctl", "-n", "machdep.cpu.signature"]).strip()
    if system == "Linux":
        try:
            with open("/proc/cpuinfo", "r") as handle:
                for line in handle:
                    if line.startswith("Serial") or line.startswith("Hardware") or line.startswith("model name"):
                        return line.split(":", 1)[1].strip()
        except Exception:
            pass
    return ""


def get_machine_code() -> str:
    global _CACHED_MACHINE_CODE
    if _CACHED_MACHINE_CODE:
        return _CACHED_MACHINE_CODE

    fast_id = _get_fast_machine_id()
    if os.path.exists(HW_CACHE_FILE):
        try:
            with open(HW_CACHE_FILE, "r", encoding="utf-8") as handle:
                lines = handle.readlines()
            if len(lines) >= 2 and lines[0].strip() == "NUKE_HW_CACHE_V1":
                data = json.loads(base64.b64decode(lines[1].strip()).decode("utf-8"))
                if data.get("fast_id") == fast_id and data.get("heavy_id"):
                    _CACHED_MACHINE_CODE = data.get("heavy_id")
                    return _CACHED_MACHINE_CODE
        except Exception:
            pass

    mb_serial = _get_motherboard_serial()
    cpu_id = _get_cpu_id()
    if not mb_serial and not cpu_id:
        mb_serial = platform.node()
        cpu_id = platform.machine() + platform.processor()

    raw_string = "MB_S:%s___CPU_I:%s" % (mb_serial, cpu_id)
    hash1 = hashlib.sha256(raw_string.encode("utf-8")).hexdigest()
    hash2 = hashlib.md5((hash1 + "NukeRelinkerAuthV2").encode("utf-8")).hexdigest().upper()
    final_code = "%s-%s-%s-%s" % (hash2[:4], hash2[4:8], hash2[8:12], hash2[12:16])

    try:
        os.makedirs(os.path.dirname(HW_CACHE_FILE), exist_ok=True)
        raw_data = {"fast_id": fast_id, "heavy_id": final_code}
        obfuscated_data = base64.b64encode(json.dumps(raw_data).encode("utf-8")).decode("utf-8")
        with open(HW_CACHE_FILE, "w", encoding="utf-8") as handle:
            handle.write("NUKE_HW_CACHE_V1\n")
            handle.write(obfuscated_data)
            handle.write("\nEND_OF_CACHE")
    except Exception:
        pass

    _CACHED_MACHINE_CODE = final_code
    return final_code


def generate_key(machine_code: str) -> str:
    code = str(machine_code or "").strip().upper()
    signature = hmac.new(_get_dynamic_salt(), code.encode("utf-8"), hashlib.sha512).hexdigest().upper()
    return "%s-%s-%s-%s-%s-%s" % (
        signature[:4],
        signature[4:8],
        signature[8:12],
        signature[12:16],
        signature[16:20],
        signature[20:24],
    )


def _normalize_key(value: Optional[str]) -> str:
    return str(value or "").strip().upper()


def _read_legacy_key() -> str:
    if not os.path.exists(LEGACY_LICENSE_FILE):
        return ""
    try:
        with open(LEGACY_LICENSE_FILE, "r", encoding="utf-8") as handle:
            lines = handle.readlines()
        if len(lines) >= 2 and lines[0].strip() == "NUKE_SYSTEM_CACHE_V2_OBJ":
            data = json.loads(base64.b64decode(lines[1].strip()).decode("utf-8"))
            if isinstance(data, dict):
                return _normalize_key(data.get("k", ""))
    except Exception:
        pass
    return ""


def save_legacy_license(key: str) -> bool:
    try:
        raw_data = {"k": _normalize_key(key), "m": get_machine_code()}
        obfuscated_data = base64.b64encode(json.dumps(raw_data).encode("utf-8")).decode("utf-8")
        directory = os.path.dirname(LEGACY_LICENSE_FILE)
        os.makedirs(directory, exist_ok=True)
        fd, temp_path = tempfile.mkstemp(prefix=".tmp_auth_", suffix=".cache", dir=directory)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as handle:
                handle.write("NUKE_SYSTEM_CACHE_V2_OBJ\n")
                handle.write(obfuscated_data)
                handle.write("\nEND_OF_CACHE")
            os.replace(temp_path, LEGACY_LICENSE_FILE)
            return True
        except Exception:
            try:
                os.remove(temp_path)
            except OSError:
                pass
            return False
    except Exception:
        return False


def _der_read_length(data: bytes, offset: int) -> Tuple[int, int]:
    first = data[offset]
    offset += 1
    if first < 0x80:
        return first, offset
    count = first & 0x7F
    length = int.from_bytes(data[offset:offset + count], "big")
    return length, offset + count


def _der_read_tlv(data: bytes, offset: int, expected_tag: Optional[int] = None) -> Tuple[bytes, int]:
    tag = data[offset]
    offset += 1
    if expected_tag is not None and tag != expected_tag:
        raise ValueError("unexpected der tag")
    length, offset = _der_read_length(data, offset)
    return data[offset:offset + length], offset + length


def _parse_public_key() -> Tuple[int, int]:
    body = "".join(line.strip() for line in PUBLIC_KEY_PEM.splitlines() if "KEY" not in line)
    spki = base64.b64decode(body)
    seq, _ = _der_read_tlv(spki, 0, 0x30)
    _, pos = _der_read_tlv(seq, 0, 0x30)
    bit_string, _ = _der_read_tlv(seq, pos, 0x03)
    rsa_der = bit_string[1:]
    rsa_seq, _ = _der_read_tlv(rsa_der, 0, 0x30)
    modulus_bytes, pos = _der_read_tlv(rsa_seq, 0, 0x02)
    exponent_bytes, _ = _der_read_tlv(rsa_seq, pos, 0x02)
    return int.from_bytes(modulus_bytes, "big"), int.from_bytes(exponent_bytes, "big")


def _verify_signature(payload: dict, signature_b64: str) -> bool:
    try:
        modulus, exponent = _parse_public_key()
        signature = base64.b64decode(signature_b64)
        key_len = (modulus.bit_length() + 7) // 8
        encoded = pow(int.from_bytes(signature, "big"), exponent, modulus).to_bytes(key_len, "big")
        digest = hashlib.sha256(
            json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
        ).digest()
        digest_info = bytes.fromhex("3031300d060960864801650304020105000420") + digest
        return encoded.startswith(b"\x00\x01") and b"\x00" in encoded[2:] and encoded.endswith(digest_info)
    except Exception:
        return False


def _validate_online_license(license_data: dict) -> bool:
    if not isinstance(license_data, dict):
        return False
    payload = license_data.get("payload")
    signature = license_data.get("signature")
    if not isinstance(payload, dict) or not isinstance(signature, str):
        return False
    if payload.get("product") != PRODUCT_ID:
        return False
    if payload.get("machine") != get_machine_code():
        return False
    expiry = str(payload.get("expiry") or "").strip()
    if expiry:
        try:
            from datetime import datetime
            if datetime.now() > datetime.strptime(expiry[:10], "%Y-%m-%d"):
                return False
        except Exception:
            return False
    return _verify_signature(payload, signature)


def _load_online_license() -> Optional[dict]:
    try:
        with open(ONLINE_LICENSE_FILE, "r", encoding="utf-8") as handle:
            data = json.load(handle)
        return data if isinstance(data, dict) else None
    except Exception:
        return None


def save_online_license(license_data: dict) -> bool:
    if not _validate_online_license(license_data):
        return False
    try:
        os.makedirs(LICENSE_DIR, exist_ok=True)
        fd, temp_path = tempfile.mkstemp(prefix="license_", suffix=".json", dir=LICENSE_DIR)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as handle:
                json.dump(license_data, handle, ensure_ascii=False, indent=2)
            os.replace(temp_path, ONLINE_LICENSE_FILE)
            return True
        except Exception:
            try:
                os.remove(temp_path)
            except OSError:
                pass
            return False
    except Exception:
        return False


def activate_online(card_key: str) -> Tuple[bool, str]:
    card_key = str(card_key or "").strip()
    if not card_key:
        return False, "请输入淘宝卡密。"

    body = json.dumps({"key": card_key, "hwid": get_machine_code(), "product": PRODUCT_ID}).encode("utf-8")
    request = urllib.request.Request(
        API_URL,
        data=body,
        headers={"Content-Type": "application/json", "User-Agent": "MissingRelinker/1.0"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=15) as response:
            result = json.loads(response.read().decode("utf-8"))
    except urllib.error.URLError as exc:
        return False, "无法连接授权服务器：%s" % exc
    except Exception as exc:
        return False, "在线激活请求失败：%s" % exc

    if result.get("status") != "success":
        return False, str(result.get("msg") or "在线激活失败")

    license_data = result.get("license_data")
    if not save_online_license(license_data):
        return False, "云端证书校验或保存失败，请联系售后。"

    return True, "在线激活成功，授权已绑定当前设备。"


def activate_offline(offline_key: str) -> Tuple[bool, str]:
    offline_key = _normalize_key(offline_key)
    if not offline_key:
        return False, "请输入离线授权码。"
    if not hmac.compare_digest(offline_key, _normalize_key(generate_key(get_machine_code()))):
        return False, "离线授权码不正确，请检查机器码是否匹配。"
    if not save_legacy_license(offline_key):
        return False, "授权码正确，但写入本地授权文件失败。"
    return True, "离线授权成功。"


def check_license() -> Tuple[bool, str]:
    if getattr(sys, "gettrace", None) and sys.gettrace() is not None:
        return False, "DEBUGGER_DETECTED_FAILED"

    machine_code = get_machine_code()
    online_license = _load_online_license()
    if online_license and _validate_online_license(online_license):
        return True, machine_code

    legacy_key = _read_legacy_key()
    if legacy_key and hmac.compare_digest(legacy_key, _normalize_key(generate_key(machine_code))):
        return True, machine_code

    return False, machine_code


def _resolve_qt():
    try:
        from PySide2 import QtCore, QtWidgets
        return QtCore, QtWidgets
    except Exception:
        pass
    try:
        from PySide6 import QtCore, QtWidgets
        return QtCore, QtWidgets
    except Exception:
        pass
    from PyQt5 import QtCore, QtWidgets
    return QtCore, QtWidgets


QtCore, QtWidgets = _resolve_qt()


def _show_message(message: str, parent=None) -> None:
    try:
        QtWidgets.QMessageBox.information(parent, "MissingRelinker", message)
    except Exception:
        print(message)


class ActivationDialog(QtWidgets.QDialog):
    def __init__(self, machine_code: str, parent: Optional[QtWidgets.QWidget] = None) -> None:
        super().__init__(parent)
        self.setObjectName("ActivationDialog")
        self.setWindowTitle("MissingRelinker 授权激活 - Nuke学习社")
        self.resize(560, 420)
        self.machine_code = machine_code

        self.setStyleSheet("""
            QDialog { background-color: #20242b; color: #e8edf5; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; }
            QLabel { color: #c9d1dc; font-size: 10pt; }
            QLineEdit { padding: 9px; border: 1px solid #4a5260; background: #11151b; color: #ffffff; border-radius: 5px; font-size: 10pt; }
            QPushButton { padding: 9px 14px; background: #2f80ed; color: white; border: none; border-radius: 6px; font-weight: bold; }
            QPushButton:hover { background: #4c95f5; }
            QTabWidget::pane { border: 1px solid #3a4250; border-radius: 6px; top: -1px; }
            QTabBar::tab { background: #151a21; color: #9aa4b2; padding: 9px 18px; border: 1px solid #333b46; border-bottom: none; }
            QTabBar::tab:selected { color: #ffffff; background: #263143; }
        """)

        layout = QtWidgets.QVBoxLayout(self)
        layout.setSpacing(14)
        layout.setContentsMargins(28, 28, 28, 28)

        title = QtWidgets.QLabel("MissingRelinker 素材重连插件")
        title.setAlignment(QtCore.Qt.AlignCenter)
        title.setStyleSheet("font-size: 17pt; font-weight: bold; color: #ffffff;")
        layout.addWidget(title)

        desc = QtWidgets.QLabel("支持在线淘宝卡密激活，也保留离线授权码激活。")
        desc.setAlignment(QtCore.Qt.AlignCenter)
        desc.setStyleSheet("color: #8fa0b6;")
        layout.addWidget(desc)

        layout.addWidget(QtWidgets.QLabel("当前机器码："))
        code_row = QtWidgets.QHBoxLayout()
        self.code_edit = QtWidgets.QLineEdit(self.machine_code)
        self.code_edit.setReadOnly(True)
        self.code_edit.setStyleSheet("color: #f9a825; font-weight: bold; background: #101318;")
        copy_btn = QtWidgets.QPushButton("复制")
        copy_btn.setFixedWidth(86)
        copy_btn.clicked.connect(self.copy_code)
        code_row.addWidget(self.code_edit)
        code_row.addWidget(copy_btn)
        layout.addLayout(code_row)

        tabs = QtWidgets.QTabWidget()
        tabs.addTab(self._build_online_tab(), "在线激活")
        tabs.addTab(self._build_offline_tab(), "离线授权")
        layout.addWidget(tabs)

        note = QtWidgets.QLabel("提示：公司内网无法联网时，请把机器码发给售后换取离线授权码。")
        note.setAlignment(QtCore.Qt.AlignCenter)
        note.setStyleSheet("color: #6f7b8a; font-size: 9pt;")
        layout.addWidget(note)

    def _build_online_tab(self):
        page = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(page)
        layout.setContentsMargins(18, 18, 18, 18)
        layout.setSpacing(12)
        layout.addWidget(QtWidgets.QLabel("请输入购买后获得的淘宝卡密："))
        self.online_key_input = QtWidgets.QLineEdit()
        self.online_key_input.setPlaceholderText("例如：RELINK-XXXXXXXXXXXX")
        layout.addWidget(self.online_key_input)
        btn = QtWidgets.QPushButton("在线激活并绑定当前设备")
        btn.clicked.connect(self.verify_online)
        layout.addWidget(btn)
        layout.addStretch()
        return page

    def _build_offline_tab(self):
        page = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout(page)
        layout.setContentsMargins(18, 18, 18, 18)
        layout.setSpacing(12)
        layout.addWidget(QtWidgets.QLabel("请输入售后生成的离线授权码："))
        self.offline_key_input = QtWidgets.QLineEdit()
        self.offline_key_input.setPlaceholderText("例如：XXXX-XXXX-XXXX-XXXX-XXXX-XXXX")
        layout.addWidget(self.offline_key_input)
        btn = QtWidgets.QPushButton("验证离线授权")
        btn.clicked.connect(self.verify_offline)
        layout.addWidget(btn)
        layout.addStretch()
        return page

    def copy_code(self) -> None:
        clipboard = QtWidgets.QApplication.clipboard()
        if clipboard:
            clipboard.setText(self.machine_code)
        _show_message("机器码已复制。", self)

    def verify_online(self) -> None:
        ok, msg = activate_online(self.online_key_input.text())
        _show_message(msg, self)
        if ok:
            self.accept()

    def verify_offline(self) -> None:
        ok, msg = activate_offline(self.offline_key_input.text())
        _show_message(msg, self)
        if ok:
            self.accept()
