import contextlib
import ctypes
import struct
from ctypes import c_void_p, c_uint16, c_uint32, c_int32, c_char_p, POINTER

from keyring.py27compat import string_types, add_metaclass


sec_keychain_ref = sec_keychain_item_ref = c_void_p
OS_status = c_int32


class error:
    item_not_found = -25300


fw = '/System/Library/Frameworks/{name}.framework/Versions/A/{name}'.format
_sec = ctypes.CDLL(fw(name='Security'))
_core = ctypes.CDLL(fw(name='CoreServices'))


SecKeychainOpen = _sec.SecKeychainOpen
SecKeychainOpen.argtypes = (
    c_char_p,
    POINTER(sec_keychain_ref),
)
SecKeychainOpen.restype = OS_status


SecKeychainCopyDefault = _sec.SecKeychainCopyDefault
SecKeychainCopyDefault.argtypes = POINTER(sec_keychain_ref),
SecKeychainCopyDefault.restype = OS_status


class Error(Exception):
    @classmethod
    def raise_for_status(cls, status, msg):
        if status == 0:
            return
        raise cls(status, msg)


class NotFound(Error):
    @classmethod
    def raise_for_status(cls, status, msg):
        if status == error.item_not_found:
            raise cls(status, msg)
        Error.raise_for_status(status, msg)


@contextlib.contextmanager
def open(name):
    ref = sec_keychain_ref()
    if name is None:
        status = SecKeychainCopyDefault(ref)
        msg = "Unable to open default keychain"
    else:
        status = SecKeychainOpen(name.encode('utf-8'), ref)
        msg = "Unable to open keychain {name}".format(**locals())
    Error.raise_for_status(status, msg)
    try:
        yield ref
    finally:
        _core.CFRelease(ref)


SecKeychainFindGenericPassword = _sec.SecKeychainFindGenericPassword
SecKeychainFindGenericPassword.argtypes = (
    sec_keychain_ref,
    c_uint32,
    c_char_p,
    c_uint32,
    c_char_p,
    POINTER(c_uint32),  # passwordLength
    POINTER(c_void_p),  # passwordData
    POINTER(sec_keychain_item_ref),  # itemRef
)
SecKeychainFindGenericPassword.restype = OS_status


def find_generic_password(kc_name, service, username):
    username = username.encode('utf-8')
    service = service.encode('utf-8')
    with open(kc_name) as keychain:
        length = c_uint32()
        data = c_void_p()
        status = SecKeychainFindGenericPassword(
            keychain,
            len(service),
            service,
            len(username),
            username,
            length,
            data,
            None,
        )

    msg = "Can't fetch password from system"
    NotFound.raise_for_status(status, msg)

    password = ctypes.create_string_buffer(length.value)
    ctypes.memmove(password, data.value, length.value)
    SecKeychainItemFreeContent(None, data)
    return password.raw.decode('utf-8')


SecKeychainFindInternetPassword = _sec.SecKeychainFindInternetPassword
SecKeychainFindInternetPassword.argtypes = (
    sec_keychain_ref,  # keychainOrArray
    c_uint32,  # serverNameLength
    c_char_p,  # serverName
    c_uint32,  # securityDomainLength
    c_char_p,  # securityDomain
    c_uint32,  # accountNameLength
    c_char_p,  # accountName
    c_uint32,  # pathLength
    c_char_p,  # path
    c_uint16,  # port
    c_uint32,  # SecProtocolType protocol,
    c_uint32,  # SecAuthenticationType authenticationType,
    POINTER(c_uint32),  # passwordLength
    POINTER(c_void_p),  # passwordData
    POINTER(sec_keychain_item_ref),  # itemRef
)
SecKeychainFindInternetPassword.restype = OS_status


class PackedAttributes(type):
    """
    Take the attributes which use magic words
    to represent enumerated constants and generate
    the constants.
    """
    def __new__(cls, name, bases, dict):
        dict.update(
            (key, cls.unpack(val))
            for key, val in dict.items()
            if not key.startswith('_')
        )
        return super(PackedAttributes, cls).__new__(cls, name, bases, dict)

    @staticmethod
    def unpack(word):
        r"""
        >>> PackedAttributes.unpack(0)
        0
        >>> PackedAttributes.unpack('\x00\x00\x00\x01')
        1
        >>> PackedAttributes.unpack('abcd')
        1633837924
        """
        if not isinstance(word, string_types):
            return word
        val, = struct.unpack('!I', word.encode('ascii'))
        return val


@add_metaclass(PackedAttributes)
class SecProtocolType(object):
    kSecProtocolTypeHTTP = 'http'
    kSecProtocolTypeHTTPS = 'htps'
    kSecProtocolTypeFTP = 'ftp '


