# Copyright (c) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

import os
import json
import traceback
import subprocess
import configparser
import exec_command
from lve_diagnostic import get_cp
from dashboard_malfunctions import criu_settings_malfunctions
from dashboard_malfunctions import lsapi_settings_malfunctions
from dashboard_malfunctions import liblsapi_malfunctions
from stat_utils import cpanel_whmapi, plesk_bin_php_handler, dump_loaded_modules, dump_lsapi, query_strings
from stat_utils import get_da_domains, get_da_php_options, read_da_config, liblsapi_path, pretty_version_keys, count_domains, StatUtilsException
import selector_usage_lib


class ModLsapiStatException(Exception):
    pass


def get(as_json=False):
    """
    Return statistics data
    :param as_json: return json string if True, dict otherwise
    :return: statistics data dict(
                `controlPanel`: EA3/EA4/Plesk/DirectAdmin/Unknown CP,
                `criu`: dict(
                        `status`: running/stopped,
                        `version`: version
                    ),
                `domainStat`: dict(
                        `version`: `domains_num`,
                        ...
                    ) or dict(`error`: description) if some error occurred,
                `lsapiConf`: dict(
                        lsapi_with_connection_pool: on/off,
                        lsapi_criu: on/off
                    )
            ) or its json-packed version
    """
    error = None
    stats = {}
    control_panel = get_cp()
    try:
        if control_panel.name == 'cPanel':
            if os.path.exists('/etc/cpanel/ea4/is_ea4'):
                stats['domainStat'] = get_cpanel_ea4_stat()
            else:
                error = 'mod_lsapi domains stat is currently unsupported for EA3'
        elif control_panel.name == 'Plesk':
            stats['domainStat'] = get_plesk_stat()
        elif control_panel.name == 'DirectAdmin':
            stats['domainStat'] = get_da_stat()
        else:
            error = 'mod_lsapi domains stat is currently unsupported for {0}'.format(control_panel.name)
    except ModLsapiStatException as e:
        error = str(e)

    if error is not None:
        stats['domainStat'] = dict()
        stats['domainStatError'] = error

    stats['controlPanel'] = str(control_panel)
    stats.update(get_lsapi_conf())
    stats.update(get_crui_stat())

    stats.update({'totalDomain': sum(stats['domainStat'].values())})
    analyze_malfunctions(stats)

    if as_json:
        return json.dumps(stats, sort_keys=True)
    else:
        return stats


def get_cpanel_ea4_stat(lsapi_only=True, with_selector=True):
    """
    Collect mod_lsapi statistics for cPanel EA4 through WHM API
    :param lsapi_only: return only lsapi domains statistics if True, or full statistics if False
    :param with_selector: take into account the statistics of php selector
    :return: if lsapi_only return lsapi domains per version
                stat dict(
                    `version`: `domains_num`,
                    ...
                )
            else return full statistics per handler
                stat dict(
                    `handler`: {`version`: `domains_num`, ... }
                    ...
                )
    """
    domains_per_version = dict()  # to store `version`: `domains_list`
    handlers_stat = dict()  # to store `handler`: {`version`: `domains_num`, ...}
    domain_users = dict()  # to store `domain`: `user` correspondence
    try:
        # get all vhosts along with versions
        vhosts_data = cpanel_whmapi('php_get_vhost_versions').get('versions')
        for vhost in vhosts_data:
            domains_per_version.setdefault(vhost.get('version'), set()).add(vhost.get('vhost'))
            domain_users[vhost.get('vhost')] = vhost.get('account')
        # get handlers for versions
        handlers_data = cpanel_whmapi('php_get_handlers').get('version_handlers')
        version_handlers = dict([(h.get('version'), h.get('current_handler')) for h in handlers_data])
        all_versions = list(version_handlers.keys())

        # map {version: domains_list} to handlers, domains count in place
        for ver, handler in version_handlers.items():
            handlers_stat.setdefault(handler, dict()).update({ver: domains_per_version.get(ver, set())})

        # reinspect handlers stat against selector
        if with_selector:
            s_checked_version_handlers = selector_usage_lib.ea4_selector_check(domains_per_version, domain_users, handlers_stat)
            handlers_stat = s_checked_version_handlers

        # update structure with versions, which are not used by one handler, e.g. `ver: 0 domains`
        for h, v in handlers_stat.items():
            v.update(dict.fromkeys(set(all_versions).difference(list(v.keys())), set()))
    except (KeyError, TypeError, StatUtilsException):
        raise ModLsapiStatException(''.join(traceback.format_exc().split('\n')))

    # return only number of domains
    return count_domains(handlers_stat, all_versions, lsapi_only)


