from past.builtins import unicode
from six import ensure_text, ensure_str

import time
import json
from functools import wraps

from hyperlink import (
    DecodedURL,
)

from eliot import (
    Message,
    start_action,
)
from eliot.twisted import (
    DeferredContext,
)

from twisted.web import (
    http,
    resource,
    template,
)
from twisted.web.iweb import (
    IRequest,
)
from twisted.web.template import (
    tags,
)
from twisted.web.server import (
    NOT_DONE_YET,
)
from twisted.web.util import (
    DeferredResource,
    FailureElement,
    redirectTo,
)
from twisted.python.reflect import (
    fullyQualifiedName,
)
from twisted.python import log
from twisted.python.failure import (
    Failure,
)
from twisted.internet.defer import (
    CancelledError,
    maybeDeferred,
)
from twisted.web.resource import (
    IResource,
)

from allmydata import blacklist
from allmydata.interfaces import (
    EmptyPathnameComponentError,
    ExistingChildError,
    FileTooLargeError,
    MustBeDeepImmutableError,
    MustBeReadonlyError,
    MustNotBeUnknownRWError,
    NoSharesError,
    NoSuchChildError,
    NotEnoughSharesError,
    MDMF_VERSION,
    SDMF_VERSION,
)
from allmydata.mutable.common import UnrecoverableFileError
from allmydata.util.time_format import (
    format_delta,
    format_time,
)
from allmydata.util.encodingutil import (
    quote_output,
    to_bytes,
)

# Originally part of this module, so still part of its API:
from .common_py3 import (  # noqa: F401
    get_arg, abbreviate_time, MultiFormatResource, WebError,
)


def get_filenode_metadata(filenode):
    metadata = {'mutable': filenode.is_mutable()}
    if metadata['mutable']:
        mutable_type = filenode.get_version()
        assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
        if mutable_type == MDMF_VERSION:
            file_format = "MDMF"
        else:
            file_format = "SDMF"
    else:
        file_format = "CHK"
    metadata['format'] = file_format
    size = filenode.get_size()
    if size is not None:
        metadata['size'] = size
    return metadata

def boolean_of_arg(arg):
    # TODO: ""
    arg = ensure_text(arg)
    if arg.lower() not in ("true", "t", "1", "false", "f", "0", "on", "off"):
        raise WebError("invalid boolean argument: %r" % (arg,), http.BAD_REQUEST)
    return arg.lower() in ("true", "t", "1", "on")

def parse_replace_arg(replace):
    replace = ensure_text(replace)
    if replace.lower() == "only-files":
        return replace
    try:
        return boolean_of_arg(replace)
    except WebError:
        raise WebError("invalid replace= argument: %r" % (ensure_str(replace),), http.BAD_REQUEST)


def get_format(req, default="CHK"):
    arg = get_arg(req, "format", None)
    if not arg:
        if boolean_of_arg(get_arg(req, "mutable", "false")):
            return "SDMF"
        return default
    if arg.upper() == b"CHK":
        return "CHK"
    elif arg.upper() == b"SDMF":
        return "SDMF"
    elif arg.upper() == b"MDMF":
        return "MDMF"
    else:
        raise WebError("Unknown format: %s, I know CHK, SDMF, MDMF" % arg,
                       http.BAD_REQUEST)

def get_mutable_type(file_format): # accepts result of get_format()
    if file_format == "SDMF":
        return SDMF_VERSION
    elif file_format == "MDMF":
        return MDMF_VERSION
    else:
        # this is also used to identify which formats are mutable. Use
        #  if get_mutable_type(file_format) is not None:
        #      do_mutable()
        #  else:
        #      do_immutable()
        return None


def parse_offset_arg(offset):
    # XXX: This will raise a ValueError when invoked on something that
    # is not an integer. Is that okay? Or do we want a better error
    # message? Since this call is going to be used by programmers and
    # their tools rather than users (through the wui), it is not
    # inconsistent to return that, I guess.
    if offset is not None:
        offset = int(offset)

    return offset


