# 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 functools
import json
import pathlib
import typing as t

from magnum.common import utils
from oslo_concurrency import processutils
from oslo_log import log as logging

from magnum_capi_helm import conf

LOG = logging.getLogger(__name__)
CONF = conf.CONF

# This code is loosely based on:
#  https://github.com/stackhpc/pyhelm3
#  Ideally we can share this code in the future.


def mergeconcat(defaults, *overrides):
    """Deep-merge two or more dictionaries together.

    Lists are concatenated.
    """

    def mergeconcat2(defaults, overrides):
        if isinstance(defaults, dict) and isinstance(overrides, dict):
            merged = dict(defaults)
            for key, value in overrides.items():
                if key in defaults:
                    merged[key] = mergeconcat2(defaults[key], value)
                else:
                    merged[key] = value
            return merged
        elif isinstance(defaults, (list, tuple)) and isinstance(
            overrides, (list, tuple)
        ):
            merged = list(defaults)
            merged.extend(overrides)
            return merged
        else:
            return overrides if overrides is not None else defaults

    return functools.reduce(mergeconcat2, overrides, defaults)


class Client:
    """Client for interacting with Helm CLI."""

    def __init__(self):
        self._default_timeout = "5m"
        self._executable = "helm"
        self._history_max_revisions = 10
        self._kubeconfig = CONF.capi_helm.kubeconfig_file

    def _run(self, command, **kwargs) -> bytes:
        command = [self._executable] + command
        if self._kubeconfig:
            command.extend(["--kubeconfig", self._kubeconfig])
        stdout, stderr = utils.execute(*command, **kwargs)
        LOG.debug(f"Ran helm {command} got out:{stdout} err:{stderr}")
        return stdout

    def install_or_upgrade(
        self,
        release_name: str,
        chart_ref: t.Union[pathlib.Path, str],
        *values: t.Dict[str, t.Any],
        namespace: str,
        repo: t.Optional[str] = None,
        version: t.Optional[str] = None,
    ) -> t.Iterable[t.Dict[str, t.Any]]:
        """Install or upgrade specified release using chart and values."""
        assert release_name is not None
        command = [
            "upgrade",
            release_name,
            chart_ref,
            "--history-max",
            self._history_max_revisions,
            "--install",
            "--output",
            "json",
            "--timeout",
            self._default_timeout,
            # We send the values in on stdin
            "--values",
            "-",
            "--namespace",
            namespace,
        ]
        if repo:
            command += ["--repo", repo]
        if version:
            command += [
                "--version",
                version,
            ]

        process_input = json.dumps(mergeconcat({}, *values))
        return json.loads(self._run(command, process_input=process_input))

    def uninstall_release(
        self,
        release_name: str,
        namespace: str,
    ):
        """Uninstall the named release."""
        assert release_name is not None
        command = [
            "uninstall",
            release_name,
            "--timeout",
            self._default_timeout,
            "--namespace",
            namespace,
        ]
        try:
            self._run(command)
        except processutils.ProcessExecutionError as exc:
            # Swallow release not found errors, as that is our desired state
            if not exc.stderr or "release: not found" not in exc.stderr:
                raise
