# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License.  See the LICENSE file in the root of this
# repository for complete details.

"""
Processors and tools specific to the `Twisted <https://twistedmatrix.com/>`_
networking engine.

See also :doc:`structlog's Twisted support <twisted>`.
"""

from __future__ import absolute_import, division, print_function

import json
import sys

from six import PY2, string_types
from twisted.python import log
from twisted.python.failure import Failure
from twisted.python.log import ILogObserver, textFromEventDict
from zope.interface import implementer

from ._base import BoundLoggerBase
from ._config import _BUILTIN_DEFAULT_PROCESSORS
from ._utils import until_not_interrupted
from .processors import JSONRenderer as GenericJSONRenderer


class BoundLogger(BoundLoggerBase):
    """
    Twisted-specific version of `structlog.BoundLogger`.

    Works exactly like the generic one except that it takes advantage of
    knowing the logging methods in advance.

    Use it like::

        configure(
            wrapper_class=structlog.twisted.BoundLogger,
        )

    """

    def msg(self, event=None, **kw):
        """
        Process event and call ``log.msg()`` with the result.
        """
        return self._proxy_to_logger("msg", event, **kw)

    def err(self, event=None, **kw):
        """
        Process event and call ``log.err()`` with the result.
        """
        return self._proxy_to_logger("err", event, **kw)


class LoggerFactory(object):
    """
    Build a Twisted logger when an *instance* is called.

    >>> from structlog import configure
    >>> from structlog.twisted import LoggerFactory
    >>> configure(logger_factory=LoggerFactory())
    """

    def __call__(self, *args):
        """
        Positional arguments are silently ignored.

        :rvalue: A new Twisted logger.

        .. versionchanged:: 0.4.0
            Added support for optional positional arguments.
        """
        return log


_FAIL_TYPES = (BaseException, Failure)


def _extractStuffAndWhy(eventDict):
    """
    Removes all possible *_why*s and *_stuff*s, analyzes exc_info and returns
    a tuple of ``(_stuff, _why, eventDict)``.

    **Modifies** *eventDict*!
    """
    _stuff = eventDict.pop("_stuff", None)
    _why = eventDict.pop("_why", None)
    event = eventDict.pop("event", None)
    if isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES):
        raise ValueError("Both _stuff and event contain an Exception/Failure.")
    # `log.err('event', _why='alsoEvent')` is ambiguous.
    if _why and isinstance(event, string_types):
        raise ValueError("Both `_why` and `event` supplied.")
    # Two failures are ambiguous too.
    if not isinstance(_stuff, _FAIL_TYPES) and isinstance(event, _FAIL_TYPES):
        _why = _why or "error"
        _stuff = event
    if isinstance(event, string_types):
        _why = event
    if not _stuff and sys.exc_info() != (None, None, None):
        _stuff = Failure()
    # Either we used the error ourselves or the user supplied one for
    # formatting.  Avoid log.err() to dump another traceback into the log.
    if isinstance(_stuff, BaseException) and not isinstance(_stuff, Failure):
        _stuff = Failure(_stuff)
    if PY2:
        sys.exc_clear()
    return _stuff, _why, eventDict


class ReprWrapper(object):
    """
    Wrap a string and return it as the ``__repr__``.

    This is needed for ``twisted.python.log.err`` that calls `repr` on
    ``_stuff``:

    >>> repr("foo")
    "'foo'"
    >>> repr(ReprWrapper("foo"))
    'foo'

    Note the extra quotes in the unwrapped example.
    """

    def __init__(self, string):
        self.string = string

    def __eq__(self, other):
        """
        Check for equality, just for tests.
        """
        return (
            isinstance(other, self.__class__) and self.string == other.string
        )

    def __repr__(self):
        return self.string


