"""Credential Store Extension"""
GSSAPI="BASE"  # This ensures that a full module is generated by Cython

from libc.string cimport memcmp, memcpy, memset
from libc.stdlib cimport free, malloc, calloc

from gssapi.raw.cython_types cimport *
from gssapi.raw.names cimport Name
from gssapi.raw.creds cimport Creds
from gssapi.raw.oids cimport OID
from gssapi.raw.cython_converters cimport c_create_oid_set
from gssapi.raw.cython_converters cimport c_get_mech_oid_set
from gssapi.raw.cython_converters cimport c_c_ttl_to_py, c_py_ttl_to_c

from collections import namedtuple

from gssapi.raw.named_tuples import AddCredResult, AcquireCredResult
from gssapi.raw.named_tuples import StoreCredResult
from gssapi.raw.misc import GSSError


cdef extern from "python_gssapi_ext.h":
    ctypedef struct gss_key_value_element_desc:
        const char *key
        const char *value

    ctypedef struct gss_key_value_set_desc:
        OM_uint32 count
        gss_key_value_element_desc *elements

    OM_uint32 gss_acquire_cred_from(OM_uint32 *min_stat,
                                    gss_name_t desired_name,
                                    OM_uint32 ttl,
                                    gss_OID_set desired_mechs,
                                    gss_cred_usage_t cred_usage,
                                    const gss_key_value_set_desc *cred_store,
                                    gss_cred_id_t *output_creds,
                                    gss_OID_set *actual_mechs,
                                    OM_uint32 *actual_ttl) nogil

    OM_uint32 gss_add_cred_from(OM_uint32 *min_stat,
                                gss_cred_id_t input_creds,
                                gss_name_t desired_name,
                                gss_OID desired_mech,
                                gss_cred_usage_t cred_usage,
                                OM_uint32 initiator_ttl,
                                OM_uint32 acceptor_ttl,
                                const gss_key_value_set_desc *cred_store,
                                gss_cred_id_t *output_creds,
                                gss_OID_set *actual_mechs,
                                OM_uint32 *actual_initiator_ttl,
                                OM_uint32 *actual_acceptor_ttl) nogil

    OM_uint32 gss_store_cred_into(OM_uint32 *min_stat,
                                  gss_cred_id_t input_creds,
                                  gss_cred_usage_t cred_usage,
                                  gss_OID desired_mech,
                                  OM_uint32 overwrite_cred,
                                  OM_uint32 default_cred,
                                  const gss_key_value_set_desc *cred_store,
                                  gss_OID_set *elements_stored,
                                  gss_cred_usage_t *actual_usage) nogil

    # null value for cred stores
    gss_key_value_set_desc *GSS_C_NO_CRED_STORE


cdef gss_key_value_set_desc* c_create_key_value_set(dict values) except NULL:
    cdef gss_key_value_set_desc* res = <gss_key_value_set_desc*>malloc(
        sizeof(gss_key_value_set_desc))
    if res is NULL:
        raise MemoryError("Could not allocate memory for "
                          "key-value set")

    res.count = len(values)

    res.elements = <gss_key_value_element_desc*>calloc(
        res.count, sizeof(gss_key_value_element_desc))

    if res.elements is NULL:
        raise MemoryError("Could not allocate memory for "
                          "key-value set elements")

    for (i, (k, v)) in enumerate(values.items()):
        res.elements[i].key = k
        res.elements[i].value = v

    return res


cdef void c_free_key_value_set(gss_key_value_set_desc *kvset):
    free(kvset.elements)
    free(kvset)


