"""TLS configuration"""
# SPDX-License-Identifier: AGPL-3.0-or-later

import json
import logging
import os
import sys

from django.utils.translation import gettext_lazy as _

from plinth import actions
from plinth.modules.email_server import interproc, postconf

from . import models

# Mozilla Guideline v5.6, Postfix 1.17.7, OpenSSL 1.1.1d, intermediate
# Generated 2021-08
# https://ssl-config.mozilla.org/
tls_medium_cipherlist = [
    'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256',
    'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES256-GCM-SHA384',
    'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305',
    'DHE-RSA-AES128-GCM-SHA256', 'DHE-RSA-AES256-GCM-SHA384'
]

postfix_config = {
    # Enable TLS
    'smtpd_tls_security_level': 'may',

    # Allow unencrypted auth on port 25, needed by Roundcube
    'smtpd_tls_auth_only': 'no',

    # Debugging information
    'smtpd_tls_received_header': 'yes',

    # Use a strong hashing algorithm
    'smtp_tls_fingerprint_digest': 'sha256',
    'smtpd_tls_fingerprint_digest': 'sha256',

    # Mozilla Intermediate Configuration
    'smtpd_tls_mandatory_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1',
    'smtpd_tls_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1',
    'smtpd_tls_mandatory_ciphers': 'medium',
    'tls_medium_cipherlist': ':'.join(tls_medium_cipherlist),
    'tls_preempt_cipherlist': 'no',

    # Postfix SMTP client
    'smtp_tls_mandatory_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1',
    'smtp_tls_protocols': '!SSLv2, !SSLv3, !TLSv1, !TLSv1.1',
    'smtp_tls_mandatory_ciphers': 'medium',

    # Use DNSSEC to validate TLS certificates
    'smtp_host_lookup': 'dns',
    'smtp_dns_support_level': 'dnssec',
    'smtp_tls_security_level': 'dane',  # Opportunistic DANE TLS

    # Maintain 1 cipherlist and keep it the most secure
    'tls_low_cipherlist': '$tls_medium_cipherlist',
    'tls_high_cipherlist': '$tls_medium_cipherlist',
}

dovecot_cert_config = '/etc/dovecot/conf.d/91-freedombox-ssl.conf'

dovecot_cert_template = """# This file is managed by FreedomBox
ssl_cert = <{cert}
ssl_key = <{key}
"""

logger = logging.getLogger(__name__)


def get():
    results = []
    _get_regular_results(results)
    _get_superuser_results(results)
    return results


def _get_regular_results(results):
    translation_table = [
        (check_tls, _('Postfix TLS parameters')),
        (check_postfix_cert_usage, _('Postfix uses a TLS certificate')),
    ]
    with postconf.mutex.lock_all():
        for check, title in translation_table:
            results.append(check(title))


def _get_superuser_results(results):
    translation = {
        'cert_availability': _('Has a TLS certificate'),
    }
    dump = actions.superuser_run('email_server', ['-i', 'tls', 'check'])
    for jmap in json.loads(dump):
        results.append(models.Diagnosis.from_json(jmap, translation.get))


def repair():
    actions.superuser_run('email_server', ['-i', 'tls', 'set_up'])


def repair_component(action):
    action_to_services = {'set_cert': ['dovecot', 'postfix']}
    if action not in action_to_services:  # action not allowed
        return
    actions.superuser_run('email_server', ['-i', 'tls', action])
    return action_to_services[action]


def check_tls(title=''):
    diagnosis = models.MainCfDiagnosis(title)
    diagnosis.compare(postfix_config, postconf.get_many_unsafe)
    return diagnosis


def repair_tls(diagnosis):
    diagnosis.apply_changes(postconf.set_many_unsafe)


def try_set_up_certificates():
    cert_folder = find_cert_folder()
    if not cert_folder:
        logger.warning('Could not find a suitable TLS certificate')
        return
    logger.info('Using TLS certificate in %s', cert_folder)

    cert = cert_folder + '/cert.pem'
    key = cert_folder + '/privkey.pem'
    write_postfix_cert_config(cert, key)
    write_dovecot_cert_config(cert, key)


def find_cert_folder() -> str:
    directory = '/etc/letsencrypt/live'
    domains_available = []
    try:
        listdir_result = os.listdir(directory)
    except OSError:
        return ''

    for item in listdir_result:
        if item[0] != '.' and os.path.isdir(directory + '/' + item):
            domains_available.append(item)
    domains_available.sort()

    if len(domains_available) == 0:
        return ''
    if len(domains_available) == 1:
        return directory + '/' + domains_available[0]
    # XXX Cannot handle the case with multiple domains
    if len(domains_available) > 1:
        return ''


def write_postfix_cert_config(cert, key):
    postconf.set_many_unsafe({
        'smtpd_tls_cert_file': cert,
        'smtpd_tls_key_file': key
    })


def write_dovecot_cert_config(cert, key):
    content = dovecot_cert_template.format(cert=cert, key=key)
    with interproc.atomically_rewrite(dovecot_cert_config) as fd:
        fd.write(content)


def check_postfix_cert_usage(title=''):
    prefix = '/etc/letsencrypt/live/'
    diagnosis = models.Diagnosis(title, action='set_cert')
    conf = postconf.get_many_unsafe(['smtpd_tls_cert_file',
                                     'smtpd_tls_key_file'])
    if not conf['smtpd_tls_cert_file'].startswith(prefix):
        diagnosis.error("Cert file not in Let's Encrypt directory")
    if not conf['smtpd_tls_key_file'].startswith(prefix):
        diagnosis.error("Privkey file not in Let's Encrypt directory")

    return diagnosis


def su_check_cert_availability(title=''):
    diagnosis = models.Diagnosis(title)
    if find_cert_folder() == '':
        diagnosis.error("Could not find a Let's Encrypt certificate")
    return diagnosis


def action_set_up():
    with postconf.mutex.lock_all():
        repair_tls(check_tls())
        try_set_up_certificates()


def action_set_cert():
    with postconf.mutex.lock_all():
        try_set_up_certificates()


def action_check():
    checks = ('cert_availability',)
    results = []
    for check_name in checks:
        check_function = globals()['su_check_' + check_name]
        results.append(check_function(check_name).to_json())
    json.dump(results, sys.stdout, indent=0)  # indent=0 adds a new line
