# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Interface for the upload spooling queue.

Brief usage summary:

  from sigma.spool import spooling
  item = spooling.new()
  f = open(item.filename, "w")
  f.write(...)
  f.close()
  item.spool(metadata)  #  or item.discard()

"""

import os
import re
import glob
import time
import tempfile

from sigma.util import config
from sigma.util import xmltime
from sigma.util import timeskew

queue = None

def new(suffix=""):
    """Public API -- return a new spool queue item."""
    global queue
    if queue is None:
        queue = Queue()
    return queue.new(suffix)

class Queue(object):

    """Class representing the queue.

    Normally there's only one queue; this mostly exists for the
    convenience of the unit tests and so that it can be subclassed by
    qmgr.QueueManager.

    """

    def __init__(self, spooldir=None, options=None):
        """Constructor.

        The optional 'spooldir' argument is the spool directory (which
        must exist if given).  If not given, $AVHOME/data/upload is
        used, which is created if it doesn't exist.  This may fail
        with an IOError.

        The optional 'options' argument is the options object used to
        derive the default spool directory path.

        """
        if options is None:
            options = config.Options()
        if spooldir is None:
            spooldir = options.datapath("spool")
            if not os.path.isdir(spooldir):
                try:
                    os.makedirs(spooldir)
                except os.error:
                    if not os.path.isdir(spooldir):
                        raise
        else:
            if not os.path.isdir(spooldir):
                raise ValueError("%s is not an existing directory" % spooldir)
        self.spooldir = spooldir
        self.skew = timeskew.get_skew(options)

    def new(self, suffix=""):
        """Return a new spool queue item."""
        return QueueItem(self, suffix)

reserved_metadata_keys = ["qfile", "created", "queued", "status"]
reserved_suffixes = list(map(os.path.normcase, [".spool", ".open"]))

class QueueItem(object):
    """A spool queue item.

    When client code needs to upload data to the server, it creates a
    queue item, and then writes data to the file provided by the queue
    item.  The item is spooled by calling the spool() method; it can
    also be discarded by calling the discard() method instead.

    Argument (optional):
    - suffix   -- a string giving the filename suffix, defaulting to ''

    Public read-only attribute:
    - filename -- a string giving the filename where to write data

    """
    def __init__(self, queue, suffix):
        """Constructor.  See class docstring for signature."""
        self.state = "init"
        if os.path.normcase(suffix) in reserved_suffixes:
            raise ValueError("%s is a reserved suffix" % suffix)
        while True:
            fd, fn = tempfile.mkstemp(suffix, "spool", queue.spooldir)
            os.close(fd)
            if len(glob.glob(fn + "*")) > 1:
                os.remove(fn)
                # A rare collision indeed.  Loop around to try again.
            else:
                break
        self.filename = fn
        self.created = time.time() + queue.skew
        self.skew = queue.skew
        self.metadatafilename = self.filename + ".open"
        f = open(self.metadatafilename, "w", encoding="utf-8")
        try:
            f.write("qfile: %s\n" % os.path.basename(self.filename))
            f.write("created: %s\n" % xmltime.formattimestamp(self.created))
            f.write("status: open\n")
        finally:
            f.close()
        self.state = "open"

    def __del__(self):
        """Destructor -- throw away temporary files if not spooled yet."""
        if self.state == "open":
            self.discard()

    def discard(self):
        """Discard the queue item.  Delete all associated files."""
        assert self.state == "open"
        self.state = "dead"
        # XXX Shouldn't we log failures here?  At what level?
        # XXX And what about logging success???
        try:
            os.remove(self.metadatafilename)
        except os.error:
            pass
        try:
            os.remove(self.filename)
        except os.error:
            pass

    def spool(self, **metadata):
        """Commit the item to the spool queue.  Rewrite the metadata file."""
        # XXX Should we log something here?
        assert self.state == "open"
        assert hasattr(metadata, "keys") # must be a mapping
        for key, value in metadata.items():
            assert isinstance(key, str)
            assert "\0" not in key
            assert "\n" not in key
            assert re.match(r"\A[a-zA-Z_]\w*\Z", key)
            assert key not in reserved_metadata_keys
            assert isinstance(value, str)
            assert "\0" not in value
            assert "\n" not in value
        now = time.time() + self.skew
        f = open(self.metadatafilename, "w", encoding="utf-8")
        try:
            f.write("qfile: %s\n" % os.path.basename(self.filename))
            f.write("created: %s\n" % xmltime.formattimestamp(self.created))
            f.write("queued: %s\n" % xmltime.formattimestamp(now))
            f.write("status: queued\n")
            for key, value in metadata.items():
                f.write("%s: %s\n" % (key, value))
        finally:
            f.close()
        newmetadatafilename = self.filename + ".spool"
        os.rename(self.metadatafilename, newmetadatafilename)
        self.metadatafilename = newmetadatafilename
        self.state = "queued"
