"""
This blueprint deals with everything under /api/boop/.

Used environment variables:
    BOOPER_TARGETS: A list of comma (`,`) separated names for valid booper targets.
                    These names are stripped to only contain alphanumeric characters.
    BOOPER_PRECISION: An integer (default 60) to define how precise the booper is.
                      Shorter values are more precise, but take up more space in the database.
"""

from threading import Thread
import logging
import string
import time
import os

import flask

import db

_log = logging.getLogger('blueprints.booper')

def stripName(name: str) -> str:
    """
    Strips a name to only include ascii characters & digits.
    """

    return ''.join([c for c in name if c in string.ascii_letters+string.digits])

class Blueprint(flask.Blueprint):
    """
    Holds all relavent data for this blueprint to function as well as interacting
    directly with the database to store and load data live.
    """

    def __init__(self, *args, **kwargs):
        self._currentBoops = {} # Holds all boops in the current precision
        self._totalBoops = {} # Holds full boop totals
        self._boops24h = {} # Holds all boops that happened in the past 24h
                            # The format of this is as follows:
                            # {'name': [{'ts': ts, 'count': count}, {...}], ...}
        self.precision = int(os.getenv('BOOPER_PRECISION', '60'))
        self._currentTS = 0
        self._runWorker = True
        self._workerThread = None

        # Load and filter names from `BOOPER_TARGETS` environment variable.
        for name in os.getenv('BOOPER_TARGETS', '').split(','):
            if name == '':
                continue # Ignore empty names
            name = stripName(name)
            self._currentBoops.update({name: 0})
            self._totalBoops.update({name: 0})

        # Throw a warning if no names are detected
        if not self._currentBoops:
            _log.warning('No names are configured. This blueprint will not function.')
            _log.warning('Please configure BOOPER_TARGETS in your .env file.')

        super().__init__(*args, **kwargs)

    def _worker(self):
        """
        This function gets ran as a thread and it handles all time splitting capabilities.
        """

        while self._runWorker:
            # Check if we've past the precision threshold
            if time.time() - self._currentTS >= self.precision:
                # Save everything to the database
                self._save()
                # Reset variables
                for name in self._currentBoops:
                    self._currentBoops[name] = 0
                self._currentTS = round(time.time())
                # Purge old data from 24h
                self._boops24h = {name: [data for data in self._boops24h[name]
                                         if time.time() - data['ts'] < 86400]
                                  for name in self._boops24h}
            time.sleep(1)

    def _save(self):
        """
        Saves everything to the database.
        """

        for name, count in self._currentBoops.items():
            # Check if this start time exists already
            if db.GLOBAL.execute('SELECT * FROM booper WHERE name=? AND startTime=?',
                                (name, self._currentTS)).fetchone() is None:
                # If not, create a new row
                db.GLOBAL.execute('INSERT INTO booper (name, startTime, count) VALUES (?, ?, ?)',
                                 (name, self._currentTS, count))
            else:
                # If it does, edit the existing row
                db.GLOBAL.execute('UPDATE booper SET count=? WHERE name=? AND startTime=?',
                                 (count, name, self._currentTS))

            # Save the new totals
            db.GLOBAL.execute('UPDATE booper SET count=? WHERE name=? AND total=1',
                             (self._totalBoops[name], name))

        # Commit everything
        db.GLOBAL.commit()

    def boop(self, name: str):
        """
        Boops `name`.
        """

        self._currentBoops[name] += 1
        self._totalBoops[name] += 1
        self._boops24h[name][-1]['count'] += 1

    # Override the `register` function from `flask.Blueprint`
    # so we can initialise the database at an appropriate time.
    def register(self, *args, **kwargs):
        super().register(*args, **kwargs)

        # Initialise the database
        db.GLOBAL.createTable('booper')
        db.GLOBAL.createColumn('booper', 'name STRING')
        db.GLOBAL.createColumn('booper', 'total BOOLEAN DEFAULT false')
        db.GLOBAL.createColumn('booper', 'startTime INTEGER')
        db.GLOBAL.createColumn('booper', 'count INTEGER DEFAULT 0')

        for name in self._currentBoops:
            # Get the latest data for this name
            latestData = db.GLOBAL.execute('SELECT * FROM booper WHERE name=?'+
                                           'ORDER BY id DESC LIMIT 1', (name,)).fetchone()
            # If there is data,
            if latestData is not None:
                # Set the current time to the latest time known
                startTime = latestData['startTime']
                if startTime is not None and startTime > self._currentTS:
                    self._currentTS = startTime
                # Update the current boop counter
                self._currentBoops[name] = latestData['count']
            # Otherwise,
            else:
                # Set the current timestamp to now
                self._currentTS = round(time.time())

            # Check for total row
            if db.GLOBAL.execute('SELECT * FROM booper WHERE name=? AND total=1',
                                (name,)).fetchone() is None:
                # If none is found, create one
                _log.debug('Creating total counter for "%s"...', name)
                db.GLOBAL.addRow('booper',
                                {'name': name, 'startTime': round(time.time()), 'total': True})
                self._totalBoops[name] = 0
            else:
                # If one is found, load the data and set the totals
                totalData = db.GLOBAL.execute('SELECT * FROM booper WHERE name=? AND total=1 ',
                                            (name,)).fetchone()
                self._totalBoops[name] = totalData['count']

        # Initialise 24h counts with registered names
        self._boops24h = {name: [] for name in self.registeredNames}

        # Load all boops from the past 24h
        boops24h = db.GLOBAL.execute('SELECT * FROM booper WHERE total=0 AND startTime>=?',
                                    (time.time()-86400,)).fetchall()
        for row in boops24h:
            name = row['name']
            count = row['count']
            ts = row['startTime']

            self._boops24h[name].append({'ts': ts, 'count': count})

        # Fix empty recents
        for name in self._boops24h:
            if self._boops24h[name] == []:
                self._boops24h[name].append({'ts': round(time.time()), 'count': 0})

        self._workerThread = Thread(target=self._worker)
        self._workerThread.start()

    def getTotalBoops(self, name: str) -> int:
        """
        Returns the total number of boops the user has recieved.
        """

        if name not in self.registeredNames:
            raise ValueError(f'Name "{name}" is not registered.')

        return self._totalBoops[name]

    def get24hBoops(self, name) -> int:
        """
        Calculates how many boops `name` has received in the past 24h.
        """

        if name not in self.registeredNames:
            raise ValueError(f'Name "{name}" is not registered.')

        return sum(data['count'] for data in self._boops24h[name])

    def onClose(self):
        """
        Safely saves and closes the booper.
        Automatically ran by the backend.
        """

        _log.info('Stopping worker...')
        self._runWorker = False
        self._workerThread.join()
        _log.info('Saving to database...')
        self._save()
        _log.info('Booper closed successfully.')

    @property
    def registeredNames(self) -> list[str]:
        """
        Returns a list of all registered names.
        """

        return list(self._currentBoops.keys())

booperBP = Blueprint('booperAPI', __name__)

@booperBP.route('/api/boop/<name>', methods=['GET', 'POST'], strict_slashes=False)
def boops(name: str):
    """
    Handles both GETting and POSTing boops through the boop API endpoint.
    """

    name = stripName(name)

    if name not in booperBP.registeredNames:
        return {'success': False, 'message': f'Name "{name}" is not configured as a target.'}, 404

    if flask.request.method == 'POST':
        booperBP.boop(name)

    return {'success': True,
            'total': booperBP.getTotalBoops(name),
            '24h': booperBP.get24hBoops(name)}