@add_metaclass(PackedAttributes)
class SecAuthenticationType(object):
    """
    >>> SecAuthenticationType.kSecAuthenticationTypeDefault
    1684434036
    """
    kSecAuthenticationTypeDefault = 'dflt'
    kSecAuthenticationTypeAny = 0


def find_internet_password(kc_name, service, username):
    username = username.encode('utf-8')
    domain = None
    service = service.encode('utf-8')
    path = None
    port = 0

    with open(kc_name) as keychain:
        length = c_uint32()
        data = c_void_p()
        status = SecKeychainFindInternetPassword(
            keychain,
            len(service), service,
            0, domain,
            len(username), username,
            0, path,
            port,
            SecProtocolType.kSecProtocolTypeHTTPS,
            SecAuthenticationType.kSecAuthenticationTypeAny,
            length,
            data,
            None,
        )

    msg = "Can't fetch password from system"
    NotFound.raise_for_status(status, msg)

    password = ctypes.create_string_buffer(length.value)
    ctypes.memmove(password, data.value, length.value)
    SecKeychainItemFreeContent(None, data)
    return password.raw.decode('utf-8')


SecKeychainAddGenericPassword = _sec.SecKeychainAddGenericPassword
SecKeychainAddGenericPassword.argtypes = (
    sec_keychain_ref,
    c_uint32,
    c_char_p,
    c_uint32,
    c_char_p,
    c_uint32,
    c_char_p,
    POINTER(sec_keychain_item_ref),
)
SecKeychainAddGenericPassword.restype = OS_status


def set_generic_password(name, service, username, password):
    username = username.encode('utf-8')
    service = service.encode('utf-8')
    password = password.encode('utf-8')
    with open(name) as keychain:
        item = sec_keychain_item_ref()
        status = SecKeychainFindGenericPassword(
            keychain,
            len(service), service,
            len(username), username, None,
            None, item)
        if status:
            if status == error.item_not_found:
                status = SecKeychainAddGenericPassword(
                    keychain,
                    len(service), service,
                    len(username), username,
                    len(password), password, None)
        else:
            status = SecKeychainItemModifyAttributesAndData(
                item, None, len(password), password)
            _core.CFRelease(item)

        NotFound.raise_for_status(status, "Unable to set password")


SecKeychainAddInternetPassword = _sec.SecKeychainAddInternetPassword
SecKeychainAddInternetPassword.argtypes = (
    sec_keychain_ref,  # keychainOrArray
    c_uint32,  # serverNameLength
    c_char_p,  # serverName
    c_uint32,  # securityDomainLength
    c_char_p,  # securityDomain
    c_uint32,  # accountNameLength
    c_char_p,  # accountName
    c_uint32,  # pathLength
    c_char_p,  # path
    c_uint16,  # port
    c_uint32,  # SecProtocolType protocol,
    c_uint32,  # SecAuthenticationType authenticationType,
    c_uint32,  # passwordLength
    c_void_p,  # passwordData
    POINTER(sec_keychain_item_ref),  # itemRef
)
SecKeychainAddInternetPassword.restype = OS_status


def set_internet_password(name, service, username, password):
    username = username.encode('utf-8')
    domain = None
    service = service.encode('utf-8')
    password = password.encode('utf-8')
    path = None
    port = 0
    with open(name) as keychain:
        # TODO: Use update or set technique as seen in set_generic_password
        status = SecKeychainAddInternetPassword(
            keychain,
            len(service), service,
            0, domain,
            len(username), username,
            0, path,
            port,
            SecProtocolType.kSecProtocolTypeHTTPS,
            SecAuthenticationType.kSecAuthenticationTypeAny,
            len(password), password,
            None,
        )

        NotFound.raise_for_status(status, "Unable to set password")


SecKeychainItemModifyAttributesAndData = _sec.SecKeychainItemModifyAttributesAndData
SecKeychainItemModifyAttributesAndData.argtypes = (
    sec_keychain_item_ref, c_void_p, c_uint32, c_void_p,
)
SecKeychainItemModifyAttributesAndData.restype = OS_status

SecKeychainItemFreeContent = _sec.SecKeychainItemFreeContent
SecKeychainItemFreeContent.argtypes = (
    c_void_p, c_void_p,
)
SecKeychainItemFreeContent.restype = OS_status

SecKeychainItemDelete = _sec.SecKeychainItemDelete
SecKeychainItemDelete.argtypes = sec_keychain_item_ref,
SecKeychainItemDelete.restype = OS_status


def delete_generic_password(name, service, username):
    username = username.encode('utf-8')
    service = service.encode('utf-8')
    with open(name) as keychain:
        length = c_uint32()
        data = c_void_p()
        item = sec_keychain_item_ref()
        status = SecKeychainFindGenericPassword(
            keychain,
            len(service),
            service,
            len(username),
            username,
            length,
            data,
            item,
        )

    Error.raise_for_status(status, "Unable to delete password")

    SecKeychainItemDelete(item)
    _core.CFRelease(item)
