from __future__ import annotations

import functools
import http.client
import logging
import os
import string
import time

import tempora


@functools.singledispatch
def log_level(level_input: str | int):
    """
    Return a log level for a string.

    >>> log_level('DEBUG') == logging.DEBUG
    True
    >>> log_level('30') == logging.WARNING
    True

    Let int pass through (useful for typer use).

    >>> log_level(30)
    30
    """
    assert isinstance(level_input, str)  # assert an invariant to workaround typing bug
    if level_input.isdigit():
        return int(level_input)
    return getattr(logging, level_input.upper())


@log_level.register
def _(level_input: int):
    return level_input


def add_arguments(parser, default_level=logging.INFO):
    """
    Add arguments to an ArgumentParser or OptionParser for purposes of
    grabbing a logging level.

    >>> import argparse
    >>> add_arguments(argparse.ArgumentParser())
    """
    adder = getattr(parser, 'add_argument', None) or getattr(parser, 'add_option')
    adder(
        '-l',
        '--log-level',
        default=default_level,
        type=log_level,
        help="Set log level (DEBUG, INFO, WARNING, ERROR)",
    )


def setup(options, **kwargs):
    """
    Setup logging with options or arguments from an OptionParser or
    ArgumentParser. Also pass any keyword arguments to the basicConfig
    call.

    >>> import argparse
    >>> parser = argparse.ArgumentParser()
    >>> add_arguments(parser)
    >>> monkeypatch = getfixture('monkeypatch')
    >>> monkeypatch.setattr(logging, 'basicConfig', lambda **kwargs: print(kwargs))
    >>> setup(parser.parse_args([]))
    {'level': 20}
    """
    params = dict(kwargs)
    params.update(level=options.log_level)
    logging.basicConfig(**params)


def setup_requests_logging(level):
    """
    Setup logging for 'requests' such that it logs details about the
    connection, headers, etc.

    >>> monkeypatch = getfixture('monkeypatch')
    >>> monkeypatch.setattr(http.client.HTTPConnection, 'debuglevel', None)
    >>> setup_requests_logging(logging.DEBUG)
    >>> http.client.HTTPConnection.debuglevel
    True
    """
    requests_log = logging.getLogger("requests.packages.urllib3")
    requests_log.setLevel(level)
    requests_log.propagate = True

    # enable debugging at httplib level
    http.client.HTTPConnection.debuglevel = level <= logging.DEBUG


class TimestampFileHandler(logging.StreamHandler):
    """
    A logging handler which will log to a file, similar to
    logging.handlers.RotatingFileHandler, but instead of
    appending a number, uses a timestamp to periodically select
    new file names to log to.

    Since this was developed, a TimedRotatingFileHandler was added to
    the Python stdlib. This class is still useful because it allows the period
    to be specified using simple english words.
    """

    def __init__(self, base_filename, mode='a', period='day'):
        self.base_filename = base_filename
        self.mode = mode
        self._set_period(period)
        logging.StreamHandler.__init__(self, None)

    def _set_period(self, period):
        """
        Set the period for the timestamp.  If period is 0 or None, no period
        will be used.
        """
        self._period = period
        if period:
            self._period_seconds = tempora.get_period_seconds(self._period)
            self._date_format = tempora.get_date_format_string(self._period_seconds)
        else:
            self._period_seconds = 0
            self._date_format = ''

    def _get_period(self):
        return self._period

    period = property(_get_period, _set_period)

    def _use_file(self, filename):
        self._ensure_directory_exists(filename)
        self.stream = open(filename, self.mode)

    def _ensure_directory_exists(self, filename):
        dirname = os.path.dirname(filename)
        if dirname and not os.path.exists(dirname):
            os.makedirs(dirname)

    def get_filename(self, t):
        """
        Return the appropriate filename for the given time
        based on the defined period.
        """
        root, ext = os.path.splitext(self.base_filename)
        # remove seconds not significant to the period
        if self._period_seconds:
            t -= t % self._period_seconds
        # convert it to a datetime object for formatting
        dt = tempora.utc.fromtimestamp(t)
        # append the datestring to the filename
        # workaround for datetime.strftime not handling '' properly
        appended_date = (
            dt.strftime(self._date_format) if self._date_format != '' else ''
        )
        if appended_date:
            # in the future, it would be nice for this format
            #  to be supplied as a parameter.
            result = root + ' ' + appended_date + ext
        else:
            result = self.base_filename
        return result

    def emit(self, record):
        """
        Emit a record.

        Output the record to the file, ensuring that the currently-
        opened file has the correct date.
        """
        now = time.time()
        current_name = self.get_filename(now)
        try:
            if not self.stream.name == current_name:
                self._use_file(current_name)
        except AttributeError:
            # a stream has not been created, so create one.
            self._use_file(current_name)
        logging.StreamHandler.emit(self, record)

    def close(self):
        """
        Closes the stream.
        """
        try:
            self.stream.close()
        except AttributeError:
            pass


class LogFileWrapper:
    """
    Emulates a file to replace stdout or stderr or
    anothe file object and redirects its output to
    a logger.

    Since data will often be send in partial lines or
    multiple lines, data is queued up until a new line
    is received.  Each line of text is send to the
    logger separately.
    """

    def __init__(self, name, lvl=logging.DEBUG):
        self.logger = logging.getLogger(name)
        self.lvl = lvl
        self.queued = ''

    def write(self, data):
        data = self.queued + data
        data = string.split(data, '\n')
        for line in data[:-1]:
            self.logger.log(self.lvl, line)
        self.queued = data[-1]
