#  (c) cavaliba.com - data - filestore.py
# v3.19

import hashlib
import os
import time
import uuid

from django.conf import settings
from django.utils import timezone

from app_data.models import DataFile
from app_home.log import ERROR, INFO, log


#  single
def uuid_to_queryset(fileid):
    if not fileid:
        return
    return DataFile.objects.filter(fileid=fileid).first()


# list
def uuids_to_queryset(vlist):
    if not vlist:
        return
    return DataFile.objects.filter(fileid__in=vlist)


def uuids_to_filenames(vlist):
    return [i.filename for i in uuids_to_queryset(vlist)]


def uuid_to_filepath(fileid):
    rootdir = settings.CAVALIBA_FILESTORE
    return os.path.join(str(rootdir), fileid)


def tmp_local_filepath():
    """tmp file don't exist un DataFile ; cleanup as orphans"""
    rootdir = settings.CAVALIBA_FILESTORE
    tmpid = "tmp-" + str(uuid.uuid4())
    return os.path.join(str(rootdir), tmpid)


def filestore_cleanup(hours=24):
    """Housekeeping: delete DataFile entries past their not_after date (and
    their backing file), then scan the filestore folder for files older than
    `hours` with no matching DataFile record (orphans) and remove them too.
    tmp-* files (from tmp_local_filepath) are expected orphans by design and
    logged at INFO; any other orphan has no legitimate reason to exist and is
    logged at ERROR. .gitignore is skipped outright (not a DataFile, but
    needed to keep the otherwise-empty folder tracked in git). Any
    os.remove() failure (permissions, file gone, ...) is
    counted and logged instead of being silently swallowed, so files left
    behind after a run show up as an ERROR rather than looking like the
    cleanup never ran. Returns (cleaned_count, orphaned_count)."""
    cleaned = 0
    failed = 0
    last_error = ""

    expired = DataFile.objects.filter(not_after__isnull=False, not_after__lt=timezone.now())
    for df in expired:
        if df.filepath:
            try:
                os.remove(df.filepath)
            except OSError as exc:
                failed += 1
                last_error = str(exc)
        df.delete()
        cleaned += 1

    log(INFO, aaa=None, app="housekeeping", view="filestore",
        action="cleanup", status="OK", data=f"{cleaned} expired datafiles removed")  # fmt: skip

    rootdir = str(settings.CAVALIBA_FILESTORE)
    cutoff = time.time() - hours * 3600
    known_fileids = set(DataFile.objects.values_list("fileid", flat=True))

    try:
        entries = os.listdir(rootdir)
    except OSError as exc:
        entries = []
        log(ERROR, aaa=None, app="housekeeping", view="filestore",
            action="orphan", status="FAILED", data=f"cannot list {rootdir}: {exc}")  # fmt: skip

    orphaned = 0
    unexpected = 0
    for name in entries:
        if name == ".gitignore":
            continue
        if name in known_fileids:
            continue
        path = os.path.join(rootdir, name)
        try:
            if os.path.isfile(path) and os.path.getmtime(path) < cutoff:
                os.remove(path)
                orphaned += 1
                if not name.startswith("tmp-"):
                    unexpected += 1
        except OSError as exc:
            failed += 1
            last_error = str(exc)

    if orphaned:
        log(INFO, aaa=None, app="housekeeping", view="filestore",
            action="orphan", status="OK", data=f"{orphaned} orphaned tmp files removed")  # fmt: skip

    if unexpected:
        log(ERROR, aaa=None, app="housekeeping", view="filestore",
            action="orphan", status="ERROR",
            data=f"{unexpected} unexpected orphaned files removed (no tmp- prefix, not in metadata)")  # fmt: skip

    if failed:
        log(ERROR, aaa=None, app="housekeeping", view="filestore",
            action="cleanup", status="FAILED",
            data=f"{failed} file(s) could not be deleted - last error: {last_error}")  # fmt: skip

    return cleaned, orphaned


def datafile_from_uploaded(file_uploaded):
    """
    Upload a file to filestore, create corresponding DataFile object

    Args:
        file_uploaded:  Django request.FILES POST uploaded_file object

    Returns:
        Datafile: Datafile object with file written to filestore
    """

    if not file_uploaded:
        return

    df = DataFile()
    df.fileid = str(uuid.uuid4())  # f5fd12d3-a65a-4cf8-b3c1-22059ea3cbd5
    df.filename = file_uploaded.name
    df.filepath = uuid_to_filepath(df.fileid)
    df.displayname = file_uploaded.name
    df.size = file_uploaded.size
    df.save()

    # write uploaded file to filestore

    sig = hashlib.md5()

    with open(df.filepath, "wb+") as destination:
        if file_uploaded.multiple_chunks():
            # django chunked file (from /tmp)
            for chunk in file_uploaded.chunks():
                sig.update(chunk)
                destination.write(chunk)
        else:
            # in-memory
            full = file_uploaded.read()
            sig.update(full)
            destination.write(full)

    #  hash
    df.hash = str(sig.hexdigest())
    df.save()

    return df
