#!/usr/bin/env python3

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

"""
Example script to populate a debusine instance with example data

This is useful to get a full example local database to use for UI development
"""


import contextlib
import datetime
import logging
import os
import shlex
import shutil
import subprocess
import tempfile
from functools import cached_property
from pathlib import Path
from typing import Any

import yaml

import django

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'debusine.project.settings')
django.setup()
from django.core.management import execute_from_command_line
from django.db import transaction  # noqa: E402

from debusine.artifacts.models import CollectionCategory
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    Collection,
    File,
    FileInArtifact,
    User,
    WorkRequest,
    Workspace,
)  # noqa: E402
from debusine.server.file_backend.interface import (
    FileBackendInterface,
)  # noqa: E402


def debusine(*args: str, **kwargs: Any) -> subprocess.CompletedProcess:
    """Run debusine client."""
    kwargs.setdefault("check", True)

    cmdline = ["python3", "-m", "debusine.client"]
    cmdline.extend(args)

    logging.info("%s", shlex.join(cmdline))
    return subprocess.run(cmdline, **kwargs)


def debusine_admin(
    *args: str, input_data: dict[str, Any] | None = None, **kwargs: Any
) -> subprocess.CompletedProcess:
    """Run debusine-admin."""
    kwargs.setdefault("check", True)

    # If debusine-admin is not installed, fall back to ./manage.py.
    # This should allow running the playground from a git checkout, while still
    # working as an example script with debusine installed
    if (debusine_admin := shutil.which("debusine-admin")) is None:
        import debusine

        debusine_admin = str(
            Path(debusine.__file__).parent.parent / "manage.py"
        )

    cmdline = [debusine_admin]
    cmdline.extend(args)

    with contextlib.ExitStack() as stack:
        if input_data is not None:
            tmpfile = stack.enter_context(tempfile.TemporaryFile(mode="w+t"))
            yaml.safe_dump(input_data, stream=tmpfile)
            tmpfile.seek(0)
            kwargs["stdin"] = tmpfile

        logging.info("%s", shlex.join(cmdline))
        return subprocess.run(cmdline, **kwargs)


