# 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"""
Remote File
-----------

The **remote_file** backend driver is the first driver implemented by
oslo.config. It extends the previous limit of only accessing local files
to a new scenario where it is possible to access configuration data over
the network. The **remote_file** driver is based on the **requests** module
and is capable of accessing remote files through **HTTP** or **HTTPS**.

To definition of a remote_file configuration data source can be as minimal as::

    [DEFAULT]
    config_source = external_config_group

    [external_config_group]
    driver = remote_file
    uri = http://mydomain.com/path/to/config/data.conf

Or as complete as::

    [DEFAULT]
    config_source = external_config_group

    [external_config_group]
    driver = remote_file
    uri = https://mydomain.com/path/to/config/data.conf
    ca_path = /path/to/server/ca.pem
    client_key = /path/to/my/key.pem
    client_cert = /path/to/my/cert.pem

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

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

.. autoclass:: URIConfigurationSourceDriver

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

.. autoclass:: URIConfigurationSource

"""

from typing import Any

import requests
import tempfile

from oslo_config import cfg
from oslo_config import sources


class URIConfigurationSourceDriver(sources.ConfigurationSourceDriver):
    """A backend driver for remote files served through http[s].

    Required options:
      - uri: URI containing the file location.

    Non-required options:
      - ca_path: The path to a CA_BUNDLE file or directory with
                 certificates of trusted CAs.

      - client_cert: Client side certificate, as a single file path
                     containing either the certificate only or the
                     private key and the certificate.

      - client_key: Client side private key, in case client_cert is
                    specified but does not includes the private key.
    """

    _uri_driver_opts = [
        cfg.URIOpt(
            'uri',
            schemes=['http', 'https'],
            required=True,
            sample_default='https://example.com/my-configuration.ini',
            help=(
                'Required option with the URI of the '
                'extra configuration file\'s location.'
            ),
        ),
        cfg.StrOpt(
            'ca_path',
            sample_default='/etc/ca-certificates',
            help=(
                'The path to a CA_BUNDLE file or directory '
                'with certificates of trusted CAs.'
            ),
        ),
        cfg.StrOpt(
            'client_cert',
            sample_default='/etc/ca-certificates/service-client-keystore',
            help=(
                'Client side certificate, as a single file path '
                'containing either the certificate only or the '
                'private key and the certificate.'
            ),
        ),
        cfg.StrOpt(
            'client_key',
            help=(
                'Client side private key, in case client_cert is '
                'specified but does not includes the private key.'
            ),
        ),
        cfg.IntOpt(
            'timeout',
            default=60,
            help=(
                'Timeout is the number of seconds the request will wait '
                'for your client to establish a connection to a remote '
                'machine call on the socket.'
            ),
        ),
    ]

    def list_options_for_discovery(self) -> list[Any]:
        return self._uri_driver_opts

    def open_source_from_opt_group(
        self,
        conf: cfg.ConfigOpts,
        group_name: str,
    ) -> 'URIConfigurationSource':
        conf.register_opts(self._uri_driver_opts, group_name)

        return URIConfigurationSource(
            conf[group_name].uri,
            conf[group_name].ca_path,
            conf[group_name].client_cert,
            conf[group_name].client_key,
            conf[group_name].timeout,
        )


class URIConfigurationSource(sources.ConfigurationSource):
    """A configuration source for remote files served through http[s].

    :param uri: The Uniform Resource Identifier of the configuration to be
          retrieved.

    :param ca_path: The path to a CA_BUNDLE file or directory with
              certificates of trusted CAs.

    :param client_cert: Client side certificate, as a single file path
                  containing either the certificate only or the
                  private key and the certificate.

    :param client_key: Client side private key, in case client_cert is
                 specified but does not includes the private key.
    """

    def __init__(
        self,
        uri: str,
        ca_path: str | None = None,
        client_cert: str | None = None,
        client_key: str | None = None,
        timeout: int = 60,
    ) -> None:
        self._uri = uri
        self._namespace = cfg._Namespace(cfg.ConfigOpts())

        data = self._fetch_uri(uri, ca_path, client_cert, client_key, timeout)

        with tempfile.NamedTemporaryFile() as tmpfile:
            tmpfile.write(data.encode("utf-8"))
            tmpfile.flush()

            cfg.ConfigParser._parse_file(tmpfile.name, self._namespace)

    def _fetch_uri(
        self,
        uri: str,
        ca_path: str | None,
        client_cert: str | None,
        client_key: str | None,
        timeout: int,
    ) -> str:
        verify: str | bool = ca_path if ca_path else True
        cert: tuple[str, str] | str | None = (
            (client_cert, client_key)
            if client_cert and client_key
            else client_cert
        )

        with requests.get(
            uri, verify=verify, cert=cert, timeout=timeout
        ) as response:
            response.raise_for_status()  # raises only in case of HTTPError
            return response.text

    def get(
        self,
        group_name: str | None,
        option_name: str,
        opt: cfg.Opt,
    ) -> tuple[Any, cfg.LocationInfo | None]:
        try:
            result: tuple[Any, cfg.LocationInfo | None] = (
                self._namespace._get_value(
                    [(group_name, option_name)], multi=opt.multi
                )
            )
            return result
        except KeyError:
            return (sources._NoValue, None)
