import logging
import re
import sys

from collections.abc import Mapping
from typing import Union, IO, Optional

_no_logger = object()


class Logging:
    _has_setup = False
    KEYS_TO_SANITIZE = (
        'Authorization',
        'access_token',
        'refresh_token',
        'subject_token',
        'token',
        'client_id',
        'client_secret',
        'code',
        'shared_link',
        'download_url',
        'jwt_private_key',
        'jwt_private_key_passphrase',
        'password',
    )

    PROXY_KEYS_TO_SANITIZE = (
        'http',
        'https',
    )

    def setup_logging(self, stream_or_file=_no_logger, debug=False, name=None):
        if not self._has_setup:
            self._has_setup = True
            self._setup_logging(stream_or_file, debug, name)

    @staticmethod
    def _setup_logging(stream_or_file=_no_logger, debug=False, name=None):
        logger = logging.getLogger(name)
        if isinstance(stream_or_file, str):
            logger.addHandler(logging.FileHandler(stream_or_file, mode='a'))
        elif stream_or_file is not _no_logger:
            logger.addHandler(logging.StreamHandler(stream_or_file or sys.stdout))
        logger.setLevel(logging.DEBUG if debug else logging.INFO)

    @staticmethod
    def sanitize_value(value):
        return f'---{value[-4:]}'

    @staticmethod
    def sanitize_proxy_value(value: str) -> str:
        return re.sub(
            '^(.*://)(.*):(.*)(@.*)$',
            lambda repl: f'{repl.group(1)}{"---"}:{"---"}{repl.group(4)}',
            value
        )

    def sanitize_dictionary(self, dictionary: Mapping) -> Mapping:
        if not isinstance(dictionary, Mapping):
            return dictionary
        sanitized_dictionary = {}
        for key, value in dictionary.items():
            if key in self.KEYS_TO_SANITIZE and isinstance(value, str):
                sanitized_dictionary[key] = self.sanitize_value(value)
            elif key in self.PROXY_KEYS_TO_SANITIZE and isinstance(value, str):
                sanitized_dictionary[key] = self.sanitize_proxy_value(value)
            elif isinstance(value, Mapping):
                sanitized_dictionary[key] = self.sanitize_dictionary(value)
            else:
                sanitized_dictionary[key] = value
        return sanitized_dictionary


_logging = Logging()


def setup_logging(
        stream_or_file: Optional[Union[str, IO]] = _no_logger,
        debug: Optional[bool] = False,
        name: Optional[str] = None
) -> None:
    """
    Create a logger for communicating with the user or writing to log files.
    Sets the level to INFO or DEBUG, depending on the debug flag.

    If a stream or file is passed (or None is passed to stream_or_file), then
    a handler to that stream or file (stdout for None) is added to the logger.

    :param stream_or_file:
        The destination of the log messages. If None, stdout will be used.
    :param debug:
        Whether or not the logger will be at the DEBUG level (if False, the logger will be at the INFO level).
    :param name:
        The logging channel. If None, a root logger will be created.
    """
    _logging.setup_logging(stream_or_file, debug, name)


def sanitize_dictionary(dictionary: Mapping) -> dict:
    """
    Get a copy of a dictionary that has sensitive information redacted. Should be called on objects that will be
    logged or printed.

    :param dictionary:      Dictionary that may contain sensitive information.
    :return:                Copy of the dictionary with sensitive information redacted.
    """
    return _logging.sanitize_dictionary(dictionary)


logging.getLogger(__name__).addHandler(logging.NullHandler())


__all__ = ['setup_logging', 'sanitize_dictionary']
