#   Copyright Peznauts <kevin@cloudnull.com>. All Rights Reserved.
#
#   Licensed under the Apache License, Version 2.0 (the "License"); you may
#   not use this file except in compliance with the License. You may obtain
#   a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#   WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#   License for the specific language governing permissions and limitations
#   under the License.

import argparse
import os
import subprocess

from multiprocessing import Event

import jinja2
from jinja2 import StrictUndefined

import yaml

from directord import logger
from directord import utils


class ComponentBase:
    """Component base class."""

    command = None
    info = None
    driver = None
    verb = None
    block_on_tasks = None
    queue_sentinel = False
    delay = Event().wait

    def __init__(self, desc=None):
        """Initialize the component base class.

        When setting up a component, the init should be inheritted allowing
        user defined components to have access to the full suite of defaults.

        > Set the `self.cacheable` object True|False according to how the
          component should be treated in terms of on system cache.
        """

        self.desc = desc
        self.log = logger.getLogger(name="directord")
        self.blueprint = jinja2.Environment(
            loader=jinja2.BaseLoader(),
            keep_trailing_newline=True,
            undefined=StrictUndefined,
        )
        self.known_args = None
        self.unknown_args = None
        self.cacheable = True  # Enables|Disables component caching
        self.requires_lock = False  # Enables|Disables component locking

    @staticmethod
    def run_command(
        command,
        shell=True,
        env=None,
        execute="/bin/sh",
        return_codes=None,
        no_block=False,
    ):
        """Run a shell command.

        The options available:

        * `shell` to be enabled or disabled, which provides the ability
        to execute arbitrary stings or not. if disabled commands must be
        in the format of a `list`

        * `env` is an environment override and or manipulation setting
        which sets environment variables within the locally executed
        shell.

        * `execute` changes the interpreter which is executing the
        command(s).

        * `return_codes` defines the return code that the command must
        have in order to ensure success. This can be a list of return
        codes if multiple return codes are acceptable.

        :param command: String
        :param shell: Boolean
        :param env: Dictionary
        :param execute: String
        :param return_codes: Integer|List
        :param no_block: Boolean
        :returns: Truple
        """

        if env:
            _env = dict(os.environ)
            _env.update(env)
            env = _env
        else:
            env = os.environ

        stdout = subprocess.PIPE

        if return_codes is None:
            return_codes = [0]
        if isinstance(return_codes, int):
            return_codes = [return_codes]

        stderr = subprocess.PIPE
        process = subprocess.Popen(
            command,
            stdout=stdout,
            stderr=stderr,
            executable=execute,
            env=env,
            shell=shell,
            start_new_session=no_block,
        )
        if no_block:
            return None, None, True

        output, error = process.communicate()
        if process.returncode not in return_codes:
            return output, error, False

        return output, error, True

    def options_converter(self, documentation):
        """Convert an options YAML to Arguments.

        :param documentation: YAML content.
        :type documentation: String
        """

        argument_spec = yaml.safe_load(documentation)["options"]
        for key, value in argument_spec.items():
            options = dict()
            description = value.pop("description", None)
            if description:
                if isinstance(description, list):
                    description = " ".join(description)
                options["help"] = description

            default = value.pop("default", None)
            if default:
                options["default"] = default

            required = value.pop("required", None)
            if isinstance(required, bool):
                options["required"] = required
            elif isinstance(required, str) and required.lower() in [
                "yes",
                "true",
            ]:
                options["required"] = True

            arg_type = value.pop("type", None)
            if isinstance(arg_type, str):
                arg_type = arg_type.lower()
                if arg_type == "bool":
                    options["action"] = "store_true"
                elif arg_type == "list":
                    options["type"] = list
                elif arg_type == "dict":
                    options["type"] = dict
                elif arg_type == "int":
                    options["type"] = int
                elif arg_type == "str":
                    options["type"] = str

            choices = value.pop("choices", None)
            if choices:
                options["choices"] = choices

            self.parser.add_argument(
                "--{}".format(key.replace("_", "-")), **options
            )

    @staticmethod
    def sanitized_args(execute):
        """Return arguments in a flattened array.

        This will inspect the execution arguments and return everything found
        as a flattened array.

        :param execute: Execution string to parse.
        :type execute: String
        :returns: List
        """

        if execute:
            return [i for g in execute if g for i in g.split()]
        else:
            return list()

    def exec_parser(self, parser, exec_array, arg_vars=None):
        """Run the parser and return parsed arguments.

        :param parser: Argument parser.
        :type parser: Object
        :param exec_array: Input array from action
        :type exec_array: List
        :param arg_vars: Pre-Formatted arguments
        :type arg_vars: Dictionary
        :returns: Tuple
        """

        self.known_args, self.unknown_args = parser.parse_known_args(
            self.sanitized_args(execute=exec_array)
        )
        if hasattr(self.known_args, "exec_help") and self.known_args.exec_help:
            raise SystemExit(parser.print_help())
        else:
            if arg_vars:
                for key, value in arg_vars.items():
                    self.known_args.__dict__[key] = value
            return self.known_args, self.unknown_args

    def args(self):
        """Set default arguments for a component."""

        self.parser = argparse.ArgumentParser(
            description=self.desc,
            allow_abbrev=False,
            add_help=False,
        )
        self.parser.add_argument(
            "--exec-help",
            action="help",
            help="Show this execution help message.",
        )
        self.parser.add_argument(
            "--skip-cache",
            action="store_true",
            help="For a task to skip the on client cache.",
        )
        self.parser.add_argument(
            "--run-once",
            action="store_true",
            help="Force a given task to run once.",
        )
        self.parser.add_argument(
            "--timeout",
            default=600,
            type=int,
            help="Set the action timeout. Default %(default)s.",
        )
        self.parser.add_argument(
            "--force-lock",
            action="store_true",
            help="Force a given task to run with a lock.",
        )

    @staticmethod
    def set_cache(
        cache,
        key,
        value,
        value_update=False,
        expire=259200,
        tag=None,
        extend=False,
    ):
        """Set a cached item.

        :param cache: Cached access object.
        :type cache: Object
        :param key: Key for the cached item.
        :type key: String
        :param value: Value for the cached item.
        :type value: ANY
        :param value_update: Instructs the method to update a Dictionary with
                             another dictionary.
        :type value_update: Boolean
        :param expire: Sets the expire time, defaults to 12 hours.
        :type expire: Integer
        :param tag: Sets the index for a given cached item.
        :type tag: String
        :param extend: Enable|Disable Extend a map
        :type extend: Boolean
        :returns" Boolean
        """

        if value_update:
            orig = cache.get(key, default=dict())
            value = utils.merge_dict(orig, value, extend=extend)
        else:
            try:
                value = value.decode()
            except (ValueError, AttributeError):
                pass

        cache_set = cache.set(key, value, tag=tag, expire=expire, retry=True)
        if not cache_set:
            return cache.set(key, value, tag=tag, expire=expire, retry=True)
        return cache_set

    def file_blueprinter(self, cache, file_to):
        """Read a file and blueprint its contents.

        :param cache: Cached access object.
        :type cache: Object
        :param file_to: String path to a file which will blueprint.
        :type file_to: String
        :returns: Boolean
        """

        try:
            with open(file_to) as f:
                success, contents = self.blueprinter(
                    content=f.read(), values=cache.get("args")
                )

            if not success:
                return False, contents

            with open(file_to, "w") as f:
                f.write(contents)
        except Exception as e:
            error = str(e)
            self.log.critical("File blueprint failure: %s", error)
            return False, error
        else:
            self.log.info("File %s has been blueprinted.", file_to)
            return True, None

    def blueprinter(self, content, values, allow_empty_values=False):
        """Return blue printed content.

        :param content: A string item that will be interpreted and blueprinted.
        :type content: String
        :param values: Dictionary items that will be used to render a
                       blueprinted item.
        :type values: Dictionary
        :param allow_empty_values: Enable|Disable null values
        :type allow_empty_values: Boolean
        :returns: Tuple
        """

        if not values:
            if allow_empty_values:
                values = dict()
            else:
                return False, "No arguments were defined for blueprinting"

        try:
            _contents = self.blueprint.from_string(content)
            rendered_content = _contents.render(**values)
        except Exception as e:
            error = str(e)
            self.log.debug("blueprint error: %s, values: %s", error, values)
            return False, error
        else:
            return True, rendered_content

    def parser_error(self):
        """Return parser help information."""

        return self.parser.print_help()

    def server(self, exec_array, data, arg_vars):
        """Server operation.

        :param exec_array: Input array from action
        :type exec_array: List
        :param data: Formatted data hash
        :type data: Dictionary
        :returns: Dictionary
        """

        self.args()
        self.exec_parser(
            parser=self.parser, exec_array=exec_array, arg_vars=arg_vars
        )

    def client(self, cache, job):
        """Client operation.

        Command operations are rendered with cached data from the args dict.

        :param cache: Caching object used to template items within a command.
        :type cache: Object
        :param job: Information containing the original job specification.
        :type job: Dictionary
        """

        pass

    def close(self):
        """Close a component."""

        pass

    def __enter__(self):
        """Return self when using a component as a context manager."""

        return self

    def __exit__(self, *args, **kwargs):
        """Exit the context manager and close."""

        self.close()
