# Copyright (c) 2013-2014 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import ldap
from OpenSSL import crypto

from barbican.common import exception as excep
from barbican.common import hrefs
import barbican.common.utils as utils
from barbican.model import models
from barbican.model import repositories as repos
from barbican.plugin.interface import certificate_manager as cert
from barbican.plugin import resources as plugin
from barbican.tasks import common

LOG = utils.getLogger(__name__)

# Order sub-status definitions
ORDER_STATUS_REQUEST_PENDING = models.OrderStatus(
    "cert_request_pending",
    "Request has been submitted to the CA.  "
    "Waiting for certificate to be generated"
)

ORDER_STATUS_CERT_GENERATED = models.OrderStatus(
    "cert_generated",
    "Certificate has been generated"
)

ORDER_STATUS_DATA_INVALID = models.OrderStatus(
    "cert_data_invalid",
    "CA rejected request data as invalid"
)

ORDER_STATUS_CA_UNAVAIL_FOR_ISSUE = models.OrderStatus(
    "cert_ca_unavail_for_issue",
    "Unable to submit certificate request.  CA unavailable"
)

ORDER_STATUS_INVALID_OPERATION = models.OrderStatus(
    "cert_invalid_operation",
    "CA returned invalid operation"
)

ORDER_STATUS_INTERNAL_ERROR = models.OrderStatus(
    "cert_internal_error",
    "Internal error during certificate operations"
)

ORDER_STATUS_CA_UNAVAIL_FOR_CHECK = models.OrderStatus(
    "cert_ca_unavail_for_status_check",
    "Unable to get certificate request status.  CA unavailable."
)


def issue_certificate_request(order_model, project_model, result_follow_on):
    """Create the initial order with CA.

    Note that this method may be called more than once if retries are
    required. Barbican metadata is used to store intermediate information,
    including selected plugins by name, to support such retries.

    :param: order_model - order associated with this cert request
    :param: project_model - project associated with this request
    :param: result_follow_on - A :class:`FollowOnProcessingStatusDTO` instance
        instantiated by the client that this function may optionally update
        with information on how to process this task into the future.
    :returns: container_model - container with the relevant cert if
        the request has been completed.  None otherwise
    """
    container_model = None

    plugin_meta = _get_plugin_meta(order_model)
    barbican_meta = _get_barbican_meta(order_model)

    # TODO(john-wood-w) We need to de-conflict barbican_meta (stored with order
    # and not shown to plugins) with barbican_meta_dto (shared with plugins).
    # As a minimum we should change the name of the DTO to something like
    # 'extended_meta_dto' or some such.
    barbican_meta_for_plugins_dto = cert.BarbicanMetaDTO()

    # refresh the CA table.  This is mostly a no-op unless the entries
    # for a plugin are expired.
    cert.CertificatePluginManager().refresh_ca_table()

    # Locate the required certificate plugin.
    cert_plugin_name = barbican_meta.get('plugin_name')
    if cert_plugin_name:
        cert_plugin = cert.CertificatePluginManager().get_plugin_by_name(
            cert_plugin_name)
    else:
        ca_id = _get_ca_id(order_model.meta, project_model.id)
        if ca_id:
            barbican_meta_for_plugins_dto.plugin_ca_id = ca_id
            cert_plugin = cert.CertificatePluginManager().get_plugin_by_ca_id(
                ca_id)
        else:
            cert_plugin = cert.CertificatePluginManager().get_plugin(
                order_model.meta)
    barbican_meta['plugin_name'] = utils.generate_fullname_for(cert_plugin)

    # Generate CSR if needed.
    request_type = order_model.meta.get(cert.REQUEST_TYPE)
    if request_type == cert.CertificateRequestType.STORED_KEY_REQUEST:
        csr = barbican_meta.get('generated_csr')
        if csr is None:
            # TODO(alee) Fix this to be a non-project specific call once
            # the ACL patches go in.
            csr = _generate_csr(order_model, project_model)
            barbican_meta['generated_csr'] = csr
        barbican_meta_for_plugins_dto.generated_csr = csr

    result = cert_plugin.issue_certificate_request(
        order_model.id, order_model.meta,
        plugin_meta, barbican_meta_for_plugins_dto)

    # Save plugin and barbican metadata for this order.
    _save_plugin_metadata(order_model, plugin_meta)
    _save_barbican_metadata(order_model, barbican_meta)

    # Handle result
    if cert.CertificateStatus.WAITING_FOR_CA == result.status:
        _update_result_follow_on(
            result_follow_on,
            order_status=ORDER_STATUS_REQUEST_PENDING,
            retry_task=common.RetryTasks.INVOKE_CERT_STATUS_CHECK_TASK,
            retry_msec=result.retry_msec)
    elif cert.CertificateStatus.CERTIFICATE_GENERATED == result.status:
        _update_result_follow_on(
            result_follow_on,
            order_status=ORDER_STATUS_CERT_GENERATED)
        container_model = _save_secrets(result, project_model)
    elif cert.CertificateStatus.CLIENT_DATA_ISSUE_SEEN == result.status:
        raise cert.CertificateStatusClientDataIssue(result.status_message)
    elif cert.CertificateStatus.CA_UNAVAILABLE_FOR_REQUEST == result.status:
        _update_result_follow_on(
            result_follow_on,
            order_status=ORDER_STATUS_CA_UNAVAIL_FOR_ISSUE,
            retry_task=common.RetryTasks.INVOKE_SAME_TASK,
            retry_msec=cert.ERROR_RETRY_MSEC)
        _notify_ca_unavailable(order_model, result)
    elif cert.CertificateStatus.INVALID_OPERATION == result.status:
        raise cert.CertificateStatusInvalidOperation(result.status_message)
    else:
        raise cert.CertificateStatusNotSupported(result.status)

    return container_model