def get_plesk_stat(lsapi_only=True, with_selector=True):
    """
    Collect mod_lsapi statistics for Plesk
    Collects lsapi domains ONLY
    :param lsapi_only: return only lsapi domains statistics if True, or full statistics if False
    :param with_selector: take into account the statistics of php selector
    :return: if lsapi_only return lsapi domains per version dict(
                    `version`: `domains_num`
                    ...
                )
            else return stats with handler dict(
                    `lsapi`: {`version`: `domains_num`, ... }
                )
    """
    handler_tmpl = 'alt-php{v}'
    custom_version = 'alt-php56'
    domain_version_stat = dict()
    try:
        all_handlers = plesk_bin_php_handler('list')
        # on Plesk mod_lsapi is used only through handlers x-httpd-lsphp-*, which are added by --setup
        lsphp_handlers = [h for h in all_handlers if 'lsphp' in h.get('id')]
        for handler in lsphp_handlers:
            handler_id = handler.get('id')
            php_version = ''.join(handler.get('fullVersion').split('.')[:-1])
            domains = set([d.get('domain') for d in plesk_bin_php_handler('get-usage', id=handler_id)])
            # x-httpd-lsphp-custom domains are to be checked withon selector
            if 'custom' in handler_id:
                version_id = 'custom'
                custom_version = handler_tmpl.format(v=php_version)
            else:
                version_id = handler_tmpl.format(v=php_version)
            domain_version_stat[version_id] = domains
        # PLACE SELECTOR CHECK HERE (should be done for custom handler)
        if with_selector:
            domain_version_stat = selector_usage_lib.plesk_selector_check(domain_version_stat, custom_version)
    except (KeyError, TypeError, AttributeError, StatUtilsException, selector_usage_lib.SelectorStatException):
        raise ModLsapiStatException(''.join(traceback.format_exc().split('\n')))
    # return only number of domains
    result_stat = {
        'lsapi': {k: len(v) for k, v in domain_version_stat.items()}
    }
    return result_stat['lsapi'] if lsapi_only else result_stat


def get_da_stat(lsapi_only=True, with_selector=True):
    """
    Collect lsapi domains statistics fro DirectAdmin
    :param lsapi_only: return only lsapi domains statistics if True, or full statistics if False
    :param with_selector: take into account the statistics of php selector
    :return: if lsapi_only return lsapi domains per version
                stat dict(
                    `version`: `domains_num`,
                    ...
                )
            else return full statistics per handler
                stat dict(
                    `handler`: {`version`: `domains_num`, ... }
                    ...
                )
    """
    domain_conf_path = '/usr/local/directadmin/data/users/{user}/domains/{domain}.conf'
    handler_stat = dict()
    try:
        # get php settings from option.conf
        php_options = get_da_php_options()
        php1_ver = php_options[1]['version']
        php2_ver = php_options[2]['version']
        php1_handler = php_options[1]['handler']
        php2_handler = php_options[2]['handler']

        php1_label = pretty_version_keys(php1_ver)
        php2_label = pretty_version_keys(php2_ver)

        # get user: domains correspondence
        user_domains = get_da_domains()
        joined = set()
        [joined.update(v) for v in user_domains.values()]
        # analyze options.conf settings for versions
        if php2_ver == 'no':
            # no secondary php set, assume all domains use primary
            version_stat = {php1_label: joined}
            handler_stat[php1_handler] = {php1_label: joined}
        elif php1_ver == 'no':
            # no primary php set, assume all domains use secondary
            version_stat = {php2_label: joined}
            handler_stat[php2_handler] = {php2_label: joined}
        else:
            version_stat = {php1_label: set(), php2_label: set()}
            # if both php releases in options.conf are set, that means that DA PHP selector is enabled
            for user, domains in user_domains.items():
                # find php release settings for each domain
                for domain in domains:
                    config_path = domain_conf_path.format(user=user, domain=domain)
                    try:
                        # try to find which version domain uses as primary
                        conf_parser, global_section = read_da_config(config_path)
                        php_setting = conf_parser.getint(global_section, 'php1_select')
                        version = pretty_version_keys(php_options[php_setting]['version'])
                        version_stat.get(version).add(domain)
                    except configparser.NoOptionError:
                        # means that domain do not use DA PHP selector and uses primary php version from options.conf
                        version_stat.get(php1_label).add(domain)
            # if both php releases in options.conf are set, that means that DA PHP selector is enabled
            # create per-handler statistics
            if php1_handler == php2_handler:
                handler_stat[php1_handler] = {php1_label: version_stat[php1_label],
                                              php2_label: version_stat[php2_label]}
            else:
                handler_stat[php1_handler] = {php1_label: version_stat[php1_label],
                                              php2_label: set()}
                handler_stat[php2_handler] = {php2_label: version_stat[php2_label],
                                              php1_label: set()}

        if with_selector:
            selector_updated = selector_usage_lib.da_selector_check(version_stat.get(php1_label), user_domains, php1_label)
            handler_stat.get(php1_handler).update(selector_updated)
    except (KeyError, TypeError, AttributeError, StatUtilsException):
        raise ModLsapiStatException(''.join(traceback.format_exc().split('\n')))

    # return only number of domains
    return count_domains(handler_stat, [php1_label, php2_label], lsapi_only)