def get_root(req):
    """
    Get a relative path with parent directory segments that refers to the root
    location known to the given request.  This seems a lot like the constant
    absolute path **/** but it will behave differently if the Tahoe-LAFS HTTP
    server is reverse-proxied and mounted somewhere other than at the root.

    :param twisted.web.iweb.IRequest req: The request to consider.

    :return: A string like ``../../..`` with the correct number of segments to
        reach the root.
    """
    if not IRequest.providedBy(req):
        raise TypeError(
            "get_root requires IRequest provider, got {!r}".format(req),
        )
    depth = len(req.prepath) + len(req.postpath)
    link = "/".join([".."] * depth)
    return link


def convert_children_json(nodemaker, children_json):
    """I convert the JSON output of GET?t=json into the dict-of-nodes input
    to both dirnode.create_subdirectory() and
    client.create_directory(initial_children=). This is used by
    t=mkdir-with-children and t=mkdir-immutable"""
    children = {}
    if children_json:
        data = json.loads(children_json)
        for (namex, (ctype, propdict)) in data.iteritems():
            namex = unicode(namex)
            writecap = to_bytes(propdict.get("rw_uri"))
            readcap = to_bytes(propdict.get("ro_uri"))
            metadata = propdict.get("metadata", {})
            # name= argument is just for error reporting
            childnode = nodemaker.create_from_cap(writecap, readcap, name=namex)
            children[namex] = (childnode, metadata)
    return children


def compute_rate(bytes, seconds):
    if bytes is None:
      return None

    if seconds is None or seconds == 0:
      return None

    # negative values don't make sense here
    assert bytes > -1
    assert seconds > 0

    return 1.0 * bytes / seconds

def abbreviate_rate(data):
    """
    Convert number of bytes/second into human readable strings (unicode).

    Uses metric measures, so 1000 not 1024, e.g. 21.8kBps, 554.4kBps, 4.37MBps.

    :param data: Either ``None`` or integer.

    :return: Unicode string.
    """
    if data is None:
        return u""
    r = float(data)
    if r > 1000000:
        return u"%1.2fMBps" % (r/1000000)
    if r > 1000:
        return u"%.1fkBps" % (r/1000)
    return u"%.0fBps" % r

def abbreviate_size(data):
    """
    Convert number of bytes into human readable strings (unicode).

    Uses metric measures, so 1000 not 1024, e.g. 21.8kB, 554.4kB, 4.37MB.

    :param data: Either ``None`` or integer.

    :return: Unicode string.
    """
    if data is None:
        return u""
    r = float(data)
    if r > 1000000000:
        return u"%1.2fGB" % (r/1000000000)
    if r > 1000000:
        return u"%1.2fMB" % (r/1000000)
    if r > 1000:
        return u"%.1fkB" % (r/1000)
    return u"%.0fB" % r

def plural(sequence_or_length):
    if isinstance(sequence_or_length, int):
        length = sequence_or_length
    else:
        length = len(sequence_or_length)
    if length == 1:
        return ""
    return "s"

def text_plain(text, req):
    req.setHeader("content-type", "text/plain")
    req.setHeader("content-length", b"%d" % len(text))
    return text

def spaces_to_nbsp(text):
    return unicode(text).replace(u' ', u'\u00A0')

def render_time_delta(time_1, time_2):
    return spaces_to_nbsp(format_delta(time_1, time_2))

def render_time(t):
    return spaces_to_nbsp(format_time(time.localtime(t)))

def render_time_attr(t):
    return format_time(time.localtime(t))


# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
# Internal Server Error, we either need to do that ICanHandleException trick,
# or make sure that childFactory returns a WebErrorResource (and never an
# actual exception). The latter is growing increasingly annoying.

def should_create_intermediate_directories(req):
    t = get_arg(req, "t", "").strip()
    return bool(req.method in ("PUT", "POST") and
                t not in ("delete", "rename", "rename-form", "check"))

