#!/usr/bin/python3
# Filename:		grml-paste
# Purpose:		REST API Pastebin interface client
# Author:		Michael Gebetsroither <gebi@grml.org>, Alexander Wirt <formorer@formorer.de>
# License:		This file is licensed under the GPL v2.
################################################################################

import getpass
import inspect
import optparse
import sys
import json
import urllib.request
import urllib.error

# program defaults
DEFAULT_SERVER = "https://paste.debian.net/api/v1"


class ActionFailedException(Exception):
    """Thrown if server returned an error"""

    def __init__(self, errormsg, ret):
        Exception.__init__(self, errormsg, ret)

    def what(self):
        """Get errormessage"""
        return self.args[0]

    def dwhat(self):
        """Get more verbose errormessage"""
        return self.args[1]


def _request(opts, endpoint, method="GET", data=None, headers=None):
    """Make HTTP request to the API"""
    if headers is None:
        headers = {}

    url = opts.server.rstrip("/") + endpoint

    if data is not None:
        body = json.dumps(data).encode("utf-8")
        headers["Content-Type"] = "application/json"
    else:
        body = None

    req = urllib.request.Request(
        url, data=body, headers=headers, method=method)

    try:
        with urllib.request.urlopen(req) as response:
            resp_data = response.read()
            if not resp_data:
                return {}
            return json.loads(resp_data)
    except urllib.error.HTTPError as e:
        try:
            err_data = json.loads(e.read())
            # Extract messages from DefaultResponse errors array or simple error field
            if "errors" in err_data:
                msg = ", ".join([err.get("message", "Unknown error")
                                for err in err_data.get("errors", [])])
            elif "error" in err_data:
                msg = err_data["error"]
            else:
                msg = str(e)
        except:
            msg = str(e)
        raise ActionFailedException(msg, {"rc": 1, "statusmessage": msg})


def action_add_paste(opts, code):
    """Add paste to the server: <1.line> <2.line> ...

    default     Read paste from stdin.
    [text]      Every argument on the commandline will be interpreted as
                a seperate line of paste.
    """
    if len(code) == 0:
        code = [line.rstrip() for line in sys.stdin.readlines()]
    code = "\n".join(code)

    payload = {
        "code": code,
        "poster": opts.name,
        "expire": opts.expire * 3600,
        "lang": opts.lang
    }

    # Check for token header
    headers = {}
    if opts.token:
        headers["X-Paste-Token"] = opts.token

    result = _request(opts, "/paste", method="POST",
                      data=payload, headers=headers)

    # Construct status message
    msg = "Paste created: %s" % result.get("url")
    if "digest" in result:
        msg += "\nDigest: %s" % result.get("digest")

    return msg, result


def action_del_paste(opts, args):
    """Delete paste from server: <id>

    <id>        ID of paste you want to remove.
    """
    if not args:
        raise ActionFailedException("Missing paste ID", {})

    paste_id = args.pop(0)

    headers = {}
    if opts.token:
        headers["X-Paste-Token"] = opts.token

    result = _request(opts, "/paste/%s" %
                      paste_id, method="DELETE", headers=headers)
    return result.get("message", "Paste deleted"), result


def action_get_paste(opts, args):
    """Get paste from server: <id>

    <id>        ID of paste you want to receive.
    """
    if not args:
        raise ActionFailedException("Missing paste ID", {})

    paste_id = args.pop(0)
    result = _request(opts, "/paste/%s" % paste_id)
    return result["code"], result


def action_get_langs(opts, args):
    """Get supported language highlighting types from server"""
    result = _request(opts, "/langs")
    # API returns list of objects {id, code, desc}
    langs = []
    for l in result:
        code = l.get("code")
        desc = l.get("desc")
        if code == desc:
            langs.append(code)
        else:
            langs.append("%s: %s" % (code, desc))
    return "\n".join(langs), result


def action_help(opts, args):
    """Print more verbose help about specific action: <action>

    <action>	Topic on which you need more verbose help.
    """
    if len(args) < 1:
        alias = "help"
    else:
        alias = args.pop(0)

    if alias in actions:
        function_name = actions[alias]
        print(inspect.getdoc(globals()[function_name]))
        print(
            "\naliases: "
            + " ".join([i for i in actions_r[function_name] if i != alias])
        )
    else:
        print("Error: No such command - %s" % (alias))
        OPT_PARSER.print_usage()
    sys.exit(0)


# action_add_paste -> [add, a]
actions_r = {}

# add -> action_add_paste
# a   -> action_add_paste
actions = {}

# option parser
OPT_PARSER = None


##
# MAIN
##
if __name__ == "__main__":
    action_specs = [
        "action_add_paste add a",
        "action_del_paste del d rm",
        "action_get_paste get g",
        "action_get_langs getlangs gl langs l",
        "action_help     help",
    ]
    for action_spec in action_specs:
        aliases = action_spec.split()
        cmd = aliases.pop(0)
        actions_r[cmd] = aliases
    for (action_name, v) in actions_r.items():
        for i in v:
            actions[i] = action_name

    usage = (
        "usage: %prog [options] ACTION <args>\n\n"
        + "actions:\n"
        + "\n".join(
            [
                "%12s\t%s"
                % (v[0], inspect.getdoc(globals()[action_name]).splitlines()[0])
                for (action_name, v) in actions_r.items()
            ]
        )
    )
    running_user = getpass.getuser()
    parser = optparse.OptionParser(usage=usage)
    parser.add_option("-n", "--name", default=running_user,
                      help="Name of poster")
    parser.add_option(
        "-e",
        "--expire",
        type=int,
        default=72,
        metavar="HOURS",
        help="Time at which paste should expire",
    )
    parser.add_option(
        "-l", "--lang", default="text", help="Language for syntax highlighting (default: text)"
    )
    parser.add_option(
        "-t",
        "--token",
        dest="token",
        default=None,
        help="X-Paste-Token for authentication",
    )
    parser.add_option("-s", "--server",
                      default=DEFAULT_SERVER, help="Paste server")
    parser.add_option("-v", "--verbose", action="count",
                      default=0, help="More output")
    (opts, args) = parser.parse_args()
    OPT_PARSER = parser

    if len(args) == 0:
        parser.error("Please provide me with an action")
    elif args[0] in actions:
        cmd = args.pop(0)
        action = actions[cmd]
        try:
            (msg, ret) = globals()[action](opts, args)
            if opts.verbose == 0:
                print(msg)
            else:
                print(ret)
        except ActionFailedException as except_inst:
            sys.stderr.write("Server Error: %s\n" % except_inst.what())
            if opts.verbose > 0:
                print(except_inst.dwhat())
            sys.exit(1)
    else:
        parser.error("Unknown action: %s" % args[0])
