from __future__ import annotations

import hashlib
import os
import platform
import re
import subprocess
import uuid
from functools import lru_cache
from typing import Iterable

from ._exceptions import HWIDError


_FALLBACK_ID_PATH = os.path.join(
    os.path.expanduser("~"),
    ".novalib_hwid_fallback",
)


def _safe_run(cmd: list[str] | str, *, shell: bool = False) -> str:
    try:
        out = subprocess.check_output(
            cmd,
            shell=shell,
            stderr=subprocess.DEVNULL,
            timeout=3,
        )
        return out.decode(errors="ignore").strip()
    except Exception:
        return ""


def _filter_tokens(tokens: Iterable[str]) -> list[str]:
    junk_substrings = (
        "To Be Filled",
        "Default string",
        "None",
        "00000000",
        "0000000000000000",
        "Not Specified",
        "System Serial Number",
        "N/A",
        "Unknown",
    )
    clean: list[str] = []
    seen: set[str] = set()

    for token in tokens:
        if not token:
            continue
        token = token.strip()
        if not token:
            continue

        token = re.sub(r"\s+", " ", token)

        if any(junk.lower() in token.lower() for junk in junk_substrings):
            continue
        if token in seen:
            continue
        seen.add(token)
        clean.append(token)

    return clean


def _read_text_file(path: str) -> str:
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            return f.read().strip()
    except Exception:
        return ""

def _get_windows_tokens() -> list[str]:
    tokens: list[str] = []

    try:
        import winreg  # type: ignore
    except ImportError:
        winreg = None

    uuid_out = _safe_run([
        "powershell",
        "-NoProfile",
        "-Command",
        "(Get-CimInstance -ClassName Win32_ComputerSystemProduct).UUID",
    ])
    if uuid_out:
        tokens.append(uuid_out)

    if winreg is not None:
        def reg_read(root, path: str, value: str) -> str:
            try:
                with winreg.OpenKey(root, path) as key:
                    v, _ = winreg.QueryValueEx(key, value)
                    return str(v)
            except OSError:
                return ""

        bios_path = r"HARDWARE\DESCRIPTION\System\BIOS"
        cpu_path = r"HARDWARE\DESCRIPTION\System\CentralProcessor\0"
        tokens.extend([
            reg_read(winreg.HKEY_LOCAL_MACHINE, bios_path, "BaseBoardSerialNumber"),
            reg_read(winreg.HKEY_LOCAL_MACHINE, bios_path, "BIOSSerialNumber"),
            reg_read(winreg.HKEY_LOCAL_MACHINE, cpu_path, "ProcessorNameString"),
            reg_read(winreg.HKEY_LOCAL_MACHINE, cpu_path, "Identifier"),
        ])

    disk_serials = _safe_run([
        "powershell",
        "-NoProfile",
        "-Command",
        "Get-PhysicalDisk | Where-Object {$_.BusType -ne 'USB'} | "
        "Select-Object -ExpandProperty SerialNumber",
    ])
    if disk_serials:
        tokens.extend(line.strip() for line in disk_serials.splitlines() if line.strip())

    return tokens

def _get_linux_tokens() -> list[str]:
    tokens: list[str] = []
    for path in (
        "/sys/class/dmi/id/product_uuid",
        "/sys/class/dmi/id/board_serial",
        "/sys/class/dmi/id/product_serial",
        "/etc/machine-id",
        "/var/lib/dbus/machine-id",
    ):
        value = _read_text_file(path)
        if value:
            tokens.append(value)

    dmidecode = _safe_run(["dmidecode", "-s", "system-uuid"])
    if dmidecode:
        tokens.append(dmidecode)

    cpuinfo = _read_text_file("/proc/cpuinfo")
    if cpuinfo:
        match = re.search(r"^Serial\s*:\s*(.+)$", cpuinfo, flags=re.MULTILINE)
        if match:
            tokens.append(match.group(1).strip())

    return tokens

def _get_darwin_tokens() -> list[str]:
    tokens: list[str] = []

    out = _safe_run(
        "ioreg -l -d 1 -w 0 | grep -E 'IOPlatformUUID|IOPlatformSerialNumber'",
        shell=True,
    )
    if out:
        for line in out.splitlines():
            match = re.search(r'"([^"]+)"\s*=\s*"([^"]+)"', line)
            if match:
                key, value = match.groups()
                if key in ("IOPlatformUUID", "IOPlatformSerialNumber"):
                    tokens.append(value.strip())

    sp_out = _safe_run(["system_profiler", "SPHardwareDataType"])
    if sp_out:
        uuid_match = re.search(r"Hardware UUID:\s*([A-F0-9-]+)", sp_out, re.IGNORECASE)
        serial_match = re.search(
            r"Serial Number \(system\):\s*(\S+)", sp_out, re.IGNORECASE
        )
        if uuid_match:
            tokens.append(uuid_match.group(1).strip())
        if serial_match:
            tokens.append(serial_match.group(1).strip())

    scutil_uuid = _safe_run(["scutil", "--get", "HardwareUUID"])
    if scutil_uuid:
        tokens.append(scutil_uuid.strip())

    return tokens

def _get_bsd_tokens() -> list[str]:
    tokens: list[str] = []

    out = _safe_run(["kenv", "smbios.system.uuid"])
    if out:
        tokens.append(out)

    hostuuid = _safe_run(["sysctl", "-n", "kern.hostuuid"])
    if hostuuid:
        tokens.append(hostuuid)

    return tokens

def _collect_tokens() -> list[str]:
    os_type = platform.system()

    if os_type == "Windows":
        raw = _get_windows_tokens()
    elif os_type == "Linux":
        raw = _get_linux_tokens()
    elif os_type == "Darwin":
        raw = _get_darwin_tokens()
    else:
        raw = _get_bsd_tokens()

    tokens = _filter_tokens(raw)

    if not tokens:
        uname = platform.uname()
        generic_bits = [
            uname.system,
            uname.node,
            uname.release,
            uname.version,
            uname.machine,
            uname.processor,
        ]
        tokens = _filter_tokens(generic_bits)

    return tokens


def _get_or_create_fallback_id() -> str:
    try:
        if os.path.exists(_FALLBACK_ID_PATH):
            existing = _read_text_file(_FALLBACK_ID_PATH)
            if existing:
                return existing.strip()

        new_id = uuid.uuid4().hex
        os.makedirs(os.path.dirname(_FALLBACK_ID_PATH), exist_ok=True)
        with open(_FALLBACK_ID_PATH, "w", encoding="utf-8") as f:
            f.write(new_id)
        return new_id
    except Exception:
        return ""


@lru_cache(maxsize=1)
def get_hwid(*, algorithm: str = "sha256") -> str:
    tokens = _collect_tokens()

    if not tokens:
        fallback_id = _get_or_create_fallback_id()
        if not fallback_id:
            raise HWIDError("No hardware identifiers or fallback ID available for this device.")
        tokens = [fallback_id]

    normalized = "\x1f".join(tokens).encode("utf-8", errors="ignore")

    try:
        hasher = hashlib.new(algorithm)
    except ValueError as e:
        raise ValueError(f"Unsupported hash algorithm: {algorithm}") from e

    hasher.update(normalized)
    return hasher.hexdigest()


def get_hwid_components() -> list[str]:
    tokens = _collect_tokens()

    if not tokens:
        fallback_id = _get_or_create_fallback_id()
        if not fallback_id:
            raise HWIDError("No hardware identifiers or fallback ID available for this device.")
        return [fallback_id]

    return tokens