# 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 urllib.parse import quote

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, category: str | CollectionCategory) -> str:
        """Markup a collection category for rich."""
        if category != CollectionCategory.WORKFLOW_INTERNAL:
            category_url = self.debusine.webui_url(
                f"/{quote(self.workspace)}/collection/{quote(category)}/"
            )
            return f"[link={category_url}]{category}[/]"
        else:
            return category

    def _url(self, instance: CollectionData) -> str:
        return self.debusine.webui_url(
            f"/{quote(self.workspace)}/collection"
            f"/{quote(instance.category)}/{quote(instance.name)}/"
        )

    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.category),
                (
                    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.category))
        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))
        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 Show(CollectionCommand, group="collection"):
    """Show an existing collection."""

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

    def run(self) -> None:
        """Run the command."""
        with self._api_call_or_fail():
            collection = self.debusine.collection_get(
                self.workspace, self.args.category, self.args.name
            )
        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(RequiredInputDataCommand, CollectionInput, group="collection"):
    """Create a new collection."""

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

    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.args.category, name=self.args.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(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("category", type=str, help="collection category")
        parser.add_argument(
            "name", type=str, default="_", help="collection name"
        )
        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.args.category, self.args.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)
