import hashlib
import json
import logging
import mimetypes
import os
import pathlib
import re
import uuid

import django
import django.utils

from mini_buildd import api, config, dist, util

LOG = logging.getLogger(__name__)

register = django.template.Library()


@register.filter
def mbd_sorted(iterable):
    """Just do (python) sorting -- useful for models that use custom python ordering"""
    return sorted(iterable)


@register.simple_tag(takes_context=True)
def mbd_menu_active(context, name):
    return "mbd-menu-active" if context["request"].path == config.URIS[name]["view"].uri else ""


@register.simple_tag
def mbd_get(obj):
    """Identity function. Handy to set variables in templates (``{% mbd_get some_value as myvar %}``)"""
    return obj


@register.simple_tag
def mbd_dict_get(dict_, key, default=None):
    """Get value from dict even if the key has a special name (like hyphens in a changes file field), or is in another variable"""
    return dict_.get(key, default)


@register.filter
def mbd_fromtimestamp(stamp):
    return util.Datetime.from_stamp(stamp)


@register.filter
def mbd_parent(path):
    """
    Get parent path (uri), always w/ trailing slash

    >>> mbd_parent("/parent/file")
    '/parent/'
    >>> mbd_parent("/parent/dir/")
    '/parent/'
    >>> mbd_parent("/parent/dir/dir/")
    '/parent/dir/'
    """
    return str(pathlib.Path(path).parent) + "/"


@register.filter
def mbd_basename(path):
    return os.path.basename(path)


@register.simple_tag
def mbd_join(path, *paths):
    return os.path.join(path, *paths)


@register.filter
def mbd_jsonpp(obj):
    """Get pretty print from json-serializable object"""
    return util.json_pretty(obj)


@register.simple_tag
def mbd_token():
    return f"mbd-token-{uuid.uuid4()}"


@register.simple_tag
def mbd_hash(data):
    return f"mbd-hash-{hashlib.sha224(bytes(data, encoding=config.CHAR_ENCODING)).hexdigest()}"


@register.filter
def mbd_codename(diststr):
    """Get codename from diststr. On any error, just return empty string"""
    try:
        return dist.Dist(diststr).codename
    except Exception:
        return ""


@register.filter
def mbd_repository(diststr):
    """Get repository from diststr. On any error, just return empty string"""
    try:
        return dist.Dist(diststr).repository
    except Exception:
        return ""


@register.simple_tag
def mbd_model_stats(model):
    ret = {}
    model_class = getattr(util.models(), model)
    if getattr(model_class, "mbd_is_prepared", None):
        # Status model
        ret["active"] = model_class.objects.filter(status__exact=model_class.STATUS_ACTIVE).count()
        ret["prepared"] = model_class.objects.filter(status__exact=model_class.STATUS_PREPARED).count()
        ret["removed"] = model_class.objects.filter(status__exact=model_class.STATUS_REMOVED).count()
    ret["total"] = model_class.objects.all().count()
    return ret


@register.simple_tag(takes_context=True)
def mbd_absolute_uri(context, location):
    return context.get("request").build_absolute_uri(location)


@register.inclusion_tag("mini_buildd/includes/tags/api.html", takes_context=True)
def mbd_api(context, call, name=None, hide_options=False, verbose=False, popup=True, **kwargs):
    prefix = "value_"
    api_args = {k[len(prefix):]: v for k, v in kwargs.items() if k.startswith(prefix)}
    if isinstance(call, str):
        api_cls = api.CALLS.get(call)
        if api_cls is None:
            raise util.HTTPBadRequest(f"Unknown API call: {call}")
        api_call = api_cls(**api_args)
    else:
        api_call = call
        api_call.set_args(**api_args)

    args = {"mandatory": [], "optional": [], "hidden": []}
    for arg in api_call.args.values():
        args["mandatory" if arg.needs_value() else "hidden" if hide_options else "optional"].append(arg)
    args["optional" if args["optional"] else "hidden"].append(api.Output())

    return {
        "request": context.get("request"),
        "args": args,
        "call": api_call,
        "name": api_call.name if name is None else name,
        "is_authorized": api_call.AUTH.is_authorized(context.get("user")),
        "verbose": verbose,
        "popup": popup,
    }


@register.inclusion_tag("mini_buildd/includes/tags/sbuild_status.html")
def mbd_sbuild_status(bres):
    return {
        "bres": bres,
        "checks": {
            "lintian": bres.get("sbuild_lintian", "none"),
            "piuparts": bres.get("sbuild_piuparts", "none"),
            "autopkgtest": bres.get("sbuild_autopkgtest", "none"),
        }}


