# -*- coding: utf-8 -*-

import getpass
import os
import shutil
import stat
import sys
import tempfile
import subprocess

import iniparse
import requests

from slapos.cli.command import Command, must_be_root
from slapos.util import parse_certificate_key_pair


class RegisterCommand(Command):
    """
    register a node in the SlapOS cloud
    """
    command_group = 'node'

    def get_parser(self, prog_name):
        ap = super(RegisterCommand, self).get_parser(prog_name)

        ap.add_argument('node_name',
                        help='Name of the node')

        ap.add_argument('--interface-name',
                        default='eth0',
                        help='Primary network interface. IP of Partitions '
                             'will be added to it'
                             ' (default: %(default)s)')

        ap.add_argument('--master-url',
                        default='https://slap.vifib.com',
                        help='URL of SlapOS Master REST API'
                             ' (default: %(default)s)')

        ap.add_argument('--master-url-web',
                        default='https://www.slapos.org',
                        help='URL of SlapOS Master webservice to register certificates'
                             ' (default: %(default)s)')

        ap.add_argument('--partition-number',
                        default=10,
                        type=int,
                        help='Number of partitions to create in the SlapOS Node'
                             ' (default: %(default)s)')

        ap.add_argument('--ipv4-local-network',
                        default='10.0.0.0/16',
                        help='Subnetwork used to assign local IPv4 addresses. '
                             'It should be a not used network in order to avoid conflicts'
                             ' (default: %(default)s)')

        ap.add_argument('--ipv6-interface',
                        help='Interface name to get ipv6')

        ap.add_argument('--login-auth',
                        action='store_true',
                        help='Force login and password authentication')

        ap.add_argument('--login',
                        help='Your SlapOS Master login. '
                             'Asks it interactively, then password.')

        ap.add_argument('--password',
                        help='Your SlapOS Master password. If not provided, '
                             'asks it interactively. NOTE: giving password as parameter '
                             'should be avoided for security reasons.')

        ap.add_argument('--token',
                        help="SlapOS 'computer security' authentication token")

        ap.add_argument('-t', '--create-tap',
                        action='store_true',
                        help='Will trigger creation of one virtual "tap" interface per '
                             'Partition and attach it to primary interface. Requires '
                             'primary interface to be a bridge. '
                             'Needed to host virtual machines'
                             ' (default: %(default)s)')

        ap.add_argument('-n', '--dry-run',
                        action='store_true',
                        help='Simulate the execution steps'
                             ' (default: %(default)s)')

        return ap

    @must_be_root
    def take_action(self, args):
        try:
            conf = RegisterConfig(logger=self.app.log)
            conf.setConfig(args)
            return_code = do_register(conf)
        except SystemExit as err:
            return_code = err

        sys.exit(return_code)


# XXX dry_run will happily register a new node on the slapos master. Isn't it supposed to be no-op?


def check_credentials(url, login, password):
    """Check if login and password are correct"""
    req = requests.get(url, auth=(login, password), verify=False)
    return 'Logout' in req.text


def get_certificate_key_pair(logger, master_url_web, node_name, token=None, login=None, password=None):
    """Download certificates from SlapOS Master"""

    if token:
        req = requests.post('/'.join([master_url_web, 'add-a-server/WebSection_registerNewComputer']),
                            data={'title': node_name},
                            headers={'X-Access-Token': token},
                            verify=False)
    else:
        register_server_url = '/'.join([master_url_web, ("add-a-server/WebSection_registerNewComputer?dialog_id=WebSection_viewServerInformationDialog&dialog_method=WebSection_registerNewComputer&title={}&object_path=/erp5/web_site_module/hosting/add-a-server&update_method=&cancel_url=https%3A//www.vifib.net/add-a-server/WebSection_viewServerInformationDialog&Base_callDialogMethod=&field_your_title=Essai1&dialog_category=None&form_id=view".format(node_name))])
        req = requests.get(register_server_url, auth=(login, password), verify=False)

    if not req.ok and 'Certificate still active.' in req.text:
        # raise a readable exception if the computer name is already used,
        # instead of an opaque 500 Internal Error.
        # this will not work with the new API.
        logger.error('The node name "%s" is already in use. '
                     'Please change the name, or revoke the active '
                     'certificate if you want to replace the node.', node_name)
        sys.exit(1)

    if req.status_code == 403:
        if token:
            msg = 'Please check the authentication token or require a new one.'
        else:
            msg = 'Please check username and password.'
        logger.critical('Access denied to the SlapOS Master. %s', msg)
        sys.exit(1)
    elif not req.ok and 'NotImplementedError' in req.text and not token:
        logger.critical('This SlapOS server does not support login/password '
                        'authentication. Please use the token.')
        sys.exit(1)
    else:
        req.raise_for_status()

    return parse_certificate_key_pair(req.text)