def humanize_exception(exc):
    """
    Like ``humanize_failure`` but for an exception.

    :param Exception exc: The exception to describe.

    :return: See ``humanize_failure``.
    """
    if isinstance(exc, EmptyPathnameComponentError):
        return ("The webapi does not allow empty pathname components, "
                "i.e. a double slash", http.BAD_REQUEST)
    if isinstance(exc, ExistingChildError):
        return ("There was already a child by that name, and you asked me "
                "to not replace it.", http.CONFLICT)
    if isinstance(exc, NoSuchChildError):
        quoted_name = quote_output(exc.args[0], encoding="utf-8", quotemarks=False)
        return ("No such child: %s" % quoted_name, http.NOT_FOUND)
    if isinstance(exc, NotEnoughSharesError):
        t = ("NotEnoughSharesError: This indicates that some "
             "servers were unavailable, or that shares have been "
             "lost to server departure, hard drive failure, or disk "
             "corruption. You should perform a filecheck on "
             "this object to learn more.\n\nThe full error message is:\n"
             "%s") % str(exc)
        return (t, http.GONE)
    if isinstance(exc, NoSharesError):
        t = ("NoSharesError: no shares could be found. "
             "Zero shares usually indicates a corrupt URI, or that "
             "no servers were connected, but it might also indicate "
             "severe corruption. You should perform a filecheck on "
             "this object to learn more.\n\nThe full error message is:\n"
             "%s") % str(exc)
        return (t, http.GONE)
    if isinstance(exc, UnrecoverableFileError):
        t = ("UnrecoverableFileError: the directory (or mutable file) could "
             "not be retrieved, because there were insufficient good shares. "
             "This might indicate that no servers were connected, "
             "insufficient servers were connected, the URI was corrupt, or "
             "that shares have been lost due to server departure, hard drive "
             "failure, or disk corruption. You should perform a filecheck on "
             "this object to learn more.")
        return (t, http.GONE)
    if isinstance(exc, MustNotBeUnknownRWError):
        quoted_name = quote_output(exc.args[1], encoding="utf-8")
        immutable = exc.args[2]
        if immutable:
            t = ("MustNotBeUnknownRWError: an operation to add a child named "
                 "%s to a directory was given an unknown cap in a write slot.\n"
                 "If the cap is actually an immutable readcap, then using a "
                 "webapi server that supports a later version of Tahoe may help.\n\n"
                 "If you are using the webapi directly, then specifying an immutable "
                 "readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
                 "omitting the write slot (rw_uri), would also work in this "
                 "case.") % quoted_name
        else:
            t = ("MustNotBeUnknownRWError: an operation to add a child named "
                 "%s to a directory was given an unknown cap in a write slot.\n"
                 "Using a webapi server that supports a later version of Tahoe "
                 "may help.\n\n"
                 "If you are using the webapi directly, specifying a readcap in "
                 "the read slot (ro_uri) of the JSON PROPDICT, as well as a "
                 "writecap in the write slot if desired, would also work in this "
                 "case.") % quoted_name
        return (t, http.BAD_REQUEST)
    if isinstance(exc, MustBeDeepImmutableError):
        quoted_name = quote_output(exc.args[1], encoding="utf-8")
        t = ("MustBeDeepImmutableError: a cap passed to this operation for "
             "the child named %s, needed to be immutable but was not. Either "
             "the cap is being added to an immutable directory, or it was "
             "originally retrieved from an immutable directory as an unknown "
             "cap.") % quoted_name
        return (t, http.BAD_REQUEST)
    if isinstance(exc, MustBeReadonlyError):
        quoted_name = quote_output(exc.args[1], encoding="utf-8")
        t = ("MustBeReadonlyError: a cap passed to this operation for "
             "the child named '%s', needed to be read-only but was not. "
             "The cap is being passed in a read slot (ro_uri), or was retrieved "
             "from a read slot as an unknown cap.") % quoted_name
        return (t, http.BAD_REQUEST)
    if isinstance(exc, blacklist.FileProhibited):
        t = "Access Prohibited: %s" % quote_output(exc.reason, encoding="utf-8", quotemarks=False)
        return (t, http.FORBIDDEN)
    if isinstance(exc, WebError):
        return (exc.text, exc.code)
    if isinstance(exc, FileTooLargeError):
        return ("FileTooLargeError: %s" % (exc,), http.REQUEST_ENTITY_TOO_LARGE)
    return (str(exc), None)


def humanize_failure(f):
    """
    Create an human-oriented description of a failure along with some HTTP
    metadata.

    :param Failure f: The failure to describe.

    :return (bytes, int): A tuple of some prose and an HTTP code describing
        the failure.
    """
    return humanize_exception(f.value)


