#  (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. 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 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
