###################################################################################
# LAVA QA tool
# Copyright (C) 2015 Collabora Ltd.

# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

# This library 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
# Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  US
###################################################################################

from __future__ import print_function

import re
import yaml
import json
import os
import os.path
try:
    from urllib.parse import urlparse
except ImportError:
    from urlparse import urlparse
import jinja2.exceptions
import jinja2
import requests

try:
    from xmlrpc.client import Fault
except ImportError:
    from xmlrpclib import Fault
from jinja2 import Environment, FileSystemLoader, \
    StrictUndefined, DebugUndefined
from lqa_api.exit_codes import APPLICATION_ERROR
from lqa_api.job import Job
from lqa_api.waitqueue import WaitQueue
from lqa_api.outputlog import OutputLog, OutputLogError
from lqa_tool.settings import lqa_logger
from lqa_tool.utils import merge_profiles
from lqa_tool.commands import Command
from lqa_tool.exceptions import ProfileNotFound
from lqa_tool.utils import print_add_msg, print_wait_msg, \
    print_remove_msg, print_timeout_msg


class Profiles(object):
    """Initialize profiles for command objects.

    :param cmd: The command object to setup
    :param default_config_file: The default configuration file path"""

    def __init__(self, cmd, default_config_file):
        # Set yaml configuration
        try:
            with open(default_config_file) as conf_data:
                env = jinja2.Environment(loader = jinja2.FileSystemLoader('.'))
                t = env.get_template(default_config_file)
                self.config = yaml.safe_load(t.render())
        except EnvironmentError as e:
            lqa_logger.error(e)
            exit(APPLICATION_ERROR)
        except yaml.scanner.ScannerError as e:
            lqa_logger.error(e)
            exit(APPLICATION_ERROR)

