# Copyright 2012 OpenStack Foundation
#
# 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 collections.abc
import json
from urllib import parse
from urllib import request

from openstack import exceptions
from openstack.orchestration.util import environment_format
from openstack.orchestration.util import template_format
from openstack.orchestration.util import utils


def get_template_contents(
    template_file=None,
    template_url=None,
    template_object=None,
    object_request=None,
    files=None,
    existing=False,
):
    is_object = False
    tpl = None

    # Transform a bare file path to a file:// URL.
    if template_file:
        template_url = utils.normalise_file_path_to_url(template_file)

    if template_url:
        tpl = request.urlopen(template_url).read()  # noqa: S310

    elif template_object:
        is_object = True
        template_url = template_object
        tpl = object_request and object_request('GET', template_object)
    elif existing:
        return {}, None
    else:
        raise exceptions.SDKException(
            'Must provide one of template_file, template_url or '
            'template_object'
        )

    if not tpl:
        raise exceptions.SDKException(
            f'Could not fetch template from {template_url}'
        )

    try:
        if isinstance(tpl, bytes):
            tpl = tpl.decode('utf-8')
        template = template_format.parse(tpl)
    except ValueError as e:
        raise exceptions.SDKException(
            f'Error parsing template {template_url} {e}'
        )

    tmpl_base_url = utils.base_url_for_url(template_url)
    if files is None:
        files = {}
    resolve_template_get_files(
        template, files, tmpl_base_url, is_object, object_request
    )
    return files, template


def resolve_template_get_files(
    template, files, template_base_url, is_object=False, object_request=None
):
    def ignore_if(key, value):
        if key != 'get_file' and key != 'type':
            return True
        if not isinstance(value, str):
            return True
        if key == 'type' and not value.endswith(('.yaml', '.template')):
            return True
        return False

    def recurse_if(value):
        return isinstance(value, dict | list)

    get_file_contents(
        template,
        files,
        template_base_url,
        ignore_if,
        recurse_if,
        is_object,
        object_request,
    )


def is_template(file_content):
    try:
        if isinstance(file_content, bytes):
            file_content = file_content.decode('utf-8')
        template_format.parse(file_content)
    except (ValueError, TypeError):
        return False
    return True


def get_file_contents(
    from_data,
    files,
    base_url=None,
    ignore_if=None,
    recurse_if=None,
    is_object=False,
    object_request=None,
):
    if recurse_if and recurse_if(from_data):
        if isinstance(from_data, dict):
            recurse_data = from_data.values()
        else:
            recurse_data = from_data
        for value in recurse_data:
            get_file_contents(
                value,
                files,
                base_url,
                ignore_if,
                recurse_if,
                is_object,
                object_request,
            )

    if isinstance(from_data, dict):
        for key, value in from_data.items():
            if ignore_if and ignore_if(key, value):
                continue

            if base_url and not base_url.endswith('/'):
                base_url = base_url + '/'

            str_url = parse.urljoin(base_url, value)
            if str_url not in files:
                if is_object and object_request:
                    file_content = object_request('GET', str_url)
                else:
                    file_content = utils.read_url_content(str_url)
                if is_template(file_content):
                    if is_object:
                        template = get_template_contents(
                            template_object=str_url,
                            files=files,
                            object_request=object_request,
                        )[1]
                    else:
                        template = get_template_contents(
                            template_url=str_url, files=files
                        )[1]
                    file_content = json.dumps(template)
                files[str_url] = file_content
            # replace the data value with the normalised absolute URL
            from_data[key] = str_url


def deep_update(old, new):
    '''Merge nested dictionaries.'''

    # Prevents an error if in a previous iteration
    # old[k] = None but v[k] = {...},
    if old is None:
        old = {}

    for k, v in new.items():
        if isinstance(v, collections.abc.Mapping):
            r = deep_update(old.get(k, {}), v)
            old[k] = r
        else:
            old[k] = new[k]
    return old


