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

import django.utils
import django
import mini_buildd
import mini_buildd.api


LOG = logging.getLogger(__name__)

register = django.template.Library()  # pylint: disable=invalid-name  # django needs this lower-case afaiu


class Css():
    """
    Mappings: Internal status strings to CSS class from ``mini_buildd.css``.
    """

    ERROR = "mbd-error"
    WARNING = "mbd-warning"
    NOTICE = "mbd-notice"
    INFO = "mbd-info"
    OKAY = "mbd-okay"
    SUCCESS = "mbd-success"
    NOT_USED = "mbd-not-used"

    MAP = {
        "sbuild_status": {
            "attempted": ERROR,
            "given-back": ERROR,
            "skipped": INFO,
            "successful": SUCCESS,
        },
        "sbuild_check": {
            "error": ERROR,
            "fail": ERROR,
            "warn": WARNING,
            "no-tests": NOTICE,
            "info": INFO,
            "pass": SUCCESS,
            "none": NOT_USED,
        },
        "events": {
            "REJECTED": ERROR,
            "BUILDING": INFO,
            "BUILT": OKAY,
            "PACKAGING": INFO,
            "INSTALLED": SUCCESS,
            "FAILED": ERROR,
            "MIGRATED": SUCCESS,
            "REMOVED": NOTICE,
        },
        "model_status": {
            "Removed": ERROR,
            "Prepared": NOTICE,
            "Active": SUCCESS,
        },
        "django_messages": {
            "debug": INFO,
            "info": INFO,
            "success": SUCCESS,
            "warning": WARNING,
            "error": ERROR,
        },
    }


@register.simple_tag
def mbd_cssmap(kind, key):
    return Css.MAP[kind].get(key, "mbd-unknown")


@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):
    """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)


@register.filter
def mbd_fromtimestamp(stamp):
    return datetime.datetime.fromtimestamp(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 json.dumps(obj, indent=2)


@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=mini_buildd.config.CHAR_ENCODING)).hexdigest()}"


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


@register.simple_tag
def mbd_model_stats(model):
    ret = {}
    model_class = eval(f"mini_buildd.models.{model}")  # pylint: disable=eval-used
    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_next(context):
    request = context.get("request")
    return request.get_full_path() if request.GET.get("output") != "snippet" else request.META.get("HTTP_REFERER")


@register.inclusion_tag("mini_buildd/includes/tags/accounts.html", takes_context=True)
def mbd_accounts(context, url_name, name=None, title=None, nxt=None, cls=""):
    return {
        "url_name": url_name,
        "name": name if name is not None else url_name.replace("_", " "),
        "title": title,
        "next": nxt if nxt is not None else mbd_next(context),
        "cls": cls,
    }


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

    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)

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


@register.inclusion_tag("mini_buildd/includes/tags/api.html", takes_context=True)
def mbd_api_popup(context, call, **kwargs):
    return mbd_api(context, call, output="snippet", **kwargs)


@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):
    #: 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(rf"^/.+/{mini_buildd.sbuild.SETUP_D_DIR}/(.+_.+)"), 1, "mbd-info"),      # .. attention:: mini-buildd setup scripts -- alternative to the banner approach above (maybe nicer if we can also drop banner support)
        (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=mini_buildd.config.CHAR_ENCODING, errors="ignore") as f:
            log = ""
            for line in f:
                for m in regexes:
                    match = m[0].match(line)
                    if match:
                        token = mbd_token()
                        log += f"<span id='{token}'></span>"
                        title = django.utils.html.escape(match.group(m[1]))
                        self["toc"].append({"class": m[2], "token": token, "title": title})
                        break
                log += django.utils.html.escape(line)
            return django.utils.safestring.mark_safe(log)

    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 mini_buildd.fopen(file_path) as f:
            return json.dumps(json.load(f), indent=2)

    @classmethod
    def filter_default(cls, file_path):
        with mini_buildd.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):
            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, length_menu="[[5, 10, 50, -1], [5, 10, 50, 'All']]", page_length=10, order=None, columns="null", searching="true", paging="true", info="true"):
    return {
        "identity": identity,
        "length_menu": django.utils.safestring.mark_safe(length_menu),
        "page_length": page_length,
        "order": order,
        "columns": columns,
        "searching": searching,
        "paging": paging,
        "info": info,
    }
