"""sellkeys Auth – Python SDK

    pip install requests cryptography

    from sellkeys_auth import SellkeysAuth, SellkeysAuthError

    auth = SellkeysAuth("APP_ID", "PUBLIC_KEY", "1.0")
    try:
        auth.init()
        lic = auth.license(input("License key: "))
        print("Welcome! Level", lic["level"], "expires", lic["expires_at"])
    except SellkeysAuthError as e:
        print("Login failed:", e.code, e)

Every answer from sellkeys is signed with your app's Ed25519 key. The SDK rejects answers with a wrong signature or a
replayed nonce, so a fake server cannot unlock your program. (It does not protect against patching your program.)
"""
import base64
import hashlib
import json
import os
import platform
import secrets
import subprocess
import sys
import uuid

import requests
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

API_URL = "https://auth.sellkeys.cc/v1"


class SellkeysAuthError(Exception):
    def __init__(self, code, message, data=None):
        super().__init__(message)
        self.code = code
        self.data = data or {}  # full server answer, e.g. data["update_url"] for code "update_required"


def default_hwid():
    """Stable per-machine id (hashed, the raw value never leaves the machine)."""
    raw = ""
    try:
        if sys.platform == "win32":
            import winreg
            with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography", 0, winreg.KEY_READ | winreg.KEY_WOW64_64KEY) as k:
                raw = winreg.QueryValueEx(k, "MachineGuid")[0]
        elif sys.platform == "darwin":
            out = subprocess.run(["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], capture_output=True, text=True, timeout=5).stdout
            raw = next((line.split('"')[-2] for line in out.splitlines() if "IOPlatformUUID" in line), "")
        else:
            for p in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
                if os.path.exists(p):
                    raw = open(p).read().strip()
                    break
    except Exception:
        raw = ""
    raw = raw or f"{uuid.getnode()}-{platform.node()}"
    return hashlib.sha256(raw.encode()).hexdigest()


def file_hash(path=None):
    """SHA-256 of your program file (for the app's hash check). Defaults to the frozen executable / main script."""
    path = path or (sys.executable if getattr(sys, "frozen", False) else os.path.abspath(sys.argv[0]))
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


class SellkeysAuth:
    def __init__(self, app_id, public_key, version, api_url=API_URL, hwid=None, timeout=15):
        self.app_id = app_id
        self.version = version
        self.api_url = api_url
        self.hwid = hwid or default_hwid()
        self.timeout = timeout
        self._pub = Ed25519PublicKey.from_public_bytes(base64.b64decode(public_key))
        self.session = None
        self.app = None
        self.license_info = None
        self.user = None

    def _call(self, type_, **fields):
        nonce = secrets.token_hex(16)
        data = {"type": type_, "app": self.app_id, "nonce": nonce, **{k: v for k, v in fields.items() if v is not None}}
        try:
            r = requests.post(self.api_url, data=data, timeout=self.timeout)
        except requests.RequestException as e:
            raise SellkeysAuthError("network_error", f"Could not reach the license server: {e}") from e
        sig = r.headers.get("X-Sellkeys-Signature")
        if not sig:  # only errors before the app is known (rate limit, unknown app) are unsigned
            try:
                body = r.json()
            except ValueError:
                body = {}
            raise SellkeysAuthError(body.get("code", f"http_{r.status_code}"), body.get("message", "Unexpected server response."))
        try:
            self._pub.verify(base64.b64decode(sig), r.content)
        except (InvalidSignature, ValueError) as e:
            raise SellkeysAuthError("bad_signature", "The server response is not signed by sellkeys.") from e
        body = json.loads(r.content)
        if body.get("nonce") != nonce:
            raise SellkeysAuthError("bad_nonce", "The server response does not belong to this request.")
        if not body.get("success"):
            raise SellkeysAuthError(body.get("code", "error"), body.get("message", "Request failed."), body)
        return body

    def init(self, hash=None):
        """Starts a session. Raises with code 'update_required' (see .update_url) if your version is too old."""
        body = self._call("init", version=self.version, hash=hash)
        self.session = body["session"]
        self.app = body.get("app")
        return self.app

    def license(self, key):
        if not self.session:
            self.init()
        body = self._call("license", session=self.session, key=key.strip(), hwid=self.hwid)
        self.license_info = body["license"]
        return self.license_info

    def check(self):
        """Call periodically: raises if the license/account was banned, expired or the session ended."""
        body = self._call("check", session=self.session)
        if "user" in body:
            self.user = body["user"]
            return self.user
        self.license_info = body["license"]
        return self.license_info

    # ---- accounts (username + password; the key is redeemed to the account) ----
    def register(self, username, password, key):
        if not self.session:
            self.init()
        body = self._call("register", session=self.session, username=username, password=password, key=key.strip(), hwid=self.hwid)
        self.user = body["user"]
        return self.user

    def login(self, username, password):
        if not self.session:
            self.init()
        body = self._call("login", session=self.session, username=username, password=password, hwid=self.hwid)
        self.user = body["user"]
        return self.user

    def upgrade(self, username, key):
        """Adds another license (e.g. a renewal or higher level) to an existing account."""
        if not self.session:
            self.init()
        return self._call("upgrade", session=self.session, username=username, key=key.strip())["user"]

    # ---- content ----
    def var(self, name):
        """Global app variable (set in the dashboard)."""
        return self._call("var", session=self.session, name=name)["value"]

    def get_user_var(self, name):
        return self._call("getvar", session=self.session, name=name)["value"]

    def set_user_var(self, name, value):
        self._call("setvar", session=self.session, name=name, value=str(value))

    def download(self, file_id, path=None):
        """Downloads an app file; the signed answer carries its SHA-256, which is checked. Returns bytes (or writes `path`)."""
        info = self._call("file", session=self.session, file=file_id)
        try:
            r = requests.get(info["url"], timeout=max(self.timeout, 120))
            r.raise_for_status()
        except requests.RequestException as e:
            raise SellkeysAuthError("network_error", f"Download failed: {e}") from e
        if hashlib.sha256(r.content).hexdigest() != info["sha256"]:
            raise SellkeysAuthError("bad_file_hash", "The downloaded file does not match its signed checksum.")
        if path:
            with open(path, "wb") as f:
                f.write(r.content)
        return r.content

    def webhook(self, name, params="", body=None, content_type=None):
        """Calls a webhook set in the dashboard through sellkeys (its URL and keys stay on the server). Returns (status, text)."""
        r = self._call("webhook", session=self.session, webid=name, params=params or None, body=body, content_type=content_type)
        return r["status"], r["response"]

    def log(self, message):
        """Writes a line to the app's logs in the sellkeys dashboard."""
        self._call("log", session=self.session, message=str(message)[:500])

    def logout(self):
        if self.session:
            self._call("logout", session=self.session)
            self.session = None
