import asyncio
import shutil
import stat
import time
from functools import partialmethod
from pathlib import Path

from tarka.utility.aio_sqlite import AbstractAioSQLiteDatabase


class InterTemporaryFileDatabase(AbstractAioSQLiteDatabase):
    """
    For writing files by internal processes like backup or custom inter-nano copy/download.
    """

    def _setup(self):
        self._con.execute(
            "CREATE TABLE IF NOT EXISTS TempFile0 ("
            " id INTEGER PRIMARY KEY,"
            " target_path TEXT,"  # final target path
            " account_id INTEGER,"  # executor is always owner, but initiator may be different
            " tmp_name TEXT,"  # name of the tmp file in the directory of the target_path
            " meta BLOB,"  # arbitrary data to conflict/session manage the temporary file
            " last_used REAL"  # UTC timestamp when this temporary entry was used, cleanup is started by this
            ")"
        )
        self._con.execute("CREATE UNIQUE INDEX IF NOT EXISTS TempFile0__target_path ON TempFile0(target_path)")

    def _get_tmp_file(self, target_path, update_last_used=True):
        """
        Get data for use of the temporary file. Update the last_used timestamp unless undesired.
        """
        rows = self._con.execute(
            "SELECT id, account_id, tmp_name, meta FROM TempFile0 WHERE target_path = ?", (target_path,)
        ).fetchall()
        if rows:
            if update_last_used:
                self._con.execute(
                    "UPDATE TempFile0 SET last_used = ? WHERE target_path = ?", (time.time(), target_path)
                )
            return rows[0]

    def _set_tmp_file_meta(self, tmp_id, meta):
        self._con.execute("UPDATE TempFile0 SET meta = ? WHERE id = ?", (meta, tmp_id))

    def _create_tmp_file(self, account_id, target_path, tmp_name, meta):
        """
        Create or reset a tmp file row and clear the part CMACs.
        """
        c = self._con.cursor()
        c.execute(
            "REPLACE INTO TempFile0(target_path, account_id, tmp_name, meta, last_used) VALUES (?, ?, ?, ?, ?)",
            (target_path, account_id, tmp_name, meta, time.time()),
        )
        id_ = c.lastrowid
        return id_

    def _drop_tmp_file(self, id_):
        self._con.execute("DELETE FROM TempFile0 WHERE id = ?", (id_,))

    def _get_old_tmp_files(self, age, limit):
        # Age less then *or equals* helps with sanity when the system clock's resolution is low. (Windows)
        return self._con.execute(
            "SELECT id, target_path, account_id, tmp_name, meta, last_used FROM TempFile0 "
            "WHERE last_used <= ? ORDER BY last_used ASC LIMIT ?",
            (time.time() - age, limit),
        ).fetchall()

    get_tmp_file = partialmethod(AbstractAioSQLiteDatabase._run_job, _get_tmp_file)
    create_tmp_file = partialmethod(
        AbstractAioSQLiteDatabase._run_job,
        _create_tmp_file,
        post_process_fn=AbstractAioSQLiteDatabase._checkpoint_retrying,
    )
    set_tmp_file_meta = partialmethod(AbstractAioSQLiteDatabase._run_job, _set_tmp_file_meta)
    drop_tmp_file = partialmethod(AbstractAioSQLiteDatabase._run_job, _drop_tmp_file)
    get_old_tmp_files = partialmethod(AbstractAioSQLiteDatabase._run_job, _get_old_tmp_files, begin_immediate=False)

    TEMP_FILE_CLEANUP_AFTER = 60 * 20  # 20 minutes
    TEMP_FILE_ABANDON_AFTER = 60 * 60 * 14  # 14 hours

    async def cleanup_old_temp_files(self):
        await asyncio.sleep(30.0)
        while True:
            old_rows = await self.get_old_tmp_files(self.TEMP_FILE_CLEANUP_AFTER, 100)  # TODO: limit
            for tmp_id, _, target_path, tmp_name, _, last_used in old_rows:
                try:
                    path = Path(target_path).parent / tmp_name
                    s = path.lstat()  # raises if target does not exist
                    if stat.S_ISDIR(s.st_mode):
                        shutil.rmtree(path)
                    else:
                        path.unlink()
                except Exception:
                    # Several issues could exist to prevent removing the temporary file, and we can't discern if
                    # they are temporary or permanent generally. For example, after a long shutdown a drive is
                    # missing, where a file shall be cleaned up. In this case abandoning it may be okay if the
                    # drive/path never comes online again, but we can't know that.
                    if last_used + self.TEMP_FILE_ABANDON_AFTER > time.time():
                        # Do not abandon cleaning up this temp file yet.
                        continue
                await self.drop_tmp_file(tmp_id)
            await asyncio.sleep(self.TEMP_FILE_CLEANUP_AFTER >> 2)
