__author__ = "Vanessa Sochat"
__copyright__ = "Copyright 2020-2022, Vanessa Sochat"
__license__ = "MPL 2.0"

from watchme.logger import bot
from watchme.utils import write_file, write_json

import shutil
import os


class TaskBase:
    def __init__(self, name, params=None, **kwargs):

        # If params not defined, create as dict
        if params is None:
            params = {}

        # Ensure subclass was created correctly
        for req in ["required_params", "type", "export_func"]:
            if not hasattr(self, req):
                bot.exit("A Task must have a %s function or attribute." % req)

        self.name = name
        self.valid = False
        self.params = {}
        self.set_params(params)
        self.validate()

    def get_type(self):
        """get the watcher type."""
        return self.type

    # Identification

    def __repr__(self):
        return "[task|%s]" % self.name

    def __str__(self):
        return "[task|%s]" % self.name

    # Parameters

    def set_params(self, params):
        """iterate through parameters, set into dictionary.

        Parameters
        ==========
        params: a list of key@value pairs to set.
        """
        for key, value in params.items():
            key = key.lower()
            self.params[key] = value

    def export_params(self, active="true"):
        """export parameters, meaning returning a dictionary of the task
        parameters plus the addition of the task type and active status.
        """
        params = self.params.copy()
        params["active"] = active
        params["type"] = self.type
        return params

    # Validation

    def validate(self):
        """validate the parameters set for the Task. Exit if there are any
        errors. Ensure required parameters are defined, and have correct
        values.
        """
        self.valid = True

        for param in self.required_params:
            if param not in self.params:
                bot.error("Missing required parameter: %s" % param)
                self.valid = False

        # Call subclass validation function
        self._validate()

    def _validate(self):
        """validation function intended to be implemented by subclass."""
        return

    # Run Single Task

    def run(self):
        """run an isolated task, meaning no update or communication with
        the watcher. This will return the raw result.
        """
        params = self.export_params()
        func = self.export_func()
        if func is not None:
            return func(**params)
        bot.error("Cannot find function.")

    # Save Entrypoint

    def write_results(self, result, repo):
        """an entrypoint function for a general task. By default, we parse
        results based on the result type. Any particular subclass of the
        TaskBase can modify or extend these functions.

        Parameters
        ==========
        result: the result object to parse
        repo: the repo base (watcher.repo)
        """
        files = []

        # Case 1. The result is a list
        if isinstance(result, list):

            # Get rid of Nones, if the user accidentally added
            result = [r for r in result if r]

            if len(result) == 0:
                bot.error("%s returned empty list of results." % self.name)

            # multiple jsons save specified, regardless
            elif self.params.get("save_as") == "jsons":
                bot.debug("Saving single list as multiple json...")
                files += self._save_json_list(result, repo)

            # json output is specified by the user or we find dict results
            elif self.params.get("save_as") == "json" or isinstance(result[0], dict):
                bot.debug("Saving single list as one json...")
                files.append(self._save_json(result, repo))

            # Otherwise, sniff for list of paths
            elif os.path.exists(result[0]):
                bot.debug("Found list of paths...")
                files += self._save_files_list(result, repo)

            # Finally, assume just writing text to file
            else:
                bot.debug("Saving content from list to file...")
                files += self._save_text_list(result, repo)

        # Case 2. The result is a string
        elif isinstance(result, str):
            files = self._save_str_result(files, result, repo)

        # Case 3. The result is a dictionary
        elif isinstance(result, dict):
            files.append(self._save_json(result, repo))

        elif result is None:
            bot.error("Result for task %s is None" % self.name)

        elif hasattr(self, "_write_results"):
            return self._write_results(result)

        # If it's unicode, try encoding, and then fail (repetitive)
        else:
            try:
                result = result.encode("utf-8")
                files = self._save_str_result(files, result, repo)

            except:
                bot.error("Unsupported result format %s" % type(result))

        # Get rid of None results (don't check excessively for None above)
        files = [f for f in files if f]
        return files

    # Saving

    def _save_str_result(self, files, result, repo):
        """a helper function to return a list of files with added string
        results. We do this twice in the saving function.

        Parameters
        ==========
        files: the list of files to save (returned at the end)
        result: the result object to parse, should be string
        repo: the repo to write it to.
        """
        # if it's a path to a file, just save to repository
        if os.path.exists(result):
            files.append(self._save_file(result, repo))

        # Otherwise, it's a string that needs to be saved to file
        else:
            files.append(self._save_text(result, repo))

        return files

    def _save_list(self, results, repo, func, file_name):
        """general function to perform saving a list of content to json,
        text, or moving paths. Used by save_file_list, save_json_list,
        and save_text_list.

        Parameters
        ==========
        results: list of results, assumed to be the correct type
        repo: the repository base with the task folder
        func: the function to call.
        file_name: the base file name to use.
        """
        file_name, ext = os.path.splitext(file_name)
        files = []

        for r, result in enumerate(results):
            filename = "%s-%s%s" % (file_name, str(r), ext)
            saved = func(result, repo, filename)
            files.append(saved)

        return files

    def _save_text(self, result, repo, file_name=None):
        """save a general text object to file.

        Parameters
        ==========
        result: the result object to save, should be path to a file
        repo: the repository base with the task folder
        """
        file_name = self.params.get("file_name", file_name) or "result.txt"
        task_folder = os.path.join(repo, self.name)
        destination = os.path.join(task_folder, file_name)
        write_file(destination, result)
        return destination

    def _save_file(self, result, repo, file_name=None):
        """for a result that exists, move the file to final destination.

        Parameters
        ==========
        result: the result object to save, should be path to a file
        repo: the repository base with the task folder
        """
        if os.path.exists(result):
            name = os.path.basename(result)
            file_name = self.params.get("file_name", file_name) or name
            task_folder = os.path.join(repo, self.name)

            # HOME/.watchme/watcher/<task>/<result>
            destination = os.path.join(task_folder, file_name)
            shutil.move(result, destination)

            # <task>/<result>
            return os.path.join(name, file_name)

        bot.warning("%s does not exist." % result)

    def _save_json(self, result, repo, file_name=None):
        """for a result that is a dictionary or list, save as json

        Parameters
        ==========
        result: the result object to save, should be dict or list
        """
        file_name = self.params.get("file_name", file_name) or "result.json"
        destination = os.path.join(repo, self.name, file_name)
        write_json(result, destination)
        return destination

    def _save_files_list(self, results, repo):
        """If the user provides already existing files, we simply move them
        into the task folder in the repository.

        Parameters
        ==========
        results: the results to save, should a list of files
        """
        files = []
        for result in results:
            filename = os.path.basename(result)
            filename = os.path.join(repo, self.name, filename)
            shutil.move(result, filename)
            files.append(filename)

        return files

    def _save_json_list(self, results, repo):
        """A wrapper around save json for a list, handles file naming.

        Parameters
        ==========
        results: the results to save, should a list
        """
        file_name = self.params.get("file_name", "result.json")
        return self._save_list(results, repo, self._save_json, file_name)

    def _save_file_list(self, results, repo):
        """for a list of results that exist, move the files to
        final destination.

        Parameters
        ==========
        results: list of paths to a file, those not existing are skipped
        repo: the repository base with the task folder
        """
        file_name = self.params.get("file_name", os.path.basename(results))
        return self._save_list(results, repo, self._save_file, file_name)

    def _save_text_list(self, results, repo):
        """for a list of general text results, write them to output files.

        Parameters
        ==========
        results: list of string results to write to file
        repo: the repository base with the task folder
        """
        file_name = self.params.get("file_name", "result.txt")
        return self._save_list(results, repo, self._save_text, file_name)