class PlainFilter(dict):
    """
    Enrich plain text with HTML hints (TOC)

    .. note:: http://tools.ietf.org/html/rfc5147: This, in theory, could be some way to create a TOC for text/plain (buildlogs and such). However

      * RFC only describes line/char based links, not text matching based (so scanning the text file would be needed still)
      * As it seems, main browsers simply don't support it
        * https://bugzilla.mozilla.org/show_bug.cgi?id=660583
        * https://bugs.chromium.org/p/chromium/issues/detail?id=77024
    """

    #: Allow (non 'text/') mime types
    ALLOWED_MIME_TYPES = ["application/json"]
    #: Allow extensions that have no official mime type
    ALLOWED_EXTENSIONS = ["buildlog", "changes", "dsc", "buildinfo", "log", "upload"]
    #: Allow complete (base) file names
    ALLOWED_FILES = ["InRelease", "Release", "Sources", "Packages", ".sbuildrc"]

    def ok_to_filter(self, file_path):
        mime_type = mimetypes.guess_type(file_path)[0]
        name = os.path.basename(file_path)
        return \
            mime_type is not None and mime_type.startswith("text/") or \
            mime_type in self.ALLOWED_MIME_TYPES or \
            self["extension"] in self.ALLOWED_EXTENSIONS or \
            name in self.ALLOWED_FILES

    #: Regex filters for ``*.buildlog``.
    BUILDLOG_INDEX_REGEXES = [
        (re.compile(r"\| (.*) \|"), 1, "mbd-info"),                                            # sbuild section headers
        (re.compile(r"^MINI_BUILDD: .+"), 0, "mbd-info"),                                      # mini-buildd banners (see files.py)
        (re.compile(r".*make.*\*\*\*.*"), 0, "mbd-error"),                                     # make fatal error
        (re.compile(r"(^W(ARNING)?: .+)|(^W(ARN)?: .+)", re.IGNORECASE), 0, "mbd-warning"),    # warnings (lintian, others)
        (re.compile(r"(^E(RROR)?: .+)|(^E(RR)?: .+)", re.IGNORECASE), 0, "mbd-error"),         # errors (lintian, others)
        (re.compile(r"(^C(RITICAL)?: .+)|(^C(RIT)?: .+)", re.IGNORECASE), 0, "mbd-error"),     # critical (lintian, others)
    ]

    #: Regex filters for ``*.log``.
    LOG_INDEX_REGEXES = [
        (re.compile(r"^.+ W: (.*)", re.IGNORECASE), 0, "mbd-warning"),
        (re.compile(r"^.+ E: (.*)", re.IGNORECASE), 0, "mbd-error"),
        (re.compile(r"^.+ C: (.*)", re.IGNORECASE), 0, "mbd-error"),
    ]

    def regex_filter(self, file_path, regexes):
        with open(file_path, "r", encoding=config.CHAR_ENCODING, errors="ignore") as f:
            logs = []
            for line in f:
                for m in regexes:
                    match = m[0].match(line)
                    if match:
                        token = mbd_token()
                        logs.append(f"<span id='{token}' class='mbd-plain-toc-item {m[2]}'></span>")
                        title = django.utils.html.escape(match.group(m[1]))
                        self["toc"].append({"class": m[2], "token": token, "title": title})
                        break
                logs.append(django.utils.html.escape(line))
            return django.utils.safestring.mark_safe("".join(logs))

    def filter_buildlog(self, file_path):
        return self.regex_filter(file_path, self.BUILDLOG_INDEX_REGEXES)

    def filter_log(self, file_path):
        return self.regex_filter(file_path, self.LOG_INDEX_REGEXES)

    @classmethod
    def filter_json(cls, file_path):
        with util.fopen(file_path) as f:
            return util.json_pretty(json.load(f))

    @classmethod
    def filter_default(cls, file_path):
        with util.fopen(file_path, "r", errors="ignore") as f:
            return f.read()

    def __init__(self, content, file_path, uri, title):
        super().__init__()
        self["content"] = content
        self["file_path"] = file_path
        self["uri"] = uri
        self["title"] = title
        self["toc"] = []  # May be filled by filters (table of contents)

        if file_path is not None and os.path.exists(file_path):
            self["mtime"] = os.path.getmtime(file_path)

            dummy_, extension = os.path.splitext(file_path)
            self["extension"] = extension[1:]

            if self.ok_to_filter(file_path):
                self["content"] = getattr(self, "filter_" + self["extension"], self.filter_default)(file_path)


@register.inclusion_tag("mini_buildd/includes/tags/file.html")
def mbd_file(content=None, file_path=None, uri=None, title=None):
    return PlainFilter(content, file_path, uri, title)


@register.inclusion_tag("mini_buildd/includes/tags/datatable.html")
def mbd_datatable(identity, order=None, columns="null"):
    return {
        "identity": identity,
        "order": order,
        "columns": columns,
    }
