#    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.

r"""
Castellan Oslo Config Driver
----------------------------

This driver is an oslo.config backend driver implemented with Castellan. It
extends oslo.config's capabilities by enabling it to retrieve configuration
values from a secret manager behind Castellan.

The setup of a Castellan configuration source is as follow::

    [DEFAULT]
    config_source = castellan_config_group

    [castellan_config_group]
    driver = castellan
    config_file = castellan.conf
    mapping_file = mapping.conf

In the following sessions, you can find more information about this driver's
classes and its options.

The Driver Class
================

.. autoclass:: CastellanConfigurationSourceDriver

The Configuration Source Class
==============================

.. autoclass:: CastellanConfigurationSource

"""

from typing import Any

from oslo_config import cfg
from oslo_config import sources
from oslo_log import log

from castellan.common.credentials import credential
from castellan.common.exception import AuthTypeInvalidError
from castellan.common.exception import KeyManagerError
from castellan.common.exception import ManagedObjectNotFoundError
from castellan.common import utils
from castellan import key_manager
from castellan.key_manager import key_manager as km

LOG = log.getLogger(__name__)


class CastellanConfigurationSourceDriver(sources.ConfigurationSourceDriver):
    """A backend driver for configuration values served through castellan.

    Required options:
      - config_file: The castellan configuration file.

      - mapping_file: A configuration/castellan_id mapping file. This file
                      creates connections between configuration options and
                      castellan ids. The group and option name remains the
                      same, while the value gets stored a secret manager behind
                      castellan and is replaced by its castellan id. The ids
                      will be used to fetch the values through castellan.
    """

    _castellan_driver_opts: list[cfg.Opt] = [
        cfg.StrOpt(
            'config_file',
            required=True,
            sample_default='etc/castellan/castellan.conf',
            help=('The path to a castellan configuration file.'),
        ),
        cfg.StrOpt(
            'mapping_file',
            required=True,
            sample_default='etc/castellan/secrets_mapping.conf',
            help=('The path to a configuration/castellan_id mapping file.'),
        ),
    ]

    def list_options_for_discovery(self) -> list[cfg.Opt]:
        return self._castellan_driver_opts

    def open_source_from_opt_group(
        self, conf: cfg.ConfigOpts, group_name: str
    ) -> "CastellanConfigurationSource":
        conf.register_opts(self._castellan_driver_opts, group_name)

        return CastellanConfigurationSource(
            group_name,
            conf[group_name].config_file,
            conf[group_name].mapping_file,
        )


class CastellanConfigurationSource(sources.ConfigurationSource):
    """A configuration source for values served through castellan.

    :param config_file: The path to a castellan configuration file.
    :param mapping_file: The path to a configuration/castellan_id mapping file.
    """

    _mngr: km.KeyManager
    _mapping: dict[str, dict[str, list[str]]]
    _context: credential.Credential | None

    def __init__(
        self, group_name: str, config_file: str, mapping_file: str
    ) -> None:
        conf = cfg.ConfigOpts()
        conf(args=[], default_config_files=[config_file])

        self._name = group_name
        self._mngr = key_manager.API(conf)
        self._mapping = {}

        try:
            self._context = utils.credential_factory(conf)
        except AuthTypeInvalidError:
            self._context = None

            LOG.warning(
                "Invalid 'auth_type' in '%s', auth_type: %s. "
                "Context set to 'None'. Supported 'auth_type' values: "
                "'token', 'password', 'keystone_token', "
                "'keystone_password'.",
                config_file,
                conf.key_manager.auth_type,
            )

        cfg.ConfigParser(mapping_file, self._mapping).parse()

    def get(
        self, group_name: str | None, option_name: str, opt: cfg.Opt
    ) -> tuple[Any, cfg.LocationInfo | None]:
        try:
            group_name = group_name or "DEFAULT"

            castellan_id = self._mapping[group_name][option_name][0]

            encoded = self._mngr.get(self._context, castellan_id).get_encoded()
            if encoded is None:
                return (sources._NoValue, None)

            return (
                encoded.decode(),
                cfg.LocationInfo(cfg.Locations.user, castellan_id),
            )

        except KeyError:
            # no mapping 'option = castellan_id'
            LOG.debug(
                "option '[%s] %s' not present in '[%s] mapping_file'",
                group_name,
                option_name,
                self._name,
            )

        except KeyManagerError:
            # error retrieving the secret from the key manager
            LOG.exception(
                "Failed to retrieve secret for option '[%s] %s' in "
                "'[%s] mapping_file'",
                group_name,
                option_name,
                self._name,
            )

        except ManagedObjectNotFoundError:
            # good mapping, but unknown castellan_id by secret manager
            LOG.error(
                "invalid castellan_id for option '[%s] %s' in '[%s] "
                "mapping_file'",
                group_name,
                option_name,
                self._name,
            )

        return (sources._NoValue, None)
