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

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

"""
This module contains a class with basic apply action for WPOS modules
"""
import json
import logging
import subprocess
from dataclasses import dataclass
from functools import wraps
from typing import Callable, Optional, Tuple

from clcommon.const import Feature
from clcommon.cpapi import is_panel_feature_supported
from xray.internal.user_plugin_utils import get_xray_exec_user
from xray import gettext as _
from clcommon.clwpos_lib import get_locale_from_envars

from ..exceptions import ApplyAdviceException, RollbackAdviceException
from ..progress import SmartAdviceProgress

logger = logging.getLogger('roc_advice')


@dataclass
class Cmd:
    action: str
    wrapped: bool = False


def inc_total_for_admin(func: Callable) -> Callable:
    """
    Increment total stages in admin mode:
     account 'allow' command as an extra stage.
    Applies to get_progress method of WPOSModuleApply
    """

    @wraps(func)
    def wrapper(*args, **kwargs):
        """
        Wraps func
        """
        data = func(*args, **kwargs)
        if get_xray_exec_user() is None:
            # admin mode
            try:
                if data.total_stages:
                    data.total_stages += 1
            except AttributeError:
                return data
        return data

    return wrapper


class WPOSModuleApply:
    """Basic advice on WPOS module"""

    module_name = 'base_apply'
    suite = 'base_suite'

    short_description: str
    detailed_description: str
    apply_advice_button_text = Optional[str]
    upgrade_to_apply_button_text = Optional[str]
    email_view_advice_text = Optional[str]
    email_subject = Optional[str]

    is_premium_feature: bool

    @property
    def wrapper(self) -> Optional[str]:
        """Special hack for executing user commands on Solo"""
        if not is_panel_feature_supported(Feature.CAGEFS):
            return f'sudo -u  %(username)s -s /bin/bash -c'

    def action_cmds(self, action) -> tuple:

        if action == 'apply':
            admin_cmd = Cmd(
                f'/usr/bin/clwpos-admin set-suite --allowed --suites={self.suite} --users=%(username)s')
        else:
            # do not disallow features during rollback
            admin_cmd = None

        if action == 'apply':
            if not is_panel_feature_supported(Feature.CAGEFS):
                user_cmd = Cmd(f'/usr/bin/clwpos-user enable --domain=%(domain)s --wp-path=%(website)s '
                                 f'--feature={self.module_name} %(ignore_errors)s',
                                 wrapped=True)
            else:
                user_cmd = Cmd(f'/sbin/cagefs_enter_user %(username)s /usr/bin/clwpos-user enable '
                                 f'--domain=%(domain)s --wp-path=%(website)s '
                                 f'--feature={self.module_name} %(ignore_errors)s')
        elif action == 'rollback':
            if not is_panel_feature_supported(Feature.CAGEFS):
                user_cmd = Cmd(f'/usr/bin/clwpos-user disable --domain=%(domain)s --wp-path=%(website)s '
                                 f'--feature={self.module_name}',
                                 wrapped=True)
            else:
                user_cmd = Cmd(f'/sbin/cagefs_enter_user %(username)s /usr/bin/clwpos-user disable '
                                 f'--domain=%(domain)s --wp-path=%(website)s --feature={self.module_name}')
        else:
            raise ValueError(_('Unsupported action with advice, passed action: %s') % str(action))
        if get_xray_exec_user() is None and admin_cmd is not None:
            # admin mode
            return admin_cmd, user_cmd
        else:
            # user mode
            return (user_cmd, )

    @property
    def progress_cmd(self) -> Cmd:
        """Resolve the get-progress command"""
        if not is_panel_feature_supported(Feature.CAGEFS):
            return Cmd(
                f'/usr/bin/clwpos-user get-progress',
                wrapped=True)
        else:
            return Cmd(
                f'/sbin/cagefs_enter_user %(username)s /usr/bin/clwpos-user get-progress')

    @property
    def progress_fields(self) -> tuple:
        """Expected fields of progress"""
        return 'total_stages', 'completed_stages'

    @inc_total_for_admin
    def get_progress(self, as_user: str) -> SmartAdviceProgress:
        """
        Get progress for currently executed command of given user
        """
        try:
            _, result = self._exec_external(self.progress_cmd,
                                            {'username': as_user})
        except ApplyAdviceException:
            return SmartAdviceProgress()
        stages_data = {k: v for k, v in result.items() if k in self.progress_fields}
        return SmartAdviceProgress(**stages_data)

    def apply(self, **kwargs) -> tuple:
        """
        Apply actions for object cache advice
        """
        return self.apply_actions(kwargs)

    def rollback(self, **kwargs) -> tuple:
        """
        Rollback actions for <feature> advice
        """
        return self.rollback_actions(kwargs)

    def apply_actions(self, data: dict) -> tuple:
        """
        Run commands in the given sequence of actions
        """
        data['ignore_errors'] = '--ignore-errors' if data.pop('ignore_errors',
                                                              False) else ''
        applied, result = False, ''
        for cmd in self.action_cmds('apply'):
            try:
                result, _ = self._exec_external(cmd, data)
            except ApplyAdviceException as e:
                result = str(e)
                break
        else:
            applied = True

        return applied, result

    def rollback_actions(self, data: dict) -> tuple:
        rollback, result = False, ''
        for cmd in self.action_cmds('rollback'):
            try:
                result, _ = self._exec_external(cmd, data)
            except RollbackAdviceException as e:
                result = str(e)
                break
        else:
            rollback = True

        return rollback, result

    def _exec_external(self, cmd: Cmd, args: dict) -> Optional[str]:
        """
        Execute external command and return its output or error message
        """
        logger.debug('Attempt to run: %s', cmd)

        # resolve concrete command to execute
        if cmd.wrapped and self.wrapper is not None:
            _exec = (self.wrapper % args).split()
            _exec.append((cmd.action % args).strip())
        else:
            _exec = (cmd.action % args).split()

        try:
            p = subprocess.run(_exec, capture_output=True,
                               text=True, check=True,
                               env={'CL_WPOS_WAIT_CHILD_PROCESS': '1',
                                    'LANG': get_locale_from_envars()})
        except subprocess.CalledProcessError as e:
            logger.debug('non-zero returncode: %s; %s', e.stdout, e.stderr)
            raise ApplyAdviceException(e.stdout) from e
        except (OSError, ValueError, subprocess.SubprocessError) as e:
            logger.debug('cmd failed: %s', str(e))
            raise ApplyAdviceException(str(e)) from e
        else:
            logger.debug('Success! %s', p.stdout)
            return self._postprocess_exec_external(p.stdout)

    @staticmethod
    def _postprocess_exec_external(output: str) -> Optional[Tuple[str, dict]]:
        """Check if 'result' == 'success', raise exception otherwise"""
        try:
            response = json.loads(output)
            if response['result'] != 'success':
                logger.debug('bad result from utility (result != success)')
                raise ApplyAdviceException(output)
        except (json.JSONDecodeError, KeyError) as e:
            logger.debug('malformed result from utility')
            raise ApplyAdviceException(output) from e
        else:
            return output, response
