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

import hashlib
import os
import shutil
import requests
import yaml
import tempfile

from jinja2 import FileSystemLoader, Environment
from pykwalify.core import Core
from pykwalify.errors import SchemaError

from dogen.template_helper import TemplateHelper
from dogen.tools import Tools
from dogen import version, DEFAULT_SCRIPT_EXEC, DEFAULT_SCRIPT_USER
from dogen.errors import Error

SUPPORTED_HASH_ALGORITHMS = ['sha256', 'sha1', 'md5']

class Generator(object):
    def __init__(self, log, args, plugins=[]):
        self.log = log
        self.pwd = os.path.realpath(os.path.dirname(os.path.realpath(__file__)))
        self.descriptor = os.path.realpath(args.path)
        self.without_sources = args.without_sources
        self.output = args.output
        self.dockerfile = os.path.join(self.output, "Dockerfile")
        self.template = args.template
        self.scripts_path = args.scripts_path

        ssl_verify = None
        if args.skip_ssl_verification:
            ssl_verify = False
        self.ssl_verify = ssl_verify

        self.plugins = []
        for plugin in plugins:
            self.plugins.append(plugin(self, args))

    def _fetch_file(self, location, output=None):
        """
        Fetches remote file and saves it under output. If no
        output path is provided, a temporary file is created
        and path to this file is returned.

        SSL verification could be disabled by setting
        self.ssl_verify to False.
        """

        self.log.debug("Fetching '%s' file..." % location)

        if not output:
            output = tempfile.mktemp("-dogen")

        self.log.debug("Fetched file will be saved as '%s'..." % os.path.basename(output))

        r = requests.get(location, verify=self.ssl_verify, stream=True)

        if r.status_code != 200:
            raise Error("Could not download file from %s" % location)

        with open(output, 'wb') as f:
            for chunk in r.iter_content(chunk_size=1024):
                f.write(chunk)

        return output

    def _handle_custom_template(self):
        """
        Fetches custom template (if provided) and saves as temporary
        file. This file is removed later in the process.
        """

        if not self.template:
            return

        self.log.info("Using custom provided template file: '%s'" % self.template)

        if Tools.is_url(self.template):
            self.template = self._fetch_file(self.template)

        if not os.path.exists(self.template):
            raise Error("Template file '%s' could not be found. Please make sure you specified correct path or check if the file was successfully fetched." % self.template)


    def configure(self):
        """
        Reads configuration values from the descriptor, if provided.

        Some Dogen configuration values can be set in the YAML
        descriptor file using the 'dogen' section.
        """
        self._validate_cfg()

        if not self.scripts_path:
            # If scripts directory is not provided, see if there is a "scripts"
            # directory next to the descriptor. If found - assume that's the
            # directory containing scripts.
            scripts = os.path.join(os.path.dirname(self.descriptor), "scripts")
            if os.path.exists(scripts) and os.path.isdir(scripts):
                self.scripts_path = scripts

        if 'user' not in self.cfg:
            self.cfg['user'] = 0

        dogen_cfg = self.cfg.get('dogen')

        if not dogen_cfg:
            return

        required_version = dogen_cfg.get('version')

        if required_version:
            # Check if the current runnig version of Dogen
            # is the one the descriptor is expecting.
            if required_version != version:
                raise Error("You try to parse descriptor that requires Dogen version %s, but you run version %s" % (required_version, version))

        ssl_verify = dogen_cfg.get('ssl_verify')

        if self.ssl_verify is None and ssl_verify is not None:
            self.ssl_verify = ssl_verify

        template = dogen_cfg.get('template')

        if template and not self.template:
            self.template = template

        scripts = dogen_cfg.get('scripts_path')

        if scripts and not self.scripts_path:
            self.scripts_path = scripts

        if self.scripts_path and not os.path.exists(self.scripts_path):
            raise Error("Provided scripts directory '%s' does not exist" % self.scripts_path)

    def _handle_scripts(self):
        if not self.cfg.get('scripts'):
            return

        for script in self.cfg['scripts']:
            package = script['package']
            src_path = os.path.join(self.scripts_path, package)
            output_path = os.path.join(self.output, "scripts", package)

            possible_exec = os.getenv('DOGEN_SCRIPT_EXEC', DEFAULT_SCRIPT_EXEC)

            if "exec" not in script and os.path.exists(os.path.join(src_path, possible_exec)):
                script['exec'] = possible_exec

            if "user" not in script:
                script['user'] = os.getenv('DOGEN_SCRIPT_USER', DEFAULT_SCRIPT_USER)

            # Poor-man's workaround for not copying multiple times the same thing
            if not os.path.exists(output_path):
                self.log.info("Copying package '%s'..." % package)
                shutil.copytree(src=src_path, dst=output_path)
                self.log.debug("Done.")

    def _validate_cfg(self):
        """
        Open and parse the YAML configuration file and ensure it matches
        our Schema for a Dogen configuration.
        """
        # Fail early if descriptor file is not found
        if not os.path.exists(self.descriptor):
            raise Error("Descriptor file '%s' could not be found. Please make sure you specified correct path." % self.descriptor)

        schema_path = os.path.join(self.pwd, "schema", "kwalify_schema.yaml")
        schema = {}
        with open(schema_path, 'r') as fh:
            schema = yaml.safe_load(fh)

        if schema is None:
            raise Error("couldn't read a valid schema at %s" % schema_path)

        for plugin in self.plugins:
            plugin.extend_schema(schema)

        with open(self.descriptor, 'r') as stream:
            self.cfg = yaml.safe_load(stream)

        c = Core(source_data=self.cfg, schema_data=schema)
        try:
            c.validate(raise_exception=True)
        except SchemaError as e:
            raise Error(e)

    def run(self):

        # Set Dogen settings if  provided in descriptor
        self.configure()

        # Special case for ssl_verify setting. Setting it to None
        # in CLI if --skip-ssl-verification is not set to make it
        # possible to determine which setting should be used.
        # This means that we need to se the ssl_verify to the
        # default value of True is not set.
        if self.ssl_verify is None:
            self.ssl_verify = True

        for plugin in self.plugins:
            plugin.prepare(cfg=self.cfg)

        if self.template:
            self._handle_custom_template()

        # Remove the target scripts directory
        shutil.rmtree(os.path.join(self.output, "scripts"), ignore_errors=True)

        if not os.path.exists(self.output):
            os.makedirs(self.output)

        if self.scripts_path:
            self._handle_scripts()
        else:
            self.log.warn("No scripts will be copied, mistake?")

        for plugin in self.plugins:
            plugin.before_sources(cfg=self.cfg)

        self.handle_sources()
        self.render_from_template()

        for plugin in self.plugins:
            plugin.after_sources(files=self.cfg.get('artifacts'))

        self.log.info("Finished!")

    def render_from_template(self):
        if not self.cfg.get('labels'):
            self.cfg['labels'] = []

        labels = {}

        for label in self.cfg.get('labels'):
            labels[label['name']] = label['value']

        # https://github.com/jboss-dockerfiles/dogen/issues/129
        # https://github.com/jboss-dockerfiles/dogen/issues/137
        for label in ['maintainer', 'description']:
            value = self.cfg.get(label)

            if value and not label in labels:
                self.cfg['labels'].append({'name': label, 'value': value})
                labels[label] = value

        # https://github.com/jboss-dockerfiles/dogen/issues/195
        if 'summary' not in labels:
            if 'description' in labels:
                self.cfg['labels'].append({'name': 'summary', 'value': labels.get('description')})

        if self.template:
            template_file = self.template
        else:
            self.log.debug("Using dogen provided template file")
            template_file = os.path.join(self.pwd, "templates", "template.jinja")

        self.log.info("Rendering Dockerfile...")
        loader = FileSystemLoader(os.path.dirname(template_file))
        env = Environment(loader=loader, trim_blocks=True, lstrip_blocks=True)
        env.globals['helper'] = TemplateHelper()
        template = env.get_template(os.path.basename(template_file))

        with open(self.dockerfile, 'wb') as f:
            f.write(template.render(self.cfg).encode('utf-8'))
        self.log.debug("Done")

        if self.template and Tools.is_url(self.template):
            self.log.debug("Removing temporary template file...")
            os.remove(self.template)

    def handle_sources(self):
        if 'sources' not in self.cfg or self.without_sources:
            return []

        self.log.info("Handling artifacts...")
        self.cfg['artifacts'] = {}

        sources_cache = os.environ.get("DOGEN_SOURCES_CACHE")
        self.log.debug("Source cache will be used for all artifacts")

        for source in self.cfg['sources']:
            url = source.get('url')
            artifact = source.get('artifact')

            if url:
                self.log.warn("The 'url' key is deprecated, please use 'artifact' for specifying the %s artifact location" % url)

                if artifact:
                    self.log.warn("You specified both: 'artifact' and 'url' for a source file, 'artifact': will be used: %s" % artifact)
                else:
                    # Backward compatibility
                    artifact = url

            if not artifact:
                raise Error("Artifact location for one or more sources was not provided, please check your image descriptor!")