#!/usr/bin/env python3
"""
License Client Example
----------------------
A ready-to-run Python client for the License API.

Usage:
    python license_client.py

The script will:
  1. Extract a stable HWID (hardware ID) for this machine.
  2. Prompt for username and password.
  3. POST /api/login with the credentials + HWID.
  4. If login succeeds, POST /api/check with the returned token.
  5. Print remaining rental time and block access if expired or invalid.

Configuration:
    Set API_BASE_URL below or via the LICENSE_API_BASE_URL environment variable.

Dependencies:
    pip install requests

Windows note:
    machine-id may not be available on some Windows editions. In that case the
    client falls back to a composite ID built from platform, machine name, and
    a stable UUID derived from the user's SID / home path.
"""

from __future__ import annotations

import hashlib
import json
import os
import platform
import sys
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional

import requests

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

API_BASE_URL = os.environ.get(
    "LICENSE_API_BASE_URL",
    "https://kongkandit.poke.site",  # Replace with your deployed API host.
)

# If true, the script will prompt for credentials interactively.
# If false, fill in USERNAME and PASSWORD below.
INTERACTIVE = True

# Hard-coded credentials (used only when INTERACTIVE is False).
USERNAME = ""
PASSWORD = ""

# ---------------------------------------------------------------------------
# HWID extraction
# ---------------------------------------------------------------------------


def get_machine_id() -> str:
    """
    Return a stable, machine-bound identifier.

    Order of preference:
      1. /etc/machine-id (Linux, systemd)
      2. /var/lib/dbus/machine-id (Linux, legacy)
      3. Windows registry MachineGuid (best-effort)
      4. macOS IOPlatformUUID (best-effort)
      5. Composite fallback (platform + node + user-bound UUID)
    """
    candidates = [
        _read_linux_machine_id,
        _read_windows_machine_guid,
        _read_macos_platform_uuid,
    ]
    for fn in candidates:
        value = fn()
        if value:
            return value
    return _composite_fallback()


def _read_linux_machine_id() -> Optional[str]:
    """Read systemd or D-Bus machine ID on Linux."""
    for path in ("/etc/machine-id", "/var/lib/dbus/machine-id"):
        try:
            text = Path(path).read_text(encoding="utf-8").strip()
            if text and text != "00000000000000000000000000000000":
                return text
        except OSError:
            continue
    return None


def _read_windows_machine_guid() -> Optional[str]:
    """Read the Windows MachineGuid from the registry."""
    if platform.system() != "Windows":
        return None
    try:
        import winreg  # type: ignore[import-not-found]

        with winreg.OpenKey(
            winreg.HKEY_LOCAL_MACHINE,
            r"SOFTWARE\Microsoft\Cryptography",
            0,
            winreg.KEY_READ,
        ) as key:
            value, _ = winreg.QueryValueEx(key, "MachineGuid")
            if value:
                return str(value).strip()
    except Exception:
        pass
    return None


def _read_macos_platform_uuid() -> Optional[str]:
    """Read the IOPlatformUUID on macOS."""
    if platform.system() != "Darwin":
        return None
    import subprocess

    try:
        result = subprocess.run(
            ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
            capture_output=True,
            text=True,
            check=True,
        )
        for line in result.stdout.splitlines():
            if "IOPlatformUUID" in line:
                # "IOPlatformUUID" = "xxxxx"
                parts = line.split('"')
                if len(parts) >= 4:
                    return parts[3].strip()
    except Exception:
        pass
    return None


def _composite_fallback() -> str:
    """
    Build a deterministic, machine-specific identifier from components that are
    usually stable per machine + user profile.
    """
    components = [
        platform.system(),
        platform.release(),
        platform.node(),
        platform.processor(),
        os.getlogin() if hasattr(os, "getlogin") else "",
        str(Path.home()),
    ]
    seed = "|".join(components).encode("utf-8")
    return uuid.uuid5(uuid.NAMESPACE_OID, seed.hex()).hex


