#!/usr/bin/env python3

import json, logging, os, pwd, re, sys

# add the development directories or the current prefix to PYTHONPATH
sys.path.insert(0, "${PYTHON_SITEPACKAGES_PATH}")
sys.path.insert(0, "${PYTHON_SITELIB_PATH}")

#binpath = os.path.abspath(os.path.dirname(sys.argv[0]))
#if binpath.endswith("bin"):
#    from distutils import sysconfig
#    path = [sysconfig.get_python_lib(0,0,prefix=os.path.abspath(binpath+"/.."))]
#else:
#    path = [os.path.abspath("/".join((binpath, "../../libs/python"))), binpath]
#for p in path:
#    if path not in sys.path: sys.path.insert(0, p)

from bongo.cmdparse import CommandParser, Command
from bongo.store.QueueClient import QueueClient, RoutingFlags

parser = CommandParser()
parser.add_option("", "--host", type="string", default="localhost",
                  help="hostname [default %default]")
parser.add_option("", "--port", type="int", default=8670,
                  help="port [default %default]")
parser.add_option("", "--debug", action="store_true",
                  help="enable debugging output")

class FlushCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "flush", aliases=["f"],
                         summary="Flush the mail queue",
                         usage="%prog %cmd")

    def Run(self, options, args):
        queue = QueueClient(host=options.host, port=options.port)
        queue.Flush()

class SpaceCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "space",
                         summary="Show usable Queue spool bytes after reserve",
                         usage="%prog %cmd [details]")

    def Run(self, options, args):
        if len(args) > 1 or (args and args[0] != "details"):
            self.error("space accepts only the optional 'details' argument")
        details = bool(args)
        config_path = os.environ.get(
            "BONGO_QUEUE_CONFIG", "/etc/bongo/config.d/queue")
        spool_path = os.environ.get(
            "BONGO_QUEUE_SPOOL", "/var/lib/bongo/spool")

        if details:
            with open(config_path, "r", encoding="utf-8") as config_file:
                config = json.load(config_file)
            reserve = config.get("minimumfreespace")
            if (isinstance(reserve, bool) or not isinstance(reserve, int) or
                    reserve < 0):
                self.error("minimumfreespace must be a non-negative integer")
            stat_before = os.statvfs(spool_path)
            free_before = stat_before.f_bavail * stat_before.f_frsize

        queue = QueueClient(host=options.host, port=options.port)
        try:
            reported = queue.Diskspace()
        finally:
            queue.Quit()

        if details:
            stat_after = os.statvfs(spool_path)
            free_after = stat_after.f_bavail * stat_after.f_frsize
            block = max(stat_before.f_frsize, stat_after.f_frsize)
            print(f"{free_before} {free_after} {reserve} {reported} {block}")
        else:
            print(reported)

class ShowCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "show", aliases=["s"],
                         summary="Show a queue entry envelope",
                         usage="%prog %cmd <queue-id>")

    def Run(self, options, args):
        if len(args) != 1:
            self.error("exactly one queue ID is required")
        queue = QueueClient(host=options.host, port=options.port)
        data = queue.RetrieveInfo(args[0])
        sys.stdout.buffer.write(data)

class CollectedCountCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "collected-count",
            summary="Count pending externally collected messages",
            usage="%prog %cmd [<user>]")

    def Run(self, options, args):
        if len(args) > 1:
            self.error("collected-count accepts at most one user")
        queue = QueueClient(host=options.host, port=options.port)
        try:
            total, user_total = queue.CountCollected(
                args[0] if args else None)
        finally:
            queue.Quit()
        if args:
            print(f"{total} {user_total}")
        else:
            print(total)

class ListCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "list", aliases=["l"],
                         summary="List committed queue entries",
                         usage="%prog %cmd")

    def Run(self, options, args):
        if args:
            self.error("list does not accept arguments")
        queue = QueueClient(host=options.host, port=options.port)
        try:
            for queue_id, size in queue.List():
                print(f"{queue_id} {size}")
        finally:
            queue.Quit()

class MessageCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "message", aliases=["m"],
                         summary="Write a queued message to standard output",
                         usage="%prog %cmd <queue-id>")

    def Run(self, options, args):
        if len(args) != 1:
            self.error("exactly one queue ID is required")
        queue = QueueClient(host=options.host, port=options.port)
        data = queue.RetrieveMessage(args[0])
        sys.stdout.buffer.write(data)

class HoldLocalCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "hold-local",
            summary="Create an isolated local-delivery queue fixture",
            usage="%prog %cmd <message-file> [<target>]")

    def Run(self, options, args):
        if len(args) < 1 or len(args) > 2:
            self.error("a message file and optional queue target are required")
        target = int(args[1]) if len(args) == 2 else 999
        if target < 0 or target > 999:
            self.error("queue target must be between 0 and 999")
        with open(args[0], "rb") as message_file:
            message = message_file.read()

        queue = QueueClient(host=options.host, port=options.port)
        try:
            queue.Create(target)
            queue.StoreMessage(message)
            response = queue.Run()
            print(response.message.split(" ", 1)[0])
        except Exception:
            try:
                queue.Abort()
            except Exception:
                pass
            raise
        finally:
            queue.Quit()

class EnqueueRemoteCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "enqueue-remote",
            summary="Create a complete remote-delivery queue fixture",
            usage=("%prog %cmd <message-file> <sender> <recipient> "
                   "[<target>]"))

    def Run(self, options, args):
        if len(args) < 3 or len(args) > 4:
            self.error(
                "message file, sender, recipient, and optional target are required")
        target = int(args[3]) if len(args) == 4 else 7
        if target < 0 or target > 999:
            self.error("queue target must be between 0 and 999")
        with open(args[0], "rb") as message_file:
            message = message_file.read()

        queue = QueueClient(host=options.host, port=options.port)
        try:
            queue.Create(target)
            queue.StoreFrom(args[1])
            queue.StoreTo(
                args[2], args[2],
                RoutingFlags.Failure | RoutingFlags.Header | RoutingFlags.Body)
            queue.StoreMessage(message)
            response = queue.Run()
            print(response.message.split(" ", 1)[0])
        except Exception:
            try:
                queue.Abort()
            except Exception:
                pass
            raise
        finally:
            queue.Quit()

class EnqueueCollectedCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    COLLECTED_FLAGS = (1 << 2) | (1 << 5) | (1 << 7) | (1 << 15)
    NO_FORWARD = 1 << 5

    def __init__(self):
        Command.__init__(
            self, "enqueue-collected",
            summary="Create a Collector-owned local-delivery fixture",
            usage=("%prog %cmd <message-file> <recipient> "
                   "[<mailbox>]"))

    def Run(self, options, args):
        if len(args) < 2 or len(args) > 3:
            self.error("message file, recipient, and optional mailbox are required")
        recipient = args[1]
        mailbox = args[2] if len(args) == 3 else "INBOX"
        with open(args[0], "rb") as message_file:
            message = message_file.read()

        queue = QueueClient(host=options.host, port=options.port)
        try:
            queue.Create()
            queue.StoreFrom("-", "-")
            queue.StoreFlags(self.COLLECTED_FLAGS)
            queue.StoreMailbox(
                recipient, recipient, self.NO_FORWARD, mailbox)
            queue.StoreMessage(message)
            response = queue.Run()
            print(response.message.split(" ", 1)[0])
        except Exception:
            try:
                queue.Abort()
            except Exception:
                pass
            raise
        finally:
            queue.Quit()

class DeliverLocalCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "deliver-local",
            summary="Deliver a queued message to a local mailbox",
            usage=("%prog %cmd <queue-id> <sender> <recipient> "
                   "[<mailbox>]"))

    def Run(self, options, args):
        if len(args) < 3 or len(args) > 4:
            self.error("queue ID, sender, recipient, and optional mailbox are required")
        queue_id, sender, recipient = args[:3]
        mailbox = args[3] if len(args) == 4 else "INBOX"
        queue = QueueClient(host=options.host, port=options.port)
        try:
            queue.DeliverToMailbox(
                queue_id, sender, sender, recipient, mailbox)
        finally:
            queue.Quit()

class DeleteCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "delete", aliases=["d"],
                         summary="Delete a queue entry",
                         usage="%prog %cmd <queue-id>")

    def Run(self, options, args):
        if len(args) != 1:
            self.error("exactly one queue ID is required")
        queue = QueueClient(host=options.host, port=options.port)
        try:
            queue.Delete(args[0])
        finally:
            queue.Quit()

class HoldCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(self, "hold",
                         summary="Move a committed entry to the hold queue",
                         usage="%prog %cmd <queue-id>")

    def Run(self, options, args):
        if len(args) != 1:
            self.error("exactly one queue ID is required")
        queue = QueueClient(host=options.host, port=options.port)
        try:
            response = queue.Move(args[0], 999)
            print(response.message.split(" ", 1)[0])
        finally:
            queue.Quit()

class ReleaseCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "release",
            summary="Release a held entry and process it immediately",
            usage="%prog %cmd <queue-id> [<target>]")

    def Run(self, options, args):
        if len(args) < 1 or len(args) > 2:
            self.error("a queue ID and optional target are required")
        if not args[0].startswith("999-"):
            self.error("release requires an entry from queue 999")
        target = int(args[1]) if len(args) == 2 else 0
        queue = QueueClient(host=options.host, port=options.port)
        try:
            response = queue.Move(args[0], target)
            released_id = response.message.split(" ", 1)[0]
            queue.Run(released_id)
            print(released_id)
        finally:
            queue.Quit()

class RetryCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "retry",
            summary="Process a committed queue entry immediately",
            usage="%prog %cmd <queue-id>")

    def Run(self, options, args):
        if len(args) != 1:
            self.error("exactly one queue ID is required")
        if not re.fullmatch(r"[0-9]{3}-[0-9a-f]+", args[0]):
            self.error("invalid queue ID")
        queue = QueueClient(host=options.host, port=options.port)
        try:
            queue.Run(args[0])
        finally:
            queue.Quit()

class ExpireCommand(Command):
    log = logging.getLogger("Bongo.QueueTool")

    def __init__(self):
        Command.__init__(
            self, "expire",
            summary="Expire an entry and generate its configured DSN",
            usage="%prog %cmd <queue-id>")

    def Run(self, options, args):
        if len(args) != 1:
            self.error("exactly one queue ID is required")
        queue = QueueClient(host=options.host, port=options.port)
        try:
            response = queue.Expire(args[0])
            expired_id = response.message.split(" ", 1)[0]
            queue.Run(expired_id)
            print(expired_id)
        finally:
            queue.Quit()

parser.add_command(FlushCommand(), "Queue Management")
parser.add_command(SpaceCommand(), "Queue Management")
parser.add_command(ListCommand(), "Queue Management")
parser.add_command(CollectedCountCommand(), "Queue Management")
parser.add_command(ShowCommand(), "Queue Management")
parser.add_command(MessageCommand(), "Queue Management")
parser.add_command(HoldLocalCommand(), "Queue Recovery and Diagnostics")
parser.add_command(EnqueueRemoteCommand(), "Queue Recovery and Diagnostics")
parser.add_command(EnqueueCollectedCommand(), "Queue Recovery and Diagnostics")
parser.add_command(DeliverLocalCommand(), "Queue Recovery and Diagnostics")
parser.add_command(DeleteCommand(), "Queue Recovery and Diagnostics")
parser.add_command(HoldCommand(), "Queue Recovery and Diagnostics")
parser.add_command(ReleaseCommand(), "Queue Recovery and Diagnostics")
parser.add_command(RetryCommand(), "Queue Recovery and Diagnostics")
parser.add_command(ExpireCommand(), "Queue Recovery and Diagnostics")

if __name__ == '__main__':
    (command, options, args) = parser.parse_args()

    logging.basicConfig()

    if options.debug:
        logging.root.setLevel(logging.DEBUG)
    else:
        logging.root.setLevel(logging.ERROR)

    if command is None:
        parser.print_help()
        parser.exit()

    command.Run(options, args)
