""" HTTP interface and processing

Various HTTP routes the external world uses to communicate with the application.
"""

import bottle as bt
import cgi
import logging
import re
import os.path
import secrets
from metrics import Time
from bin import root, config, models
from bin.highlight import highlight, languages, langtoext, exttolang


logger = logging.getLogger(__name__)
BOTUARE = re.compile(r'|'.join([
    re.escape('Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)'),
    re.escape('facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)'),
]))


@bt.route('/health', method='GET')
def healthcheck():
    """ Get a dummy string 'alive' to ensure the server is responding """
    return "alive"


@bt.route('/', method='GET')
def get_new_form():
    """
    Get the browser-friendly html form to easily post a new snippet

    :param lang: (query) optional lang that is selected in the lang selection instead of the configured default
    :param parentid: (query) optional 'parent' snippet to duplicate the code from

    :raises HTTPError: code 404 when the 'parent' snippet is not found
    """
    parentid = bt.request.query.parentid
    lang = bt.request.query.lang or config.DEFAULT_LANGUAGE

    try:
        code = models.Snippet.get_by_id(parentid).code if parentid else ""
    except KeyError:
        raise bt.HTTPError(404, "Parent snippet not found")

    return bt.template(
        'newform.html',
        languages=languages,
        default_language=lang,
        code=code,
        parentid=parentid,
    )


@bt.route('/assets/<filepath:path>')
def assets(filepath):
    """
    Get a static css/js/media file that is stored in the filesystem.

    This route exists for developers of bin who wish to run the service
    with minimum system requirements. In production, we suggest you use
    a web server to deliver the static content.
    """
    return bt.static_file(filepath, root=root.joinpath('assets'))


@bt.route('/new', method='POST')
def post_new():
    """
    Post a new snippet and redirect the user to the generated unique URL
    for the snippet.

    :param code: (form) required snippet text, can alternativaly be sent as a Multi-Part utf-8 file
    :param lang: (form) optional language
    :param maxusage: (form) optional maximum download of the snippet before it is deleted
    :param lifetime: (form) optional time (defined in seconds) the snippet is keep in the database before it is deleted
    :param parentid: (form) optional snippet id this new snippet is a duplicate of
    :param token: (form) optional the "admin" token allows you to delete your snippet

    :raises HTTPError: code 411 when the ``Content-Length`` http header is missing
    :raises HTTPError: code 413 when the http request is too large (mostly because the snippet is too long)
    :raises HTTPError: code 400 with a sensible status when the form processing fails
    """
    content_length = bt.request.get_header('Content-Length')
    if not content_length:
        raise bt.HTTPError(411, "Content-Length required")
    if int(content_length) > config.MAXSIZE:
        raise bt.HTTPError(413, f"Payload too large, we accept maximum {config.MAXSIZE}")

    files = bt.request.files
    forms = bt.request.forms

    token = None
    code = None
    lang = None
    ext = None
    maxusage = config.DEFAULT_MAXUSAGE
    lifetime = config.DEFAULT_LIFETIME
    parentid = ''

    try:
        # Form extraction
        if files:
            part = next(files.values())
            charset = cgi.parse_header(part.content_type)[1].get('charset', 'utf-8')
            code = part.file.read(config.MAXSIZE).decode(charset)
            ext = os.path.splitext(part.filename)[1][1:] or langtoext[config.DEFAULT_LANGUAGE]
        if forms:
            # WSGI forces latin-1 decoding, this is wrong, we recode it in utf-8
            code = forms.get('code', '').encode('latin-1').decode() or code
            lang = forms.get('lang') or config.DEFAULT_LANGUAGE
            maxusage = int(forms.get('maxusage') or maxusage)
            lifetime = Time(forms.get('lifetime') or lifetime)
            parentid = forms.get('parentid', '')
            token = forms.get('token')

        # Form validation
        if lang:
            ext = langtoext.get(lang)
            if ext is None:
                logger.warning('Unknown lang %r, using %r.', lang, config.DEFAULT_LANGUAGE)
                lang = config.DEFAULT_LANGUAGE
                ext = langtoext[config.DEFAULT_LANGUAGE]

        if ext:
            lang = exttolang.get(ext)
            if lang is None:
                logger.warning('Unknown file extension %r, using %r.', ext, langtoext[config.DEFAULT_LANGUAGE])
                lang = config.DEFAULT_LANGUAGE
                ext = langtoext[config.DEFAULT_LANGUAGE]
        if not code:
            raise ValueError("Code is missing")
        if maxusage < 0:
            raise ValueError("Maximum usage must be positive")
        if lifetime < 0:
            raise ValueError("Lifetime must be positive")
        if parentid:
            try:
                models.Snippet.get_by_id(parentid)
            except KeyError:
                raise ValueError("Parent does not exist")
        if token and len(token) > 22:
            raise ValueError("Token must not exceed 22 chars as of 16 random bytes base64 encoded")
    except ValueError as exc:
        raise bt.HTTPError(400, str(exc))

    snippet = models.Snippet.create(code, maxusage, lifetime, parentid, token)
    logger.info("New %s snippet of %s chars: %s", lang, len(code), snippet.id)
    bt.redirect(f'/{snippet.id}.{ext}')


