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

"""Utility methods used in management module."""

import abc
import datetime
import io
from collections.abc import Generator
from dataclasses import dataclass
from typing import Any, ClassVar, IO, cast

import rich
import yaml
from django.db.models import QuerySet
from rich.table import Table


@dataclass
class Column:
    """Represent a column in a tabular output."""

    #: Name in YAML
    name: str
    #: Label in tables
    label: str

    def to_data(self, value: Any) -> Any:
        """Return a value for YAML."""
        return value

    def to_rich(self, value: Any) -> str:
        """Return a value for human-readable output."""
        match value:
            case None:
                return "-"
            case datetime.datetime():
                return value.isoformat()
            case _:
                return str(value)


@dataclass
class AttrColumn(Column):
    """Column displaying an attribute of an object."""

    #: Attribute to use for value
    attr: str

    def to_data(self, value: Any) -> Any:
        """Return a value for YAML."""
        return getattr(value, self.attr) if value is not None else None

    def to_rich(self, value: Any) -> str:
        """Return a value for human-readable output."""
        if value is None:
            return "-"
        value = getattr(value, self.attr)
        return super().to_rich(value)


class Printer(abc.ABC):
    """Print tabular values in human or machine readable formats."""

    #: Description of tabular columns
    columns: ClassVar[list[Column]]

    def __init__(self, yaml: bool) -> None:
        """
        Choose the output type.

        :param yaml: use machine-readable YAML
        """
        self.yaml = yaml

    @abc.abstractmethod
    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""

    def print(
        self, qs: QuerySet[Any], file: IO[str] | io.TextIOBase | None = None
    ) -> None:
        """Generate and print output."""
        if self.yaml:
            self.print_yaml(qs, file)
        else:
            self.print_rich(qs, file)

    def print_yaml(
        self, qs: QuerySet[Any], file: IO[str] | io.TextIOBase | None = None
    ) -> None:
        """Generate machine-readable YAML."""
        rows: list[dict[str, Any]] = []
        for row in self.rows(qs):
            rows.append(
                {
                    col.name: col.to_data(val)
                    for col, val in zip(self.columns, row)
                }
            )
        print(yaml.dump(rows), file=file)

    def print_rich(
        self, qs: QuerySet[Any], file: IO[str] | io.TextIOBase | None = None
    ) -> None:
        """Generate human-readable output."""
        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
        for col in self.columns:
            table.add_column(col.label)
        for row in self.rows(qs):
            table.add_row(
                *(col.to_rich(val) for col, val in zip(self.columns, row))
            )
        rich.print(table, file=cast(IO[str], file))


class Tokens(Printer):
    """Display a queryset of tokens."""

    columns = [
        Column("hash", 'Token hash (do not copy)'),
        AttrColumn("user", 'User', "username"),
        Column("created", 'Created'),
        Column("enabled", 'Enabled'),
        Column("comment", 'Comment'),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for token in qs.order_by('user__username', 'created_at'):
            yield [
                token.hash,
                token.user,
                token.created_at,
                token.enabled,
                token.comment,
            ]


class Workers(Printer):
    """Display a queryset of workers."""

    columns = [
        Column("name", 'Name'),
        Column("type", 'Type'),
        Column("registered", 'Registered'),
        Column("connected", 'Connected'),
        AttrColumn("hash", 'Token hash (do not copy)', "hash"),
        AttrColumn("enabled", 'Enabled', "enabled"),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for worker in qs.order_by('registered_at'):
            token = worker.token
            yield [
                worker.name,
                worker.worker_type,
                worker.registered_at,
                worker.connected_at,
                token,
                token,
            ]


class Workspaces(Printer):
    """Display a queryset of workspaces."""

    columns = [
        Column("name", 'Name'),
        Column("public", 'Public'),
        Column("expiration", 'Default Expiration Delay (days)'),
        Column("default_file_store", 'Default File Store'),
        Column("other_file_store", '# Other File Stores'),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for workspace in qs.order_by('id'):
            store_desc = (
                f"{workspace.default_file_store.name}"
                f" ({workspace.default_file_store.backend})"
            )
            yield [
                str(workspace),
                workspace.public,
                workspace.default_expiration_delay.days or "Never",
                store_desc,
                workspace.other_file_stores.count(),
            ]


class WorkspaceRoles(Printer):
    """Display a queryset of workspace roles."""

    columns = [
        Column("group", 'Group'),
        Column("role", 'Role'),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for role in qs.order_by('group__name', 'role'):
            yield [
                role.group.name,
                role.role,
            ]


class WorkRequests(Printer):
    """Display a queryset of work requests."""

    columns = [
        Column("id", "ID"),
        AttrColumn("worker", "Worker", "name"),
        Column("created_at", "Created"),
        Column("started_at", "Started"),
        Column("completed_at", "Completed"),
        Column("status", "Status"),
        Column("result", "Result"),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for work_request in qs.order_by("created_at"):
            yield [
                work_request.id,
                work_request.worker,
                work_request.created_at,
                work_request.started_at,
                work_request.completed_at,
                work_request.status,
                work_request.result,
            ]


class Users(Printer):
    """Display a queryset of users."""

    columns = [
        Column("username", "User"),
        Column("email", "Email"),
        Column("date_joined", "Joined"),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for user in qs.order_by("date_joined"):
            yield [user.username, user.email, user.date_joined]


class Groups(Printer):
    """Display a queryset of groups."""

    columns = [
        Column("scope", "Scope"),
        Column("group", "Group"),
        Column("users", "Users"),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for group in qs.order_by("name"):
            yield [group.scope.name, group.name, group.users.count()]


def sort_keys(d: dict[str, Any]) -> dict[str, Any]:
    """Order keys of a dict."""
    return dict(sorted(d.items()))


class NotificationChannels(Printer):
    """Display a queryset of notification channels."""

    columns = [
        Column("name", "Name"),
        Column("method", "Method"),
        Column("data", "Data"),
    ]

    def rows(self, qs: QuerySet[Any]) -> Generator[list[Any], None, None]:
        """Generate rows to display."""
        for notification_channel in qs.order_by("name"):
            yield [
                notification_channel.name,
                notification_channel.method,
                sort_keys(notification_channel.data),
            ]
