import ctypes
import ctypes.util
import platform


_libcrypto = None
_signatures_set = False
_LOAD_ERROR = None

_USE_LEGACY_CTX = False

_LEGACY_CTX_BUF_SIZE = 4096


def _candidate_paths():
    sysname = platform.system().lower()
    cands = []

    try:
        name = ctypes.util.find_library('crypto')
        if name:
            cands.append(name)
    except Exception:
        pass

    if 'aix' in sysname:
        import struct as _struct
        is_64bit = _struct.calcsize("P") == 8
        archives = [
            '/usr/lib/libcrypto.a',
            '/opt/freeware/lib/libcrypto.a',
            '/usr/local/lib/libcrypto.a',
        ]
        members = []
        if is_64bit:
            members.extend([
                'libcrypto64.so.3',
                'libcrypto64.so.1.1',
                'libcrypto64.so.1.0.2',
                'libcrypto64.so.1.0.0',
                'libcrypto64.so',
            ])
        members.extend([
            'libcrypto.so.3',
            'libcrypto.so.1.1',
            'libcrypto.so.1.0.2',
            'libcrypto.so.1.0.0',
            'libcrypto.so',
        ])
        for ar in archives:
            for mem in members:
                cands.append('%s(%s)' % (ar, mem))
    elif 'hp-ux' in sysname or 'hpux' in sysname:
        import struct as _struct
        is_64bit = _struct.calcsize("P") == 8
        if is_64bit:
            cands.extend([
            ])
        else:
            cands.extend([
            ])
        cands.extend([
        ])
    elif 'sunos' in sysname or 'solaris' in sysname:
        cands.extend([
            '/usr/lib/64/libcrypto.so',
            '/usr/lib/libcrypto.so',
            '/usr/sfw/lib/64/libcrypto.so',
            '/usr/sfw/lib/libcrypto.so',
            '/opt/csw/lib/libcrypto.so',
        ])
    elif 'darwin' in sysname:
        cands.extend([
            '/usr/lib/libcrypto.dylib',
            '/opt/homebrew/lib/libcrypto.dylib',
            '/usr/local/opt/openssl/lib/libcrypto.dylib',
            '/usr/local/lib/libcrypto.dylib',
        ])
    else:
        cands.extend([
            'libcrypto.so.3',
            'libcrypto.so.1.1',
            'libcrypto.so.1.0.0',
            'libcrypto.so.10',
            'libcrypto.so',
        ])

    return cands, sysname


_AIX_RTLD_NOW = 0x00000002
_AIX_RTLD_MEMBER = 0x00040000


def _try_load():
    global _libcrypto, _LOAD_ERROR
    if _libcrypto is not None:
        return _libcrypto

    candidates, sysname = _candidate_paths()
    is_aix = 'aix' in sysname
    rtld_member_val = getattr(ctypes, 'RTLD_MEMBER', None)
    if rtld_member_val is None:
        rtld_member_val = _AIX_RTLD_MEMBER
    rtld_now_val = getattr(ctypes, 'RTLD_NOW', None)
    if rtld_now_val is None:
        rtld_now_val = _AIX_RTLD_NOW

    attempts = []  # list of (path, error) for diagnostics

    for cand in candidates:
        try:
            if is_aix and '(' in cand:
                lib = ctypes.CDLL(cand, mode=rtld_now_val | rtld_member_val)
            else:
                lib = ctypes.CDLL(cand)
            _libcrypto = lib
            return lib
        except OSError as e:
            attempts.append((cand, str(e)))
            continue
        except Exception as e:
            attempts.append((cand, "%s: %s" % (type(e).__name__, e)))
            continue

    if attempts:
        tail = attempts[-5:]
        _LOAD_ERROR = "tried %d paths, last failures: %s" % (
            len(attempts),
            "; ".join("%s -> %s" % (p, e) for p, e in tail),
        )
    else:
        _LOAD_ERROR = "no candidate paths to try"
    return None


def _setup_signatures(lib):
    global _USE_LEGACY_CTX

    has_new = hasattr(lib, 'EVP_CIPHER_CTX_new')
    has_init = hasattr(lib, 'EVP_CIPHER_CTX_init')

    if has_new:
        lib.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p
        lib.EVP_CIPHER_CTX_new.argtypes = []
        lib.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p]
        lib.EVP_CIPHER_CTX_free.restype = None
        _USE_LEGACY_CTX = False
    elif has_init:
        lib.EVP_CIPHER_CTX_init.argtypes = [ctypes.c_void_p]
        lib.EVP_CIPHER_CTX_init.restype = None
        if hasattr(lib, 'EVP_CIPHER_CTX_cleanup'):
            lib.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p]
            lib.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int
        _USE_LEGACY_CTX = True
    else:
        raise RuntimeError(
            "neither EVP_CIPHER_CTX_new nor EVP_CIPHER_CTX_init available")

    lib.EVP_aes_128_ecb.restype = ctypes.c_void_p
    lib.EVP_aes_128_ecb.argtypes = []
    lib.EVP_aes_192_ecb.restype = ctypes.c_void_p
    lib.EVP_aes_192_ecb.argtypes = []
    lib.EVP_aes_256_ecb.restype = ctypes.c_void_p
    lib.EVP_aes_256_ecb.argtypes = []

    lib.EVP_EncryptInit_ex.argtypes = [
        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
        ctypes.c_char_p, ctypes.c_char_p,
    ]
    lib.EVP_EncryptInit_ex.restype = ctypes.c_int
    lib.EVP_EncryptUpdate.argtypes = [
        ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int),
        ctypes.c_char_p, ctypes.c_int,
    ]
    lib.EVP_EncryptUpdate.restype = ctypes.c_int
    lib.EVP_EncryptFinal_ex.argtypes = [
        ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int),
    ]
    lib.EVP_EncryptFinal_ex.restype = ctypes.c_int

    lib.EVP_DecryptInit_ex.argtypes = [
        ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p,
        ctypes.c_char_p, ctypes.c_char_p,
    ]
    lib.EVP_DecryptInit_ex.restype = ctypes.c_int
    lib.EVP_DecryptUpdate.argtypes = [
        ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int),
        ctypes.c_char_p, ctypes.c_int,
    ]
    lib.EVP_DecryptUpdate.restype = ctypes.c_int
    lib.EVP_DecryptFinal_ex.argtypes = [
        ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_int),
    ]
    lib.EVP_DecryptFinal_ex.restype = ctypes.c_int

    lib.EVP_CIPHER_CTX_set_padding.argtypes = [ctypes.c_void_p, ctypes.c_int]
    lib.EVP_CIPHER_CTX_set_padding.restype = ctypes.c_int