class Playground:
    """Populate a database with simulated activity."""

    def __init__(self):
        pass

    @cached_property
    def workspace(self) -> Workspace:
        """Return the Playground workspace."""
        return Workspace.objects.get(name="Playground")

    @cached_property
    def user(self) -> User:
        """Return the Playground user."""
        try:
            return User.objects.get(username="playground")
        except User.DoesNotExist:
            debusine_admin(
                "create_user",
                "playground",
                "playground@example.org",
                stdout=subprocess.DEVNULL,
            )
        return User.objects.get(username="playground")

    @cached_property
    def suite_collection(self) -> Collection:
        return Collection.objects.get(
            name="play_bookworm", category=CollectionCategory.SUITE
        )

    @cached_property
    def archive_collection(self) -> Collection:
        return Collection.objects.get(
            name="play_debian", category="debian:archive"
        )

    @staticmethod
    def create_artifact_relation(
        artifact: Artifact,
        target: Artifact,
        relation_type: ArtifactRelation.Relations = (
            ArtifactRelation.Relations.RELATES_TO
        ),
    ) -> ArtifactRelation:
        """Create an ArtifactRelation."""
        return ArtifactRelation.objects.create(
            artifact=artifact, target=target, type=relation_type
        )

    def create_file_in_backend(
        self,
        backend: FileBackendInterface[Any],
        contents: bytes = b"test",
    ) -> File:
        """
        Create a temporary file and adds it in the backend.

        :param backend: file backend to add the file in
        :param contents: contents of the file
        :param remove_file_from_backend: schedule removing file from the backend
        """
        with tempfile.NamedTemporaryFile() as tf:
            tf.write(contents)
            tf.flush()
            temporary_file = Path(tf.name)

            fileobj = backend.add_file(temporary_file)
        return fileobj

    def create_artifact(
        self,
        files: dict[str, bytes],
        *,
        category: str = "test",
        data: dict[str, Any] | None = None,
        expiration_delay: int | None = None,
        work_request: WorkRequest | None = None,
    ) -> Artifact:
        """
        Create an artifact and return tuple with the artifact and files data.

        :param paths: list of paths to create (will contain random data)
        :param files_size: size of the test data
        :param category: this artifact's category (see :ref:`artifacts`)
        :param data: key-value data for this artifact (see :ref:`artifacts`)
        :param expiration_delay: set expiration_delay field (in days)
        :param work_request: work request that created this artifact
        :param created_by: set Artifact.created_by to it
        :param create_files: create a file and add it into the LocalFileBackend

        This method returns a tuple:
        - artifact: Artifact
        - files_contents: Dict[str, bytes] (paths and test data)
        """
        artifact = Artifact.objects.create(
            category=category,
            workspace=self.workspace,
            data=data or {},
            expiration_delay=(
                datetime.timedelta(expiration_delay)
                if expiration_delay is not None
                else None
            ),
            created_by_work_request=work_request,
            created_by=self.user,
        )

        file_backend = self.workspace.default_file_store.get_backend_object()
        for path, contents in files.items():
            fileobj = self.create_file_in_backend(file_backend, contents)
            FileInArtifact.objects.create(
                artifact=artifact, path=path, file=fileobj
            )

        return artifact

    def create_source_package(
        self, name: str, version: str, paths: list[str]
    ) -> Artifact:
        """Create a minimal `debian:source-package` artifact."""
        return self.create_artifact(
            category="debian:source-package",
            data={
                "name": name,
                "version": version,
                "type": "dpkg",
                "dsc_fields": {},
            },
            files={path: path.encode() for path in paths},
        )

    def create_binary_package(
        self,
        srcpkg_name: str,
        srcpkg_version: str,
        name: str,
        version: str,
        architecture: str,
        paths: list[str],
    ) -> Artifact:
        """Create a minimal `debian:binary-package` artifact."""
        return self.create_artifact(
            category="debian:binary-package",
            data={
                "srcpkg_name": srcpkg_name,
                "srcpkg_version": srcpkg_version,
                "deb_fields": {
                    "Package": name,
                    "Version": version,
                    "Architecture": architecture,
                },
                "deb_control_files": [],
            },
            files={path: path.encode() for path in paths},
        )

    def populate_suite(self):
        """Populate the debian:suite collection with artifacts."""
        srcpkg = self.create_source_package(
            "hello",
            "1.0-1",
            [
                "hello_1.0-1.dsc",
                "hello_1.0-1.debian.tar.xz",
                "hello_1.0.orig.tar.xz",
            ],
        )
        binpkg = self.create_binary_package(
            "hello", "1.0-1", "hello", "1.0-1", "all", ["hello_1.0-1_all.deb"]
        )
        self.create_artifact_relation(
            binpkg, srcpkg, ArtifactRelation.Relations.BUILT_USING
        )

        suite_manager = self.suite_collection.manager
        suite_manager.add_source_package(
            srcpkg, user=self.user, component="main", section="devel"
        )
        suite_manager.add_binary_package(
            binpkg,
            user=self.user,
            component="main",
            section="devel",
            priority="optional",
        )


def main():
    logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")

    playground = Playground()

    # Remove possible data from an old playground
    debusine_admin("delete_workspace", "Playground", "--yes", "--force")

    debusine_admin("create_workspace", "--public", "Playground")
    debusine_admin(
        "create_collection",
        "--workspace",
        "Playground",
        "play_bookworm",
        CollectionCategory.SUITE,
        input_data={
            "may_reuse_versions": False,
            "release_fields": {
                "Suite": "stable",
                "Codename": "bookworm",
                "Architectures": "all amd64 arm64 armel armhf i386"
                " mips64el mipsel ppc64el s390x",
                "Components": "main contrib non-free-firmware non-free",
            },
        },
    )

    debusine_admin(
        "create_collection",
        "--workspace",
        "Playground",
        "play_debian",
        "debian:archive",
        input_data={"may_reuse_versions": False},
    )

    # TODO: Add bookworm suite to debian archive (see #388)
    # with transaction.atomic():
    #     archive_manager = playground.archive_collection.manager()
    #     archive_manager.add_collection(
    #         playground.suite_collection, user=playground.user
    #     )

    # Add packages to the debian suite
    with transaction.atomic():
        playground.populate_suite()


if __name__ == '__main__':
    main()