class JSONRenderer(GenericJSONRenderer):
    """
    Behaves like `structlog.processors.JSONRenderer` except that it
    formats tracebacks and failures itself if called with ``err()``.

    .. note::

        This ultimately means that the messages get logged out using ``msg()``,
        and *not* ``err()`` which renders failures in separate lines.

        Therefore it will break your tests that contain assertions using
        `flushLoggedErrors <https://twistedmatrix.com/documents/
        current/api/twisted.trial.unittest.SynchronousTestCase.html
        #flushLoggedErrors>`_.

    *Not* an adapter like `EventAdapter` but a real formatter.  Also does *not*
    require to be adapted using it.

    Use together with a `JSONLogObserverWrapper`-wrapped Twisted logger like
    `plainJSONStdOutLogger` for pure-JSON logs.
    """

    def __call__(self, logger, name, eventDict):
        _stuff, _why, eventDict = _extractStuffAndWhy(eventDict)
        if name == "err":
            eventDict["event"] = _why
            if isinstance(_stuff, Failure):
                eventDict["exception"] = _stuff.getTraceback(detail="verbose")
                _stuff.cleanFailure()
        else:
            eventDict["event"] = _why
        return (
            (
                ReprWrapper(
                    GenericJSONRenderer.__call__(self, logger, name, eventDict)
                ),
            ),
            {"_structlog": True},
        )


@implementer(ILogObserver)
class PlainFileLogObserver(object):
    """
    Write only the the plain message without timestamps or anything else.

    Great to just print JSON to stdout where you catch it with something like
    runit.

    :param file: File to print to.
    :type file: file object

    .. versionadded:: 0.2.0
    """

    def __init__(self, file):
        self._write = file.write
        self._flush = file.flush

    def __call__(self, eventDict):
        until_not_interrupted(self._write, textFromEventDict(eventDict) + "\n")
        until_not_interrupted(self._flush)


@implementer(ILogObserver)
class JSONLogObserverWrapper(object):
    """
    Wrap a log *observer* and render non-`JSONRenderer` entries to JSON.

    :param ILogObserver observer: Twisted log observer to wrap.  For example
        :class:`PlainFileObserver` or Twisted's stock `FileLogObserver
        <https://twistedmatrix.com/documents/current/api/twisted.python.log.
        FileLogObserver.html>`_

    .. versionadded:: 0.2.0
    """

    def __init__(self, observer):
        self._observer = observer

    def __call__(self, eventDict):
        if "_structlog" not in eventDict:
            eventDict["message"] = (
                json.dumps(
                    {
                        "event": textFromEventDict(eventDict),
                        "system": eventDict.get("system"),
                    }
                ),
            )
            eventDict["_structlog"] = True
        return self._observer(eventDict)


def plainJSONStdOutLogger():
    """
    Return a logger that writes only the message to stdout.

    Transforms non-`JSONRenderer` messages to JSON.

    Ideal for JSONifying log entries from Twisted plugins and libraries that
    are outside of your control::

        $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web
        {"event": "Log opened.", "system": "-"}
        {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"}
        {"event": "reactor class: twisted...EPollReactor.", "system": "-"}
        {"event": "Site starting on 8080", "system": "-"}
        {"event": "Starting factory <twisted.web.server.Site ...>", ...}
        ...

    Composes `PlainFileLogObserver` and `JSONLogObserverWrapper` to a usable
    logger.

    .. versionadded:: 0.2.0
    """
    return JSONLogObserverWrapper(PlainFileLogObserver(sys.stdout))


class EventAdapter(object):
    """
    Adapt an ``event_dict`` to Twisted logging system.

    Particularly, make a wrapped `twisted.python.log.err
    <https://twistedmatrix.com/documents/current/
    api/twisted.python.log.html#err>`_ behave as expected.

    :param callable dictRenderer: Renderer that is used for the actual
        log message.  Please note that structlog comes with a dedicated
        :class:`JSONRenderer`.

    **Must** be the last processor in the chain and requires a *dictRenderer*
    for the actual formatting as an constructor argument in order to be able to
    fully support the original behaviors of ``log.msg()`` and ``log.err()``.
    """

    def __init__(self, dictRenderer=None):
        """
        :param dictRenderer: A processor used to format the log message.
        """
        self._dictRenderer = dictRenderer or _BUILTIN_DEFAULT_PROCESSORS[-1]

    def __call__(self, logger, name, eventDict):
        if name == "err":
            # This aspires to handle the following cases correctly:
            #   - log.err(failure, _why='event', **kw)
            #   - log.err('event', **kw)
            #   - log.err(_stuff=failure, _why='event', **kw)
            _stuff, _why, eventDict = _extractStuffAndWhy(eventDict)
            eventDict["event"] = _why
            return (
                (),
                {
                    "_stuff": _stuff,
                    "_why": self._dictRenderer(logger, name, eventDict),
                },
            )
        else:
            return self._dictRenderer(logger, name, eventDict)
