# 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, collection management commands."""

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

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

from debusine.artifacts.models import CollectionCategory
from debusine.client.commands.base import (
    ModelCommand,
    OptionalInputDataCommand,
    RequiredInputDataCommand,
    WorkspaceCommand,
)
from debusine.client.models import CollectionData, CollectionDataNew


class CollectionCommand(
    ModelCommand[CollectionData], WorkspaceCommand, abc.ABC
):
    """Common infrastructure for collection commands."""

    def markup_category(self, collection: CollectionData) -> str:
        """Markup a collection category for rich."""
        if collection.category == CollectionCategory.WORKFLOW_INTERNAL:
            return collection.category
        category_url = self.debusine.webui_url(
            collection.get_link_url("webui_category")
        )
        return f"[link={category_url}]{collection.category}[/]"

    def _url(self, instance: CollectionData) -> str:
        return self.debusine.webui_url(instance.get_link_url("webui_self"))

    def _list_rich(self, collections: Iterable[CollectionData]) -> None:
        """Print the list as a table."""
        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
        table.add_column("ID")
        table.add_column("Name")
        table.add_column("Category")
        table.add_column("Retention (history)")
        table.add_column("Retention (metadata)")
        table.add_column("Data")
        for collection in collections:
            url = self._url(collection)
            table.add_row(
                f"{collection.id}",
                f"[link={url}]{collection.name}[/]",
                self.markup_category(collection),
                (
                    str(period)
                    if (period := collection.full_history_retention_period)
                    is not None
                    else "unlimited"
                ),
                (
                    str(period)
                    if (period := collection.metadata_only_retention_period)
                    is not None
                    else "unlimited"
                ),
                (
                    rich.json.JSON.from_data(collection.data)
                    if collection.data
                    else None
                ),
            )
        rich.print(table)

    def _show_rich(self, collection: CollectionData) -> None:
        """Show a collection for humans."""
        url = self._url(collection)
        table = Table(box=rich.box.SIMPLE, show_header=False)
        table.add_column("Name", justify="right")
        table.add_row("ID:", f"#{collection.id}")
        table.add_row("Name:", f"[link={url}]{collection.name}[/]")
        table.add_row("Category:", self.markup_category(collection))
        full = (
            f"{period} days"
            if (period := collection.full_history_retention_period) is not None
            else "unlimited"
        )
        meta = (
            f"{period} days"
            if (period := collection.metadata_only_retention_period) is not None
            else "unlimited"
        )
        table.add_row("Full history retention:", full)
        table.add_row("Metadata only retention:", meta)
        table.add_row("Data:", rich.json.JSON.from_data(collection.data))
        if collection.stats:
            table.add_row("Stats:", rich.json.JSON.from_data(collection.stats))
        rich.print(table)


class List(CollectionCommand, group="collection"):
    """List collections in a workspace."""

    def run(self) -> None:
        """Run the command."""
        with self._api_call_or_fail():
            collections = self.debusine.collection_iter(self.workspace)
            self.list(collections)


class SingleCollectionCommand(WorkspaceCommand, abc.ABC):
    """Command operating on a single collection."""

    def __init__(
        self, parser: argparse.ArgumentParser, args: argparse.Namespace
    ) -> None:
        """Identify the selected collection."""
        super().__init__(parser, args)
        if "@" in (cat := args.category_or_fullname):
            self.collection_name, self.collection_category = cat.split("@", 1)
            if args.name is not None:
                parser.error(
                    "collection name is given"
                    f" both in `{args.category_or_fullname}`"
                    " and in the name argument"
                )
        else:
            self.collection_category = cat
            self.collection_name = args.name or "_"

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        super().configure(parser)
        parser.add_argument(
            "category_or_fullname",
            type=str,
            metavar="[name@]category",
            help="collection category or name@category full name",
        )
        parser.add_argument(
            "name",
            type=str,
            nargs="?",
            help="collection name, only needed if not specified"
            " in the first argument. Defaults to '_'.",
        )


