# -*- coding: UTF-8 -*-

# Copyright (C) 2016 Avencall
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.


import logging
import random
import re
import socket
import subprocess
import zoneinfo
from os import access, environ, listdir, X_OK
from os.path import isdir, isfile, islink, join, realpath

import netifaces

from xivo_confd import sysconfd
from xivo_confd.application import commit_database
from xivo_confd.database import wizard as wizard_db
from xivo_confd.helpers.exceptions import ConfdWizardException
from xivo_confd.plugins.wizard.notifier import build_notifier
from xivo_confd.plugins.wizard.validator import build_validator

logger = logging.getLogger(__name__)

USERNAME_VALUES = '2346789bcdfghjkmnpqrtvwxyzBCDFGHJKLMNPQRTVWXYZ'
NAMESERVER_REGEX = '^nameserver (.*)'


class WizardService(object):
    SCRIPT_DIRECTORY = '/etc/xivo/wizard.d/'

    def __init__(self, validator, notifier, infos_dao, provd_client, sysconfd):
        self.validator = validator
        self.notifier = notifier
        self.infos_dao = infos_dao
        self.provd_client = provd_client
        self.sysconfd = sysconfd

    def get(self):
        return wizard_db.get_xivo_configured()

    def create(self, wizard):
        logger.debug(f'Processing wizard: {wizard}')
        self.validator.validate_create(wizard)
        autoprov_username = self._generate_autoprov_username()
        wizard_db.create(wizard, autoprov_username)
        commit_database()
        self._send_sysconfd_start_service()
        if wizard.get('handle_system_conf'):
            logger.debug('handling system configurations')
            self._send_sysconfd_cmd(
                wizard['network']['hostname'],
                wizard['network']['domain'],
                wizard['network']['nameservers']
            )
        else:
            logger.debug('system configuration was skipped')
        self._send_sysconfd_commonconf()

        self._initialize_provd(
            wizard['network']['ip_address'], autoprov_username,
            wizard['default_french_configuration']
        )
        wizard_db.set_xivo_configured()
        self.notifier.created()
        wizard['xivo_uuid'] = self.infos_dao.get().uuid
        if wizard['run_scripts']:
            self._run_scripts()
        logger.debug('End of wizard.')
        return wizard

    def _send_sysconfd_start_service(self):
        self.sysconfd.xivo_service_enable()
        self.sysconfd.flush()
        self.sysconfd.xivo_service_start()
        self.sysconfd.flush()

    def _send_sysconfd_cmd(self, hostname, domain, nameserver):
        self.sysconfd.set_hosts(hostname, domain)
        self.sysconfd.set_resolvconf(nameserver, domain)

    def _send_sysconfd_commonconf(self):
        self.sysconfd.commonconf_generate()
        self.sysconfd.flush()
        self.sysconfd.commonconf_apply()
        self.sysconfd.flush()

    def _initialize_provd(self, address, autoprov_username, default_french_configuration):
        default_config = {'X_type': 'registrar',
                          'id': 'default',
                          'deletable': False,
                          'displayname': 'local',
                          'parent_ids': [],
                          'raw_config': {'X_key': 'xivo'},
                          'proxy_main': address,
                          'registrar_main': address}
        base_config = {'X_type': 'internal',
                       'id': 'base',
                       'deletable': False,
                       'parent_ids': [],
                       'raw_config': {'ntp_enabled': True,
                                      'ntp_ip': address,
                                      'X_xivo_phonebook_ip': address}}
        device_config = {u'X_type': u'device',
                         u'deletable': False,
                         u'id': u'defaultconfigdevice',
                         u'label': u'Default config device',
                         u'parent_ids': [],
                         u'raw_config': {u'ntp_enabled': True,
                                         u'ntp_ip': address,
                                         u'sip_dtmf_mode': u'SIP-INFO'}}
        device_config_fr = {u'X_type': u'device',
                            u'deletable': False,
                            u'id': u'defaultconfigdevice',
                            u'label': u'Default config device',
                            u'parent_ids': [],
                            u'raw_config': {u'ntp_enabled': True,
                                            u'ntp_ip': address,
                                            u'sip_dtmf_mode': u'SIP-INFO',
                                            u'locale': 'fr_FR',
                                            u'timezone': 'Europe/Paris'}}
        autoprov_config = {u'X_type': u'internal',
                           u'deletable': False,
                           u'id': u'autoprov',
                           u'parent_ids': [u'base', u'defaultconfigdevice'],
                           u'raw_config': {u'sccp_call_managers': {u'1': {u'ip': address}},
                                           u'sip_lines': {u'1': {u'display_name': u'Autoprov',
                                                                 u'number': u'autoprov',
                                                                 u'password': u'autoprov',
                                                                 u'proxy_ip': address,
                                                                 u'registrar_ip': address,
                                                                 u'username': autoprov_username}}},
                           u'role': u'autocreate'}

        self.provd_client.config_manager().add(base_config)
        self.provd_client.config_manager().add(default_config)
        if (default_french_configuration):
            self.provd_client.config_manager().add(device_config_fr)
        else:
            self.provd_client.config_manager().add(device_config)
        self.provd_client.config_manager().add(autoprov_config)

    def _generate_autoprov_username(self):
        suffix = ''.join(random.choice(USERNAME_VALUES) for _ in range(8))
        return 'ap{}'.format(suffix)

    def get_interfaces(self):
        result = []
        candidate_interfaces = (interface for interface in netifaces.interfaces()
                                if interface != 'lo')
        for interface in candidate_interfaces:
            addresses_ipv4 = netifaces.ifaddresses(interface).get(netifaces.AF_INET, [])
            for address in addresses_ipv4:
                result.append({'ip_address': address.get('addr'),
                               'netmask': address.get('netmask'),
                               'interface': interface})
        return result

    def get_gateways(self):
        gateways = []
        for gateway in netifaces.gateways().get(netifaces.AF_INET, []):
            gateways.append({'gateway': gateway[0],
                             'interface': gateway[1]})
        return gateways

    def get_nameservers(self):
        nameserver_regex = re.compile(NAMESERVER_REGEX)
        nameservers = []
        try:
            with open('/etc/resolv.conf', 'r') as f:
                for line in f.readlines():
                    nameserver = re.match(nameserver_regex, line)
                    if nameserver:
                        nameservers.append(nameserver.group(1))
        except IOError:
            pass

        return nameservers

    def get_timezone(self):
        # First we try with TZ env var
        timezone_from_env = environ.get("TZ")
        if timezone_from_env is not None:
            logger.debug(f'Found timezone {timezone_from_env} in TZ env var')
            try:
                # Validate by attempting to create a ZoneInfo object
                zoneinfo.ZoneInfo(timezone_from_env)
                logger.debug(f'Valid timezone {timezone_from_env} found via TZ env var')
                return timezone_from_env
            except zoneinfo.ZoneInfoNotFoundError:
                logger.warning(f'Not able to create a ZoneInfo from {timezone_from_env} from TZ env var')
                logger.debug('Trying to find timezone info from /etc/localtime')

        # Then trying with /etc/localtime
        localtime_path = '/etc/localtime'
        if not islink(localtime_path):
            logger.error(f'{localtime_path} is not a symlink ; cannot find the timezone')
            return None

        # Get the real path (e.g., /usr/share/zoneinfo/Europe/Paris)
        real_path = realpath(localtime_path)
        parts = real_path.split('/')

        # Try each suffix of the path to find a valid timezone
        # Start from index 1 to skip the empty string from leading '/'
        for i in range(1, len(parts)):
            timezone_candidate = '/'.join(parts[i:])
            try:
                # Validate by attempting to create a ZoneInfo object
                zoneinfo.ZoneInfo(timezone_candidate)
                logger.debug(f'Valid timezone {timezone_candidate} found via /etc/localtime')
                return timezone_candidate
            except zoneinfo.ZoneInfoNotFoundError:
                continue

        logger.error('No valid timezone info found in TZ env var nor in /etc/localtime. This should not happen.')
        return None

    def get_hostname(self):
        return self._hostname_without_domain()

    def get_domain(self):
        return self._fqdn_without_hostname()

    def _hostname_without_domain(self):
        hostname = socket.gethostname().split('.', 1)
        return hostname[0]

    def _fqdn_without_hostname(self):
        fqdn = socket.getfqdn().split('.', 1)
        if len(fqdn) == 2:
            return fqdn[1]
        return None

    def _run_scripts(self):
        """
            Runs executable shell scripts from /etc/xivo/wizards.d directory
            It is triggered after the wizard installation if the run_scripts option was chosen there
        """
        if isdir(self.SCRIPT_DIRECTORY):

            filenames = [_ for _ in listdir(self.SCRIPT_DIRECTORY) if isfile(join(self.SCRIPT_DIRECTORY, _))]
            
            for filename in sorted(filenames):

                script = join(self.SCRIPT_DIRECTORY, filename)

                logger.debug(f'Script to run: {script}')

                if not (access(script, X_OK)):
                    logger.warning(f'Missing rights to execute: {script}')
                    continue

                process = subprocess.Popen(
                    script,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT
                )
                out, _ = process.communicate()
                if process.returncode != 0:
                    logger.error(
                        f'Script {script} ended with non-zero return code ({process.returncode}). '
                        f'Output of script: {out.decode()}'
                    )
                    raise ConfdWizardException(f'Script {script} ended with non-zero return code ({process.returncode}).')
        else:
            message = f'Running of scripts was requested but script directory ({self.SCRIPT_DIRECTORY}) does not exist.'
            logger.error(message)
            raise ConfdWizardException(message)


def build_service(provd_client, infos_dao):
    return WizardService(build_validator(),
                         build_notifier(),
                         infos_dao,
                         provd_client,
                         sysconfd)