class NeedOperationHandleError(WebError):
    pass


class SlotsSequenceElement(template.Element):
    """
    ``SlotsSequenceElement` is a minimal port of Nevow's sequence renderer for
    twisted.web.template.

    Tags passed in to be templated will have two renderers available: ``item``
    and ``tag``.
    """

    def __init__(self, tag, seq):
        self.loader = template.TagLoader(tag)
        self.seq = seq

    @template.renderer
    def header(self, request, tag):
        return tag

    @template.renderer
    def item(self, request, tag):
        """
        A template renderer for each sequence item.

        ``tag`` will be cloned for each item in the sequence provided, and its
        slots filled from the sequence item. Each item must be dict-like enough
        for ``tag.fillSlots(**item)``. Each cloned tag will be siblings with no
        separator beween them.
        """
        for item in self.seq:
            yield tag.clone(deep=False).fillSlots(**item)

    @template.renderer
    def empty(self, request, tag):
        """
        A template renderer for empty sequences.

        This renderer will either return ``tag`` unmodified if the provided
        sequence has no items, or return the empty string if there are any
        items.
        """
        if len(self.seq) > 0:
            return u''
        else:
            return tag


def exception_to_child(getChild):
    """
    Decorate ``getChild`` method with exception handling behavior to render an
    error page reflecting the exception.
    """
    @wraps(getChild)
    def g(self, name, req):
        # Bind the method to the instance so it has a better
        # fullyQualifiedName later on.  This is not necessary on Python 3.
        bound_getChild = getChild.__get__(self, type(self))

        action = start_action(
            action_type=u"allmydata:web:common-getChild",
            uri=req.uri,
            method=req.method,
            name=name,
            handler=fullyQualifiedName(bound_getChild),
        )
        with action.context():
            result = DeferredContext(maybeDeferred(bound_getChild, name, req))
            result.addCallbacks(
                _getChild_done,
                _getChild_failed,
                callbackArgs=(self,),
            )
            result = result.addActionFinish()
        return DeferredResource(result)
    return g


def _getChild_done(child, parent):
    Message.log(
        message_type=u"allmydata:web:common-getChild:result",
        result=fullyQualifiedName(type(child)),
    )
    if child is None:
        return resource.NoResource()
    return child


def _getChild_failed(reason):
    text, code = humanize_failure(reason)
    return resource.ErrorPage(code, "Error", text)


def render_exception(render):
    """
    Decorate a ``render_*`` method with exception handling behavior to render
    an error page reflecting the exception.
    """
    @wraps(render)
    def g(self, request):
        # Bind the method to the instance so it has a better
        # fullyQualifiedName later on.  This is not necessary on Python 3.
        bound_render = render.__get__(self, type(self))

        action = start_action(
            action_type=u"allmydata:web:common-render",
            uri=request.uri,
            method=request.method,
            handler=fullyQualifiedName(bound_render),
        )
        if getattr(request, "dont_apply_extra_processing", False):
            with action:
                return bound_render(request)

        with action.context():
            result = DeferredContext(maybeDeferred(bound_render, request))
            # Apply `_finish` all of our result handling logic to whatever it
            # returned.
            result.addBoth(_finish, bound_render, request)
            d = result.addActionFinish()

        # If the connection is lost then there's no point running our _finish
        # logic because it has nowhere to send anything.  There may also be no
        # point in finishing whatever operation was being performed because
        # the client cannot be informed of its result.  Also, Twisted Web
        # raises exceptions from some Request methods if they're used after
        # the connection is lost.
        request.notifyFinish().addErrback(
            lambda ignored: d.cancel(),
        )
        return NOT_DONE_YET

    return g


