"""Small, dependency-free token storage for the Blender Add-on.

On Windows, access tokens are stored in Windows Credential Manager rather than
in Blender preferences or the .blend file. Other platforms deliberately keep
the token in process memory until a platform keychain backend is introduced.
This avoids silently writing a reusable account token in plaintext.
"""

import ctypes
import hashlib
import sys
from ctypes import wintypes


_CRED_TYPE_GENERIC = 1
_CRED_PERSIST_LOCAL_MACHINE = 2
_ERROR_NOT_FOUND = 1168
_MEMORY_TOKENS = {}


def _target_name(server_url):
    digest = hashlib.sha256(server_url.encode("utf-8")).hexdigest()
    return "3DRipper.Helper.{}".format(digest)


def _legacy_target_name(server_url):
    digest = hashlib.sha256(server_url.encode("utf-8")).hexdigest()
    return "3DRipper.SketchfabOSGJSImporter.{}".format(digest)


def _windows_api():
    class CREDENTIALW(ctypes.Structure):
        _fields_ = [
            ("Flags", wintypes.DWORD),
            ("Type", wintypes.DWORD),
            ("TargetName", wintypes.LPWSTR),
            ("Comment", wintypes.LPWSTR),
            ("LastWritten", wintypes.FILETIME),
            ("CredentialBlobSize", wintypes.DWORD),
            ("CredentialBlob", ctypes.POINTER(ctypes.c_byte)),
            ("Persist", wintypes.DWORD),
            ("AttributeCount", wintypes.DWORD),
            ("Attributes", ctypes.c_void_p),
            ("TargetAlias", wintypes.LPWSTR),
            ("UserName", wintypes.LPWSTR),
        ]

    advapi32 = ctypes.WinDLL("Advapi32.dll", use_last_error=True)
    advapi32.CredWriteW.argtypes = [ctypes.POINTER(CREDENTIALW), wintypes.DWORD]
    advapi32.CredWriteW.restype = wintypes.BOOL
    advapi32.CredReadW.argtypes = [
        wintypes.LPCWSTR,
        wintypes.DWORD,
        wintypes.DWORD,
        ctypes.POINTER(ctypes.POINTER(CREDENTIALW)),
    ]
    advapi32.CredReadW.restype = wintypes.BOOL
    advapi32.CredDeleteW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD]
    advapi32.CredDeleteW.restype = wintypes.BOOL
    advapi32.CredFree.argtypes = [ctypes.c_void_p]
    advapi32.CredFree.restype = None
    return CREDENTIALW, advapi32


def _write_windows_token(server_url, token):
    credential_type, advapi32 = _windows_api()
    blob = token.encode("utf-16-le")
    blob_buffer = ctypes.create_string_buffer(blob)
    credential = credential_type()
    credential.Type = _CRED_TYPE_GENERIC
    credential.TargetName = _target_name(server_url)
    credential.CredentialBlobSize = len(blob)
    credential.CredentialBlob = ctypes.cast(blob_buffer, ctypes.POINTER(ctypes.c_byte))
    credential.Persist = _CRED_PERSIST_LOCAL_MACHINE
    credential.UserName = "3DRipper Helper"
    if not advapi32.CredWriteW(ctypes.byref(credential), 0):
        raise OSError(ctypes.get_last_error(), "CredWriteW failed")


def _read_windows_target(target_name):
    credential_type, advapi32 = _windows_api()
    credential_pointer = ctypes.POINTER(credential_type)()
    if not advapi32.CredReadW(
        target_name, _CRED_TYPE_GENERIC, 0, ctypes.byref(credential_pointer)
    ):
        error = ctypes.get_last_error()
        if error == _ERROR_NOT_FOUND:
            return None
        raise OSError(error, "CredReadW failed")

    try:
        credential = credential_pointer.contents
        if not credential.CredentialBlob or not credential.CredentialBlobSize:
            return None
        value = ctypes.string_at(credential.CredentialBlob, credential.CredentialBlobSize)
        return value.decode("utf-16-le")
    finally:
        advapi32.CredFree(credential_pointer)


def _delete_windows_target(target_name):
    _credential_type, advapi32 = _windows_api()
    if not advapi32.CredDeleteW(target_name, _CRED_TYPE_GENERIC, 0):
        error = ctypes.get_last_error()
        if error != _ERROR_NOT_FOUND:
            raise OSError(error, "CredDeleteW failed")


def _read_windows_token(server_url):
    token = _read_windows_target(_target_name(server_url))
    if token:
        return token
    token = _read_windows_target(_legacy_target_name(server_url))
    if token:
        _write_windows_token(server_url, token)
        _delete_windows_target(_legacy_target_name(server_url))
    return token


def _delete_windows_token(server_url):
    _delete_windows_target(_target_name(server_url))
    _delete_windows_target(_legacy_target_name(server_url))


def store_token(server_url, token):
    """Store a token without ever persisting it to Blender preferences."""
    _MEMORY_TOKENS[server_url] = token
    if sys.platform == "win32":
        try:
            _write_windows_token(server_url, token)
        except OSError:
            # Keep the current session working if an enterprise policy disables
            # Credential Manager. The fallback intentionally is not persisted.
            pass


def load_token(server_url):
    token = _MEMORY_TOKENS.get(server_url)
    if token:
        return token
    if sys.platform == "win32":
        try:
            token = _read_windows_token(server_url)
        except OSError:
            return None
        if token:
            _MEMORY_TOKENS[server_url] = token
        return token
    return None


def clear_token(server_url):
    _MEMORY_TOKENS.pop(server_url, None)
    if sys.platform == "win32":
        try:
            _delete_windows_token(server_url)
        except OSError:
            pass


def storage_description():
    if sys.platform == "win32":
        return "Windows Credential Manager"
    return "this Blender session only"
