# Copyright 2024 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Implementation of in-memory file backend."""

import contextlib
import io
import logging
from collections import defaultdict
from pathlib import Path

from django.db import transaction

from debusine.db.models import File, FileInStore, FileStore
from debusine.server.file_backend.interface import FileBackendInterface
from debusine.server.file_backend.models import MemoryFileBackendConfiguration


logger = logging.getLogger(__name__)


class MemoryFileBackend(FileBackendInterface[MemoryFileBackendConfiguration]):
    """In-memory file backend (used for tests)."""

    # In-memory file storages, indexed by configuration names
    storages: dict[str, dict[str, bytes]] = defaultdict(dict)

    def __init__(self, file_store: FileStore):
        """Initialize MemoryFileBackend."""
        super().__init__()
        self.db_store = file_store
        self.storage = self.storages[self.configuration.name]

    def get_local_path(self, fileobj: File) -> None:  # noqa: U100
        """Return local_path for fileobj."""
        return None

    def _store_file(self, hash_hex: str, value: bytes) -> None:
        """Add/update a file in the storage."""
        # This is here mainly so it can be more easily mocked than a dict
        # assignment
        self.storage[hash_hex] = value

    @transaction.atomic
    def add_file(self, local_path: Path, fileobj: File | None = None) -> File:
        """Add local_path to the filestore. Reuse fileobj if size matches."""
        # fileobj.hash is not compared to avoid recalculating the hash
        # of the file
        if not fileobj:
            fileobj = File.from_local_path(local_path)
        elif fileobj.size != (size_in_disk := local_path.stat().st_size):
            raise ValueError(
                f"add_file file size mismatch. Path: {local_path} "
                f"Size in disk: {size_in_disk} "
                f"fileobj.size: {fileobj.size}"
            )

        file_in_store, created = FileInStore.objects.get_or_create(
            store=self.db_store, file=fileobj, data={}
        )
        if created:
            hash_hex = fileobj.hash_digest.hex()
            self._store_file(hash_hex, local_path.read_bytes())

        return fileobj

    def get_url(self, fileobj: File) -> None:  # noqa: U100
        """Return None: no remote URL for a file in MemoryFileBackend."""
        return None

    def _remove_file(self, fileobj: File):
        """Remove the file pointed by fileobj from the backend."""
        hash_hex = fileobj.hash_digest.hex()
        self.storage.pop(hash_hex, None)

    @contextlib.contextmanager
    def get_stream(self, fileobj: File):
        """Yield a file-like object that can be read."""
        hash_hex = fileobj.hash_digest.hex()
        with io.BytesIO(self.storage[hash_hex]) as out:
            yield out