def get_computer_name(certificate):
    """Parse certificate to get computer name and return it"""
    k = certificate.find("COMP-")
    i = certificate.find("/email", k)
    return certificate[k:i]


def save_former_config(conf):
    """Save former configuration if found"""
    former = '/etc/opt/slapos'
    if not os.path.exists(os.path.join(former, 'slapos.cfg')):
        return

    saved = former + '.old'
    while os.path.exists(saved):
        conf.logger.info('Slapos configuration detected in %s', saved)
        if saved[-1] == 'd':
            saved += '.1'
        else:
            # XXX this goes from 19 to 110
            saved = saved[:-1] + str(int(saved[-1]) + 1)
    conf.logger.info('Former slapos configuration detected '
                     'in %s moving to %s', former, saved)
    shutil.move(former, saved)


def get_slapos_conf_example(logger):
    """
    Get slapos.cfg.example and return its path
    """
    _, path = tempfile.mkstemp()
    with open(path, 'wb') as fout:
        req = requests.get('http://git.erp5.org/gitweb/slapos.core.git/blob_plain/HEAD:/slapos.cfg.example')
        try:
            req.content.decode('ascii')
        except UnicodeDecodeError:
            # we have to reject the file because iniparse chokes on non-ascii,
            # and similar packages (cfgparse, INITools etc) have issues with
            # multiline values, like certificates, or do not retain comments (ConfigParser).
            logger.critical('Cannot create configuration file (bad template).')
            sys.exit(1)
        fout.write(req.content)
    return path


def slapconfig(conf):
    """Base Function to configure slapos in /etc/opt/slapos"""
    dry_run = conf.dry_run
    # Create slapos configuration directory if needed
    slap_conf_dir = os.path.normpath(conf.slapos_configuration)

    # Make sure everybody can read slapos configuration directory:
    # Add +x to directories in path
    directory = os.path.dirname(slap_conf_dir)
    while True:
        if os.path.dirname(directory) == directory:
            break
        # Do "chmod g+xro+xr"
        os.chmod(directory, os.stat(directory).st_mode | stat.S_IXGRP | stat.S_IRGRP | stat.S_IXOTH | stat.S_IROTH)
        directory = os.path.dirname(directory)

    if not os.path.exists(slap_conf_dir):
        conf.logger.info('Creating directory: %s', slap_conf_dir)
        if not dry_run:
            os.mkdir(slap_conf_dir, 0o711)

    user_certificate_repository_path = os.path.join(slap_conf_dir, 'ssl')
    if not os.path.exists(user_certificate_repository_path):
        conf.logger.info('Creating directory: %s', user_certificate_repository_path)
        if not dry_run:
            os.mkdir(user_certificate_repository_path, 0o711)

    key_file = os.path.join(user_certificate_repository_path, 'key')
    cert_file = os.path.join(user_certificate_repository_path, 'certificate')

    for src, dst in [(conf.key, key_file), (conf.certificate, cert_file)]:
        conf.logger.info('Copying to %r, and setting minimum privileges', dst)
        if not dry_run:
            with open(dst, 'w') as destination:
                destination.write(''.join(src))
            os.chmod(dst, 0o600)
            os.chown(dst, 0, 0)

    certificate_repository_path = os.path.join(slap_conf_dir, 'ssl', 'partition_pki')
    if not os.path.exists(certificate_repository_path):
        conf.logger.info('Creating directory: %s', certificate_repository_path)
        if not dry_run:
            os.mkdir(certificate_repository_path, 0o711)

    # Put slapos configuration file
    config_path = os.path.join(slap_conf_dir, 'slapos.cfg')

    # Get example configuration file
    slapos_cfg_example = get_slapos_conf_example(conf.logger)
    new_configp = iniparse.RawConfigParser()
    new_configp.read(slapos_cfg_example)
    os.remove(slapos_cfg_example)

    for section, key, value in [
        ('slapos', 'computer_id', conf.computer_id),
        ('slapos', 'master_url', conf.master_url),
        ('slapos', 'key_file', key_file),
        ('slapos', 'cert_file', cert_file),
        ('slapos', 'certificate_repository_path', certificate_repository_path),
        ('slapformat', 'interface_name', conf.interface_name),
        ('slapformat', 'ipv4_local_network', conf.ipv4_local_network),
        ('slapformat', 'partition_amount', conf.partition_number),
        ('slapformat', 'create_tap', conf.create_tap)
    ]:
        new_configp.set(section, key, value)

    if conf.ipv6_interface:
        new_configp.set('slapformat', 'ipv6_interface', conf.ipv6_interface)

    if not dry_run:
        with open(config_path, 'w') as fout:
            new_configp.write(fout)

    conf.logger.info('SlapOS configuration written to %s', config_path)