def is_available():
    global _signatures_set
    lib = _try_load()
    if lib is None:
        return False
    if not _signatures_set:
        try:
            _setup_signatures(lib)
            _signatures_set = True
        except Exception as e:
            global _LOAD_ERROR
            _LOAD_ERROR = e
            return False
    return True


def get_load_error():
    return _LOAD_ERROR


def get_library_path():
    if _libcrypto is None:
        return None
    name = getattr(_libcrypto, '_name', None)
    return name or repr(_libcrypto)


class LibcryptoAES_ECB(object):
    def __init__(self, key):
        if not is_available():
            raise RuntimeError("libcrypto unavailable: %s" % _LOAD_ERROR)

        if isinstance(key, bytearray):
            key = bytes(key)
        elif not isinstance(key, bytes):
            key = bytes(key)

        keylen = len(key)
        if keylen == 16:
            self._cipher = _libcrypto.EVP_aes_128_ecb()
        elif keylen == 24:
            self._cipher = _libcrypto.EVP_aes_192_ecb()
        elif keylen == 32:
            self._cipher = _libcrypto.EVP_aes_256_ecb()
        else:
            raise ValueError(
                "AES key must be 16, 24, or 32 bytes (got %d)" % keylen)

        if not self._cipher:
            raise RuntimeError("EVP_aes_*_ecb returned NULL")

        self._key = key

    def encrypt(self, data):
        return self._run(data, encrypt_mode=True)

    def decrypt(self, data):
        return self._run(data, encrypt_mode=False)

    def _run(self, data, encrypt_mode):
        if data is None:
            return data
        if isinstance(data, bytearray):
            data = bytes(data)
        elif not isinstance(data, bytes):
            data = bytes(data)

        in_len = len(data)
        if in_len == 0:
            return b''
        if in_len % 16 != 0:
            raise ValueError(
                "data length must be a multiple of 16 (got %d)" % in_len)

        ctx_buf = None
        if _USE_LEGACY_CTX:
            ctx_buf = ctypes.create_string_buffer(_LEGACY_CTX_BUF_SIZE)
            ctx = ctx_buf
            _libcrypto.EVP_CIPHER_CTX_init(ctx)
        else:
            ctx = _libcrypto.EVP_CIPHER_CTX_new()
            if not ctx:
                raise RuntimeError("EVP_CIPHER_CTX_new failed")
        try:
            if encrypt_mode:
                rc = _libcrypto.EVP_EncryptInit_ex(
                    ctx, self._cipher, None, self._key, None)
                if rc != 1:
                    raise RuntimeError("EVP_EncryptInit_ex failed")
            else:
                rc = _libcrypto.EVP_DecryptInit_ex(
                    ctx, self._cipher, None, self._key, None)
                if rc != 1:
                    raise RuntimeError("EVP_DecryptInit_ex failed")

            _libcrypto.EVP_CIPHER_CTX_set_padding(ctx, 0)

            out_buf = ctypes.create_string_buffer(in_len + 16)
            outlen = ctypes.c_int(0)

            if encrypt_mode:
                rc = _libcrypto.EVP_EncryptUpdate(
                    ctx, out_buf, ctypes.byref(outlen), data, in_len)
                if rc != 1:
                    raise RuntimeError("EVP_EncryptUpdate failed")
            else:
                rc = _libcrypto.EVP_DecryptUpdate(
                    ctx, out_buf, ctypes.byref(outlen), data, in_len)
                if rc != 1:
                    raise RuntimeError("EVP_DecryptUpdate failed")

            ofs = outlen.value
            tail = (ctypes.c_char * 16).from_buffer(out_buf, ofs)
            finallen = ctypes.c_int(0)

            if encrypt_mode:
                rc = _libcrypto.EVP_EncryptFinal_ex(
                    ctx, tail, ctypes.byref(finallen))
                if rc != 1:
                    raise RuntimeError("EVP_EncryptFinal_ex failed")
            else:
                rc = _libcrypto.EVP_DecryptFinal_ex(
                    ctx, tail, ctypes.byref(finallen))
                if rc != 1:
                    raise RuntimeError("EVP_DecryptFinal_ex failed")

            return out_buf.raw[: outlen.value + finallen.value]
        finally:
            if _USE_LEGACY_CTX:
                if hasattr(_libcrypto, 'EVP_CIPHER_CTX_cleanup'):
                    _libcrypto.EVP_CIPHER_CTX_cleanup(ctx)
            else:
                _libcrypto.EVP_CIPHER_CTX_free(ctx)