def check_certificate_request(order_model, project_model, result_follow_on):
    """Check the status of a certificate request with the CA.

    Note that this method may be called more than once if retries are
    required. Barbican metadata is used to store intermediate information,
    including selected plugins by name, to support such retries.

    :param: order_model - order associated with this cert request
    :param: project_model - project associated with this request
    :param: result_follow_on - A :class:`FollowOnProcessingStatusDTO` instance
        instantiated by the client that this function may optionally update
        with information on how to process this task into the future.
    :returns: container_model - container with the relevant cert if the
        request has been completed.  None otherwise.
    """
    container_model = None
    plugin_meta = _get_plugin_meta(order_model)
    barbican_meta = _get_barbican_meta(order_model)

    # TODO(john-wood-w) See note above about DTO's name.
    barbican_meta_for_plugins_dto = cert.BarbicanMetaDTO()

    cert_plugin = cert.CertificatePluginManager().get_plugin_by_name(
        barbican_meta.get('plugin_name'))

    result = cert_plugin.check_certificate_status(
        order_model.id, order_model.meta,
        plugin_meta, barbican_meta_for_plugins_dto)

    # Save plugin order plugin state
    _save_plugin_metadata(order_model, plugin_meta)

    # Handle result
    if cert.CertificateStatus.WAITING_FOR_CA == result.status:
        _update_result_follow_on(
            result_follow_on,
            order_status=ORDER_STATUS_REQUEST_PENDING,
            retry_task=common.RetryTasks.INVOKE_CERT_STATUS_CHECK_TASK,
            retry_msec=result.retry_msec)
    elif cert.CertificateStatus.CERTIFICATE_GENERATED == result.status:
        _update_result_follow_on(
            result_follow_on,
            order_status=ORDER_STATUS_CERT_GENERATED)
        container_model = _save_secrets(result, project_model)
    elif cert.CertificateStatus.CLIENT_DATA_ISSUE_SEEN == result.status:
        raise cert.CertificateStatusClientDataIssue(result.status_message)
    elif cert.CertificateStatus.CA_UNAVAILABLE_FOR_REQUEST == result.status:
        _update_result_follow_on(
            result_follow_on,
            order_status=ORDER_STATUS_CA_UNAVAIL_FOR_CHECK,
            retry_task=common.RetryTasks.INVOKE_SAME_TASK,
            retry_msec=cert.ERROR_RETRY_MSEC)
        _notify_ca_unavailable(order_model, result)
    elif cert.CertificateStatus.INVALID_OPERATION == result.status:
        raise cert.CertificateStatusInvalidOperation(result.status_message)
    else:
        raise cert.CertificateStatusNotSupported(result.status)

    return container_model