def acquire_cred_from(dict store=None, Name name=None, lifetime=None,
                      mechs=None, usage='both'):
    """
    acquire_cred_from(store=None, name=None, lifetime=None, mechs=None, \
usage='both')
    Acquire credentials from the given store.

    This method acquires credentials from the store specified by the
    given credential store information.

    The credential store information is a dictionary containing
    mechanisms-specific keys and values pointing to a credential store
    or stores.

    Args:
        store (dict): the credential store information pointing to the
            credential store from which to acquire the credentials.
            See :doc:`credstore` for valid values
        name (Name): the name associated with the credentials,
            or None for the default name
        lifetime (int): the desired lifetime of the credentials, or None
            for indefinite
        mechs (list): the desired mechanisms to be used with these
            credentials, or None for the default set
        usage (str): the usage for these credentials -- either 'both',
            'initiate', or 'accept'

    Returns:
        AcquireCredResult: the acquired credentials and information about
            them

    Raises:
        GSSError
    """

    cdef gss_OID_set desired_mechs
    if mechs is not None:
        desired_mechs = c_get_mech_oid_set(mechs)
    else:
        desired_mechs = GSS_C_NO_OID_SET

    cdef OM_uint32 input_ttl = c_py_ttl_to_c(lifetime)

    cdef gss_name_t c_name
    if name is None:
        c_name = GSS_C_NO_NAME
    else:
        c_name = name.raw_name

    cdef gss_cred_usage_t c_usage
    if usage == 'initiate':
        c_usage = GSS_C_INITIATE
    elif usage == 'accept':
        c_usage = GSS_C_ACCEPT
    elif usage == 'both':
        c_usage = GSS_C_BOTH
    else:
        raise ValueError(f'Invalid usage "{usage}" - permitted values are '
                         '"initiate", "accept", and "both"')

    cdef gss_key_value_set_desc *c_store
    if store is not None:
        c_store = c_create_key_value_set(store)
    else:
        c_store = GSS_C_NO_CRED_STORE

    cdef gss_cred_id_t creds
    cdef gss_OID_set actual_mechs
    cdef OM_uint32 actual_ttl

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_acquire_cred_from(&min_stat, c_name, input_ttl,
                                         desired_mechs, c_usage, c_store,
                                         &creds, &actual_mechs, &actual_ttl)

    cdef OM_uint32 tmp_min_stat
    if mechs is not None:
        gss_release_oid_set(&tmp_min_stat, &desired_mechs)

    if store is not None:
        c_free_key_value_set(c_store)

    cdef Creds rc = Creds()
    if maj_stat == GSS_S_COMPLETE:
        rc.raw_creds = creds
        return AcquireCredResult(rc, c_create_oid_set(actual_mechs),
                                 c_c_ttl_to_py(actual_ttl))
    else:
        raise GSSError(maj_stat, min_stat)


def add_cred_from(dict store, Creds input_creds,
                  Name name not None, OID mech not None,
                  usage='both', init_lifetime=None,
                  accept_lifetime=None):
    """
    add_cred_from(store, input_creds, name, mech, usage='both', \
init_lifetime=None, accept_lifetime=None)
    Acquire credentials to add to the current set from the given store.

    This method works like :func:`acquire_cred_from`, except that it
    adds the acquired credentials for a single mechanism to a copy of
    the current set, instead of creating a new set for multiple mechanisms.
    Unlike :func:`acquire_cred`, you cannot pass None for the desired name or
    mechanism.

    The credential store information is a dictionary containing
    mechanisms-specific keys and values pointing to a credential store
    or stores.

    Args:
        store (dict): the store into which to store the credentials,
            or None for the default store.
            See :doc:`credstore` for valid values
        name (Name): the name associated with the credentials
        mech (OID): the desired mechanism to be used with these
            credentials
        usage (str): the usage for these credentials -- either 'both',
            'initiate', or 'accept'
        init_lifetime (int): the desired initiate lifetime of the
            credentials, or None for indefinite
        accept_lifetime (int): the desired accept lifetime of the
            credentials, or None for indefinite

    Returns:
        AcquireCredResult: the new credentials set and information about
            it

    Raises:
        GSSError
    """

    cdef OM_uint32 input_initiator_ttl = c_py_ttl_to_c(init_lifetime)
    cdef OM_uint32 input_acceptor_ttl = c_py_ttl_to_c(accept_lifetime)

    cdef gss_cred_usage_t c_usage
    if usage == 'initiate':
        c_usage = GSS_C_INITIATE
    elif usage == 'accept':
        c_usage = GSS_C_ACCEPT
    elif usage == 'both':
        c_usage = GSS_C_BOTH
    else:
        raise ValueError(f'Invalid usage "{usage}" - permitted values are '
                         '"initiate", "accept", and "both"')

    cdef gss_name_t c_name = name.raw_name
    cdef gss_OID c_mech = &mech.raw_oid

    cdef gss_cred_id_t c_input_creds
    if input_creds is not None:
        c_input_creds = input_creds.raw_creds
    else:
        c_input_creds = GSS_C_NO_CREDENTIAL

    cdef gss_key_value_set_desc *c_store
    if store is not None:
        c_store = c_create_key_value_set(store)
    else:
        c_store = GSS_C_NO_CRED_STORE

    cdef gss_cred_id_t creds
    cdef gss_OID_set actual_mechs
    cdef OM_uint32 actual_initiator_ttl
    cdef OM_uint32 actual_acceptor_ttl

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_add_cred_from(&min_stat, c_input_creds, c_name,
                                     c_mech, c_usage, input_initiator_ttl,
                                     input_acceptor_ttl, c_store, &creds,
                                     &actual_mechs, &actual_initiator_ttl,
                                     &actual_acceptor_ttl)

    if store is not None:
        c_free_key_value_set(c_store)

    cdef Creds rc
    if maj_stat == GSS_S_COMPLETE:
        rc = Creds()
        rc.raw_creds = creds
        return AddCredResult(rc, c_create_oid_set(actual_mechs),
                             c_c_ttl_to_py(actual_initiator_ttl),
                             c_c_ttl_to_py(actual_acceptor_ttl))
    else:
        raise GSSError(maj_stat, min_stat)