class Show(SingleCollectionCommand, CollectionCommand, group="collection"):
    """Show an existing collection."""

    def run(self) -> None:
        """Run the command."""
        with self._api_call_or_fail():
            collection = self.debusine.collection_get(
                self.workspace,
                self.collection_category,
                self.collection_name,
                with_stats=True,
            )
        self.show(collection)


def retention_period(value: str) -> int | str:
    """Validate a retention period from the command line."""
    if value.isdigit():
        return int(value)
    elif value == "-":
        return "-"
    else:
        raise argparse.ArgumentError(
            argument=None, message="retention period must be a number or '-'"
        )


class CollectionInput(CollectionCommand, abc.ABC):
    """Common code for commands that input data for collections."""

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        super().configure(parser)
        parser.add_argument(
            "--full-history-retention-period",
            type=retention_period,
            help="set full history retention period (in days)."
            " Use a dash ('-') to unset",
        )
        parser.add_argument(
            "--metadata-only-retention-period",
            type=retention_period,
            help="set metadata only retention period (in days)."
            " Use a dash ('-') to unset",
        )

    def update_collection(
        self, collection: CollectionDataNew | CollectionData
    ) -> None:
        """Update a collection record with data from command line arguments."""
        if (period := self.args.full_history_retention_period) is not None:
            if period == "-":
                collection.full_history_retention_period = None
            else:
                collection.full_history_retention_period = int(period)

        if (period := self.args.metadata_only_retention_period) is not None:
            if period == "-":
                collection.metadata_only_retention_period = None
            else:
                collection.metadata_only_retention_period = int(period)


class Create(
    SingleCollectionCommand,
    RequiredInputDataCommand,
    CollectionInput,
    group="collection",
):
    """Create a new collection."""

    def update_collection(
        self, collection: CollectionDataNew | CollectionData
    ) -> None:
        """Update a collection record with data from command line arguments."""
        super().update_collection(collection)
        collection.data = self.input_data

    def run(self) -> None:
        """Run the command."""
        collection = CollectionDataNew(
            category=self.collection_category,
            name=self.collection_name,
            data={},
        )
        self.update_collection(collection)

        with self._api_call_or_fail():
            collection_new = self.debusine.collection_create(
                self.workspace, collection
            )

        self.show(collection_new)


class Manage(
    SingleCollectionCommand,
    OptionalInputDataCommand,
    CollectionInput,
    group="collection",
):
    """Configure an existing collection."""

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        super().configure(parser)
        parser.add_argument("--rename", type=str, help="rename collection")

    def update_collection(
        self, collection: CollectionDataNew | CollectionData
    ) -> None:
        """Update a collection record with data from command line arguments."""
        super().update_collection(collection)
        if self.input_data is not None:
            collection.data = self.input_data

    def run(self) -> None:
        """Run the command."""
        with self._api_call_or_fail():
            collection = self.debusine.collection_get(
                self.workspace, self.collection_category, self.collection_name
            )
        self.update_collection(collection)

        if (name := self.args.rename) is not None:
            collection.name = name

        with self._api_call_or_fail():
            collection = self.debusine.collection_update(
                self.workspace, collection
            )

        self.show(collection)


class Delete(SingleCollectionCommand, WorkspaceCommand, group="collection"):
    """Delete a collection."""

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        super().configure(parser)
        parser.add_argument(
            "-f",
            "--force",
            action="store_true",
            help="force deleting non-empty collections",
        )

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            collection = self.debusine.collection_get(
                self.workspace,
                self.collection_category,
                self.collection_name,
                with_stats=not self.args.force,
            )

        if not self.args.force:
            assert collection.stats is not None
            items: list[str] = []
            for s in collection.stats:
                state = "active" if s["active"] else "deleted"
                items.append(
                    f"{state} {s["category"]} {s["type"]}: {s["count"]}"
                )
            if items:
                self._fail(
                    f"Collection is not empty ({", ".join(items)}):"
                    " use --force to delete it."
                )

        with self._api_call_or_fail():
            self.debusine.collection_delete(self.workspace, collection)

        self.feedback("Collection deleted.")
