# Copyright © 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 external file backends."""

import tempfile
from collections.abc import Generator
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Self, override

import requests
from requests_toolbelt.downloadutils.stream import stream_response_to_file

from debusine.db.models import File, FileInStore, FileStore
from debusine.server.file_backend.interface import (
    FileBackendEntryInterface,
    FileBackendInterface,
)
from debusine.server.file_backend.models import (
    ExternalDebianSuiteBackendConfiguration,
)
from debusine.utils import NotSupportedError, get_user_agent
from debusine.utils.urls import DEFAULT_REQUEST_TIMEOUT


class ExternalDebianSuiteFileBackendEntry(
    FileBackendEntryInterface["ExternalDebianSuiteFileBackend", str]
):
    """
    An entry in an external repository.

    External repository identifiers are relative URLs under the archive root
    URL.
    """

    @classmethod
    def _create_from_file(
        cls, backend: "ExternalDebianSuiteFileBackend", fileobj: File
    ) -> Self:
        """Make an entry based on a `File`."""
        file_in_store = FileInStore.objects.get(
            file=fileobj, store=backend.db_store
        )
        relative_url = file_in_store.data["relative_url"]
        assert isinstance(relative_url, str)
        return cls(backend=backend, identifier=relative_url)

    @override
    def __str__(self) -> str:
        """Return a string describing this entry."""
        return self.get_url()

    @override
    def get_local_path(self) -> None:
        """
        Return None: no local path for now.

        This may change if we cache the remote file locally.
        """
        return None

    @override
    @contextmanager
    def get_temporary_local_path(self) -> Generator[Path]:
        """Yield a temporary local path for the file."""
        with tempfile.NamedTemporaryFile(
            prefix="debusine-external-", delete_on_close=False
        ) as temp_file:
            headers = {"User-Agent": get_user_agent()}
            with requests.get(
                self.get_url(),
                stream=True,
                headers=headers,
                timeout=DEFAULT_REQUEST_TIMEOUT,
            ) as r:
                r.raise_for_status()
                stream_response_to_file(r, temp_file)
            temp_file.close()
            yield Path(temp_file.name)

    @override
    def get_url(
        self,
        content_type: str | None = None,  # noqa: ARG002, U100
    ) -> str:
        """
        Return a URL pointing to the content.

        We cannot override the Content-Type for external repositories.
        """
        return self.backend.archive_root_url + self._identifier

    @override
    def get_mtime(self) -> None:  # pragma: no cover
        """Return None: no last-modified times for external files."""
        return None

    @override
    def add(self, local_path: Path) -> None:
        """We cannot add files to external repositories."""
        raise NotImplementedError()

    @override
    def same_contents(self, local_path: Path) -> bool:
        """
        We cannot check file contents in external repositories.

        It would be possible, but there's no need since we cannot add files
        to them.
        """
        raise NotImplementedError()

    @override
    def remove(self) -> None:
        """
        Remove the entry from the underlying storage.

        This may change if we cache the remote file locally.
        """
        raise NotImplementedError()


class ExternalDebianSuiteFileBackend(
    FileBackendInterface[ExternalDebianSuiteBackendConfiguration]
):
    """Mirror an external repository in debusine."""

    def __init__(self, file_store: FileStore) -> None:
        """Initialize instance."""
        super().__init__()
        self.db_store = file_store
        # All files are relative to the archive root
        # https://wiki.debian.org/DebianRepository/Format
        self.archive_root_url = self.configuration.archive_root_url
        # For Collections to grab and parse the appropriate Packages files
        self.suite = self.configuration.suite
        self.components = self.configuration.components

    @override
    def get_entry(self, fileobj: File) -> ExternalDebianSuiteFileBackendEntry:
        return ExternalDebianSuiteFileBackendEntry._create_from_file(
            self, fileobj
        )

    @override
    def remove_file(self, db_file: File) -> None:  # noqa: ARG002, U100
        """
        Remove the file from the FileStore.

        This may change if we cache the remote file locally.
        """
        raise NotSupportedError(
            "ExternalDebianSuiteFileBackend is a read-only file backend"
        )

    @override
    def add_file(
        self,
        local_path: Path,  # noqa: ARG002, U100
        db_file: File | None = None,  # noqa: ARG002, U100
    ) -> File:
        raise NotSupportedError(
            "ExternalDebianSuiteFileBackend is a read-only file backend"
        )

    def add_remote_file(
        self,
        relative_url: str,
        hash_digest: bytes,
        size: int,
    ) -> File:
        """Register remote file in the DB."""
        db_file_kwargs: dict[str, Any] = {
            File.current_hash_algorithm: hash_digest,
            'size': size,
        }
        db_file, _ = File.objects.get_or_create(**db_file_kwargs)
        FileInStore.objects.get_or_create(
            store=self.db_store,
            file=db_file,
            data={'relative_url': relative_url},
        )
        return db_file

    @override
    def list_entries(self) -> Generator[ExternalDebianSuiteFileBackendEntry]:
        # We cannot currently iterate over all files in an external suite
        # (though in principle it should be possible).
        raise NotImplementedError()
