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

"""
Debusine command line interface, package management commands.

These are "porcelain": they implement opinionated operations on
:collection:`debian:suite` collections and their contents, rather than basic
"plumbing" operations.
"""

import abc
import argparse
from collections.abc import Generator, Iterable
from typing import override

import rich
import rich.json
from rich.table import Table

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.client.commands.base import ModelCommand, WorkspaceCommand
from debusine.client.models import CollectionItemData


class PackageCommand(
    ModelCommand[CollectionItemData], WorkspaceCommand, abc.ABC
):
    """Common infrastructure for archive package commands."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "--suite", required=True, help="Operate on this suite"
        )

    @override
    def _list_rich(self, items: Iterable[CollectionItemData]) -> None:
        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
        table.add_column("Name")
        table.add_column("Version")
        table.add_column("Architecture")
        for item in items:
            match item.category:
                case ArtifactCategory.SOURCE_PACKAGE:
                    table.add_row(
                        item.data["package"], item.data["version"], "source"
                    )
                case ArtifactCategory.BINARY_PACKAGE:
                    table.add_row(
                        item.data["package"],
                        item.data["version"],
                        item.data["architecture"],
                    )
        rich.print(table)

    @override
    def _show_rich(self, item: CollectionItemData) -> None:
        table = Table(box=rich.box.SIMPLE, show_header=False)
        table.add_row("Item name:", item.name)
        table.add_row("Category:", item.category)
        match item.category:
            case ArtifactCategory.SOURCE_PACKAGE:
                table.add_row("Component:", item.data["component"])
                table.add_row("Section:", item.data["section"])
            case ArtifactCategory.BINARY_PACKAGE:
                table.add_row("Source package name:", item.data["srcpkg_name"])
                table.add_row(
                    "Source package version:", item.data["srcpkg_version"]
                )
                table.add_row("Component:", item.data["component"])
                table.add_row("Section:", item.data["section"])
                table.add_row("Priority:", item.data["priority"])
        rich.print(table)


class List(PackageCommand, name="list", group="archive.package"):
    """List packages in a suite."""

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            self.list(
                self.debusine.collection_item_iter(
                    workspace=self.workspace,
                    collection_category=CollectionCategory.SUITE,
                    collection_name=self.args.suite,
                )
            )


class Show(PackageCommand, name="show", group="archive.package"):
    """Show a single package in a suite."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "name",
            help=(
                "Item name (NAME_VERSION for source packages, or"
                " NAME_VERSION_ARCHITECTURE for binary packages)"
            ),
        )

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            item = self.debusine.collection_item_get(
                workspace=self.workspace,
                collection_category=CollectionCategory.SUITE,
                collection_name=self.args.suite,
                name=self.args.name,
            )
        self.show(item)


class Remove(PackageCommand, name="remove", group="archive.package"):
    """Remove a package from a suite."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "--dry-run",
            default=False,
            action="store_true",
            help="Only show what would be removed",
        )
        parser.add_argument(
            "--package-version",
            metavar="VERSION",
            help="Only remove packages with this version",
        )
        parser.add_argument(
            "--binary",
            default=False,
            action="store_true",
            help="Only remove binary packages",
        )
        parser.add_argument(
            "--architecture",
            help="Only remove binaries of this architecture (implies --binary)",
        )
        parser.add_argument(
            "packages", metavar="PACKAGE", nargs="+", help="Package name"
        )

    def _find_items_to_remove(self) -> Generator[CollectionItemData]:
        """Generate collection items to remove."""
        for package in self.args.packages:
            name__startswith = f"{package}_"
            if self.args.package_version is not None:
                # We can't add a trailing "_" here, because that would
                # exclude the source package.  Instead, use a slightly
                # sloppy match here to cut down on the response size, and
                # then double-check afterwards.
                name__startswith += self.args.package_version
            with self._api_call_or_fail():
                for item in self.debusine.collection_item_iter(
                    workspace=self.workspace,
                    collection_category=CollectionCategory.SUITE,
                    collection_name=self.args.suite,
                    name__startswith=name__startswith,
                    category=(
                        ArtifactCategory.BINARY_PACKAGE
                        if (
                            self.args.binary
                            or self.args.architecture is not None
                        )
                        else ArtifactCategory.SOURCE_PACKAGE
                    ),
                ):
                    if self.args.package_version is not None:
                        assert item.category in {
                            ArtifactCategory.SOURCE_PACKAGE,
                            ArtifactCategory.BINARY_PACKAGE,
                        }
                        if item.data["version"] != self.args.package_version:
                            continue
                    if self.args.architecture is not None:
                        assert item.category == ArtifactCategory.BINARY_PACKAGE
                        if item.data["architecture"] != self.args.architecture:
                            continue
                    yield item

    @override
    def run(self) -> None:
        items_to_remove = self._find_items_to_remove()
        if self.args.dry_run:
            self.list(list(items_to_remove))
        else:
            for item in items_to_remove:
                with self._api_call_or_fail():
                    self.debusine.collection_item_delete(
                        workspace=self.workspace,
                        collection_category=CollectionCategory.SUITE,
                        collection_name=self.args.suite,
                        name=item.name,
                    )
                self.feedback(f"Removed: {item.name}")