def store_cred_into(dict store, Creds creds not None,
                    usage='both', OID mech=None, bint overwrite=False,
                    bint set_default=False):
    """
    store_cred_into(store, creds, usage='both', mech=None, overwrite=False, \
set_default=False)
    Store credentials into the given store.

    This method stores the given credentials into the store specified
    by the given store information.  They may then be retrieved later using
    :func:`acquire_cred_from` or :func:`add_cred_from`.

    The credential store information is a dictionary containing
    mechanisms-specific keys and values pointing to a credential store
    or stores.

    Args:
        store (dict): the store into which to store the credentials,
            or None for the default store.
            See :doc:`credstore` for valid values
        creds (Creds): the credentials to store
        usage (str): the usage to store the credentials with -- either
            'both', 'initiate', or 'accept'
        mech (OID): the mechansim to associate with the stored credentials
        overwrite (bool): whether or not to overwrite existing credentials
            stored with the same name, etc
        set_default (bool): whether or not to set these credentials as
            the default credentials for the given store.

    Returns:
        StoreCredResult: the results of the credential storing operation

    Raises:
        GSSError
    """

    cdef gss_OID desired_mech
    if mech is not None:
        desired_mech = &mech.raw_oid
    else:
        desired_mech = GSS_C_NO_OID

    cdef gss_cred_usage_t c_usage
    if usage == 'initiate':
        c_usage = GSS_C_INITIATE
    elif usage == 'accept':
        c_usage = GSS_C_ACCEPT
    elif usage == 'both':
        c_usage = GSS_C_BOTH
    else:
        raise ValueError(f'Invalid usage "{usage}" - permitted values are '
                         '"initiate", "accept", and "both"')

    cdef gss_key_value_set_desc *c_store
    if store is not None:
        c_store = c_create_key_value_set(store)
    else:
        c_store = GSS_C_NO_CRED_STORE

    cdef gss_cred_id_t c_creds = creds.raw_creds

    cdef gss_OID_set actual_mech_types
    cdef gss_cred_usage_t actual_usage

    cdef OM_uint32 maj_stat, min_stat

    with nogil:
        maj_stat = gss_store_cred_into(&min_stat, c_creds, c_usage,
                                       desired_mech, overwrite,
                                       set_default, c_store,
                                       &actual_mech_types,
                                       &actual_usage)

    if store is not None:
        c_free_key_value_set(c_store)

    if maj_stat == GSS_S_COMPLETE:
        if actual_usage == GSS_C_INITIATE:
            py_actual_usage = 'initiate'
        elif actual_usage == GSS_C_ACCEPT:
            py_actual_usage = 'accept'
        else:
            py_actual_usage = 'both'

        return StoreCredResult(c_create_oid_set(actual_mech_types),
                               py_actual_usage)
    else:
        raise GSSError(maj_stat, min_stat)