def modify_certificate_request(order_model, updated_meta):
    """Update the order with CA."""
    # TODO(chellygel): Add the modify certificate request logic.
    LOG.debug('in modify_certificate_request')
    raise NotImplementedError  # pragma: no cover


def _get_ca_id(order_meta, project_id):
    ca_id = order_meta.get(cert.CA_ID)
    if ca_id:
        return ca_id

    preferred_ca_repository = repos.get_preferred_ca_repository()
    cas, offset, limit, total = preferred_ca_repository.get_by_create_date(
        project_id=project_id, suppress_exception=True)
    if total > 0:
        return cas[0].ca_id

    global_ca = preferred_ca_repository.get_global_preferred_ca()
    if global_ca:
        return global_ca.ca_id

    return None


def _update_result_follow_on(
        result_follow_on,
        order_status=None,
        retry_task=common.RetryTasks.NO_ACTION_REQUIRED,
        retry_msec=common.RETRY_MSEC_DEFAULT):
    if order_status:
        result_follow_on.status = order_status.id
        result_follow_on.status_message = order_status.message
    result_follow_on.retry_task = retry_task
    if retry_msec and retry_msec >= 0:
        result_follow_on.retry_msec = retry_msec


def _get_plugin_meta(order_model):
    if order_model:
        order_plugin_meta_repo = repos.get_order_plugin_meta_repository()
        return order_plugin_meta_repo.get_metadata_for_order(order_model.id)
    else:
        return {}


def _get_barbican_meta(order_model):
    if order_model:
        order_barbican_meta_repo = repos.get_order_barbican_meta_repository()
        return order_barbican_meta_repo.get_metadata_for_order(order_model.id)
    else:
        return {}


def _generate_csr(order_model, project_model):
    """Generate a CSR from the public key.

    :param: order_model - order for the request
    :param: project_model - project for this request
    :return: CSR (certificate signing request) in PEM format
    :raise: :class:`StoredKeyPrivateKeyNotFound` if private key not found
            :class:`StoredKeyContainerNotFound` if container not found
    """
    container_ref = order_model.meta.get('container_ref')

    # extract container_id as the last part of the URL
    container_id = hrefs.get_container_id_from_ref(container_ref)

    container_repo = repos.get_container_repository()
    container = container_repo.get(container_id,
                                   project_model.external_id,
                                   suppress_exception=True)
    if not container:
        raise excep.StoredKeyContainerNotFound(container_id)

    passphrase = None
    private_key = None

    for cs in container.container_secrets:
        secret_repo = repos.get_secret_repository()
        if cs.name == 'private_key':
            private_key_model = secret_repo.get(
                cs.secret_id,
                project_model.external_id)
            private_key = plugin.get_secret(
                'application/pkcs8',
                private_key_model,
                project_model)
        elif cs.name == 'private_key_passphrase':
            passphrase_model = secret_repo.get(
                cs.secret_id,
                project_model.external_id)
            passphrase = plugin.get_secret(
                'text/plain;charset=utf-8',
                passphrase_model,
                project_model)
            passphrase = str(passphrase)

    if not private_key:
        raise excep.StoredKeyPrivateKeyNotFound(container_id)

    if passphrase is None:
        pkey = crypto.load_privatekey(
            crypto.FILETYPE_PEM,
            private_key
        )
    else:
        pkey = crypto.load_privatekey(
            crypto.FILETYPE_PEM,
            private_key,
            passphrase
        )

    subject_name = order_model.meta.get('subject_dn')
    subject_name_dns = ldap.dn.str2dn(subject_name)
    extensions = order_model.meta.get('extensions', None)

    req = crypto.X509Req()