# 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"))

    @override
    def _list_rich(self, collections: Iterable[CollectionData]) -> None:
        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)

    @override
    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."""

    @override
    def run(self) -> None:
        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 "_"

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        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."""

    @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=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."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        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."""

    @override
    def update_collection(
        self, collection: CollectionDataNew | CollectionData
    ) -> None:
        super().update_collection(collection)
        collection.data = self.input_data

    @override
    def run(self) -> None:
        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."""

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

    @override
    def update_collection(
        self, collection: CollectionDataNew | CollectionData
    ) -> None:
        super().update_collection(collection)
        if self.input_data is not None:
            collection.data = self.input_data

    @override
    def run(self) -> None:
        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."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        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.")