def _finish(result, render, request):
    """
    Try to finish rendering the response to a request.

    This implements extra convenience functionality not provided by Twisted
    Web.  Various resources in Tahoe-LAFS made use of this functionality when
    it was provided by Nevow.  Rather than making that application code do the
    more tedious thing itself, we duplicate the functionality here.

    :param result: Something returned by a render method which we can turn
        into a response.

    :param render: The original render method which produced the result.

    :param request: The request being responded to.

    :return: ``None``
    """
    if isinstance(result, Failure):
        if result.check(CancelledError):
            return
        Message.log(
            message_type=u"allmydata:web:common-render:failure",
            message=result.getErrorMessage(),
        )
        _finish(
            _renderHTTP_exception(request, result),
            render,
            request,
        )
    elif IResource.providedBy(result):
        # If result is also using @render_exception then we don't want to
        # double-apply the logic.  This leads to an attempt to double-finish
        # the request.  If it isn't using @render_exception then you should
        # fix it so it is.
        Message.log(
            message_type=u"allmydata:web:common-render:resource",
            resource=fullyQualifiedName(type(result)),
        )
        result.render(request)
    elif isinstance(result, unicode):
        Message.log(
            message_type=u"allmydata:web:common-render:unicode",
        )
        request.write(result.encode("utf-8"))
        request.finish()
    elif isinstance(result, bytes):
        Message.log(
            message_type=u"allmydata:web:common-render:bytes",
        )
        request.write(result)
        request.finish()
    elif isinstance(result, DecodedURL):
        Message.log(
            message_type=u"allmydata:web:common-render:DecodedURL",
        )
        _finish(redirectTo(result.to_text().encode("utf-8"), request), render, request)
    elif result is None:
        Message.log(
            message_type=u"allmydata:web:common-render:None",
        )
        request.finish()
    elif result == NOT_DONE_YET:
        Message.log(
            message_type=u"allmydata:web:common-render:NOT_DONE_YET",
        )
        pass
    else:
        Message.log(
            message_type=u"allmydata:web:common-render:unknown",
        )
        log.err("Request for {!r} handled by {!r} returned unusable {!r}".format(
            request.uri,
            fullyQualifiedName(render),
            result,
        ))
        request.setResponseCode(http.INTERNAL_SERVER_ERROR)
        _finish(b"Internal Server Error", render, request)


def _renderHTTP_exception(request, failure):
    try:
        text, code = humanize_failure(failure)
    except:
        log.msg("exception in humanize_failure")
        log.msg("argument was %s" % (failure,))
        log.err()
        text = str(failure)
        code = None

    if code is not None:
        return _renderHTTP_exception_simple(request, text, code)

    accept = request.getHeader("accept")
    if not accept:
        accept = "*/*"
    if "*/*" in accept or "text/*" in accept or "text/html" in accept:
        request.setResponseCode(http.INTERNAL_SERVER_ERROR)
        return template.renderElement(
            request,
            tags.html(
                tags.head(
                    tags.title(u"Exception"),
                ),
                tags.body(
                    FailureElement(failure),
                ),
            ),
        )

    # use plain text
    traceback = failure.getTraceback()
    return _renderHTTP_exception_simple(
        request,
        traceback,
        http.INTERNAL_SERVER_ERROR,
    )


def _renderHTTP_exception_simple(request, text, code):
    request.setResponseCode(code)
    request.setHeader("content-type", "text/plain;charset=utf-8")
    if isinstance(text, unicode):
        text = text.encode("utf-8")
    request.setHeader("content-length", b"%d" % len(text))
    return text


def handle_when_done(req, d):
    when_done = get_arg(req, "when_done", None)
    if when_done:
        d.addCallback(lambda res: DecodedURL.from_text(when_done.decode("utf-8")))
    return d


def url_for_string(req, url_string):
    """
    Construct a universal URL using the given URL string.

    :param IRequest req: The request being served.  If ``redir_to`` is not
        absolute then this is used to determine the net location of this
        server and the resulting URL is made to point at it.

    :param bytes url_string: A byte string giving a universal or absolute URL.

    :return DecodedURL: An absolute URL based on this server's net location
        and the given URL string.
    """
    url = DecodedURL.from_text(url_string.decode("utf-8"))
    if url.host == b"":
        root = req.URLPath()
        netloc = root.netloc.split(b":", 1)
        if len(netloc) == 1:
            host = netloc
            port = None
        else:
            host = netloc[0]
            port = int(netloc[1])
        url = url.replace(
            scheme=root.scheme.decode("ascii"),
            host=host.decode("ascii"),
            port=port,
        )
    return url
