import os

from pathlib import Path
from subprocess import check_call

from charms import layer
from charms.reactive import hook
from charms.reactive import set_state, remove_state
from charms.reactive import when
from charms.reactive import set_flag, clear_flag
from charms.reactive import endpoint_from_flag
from charms.reactive.helpers import data_changed

from charmhelpers.core import hookenv, unitdata
from charmhelpers.core.hookenv import log


@when('certificates.ca.available')
def store_ca(tls):
    '''Read the certificate authority from the relation object and install
    the ca on this system.'''
    # Get the CA from the relationship object.
    certificate_authority = tls.get_ca()
    if certificate_authority:
        layer_options = layer.options('tls-client')
        ca_path = layer_options.get('ca_certificate_path')
        changed = data_changed('certificate_authority', certificate_authority)
        if ca_path:
            if changed or not os.path.exists(ca_path):
                log('Writing CA certificate to {0}'.format(ca_path))
                # ensure we have a newline at the end of the certificate.
                # some things will blow up without one.
                # See https://bugs.launchpad.net/charm-kubernetes-master/+bug/1828034
                if not certificate_authority.endswith('\n'):
                    certificate_authority += '\n'
                _write_file(ca_path, certificate_authority)
                set_state('tls_client.ca.written')
            set_state('tls_client.ca.saved')
        if changed:
            # Update /etc/ssl/certs and generate ca-certificates.crt
            install_ca(certificate_authority)


@when('certificates.server.cert.available')
def store_server(tls):
    '''Read the server certificate and server key from the relation object
    and save them to the certificate directory..'''
    server_cert, server_key = tls.get_server_cert()
    chain = tls.get_chain()
    if chain:
        server_cert = server_cert + '\n' + chain
    if server_cert and server_key:
        layer_options = layer.options('tls-client')
        cert_path = layer_options.get('server_certificate_path')
        key_path = layer_options.get('server_key_path')
        cert_changed = data_changed('server_certificate', server_cert)
        key_changed = data_changed('server_key', server_key)
        if cert_path:
            if cert_changed or not os.path.exists(cert_path):
                log('Writing server certificate to {0}'.format(cert_path))
                _write_file(cert_path, server_cert)
                set_state('tls_client.server.certificate.written')
            set_state('tls_client.server.certificate.saved')
        if key_path:
            if key_changed or not os.path.exists(key_path):
                log('Writing server key to {0}'.format(key_path))
                _write_file(key_path, server_key)
            set_state('tls_client.server.key.saved')


@when('certificates.client.cert.available')
def store_client(tls):
    '''Read the client certificate and client key from the relation object
    and copy them to the certificate directory.'''
    client_cert, client_key = tls.get_client_cert()
    chain = tls.get_chain()
    if chain:
        client_cert = client_cert + '\n' + chain
    if client_cert and client_key:
        layer_options = layer.options('tls-client')
        cert_path = layer_options.get('client_certificate_path')
        key_path = layer_options.get('client_key_path')
        cert_changed = data_changed('client_certificate', client_cert)
        key_changed = data_changed('client_key', client_key)
        if cert_path:
            if cert_changed or not os.path.exists(cert_path):
                log('Writing client certificate to {0}'.format(cert_path))
                _write_file(cert_path, client_cert)
                set_state('tls_client.client.certificate.written')
            set_state('tls_client.client.certificate.saved')
        if key_path:
            if key_changed or not os.path.exists(key_path):
                log('Writing client key to {0}'.format(key_path))
                _write_file(key_path, client_key)
            set_state('tls_client.client.key.saved')


@when('certificates.certs.changed')
def update_certs():
    tls = endpoint_from_flag('certificates.certs.changed')
    certs_paths = unitdata.kv().get('layer.tls-client.cert-paths', {})
    all_ready = True
    any_changed = False
    maps = {
        'server': tls.server_certs_map,
        'client': tls.client_certs_map,
    }

    if maps.get('client') == {}:
        log(
            'No client certs found using maps. Checking for global \
            client certificates.',
            'WARNING'
        )
        # Check for global certs,
        # Backwards compatibility https://bugs.launchpad.net/charm-kubernetes-master/+bug/1825819
        cert_pair = tls.get_client_cert()
        if cert_pair is not None:
            for client_name in certs_paths.get('client', {}).keys():
                maps.get('client').update({
                    client_name: cert_pair
                })

    chain = tls.get_chain()
    for cert_type in ('server', 'client'):
        for common_name, paths in certs_paths.get(cert_type, {}).items():
            cert_pair = maps[cert_type].get(common_name)
            if not cert_pair:
                all_ready = False
                continue
            if not data_changed('layer.tls-client.'
                                '{}.{}'.format(cert_type, common_name), cert_pair):
                continue

            cert = None
            key = None
            if type(cert_pair) is not tuple:
                if paths['crt']:
                    cert = cert_pair.cert
                if paths['key']:
                    key = cert_pair.key
            else:
                cert, key = cert_pair

            if cert:
                if chain:
                    cert = cert + '\n' + chain
                _ensure_directory(paths['crt'], 0o755)
                Path(paths['crt']).write_text(cert)

            if key:
                _ensure_directory(paths['key'], 0o710)
                Path(paths['key']).write_text(key)

            any_changed = True
            # clear flags first to ensure they are re-triggered if left set
            clear_flag('tls_client.{}.certs.changed'.format(cert_type))
            clear_flag('tls_client.{}.cert.{}.changed'.format(cert_type,
                                                              common_name))
            set_flag('tls_client.{}.certs.changed'.format(cert_type))
            set_flag('tls_client.{}.cert.{}.changed'.format(cert_type,
                                                            common_name))
    if all_ready:
        set_flag('tls_client.certs.saved')
    if any_changed:
        clear_flag('tls_client.certs.changed')
        set_flag('tls_client.certs.changed')
    clear_flag('certificates.certs.changed')


def install_ca(certificate_authority):
    '''Install a certificiate authority on the system by calling the
    update-ca-certificates command.'''
    if certificate_authority:
        name = hookenv.service_name()
        # Create a path to install CAs on Debian systems.
        ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(name)
        log('Writing CA certificate to {0}'.format(ca_path))
        _write_file(ca_path, certificate_authority)
        # Update the trusted CAs on this system (a time expensive operation).
        check_call(['update-ca-certificates'])
        log('Generated ca-certificates.crt for {0}'.format(name))
        set_state('tls_client.ca_installed')


@hook('upgrade-charm')
def remove_states():
    remove_state('tls_client.ca.saved')
    remove_state('tls_client.server.certificate.saved')
    remove_state('tls_client.server.key.saved')
    remove_state('tls_client.client.certificate.saved')
    remove_state('tls_client.client.key.saved')


def _ensure_directory(path, mode=0o770):
    '''Ensure the parent directory exists creating directories if necessary.'''
    directory = os.path.dirname(path)
    if not os.path.isdir(directory):
        os.makedirs(directory)
        os.chmod(directory, mode)


def _write_file(path, content):
    '''Write the path to a file.'''
    _ensure_directory(path)
    with open(path, 'w') as stream:
        stream.write(content)
    os.chmod(path, 0o440)
