import asyncio
import time
from functools import partialmethod

from tarka.utility.aio_sqlite import AbstractAioSQLiteDatabase


class AdhocKeyDatabase(AbstractAioSQLiteDatabase):
    """
    To provide independent security for ad-hoc access tokens, their HMAC signature will be generated by a room
    specific secret that can be rotated at will.
    """

    def _initialise(self):
        """
        This database holds secret keys of rooms. We configure the db to use WAL for maximum performance and
        crash-resistant integrity, but not durability.
        """
        AbstractAioSQLiteDatabase._initialise(self)

    def _setup(self):
        self._con.execute(
            "CREATE TABLE IF NOT EXISTS AdhocTokens0 ("
            " id INTEGER PRIMARY KEY,"
            " expires INTEGER,"
            " room_id INTEGER NOT NULL,"
            " secret BLOB NOT NULL,"
            " data BLOB NOT NULL,"
            " salt BLOB NOT NULL"
            ")"
        )
        self._con.execute("CREATE INDEX IF NOT EXISTS AdhocTokens0__expires ON AdhocTokens0(expires)")
        self._con.execute("CREATE INDEX IF NOT EXISTS AdhocTokens0__room_id ON AdhocTokens0(room_id)")

    def _get(self, id_):
        rows = self._con.execute(
            "SELECT expires, room_id, secret, data, salt FROM AdhocTokens0 WHERE id = ?", (id_,)
        ).fetchall()
        if rows and rows[0]:
            row = rows[0]
            return row[0], row[1], row[2], row[3], row[4]

    def _create_token(self, expires, room_id, secret, data, salt):
        c = self._con.cursor()
        c.execute(
            "INSERT INTO AdhocTokens0(expires, room_id, secret, data, salt) VALUES (?, ?, ?, ?, ?)",
            (expires, room_id, secret, data, salt),
        )
        id_ = c.lastrowid
        return id_

    def _drop_by_room(self, room_id):
        self._con.execute("DELETE FROM AdhocTokens0 WHERE room_id = ?", (room_id,))

    def _count_by_room(self, room_id):
        row = self._con.execute(
            "SELECT COUNT(id) FROM AdhocTokens0 WHERE room_id = ? AND (expires IS NULL OR expires > ?)",
            (room_id, time.time()),
        ).fetchone()
        return row[0] if row else 0

    def _drop_expired(self):
        self._con.execute("DELETE FROM AdhocTokens0 WHERE expires IS NOT NULL AND expires < ?", (time.time() - 60,))

    def _drop_by_inverse_rooms(self, room_ids):
        if room_ids:
            self._con.execute(
                f"DELETE FROM AdhocTokens0 WHERE room_id NOT IN ({','.join('?'*len(room_ids))})", room_ids
            )
        else:
            self._con.execute("DELETE FROM AdhocTokens0 ")

    get = partialmethod(AbstractAioSQLiteDatabase._run_job, _get, begin_immediate=False)
    create_token = partialmethod(AbstractAioSQLiteDatabase._run_job, _create_token)
    drop_by_room = partialmethod(AbstractAioSQLiteDatabase._run_job, _drop_by_room)
    count_by_room = partialmethod(AbstractAioSQLiteDatabase._run_job, _count_by_room, begin_immediate=False)
    drop_expired = partialmethod(AbstractAioSQLiteDatabase._run_job, _drop_expired)
    drop_by_inverse_rooms = partialmethod(AbstractAioSQLiteDatabase._run_job, _drop_by_inverse_rooms)

    EXPIRED_CLEANUP_AFTER = 60 * 60 * 4  # 4 hours

    async def cleanup_not_served_rooms_expired_tokens(self, serves):
        await self.drop_by_inverse_rooms([tmp["id"] for tmp in serves])
        del serves  # free
        while True:
            await self.drop_expired()
            await asyncio.sleep(self.EXPIRED_CLEANUP_AFTER)