@bt.route('/<snippetid>', method='GET')
@bt.route('/<snippetid>.<ext>', method='GET')
def get_html(snippetid, ext=None):
    """
    Get a snippet in a beautiful html page

    :param snippetid: (path) required snippet id
    :param ext: (path) optional language file extension, used to determine the highlight backend
    :param token: (query) optional the "admin" token

    :raises HTTPError: code 404 when the snippet is not found
    """
    if BOTUARE.match(bt.request.headers.get('User-Agent', '')):
        return bt.template('blank.html')

    try:
        snippet = models.Snippet.get_by_id(snippetid)
    except KeyError:
        raise bt.HTTPError(404, "Snippet not found")

    lang = langtoext.get(ext, config.DEFAULT_LANGUAGE)
    ext = langtoext[lang]  # always use the prefered extension for that lang
    codehl = highlight(snippet.code, lang)

    return bt.template(
        'highlight.html',
        languages=languages,
        codehl=codehl,
        lang=lang,
        ext=ext,
        snippetid=snippetid,
        parentid=snippet.parentid,
        token=bt.request.query.token,
    )

@bt.route('/<snippetid>', method='DELETE')
@bt.route('/<snippetid>.<ext>', method='DELETE')
def delete_snippet(snippetid, ext=None):
    """
    Delete a snippet

    :param Authorization: (header) required "Authorization: Token <ADMIN_TOKEN>" the "admin" token

    :raises HTTPError: code 400 when the Authorization (token) is missing
    :raises HTTPError: code 404 when the snippet is not found
    :raises HTTPError: code 401 when the token is incorrect
    """
    auth = bt.request.get_header('Authorization', '').split(None, 1)
    if len(auth) != 2 or auth[0] != 'Token':
        raise bt.HTTPError(400, "Token is missing")

    try:
        snippet = models.Snippet.get_by_id(snippetid)
    except KeyError:
        raise bt.HTTPError(404, "Snippet not found")

    if not (snippet.token and secrets.compare_digest(snippet.token, auth[1])):
        raise bt.HTTPError(401, "Unauthorized")

    snippet.delete()
    logger.info("Snippet %s deleted by user", snippetid)


@bt.route('/raw/<snippetid>', method='GET')
@bt.route('/raw/<snippetid>.<ext>', method='GET')
def get_raw(snippetid, ext=None):
    """
    Get a snippet in plain text without code hightlight

    :param snippetid: (path) required snippet id
    :param ext: (path) ignored parameter
    """
    if BOTUARE.match(bt.request.headers.get('User-Agent', '')):
        return bt.template('blank.html')

    try:
        snippet = models.Snippet.get_by_id(snippetid)
    except KeyError:
        raise bt.HTTPError(404, "Snippet not found")

    bt.response.headers['Content-Type'] = 'text/plain'
    return snippet.code

@bt.route('/report', method='POST')
def report():
    """
    Report a problematic snippet to the system administrator.

    :param snippetid: (form) the reported snippet
    :param name: (form) the name of the user reporting the problem

    :raises HTTPError: code 400 when any of the snippetid or the name is missing
    :raises HTTPError: code 404 when the reported snippet is not found
    """
    name = bt.request.forms.get("name", "").encode('latin-1').decode().strip()
    snippetid = bt.request.forms.get("snippetid")
    if not name:
        raise bt.HTTPError(400, "Missing name")
    if not snippetid:
        raise bt.HTTPError(400, "Missing snippetid")

    try:
        models.Snippet.get_by_id(snippetid)
    except KeyError:
        raise bt.HTTPError(404, "Snippet not found")
    logger.warning("The snippet %s got reported by %s", snippetid, name)

    return bt.HTTPResponse("The snippet have been reported.")