def hash_hwid(raw: str) -> str:
    """
    Hash the raw machine ID to a shorter, opaque HWID string.
    The server stores only this hash, so the raw machine ID stays private.
    """
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:32]


# ---------------------------------------------------------------------------
# API client
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ApiResponse:
    success: bool
    data: dict


class LicenseClient:
    def __init__(self, base_url: str) -> None:
        self.base_url = base_url.rstrip("/")

    def _post(self, path: str, payload: dict) -> requests.Response:
        url = f"{self.base_url}{path}"
        try:
            return requests.post(url, json=payload, timeout=15)
        except requests.RequestException as exc:
            print(f"[ERROR] Network request failed: {exc}")
            sys.exit(1)

    def login(self, username: str, password: str, hwid: str) -> ApiResponse:
        """Call /api/login and return the parsed response."""
        resp = self._post("/api/login", {"username": username, "password": password, "hwid": hwid})
        try:
            data = resp.json()
        except json.JSONDecodeError:
            data = {"success": False, "error": {"code": "invalid_json", "message": resp.text}}
        return ApiResponse(data.get("success", False), data)

    def check(self, token: str, hwid: str) -> ApiResponse:
        """Call /api/check and return the parsed response."""
        resp = self._post("/api/check", {"token": token, "hwid": hwid})
        try:
            data = resp.json()
        except json.JSONDecodeError:
            data = {"success": False, "error": {"code": "invalid_json", "message": resp.text}}
        return ApiResponse(data.get("success", False), data)


# ---------------------------------------------------------------------------
# Formatting helpers
# ---------------------------------------------------------------------------


def format_remaining(ms: int) -> str:
    """Convert milliseconds to a human-readable remaining-time string."""
    if ms <= 0:
        return "expired"
    delta = timedelta(milliseconds=ms)
    days = delta.days
    hours, remainder = divmod(delta.seconds, 3600)
    minutes, _ = divmod(remainder, 60)
    parts = []
    if days:
        parts.append(f"{days}d")
    if hours:
        parts.append(f"{hours}h")
    if minutes:
        parts.append(f"{minutes}m")
    return " ".join(parts) if parts else "< 1 minute"


def prompt_credentials() -> tuple[str, str]:
    """Read username and password from the terminal."""
    import getpass

    user = input("Username: ").strip()
    pw = getpass.getpass("Password: ").strip()
    return user, pw


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------


def main() -> int:
    raw_machine_id = get_machine_id()
    hwid = hash_hwid(raw_machine_id)

    print(f"License client starting…")
    print(f"API host: {API_BASE_URL}")
    print(f"HWID: {hwid}")
    print()

    if INTERACTIVE:
        username, password = prompt_credentials()
    else:
        username, password = USERNAME, PASSWORD

    if not username or not password:
        print("[ERROR] Username and password are required.")
        return 1

    client = LicenseClient(API_BASE_URL)

    login_result = client.login(username, password, hwid)
    if not login_result.success:
        error = login_result.data.get("error", {})
        print(f"[ACCESS DENIED] {error.get('code', 'unknown')}")
        print(f"{error.get('message', 'Login failed.')}")
        return 1

    token = login_result.data.get("token")
    expires_at = login_result.data.get("expires_at", 0)
    remaining_ms = login_result.data.get("remaining_ms", 0)

    print(f"[LOGIN OK] Token acquired.")
    print(f"Expires: {datetime.fromtimestamp(expires_at / 1000).isoformat()}")
    print(f"Remaining: {format_remaining(remaining_ms)}")
    print()

    # Now verify the token with /api/check to demonstrate the full flow.
    check_result = client.check(token, hwid)
    if not check_result.success:
        error = check_result.data.get("error", {})
        print(f"[CHECK FAILED] {error.get('code', 'unknown')}")
        print(f"{error.get('message', 'License check failed.')}")
        return 1

    check_remaining_ms = check_result.data.get("remaining_ms", 0)
    print(f"[CHECK OK] License is active.")
    print(f"Remaining: {format_remaining(check_remaining_ms)}")
    print()
    print("Access granted. You may now run the licensed application.")
    return 0


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