class SubmitCmd(Command):

    def __init__(self, args):
        Command.__init__(self, args)
        self.waitq = WaitQueue()

    def run(self):

        """Submit job id

        It follows the priority for profiles processing:
        1) Any profile specified by '-p'
        2) All profiles from 'all-profiles'
        3) Or run without any profile....
        """
        if self.args.profile or self.args.all_profiles:
            # A profile file is required for using profiles
            if not self.args.profile_file:
                raise ProfileNotFound

            # Get the profiles from the <profiles>.yaml file
            self.profiles = Profiles(self, self.args.profile_file)
            # Fetch the main profile and merge later with any sub profile
            main_profile = self.profiles.config.get('main-profile', {})

            if self.args.profile:
                # If -p/--profile specified.
                for profile_name in self.args.profile:
                    # Filter profiles from the profiles file matching -p.
                    profiles = [
                        profile
                        for profile in self.profiles.config.get('profiles', [])
                        if profile['name'] == profile_name ]
                    # Exit with failure if no profile in the filtered list.
                    if not profiles:
                        lqa_logger.error(
                            "error: profile {} not found in profile file {}"
                            .format(profile_name, self.args.profile_file))
                        exit(APPLICATION_ERROR)
                    # Run with found profiles. 
                    # There can exist profiles with the same name.
                    for profile in profiles:
                        self._run(merge_profiles(main_profile, profile))
            else:
                # Otherwise is --all-profiles.
                for profile in self.profiles.config.get('profiles', []):
                    self._run(merge_profiles(main_profile, profile))
        else:
            self._run()

        # Wait for jobs if --wait option enabled.
        if self.waitq.has_jobs():
            try:
                self.waitq.wait(self.args.wait_timeout, print_wait_msg,
                                print_remove_msg, print_timeout_msg)
                # Exit from here if the --wait option is passed so the
                # exit code from the queue is used instead.
                exit(self.waitq.exit_code)
            except ValueError as e:
                lqa_logger.error("wait timeout: {}".format(e))
                exit(APPLICATION_ERROR)

    def _mangle_json(self, data, variables, job_file):
        # Deserialize rendered template into a json object.
        # This is required to apply the 'key fields' replacement.
        try:
            json_obj = json.loads(data)
        except ValueError as e:
            lqa_logger.error("json file '{}': {}".format(job_file, e))
            return None

        # Replace key field variables.
        r_json_obj = _replaceKeyFields(json_obj, variables)

        # Set priority.
        # This option overrides the profile values to the 'priority' field.
        if self.args.priority:
            r_json_obj['priority'] = self.args.priority

        # After all variable substitutions, check if the image url exists if
        # --check-image-url is true.
        if self.args.check_image_url:
            self._check_image_url(r_json_obj)

        # Serialize json object to json string for submitting job.
        return json.dumps(r_json_obj, indent=2)


    def _run(self, profile={}):
        variables = profile.get('variables', {})

        if self.args.template_vars:
            # The variables translates to a hash that we can easily use
            # for mapping the fields -> values in the json template
            for item in self.args.template_vars:
                k, v = item.split(':', 1)
                variables[k] = v

        # Get the job files from either the command line if available,
        # or from the 'templates' profile variable otherwise.
        job_files = []
        template_dirs = []
        if self.args.submit_job:
            for f in self.args.submit_job:
                job_files.append(os.path.basename(f))
                template_dirs.append(os.path.dirname(f))
        else:
            job_files = profile.get('templates', [])
            template_dirs.append(profile.get('template-dir', os.getcwd()))

        # Choose the 'undefined' variable strategy
        uv = (self.args.debug_vars and DebugUndefined) or StrictUndefined
        # Create a template environment.
        # Use list/set to remove any duplicate.
        env = Environment(loader=FileSystemLoader(list(set(template_dirs))),
                          undefined=uv)

        # Process job files
        for job_file in job_files:
            try:
                # Get template from environment and render it.
                data = env.get_template(job_file).render(variables)
            except TypeError as e:
                lqa_logger.error("type error in {}: {}".format(job_file, e))
                continue
            except jinja2.exceptions.TemplateNotFound as e:
                lqa_logger.error("template not found: {}".format(e))
                continue
            except jinja2.exceptions.UndefinedError as e:
                lqa_logger.error("template variable not defined in {}: {}"
                                 .format(job_file, e))
                continue

            # V1 files we replace key-value pairs not jus templated values
            if job_file.endswith(".json"):
                data = self._mangle_json(data, variables, job_file)
                if data == None:
                    continue
            elif not job_file.endswith(".yaml"):
                lqa_logger.error("Job file not recognize as v1 or v2: {}"
                                 .format(job_file))

            if self.args.verbose:
                print(data)

            if not self.args.dry_run:
                try:
                    job_id = self.server.submit_job(data)
                    # Queue jobs ids to wait for them if 'wait' option enabled.
                    if self.args.wait_timeout:
                        self.waitq.addjob(Job(job_id, self.server), print_add_msg)
                except Fault as e:
                    lqa_logger.error("Submitting job {}: {}".format(job_file, e))
                    continue
                lqa_logger.info("Submitted job {} with id {}" \
                                    .format(job_file, job_id))

                # Show live output if '--live' option passed
                if self.args.live:
                    if len(job_files) > 1:
                        lqa_logger.info("lqa submit: --live option is not valid "
                                        "with more than 1 file (skipping)")
                    elif self.args.wait_timeout:
                        lqa_logger.info("lqa submit: --live option can't be used "
                                        "together with --wait option (skipping)")
                    else:
                        # Everything is valid to use live option, then go for it!
                        try:
                            OutputLog(job_id, self.server, is_live=True,
                                      logger=lqa_logger).run()
                        except OutputLogError as e:
                            lqa_logger.error("lqa submit: error: {}".format(e))
                            exit(APPLICATION_ERROR)

    def _check_image_url(self, json_job_obj):
        """This method checks for the existence of the image field url path
        for all the 'deploy_image' actions (if available)"""
        for action in json_job_obj['actions']:
            if action['command'] == 'deploy_image':
                image = action['parameters'].get('image', None)
                if image:
                    # Test for local url path
                    url = urlparse(image)
                    if url.scheme == 'file':
                        if os.path.exists(url.path):
                            return
                    else:
                        url_code = requests.head(image)
                        if url_code.status_code == requests.codes.ok:
                            return
                    lqa_logger.error(
                        "lqa submit error: image url does not exist: {}"
                        .format(image))
                    exit(APPLICATION_ERROR)

def _replaceKeyFields(json_obj, variables):
    """Replace key fields

    This method will iterate over a JSON object and will assign values to
    those keys (json fields) with the same name of a (profile) variable.

    :param json_obj: The json object to be processed
    :param variables: Dictionary containing variable and values.

    :returns: A new json object containing the replaced values for the
              json fields with the same name of the specified variables.
    """
    def _replaceKey(variable, value, json_obj):

        if type(json_obj) == list:
            for i, json_obj_v in enumerate(json_obj):
                json_obj[i] = _replaceKey(variable, value, json_obj_v)
        elif type(json_obj) == dict:
            for k in json_obj:
                # Replace key value if the 'key' == 'variable'
                if k == variable:
                    json_obj[k] = value
                # Otherwise continue processing the json fields.
                else:
                    json_obj[k] = _replaceKey(variable, value, json_obj[k])
        return json_obj

    j = json_obj
    for variable in variables:
        j = _replaceKey(variable, variables[variable], j)
    return j