def get_crui_stat():
    """
    Get criu service info
    :return: dict(
                `criu`: dict(
                    `status`: running/stopped,
                    `version`: version
                )
            )
    """
    criu_version = ''.join(exec_command.exec_command('/usr/sbin/criu -V'))

    try:
        subprocess.check_call(['/sbin/service', 'criu', 'status'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        criu_service_status = 'running'
    except subprocess.CalledProcessError:
        criu_service_status = 'stopped'

    return {
        'criu': {
            'status': criu_service_status,
            'version': criu_version.split(' ')[1] if criu_version else 'not installed'
        }
    }


def get_lsapi_conf():
    """
    Retrieve lsapi configuration:
    - crui
    - connection pool
    and versions of module and library
    :return: dict(
                lsapiConf: dict(
                    lsapi_with_connection_pool: on/off,
                    lsapi_criu: on/off
                ),
                modVersion: version,
                libVersion: version
            )
    """
    # Plesk carries upstream apache 2.4.6, which says
    # `Passing arguments to httpd using apachectl is no longer supported.`
    # have to query with httpd instead of apachectl
    apache_conf = dump_lsapi()
    apache_modules = dump_loaded_modules()
    mod_status = apache_modules.get('lsapi_module', None)
    return {
        'lsapiConf': {
            'lsapi_criu': apache_conf.get('lsapi_criu', 'off'),
            'lsapi_with_connection_pool': apache_conf.get('lsapi_with_connection_pool', 'off')
        },
        'modVersion': apache_conf.get('version', None),
        'libVersion': query_strings(liblsapi_path(), 'liblsapi_version'),
        'modStatus': 'disabled' if not mod_status else 'enabled'
    }


def analyze_malfunctions(stats_dict):
    """
    Detect configuration malfunctions and update resulting statistics dict accordingly
    For now only criu malfunctions are presented
    See malfunctions in dashboard_malfunctions.py module
    :param stats_dict: resulting statistics dict
    """
    malfunctions = list()

    def add_malfunction(malfunc_dict, key):
        try:
            # try to detect one malfunction
            malfunctions.append(malfunc_dict[key])
        except KeyError:
            # no malfunction found
            pass

    criu_settings = '{opt}_{serv}'.format(opt=stats_dict['lsapiConf']['lsapi_criu'],
                                          serv=stats_dict['criu']['status'])

    lsapi_settings = stats_dict['modStatus']
    # for further extension of similar malfunctions:
    # malfuncs = tuple of malfunctions
    # keys = tuple of according keys
    # for malfunc, k in zip(malfuncs, keys):
    #     add_malfunction(malfunc, k)
    add_malfunction(liblsapi_malfunctions, stats_dict['libVersion'])
    add_malfunction(lsapi_settings_malfunctions, lsapi_settings)
    add_malfunction(criu_settings_malfunctions, criu_settings)

    # no need in `malfunctions` field if there are no malfunctions
    if not malfunctions:
        return
    else:
        stats_dict.update({'malfunctions': malfunctions})