class RegisterConfig(object):
    """
    Class containing all parameters needed for configuration
    """

    def __init__(self, logger):
        self.logger = logger

    def setConfig(self, options):
        """
        Set options given by parameters.
        """
        # Set options parameters
        for option, value in options.__dict__.items():
            setattr(self, option, value)

    def COMPConfig(self, slapos_configuration, computer_id, certificate, key):
        self.slapos_configuration = slapos_configuration
        self.computer_id = computer_id
        self.certificate = certificate
        self.key = key

    def displayUserConfig(self):
        self.logger.debug('Computer Name: %s', self.node_name)
        self.logger.debug('Master URL: %s', self.master_url)
        self.logger.debug('Number of partition: %s', self.partition_number)
        self.logger.info('Using Interface %s', self.interface_name)
        self.logger.debug('Ipv4 sub network: %s', self.ipv4_local_network)
        self.logger.debug('Ipv6 Interface: %s', self.ipv6_interface)


def gen_auth(conf):
    ask = True
    if conf.login:
        if conf.password:
            yield conf.login, conf.password
            ask = False
        else:
            yield conf.login, getpass.getpass()
    while ask:
        yield raw_input('SlapOS Master Login: '), getpass.getpass()


def do_register(conf):
    """Register new computer on SlapOS Master and generate slapos.cfg"""

    if conf.login or conf.login_auth:
        for login, password in gen_auth(conf):
            if check_credentials(conf.master_url_web, login, password):
                break
            conf.logger.warning('Wrong login/password')
        else:
            return 1

        certificate, key = get_certificate_key_pair(conf.logger,
                                                    conf.master_url_web,
                                                    conf.node_name,
                                                    login=login,
                                                    password=password)
    else:
        while not conf.token:
            conf.token = raw_input('Computer security token: ').strip()

        certificate, key = get_certificate_key_pair(conf.logger,
                                                    conf.master_url_web,
                                                    conf.node_name,
                                                    token=conf.token)

    # get computer id
    COMP = get_computer_name(certificate)

    # Getting configuration parameters
    conf.COMPConfig(slapos_configuration='/etc/opt/slapos/',
                    computer_id=COMP,
                    certificate=certificate,
                    key=key)

    # Save former configuration
    if not conf.dry_run:
        save_former_config(conf)
    # Prepare Slapos Configuration
    slapconfig(conf)

    conf.logger.info('Node has successfully been configured as %s.', COMP)
    # XXX hardcoded value, relying on package installation
    # We shall fix that later
    conf.logger.info('Running starting script')
    if os.path.isfile("/usr/sbin/slapos-start"):
        try:
            subprocess.check_call("/usr/sbin/slapos-start")
        except subprocess.CalledProcessError:
            conf.logger.error('Error while trying to run /usr/sbin/slapos-start')
    else:
        conf.logger.warning('Missing file /usr/sbin/slapos-start')
    return 0