def process_multiple_environments_and_files(
    env_paths=None,
    template=None,
    template_url=None,
    env_path_is_object=None,
    object_request=None,
    env_list_tracker=None,
):
    """Reads one or more environment files.

    Reads in each specified environment file and returns a dictionary
    of the filenames->contents (suitable for the files dict)
    and the consolidated environment (after having applied the correct
    overrides based on order).

    If a list is provided in the env_list_tracker parameter, the behavior
    is altered to take advantage of server-side environment resolution.
    Specifically, this means:

    * Populating env_list_tracker with an ordered list of environment file
      URLs to be passed to the server
    * Including the contents of each environment file in the returned
      files dict, keyed by one of the URLs in env_list_tracker

    :param env_paths: list of paths to the environment files to load; if
           None, empty results will be returned
    :type  env_paths: list or None
    :param template: unused; only included for API compatibility
    :param template_url: unused; only included for API compatibility
    :param env_list_tracker: if specified, environment filenames will be
           stored within
    :type  env_list_tracker: list or None
    :return: tuple of files dict and a dict of the consolidated environment
    :rtype:  tuple
    """
    merged_files: dict[str, str] = {}
    merged_env: dict[str, dict] = {}

    # If we're keeping a list of environment files separately, include the
    # contents of the files in the files dict
    include_env_in_files = env_list_tracker is not None

    if env_paths:
        for env_path in env_paths:
            files, env = process_environment_and_files(
                env_path=env_path,
                template=template,
                template_url=template_url,
                env_path_is_object=env_path_is_object,
                object_request=object_request,
                include_env_in_files=include_env_in_files,
            )

            # 'files' looks like {"filename1": contents, "filename2": contents}
            # so a simple update is enough for merging
            merged_files.update(files)

            # 'env' can be a deeply nested dictionary, so a simple update is
            # not enough
            merged_env = deep_update(merged_env, env)

            if env_list_tracker is not None:
                env_url = utils.normalise_file_path_to_url(env_path)
                env_list_tracker.append(env_url)

    return merged_files, merged_env


def process_environment_and_files(
    env_path=None,
    template=None,
    template_url=None,
    env_path_is_object=None,
    object_request=None,
    include_env_in_files=False,
):
    """Loads a single environment file.

    Returns an entry suitable for the files dict which maps the environment
    filename to its contents.

    :param env_path: full path to the file to load
    :type  env_path: str or None
    :param include_env_in_files: if specified, the raw environment file itself
           will be included in the returned files dict
    :type  include_env_in_files: bool
    :return: tuple of files dict and the loaded environment as a dict
    :rtype:  (dict, dict)
    """
    files: dict[str, str] = {}
    env: dict[str, dict] = {}

    is_object = env_path_is_object and env_path_is_object(env_path)

    if is_object:
        raw_env = object_request and object_request('GET', env_path)
        env = environment_format.parse(raw_env)
        env_base_url = utils.base_url_for_url(env_path)

        resolve_environment_urls(
            env.get('resource_registry'),
            files,
            env_base_url,
            is_object=True,
            object_request=object_request,
        )

    elif env_path:
        env_url = utils.normalise_file_path_to_url(env_path)
        env_base_url = utils.base_url_for_url(env_url)
        raw_env = request.urlopen(env_url).read()  # noqa: S310

        env = environment_format.parse(raw_env)

        resolve_environment_urls(
            env.get('resource_registry'), files, env_base_url
        )

        if include_env_in_files:
            files[env_url] = json.dumps(env)

    return files, env


def resolve_environment_urls(
    resource_registry,
    files,
    env_base_url,
    is_object=False,
    object_request=None,
):
    """Handles any resource URLs specified in an environment.

    :param resource_registry: mapping of type name to template filename
    :type  resource_registry: dict
    :param files: dict to store loaded file contents into
    :type  files: dict
    :param env_base_url: base URL to look in when loading files
    :type  env_base_url: str or None
    """
    if resource_registry is None:
        return

    rr = resource_registry
    base_url = rr.get('base_url', env_base_url)

    def ignore_if(key, value):
        if key == 'base_url':
            return True
        if isinstance(value, dict):
            return True
        if '::' in value:
            # Built in providers like: "X::Compute::Server"
            # don't need downloading.
            return True
        if key in ['hooks', 'restricted_actions']:
            return True

    get_file_contents(
        rr,
        files,
        base_url,
        ignore_if,
        is_object=is_object,
        object_request=object_request,
    )

    for res_name, res_dict in rr.get('resources', {}).items():
        res_base_url = res_dict.get('base_url', base_url)
        get_file_contents(
            res_dict,
            files,
            res_base_url,
            ignore_if,
            is_object=is_object,
            object_request=object_request,
        )
