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

import abc
import argparse
import logging
from collections.abc import Iterable
from typing import Any, cast, override

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

from debusine.client.commands.base import (
    ModelCommand,
    OptionalInputDataCommand,
    RequiredInputDataCommand,
    WorkspaceCommand,
)
from debusine.client.models import (
    RuntimeParameters,
    WorkflowTemplateData,
    WorkflowTemplateDataNew,
)
from debusine.utils.input import YamlEditor

log = logging.getLogger("debusine")


class WorkflowTemplateCommand(
    ModelCommand[WorkflowTemplateData], WorkspaceCommand, abc.ABC
):
    """Common infrastructure for workflow template commands."""

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

    @override
    def _list_rich(self, instances: Iterable[WorkflowTemplateData]) -> None:
        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
        table.add_column("ID")
        table.add_column("Name")
        table.add_column("Task name")
        table.add_column("Priority")
        table.add_column("Static parameters")
        table.add_column("Runtime parameters")
        table.add_column("Extra groups")
        table.add_column("Restricted")
        for instance in instances:
            url = self._url(instance)
            table.add_row(
                f"{instance.id}",
                f"[link={url}]{instance.name}[/]",
                instance.task_name,
                str(instance.priority),
                (
                    rich.json.JSON.from_data(instance.static_parameters)
                    if instance.static_parameters
                    else None
                ),
                (
                    rich.json.JSON.from_data(instance.runtime_parameters)
                    if instance.runtime_parameters
                    else None
                ),
                " ".join(str(g) for g in instance.extra_groups),
                str(instance.restricted),
            )
        rich.print(table)

    @override
    def _show_rich(self, instance: WorkflowTemplateData) -> None:
        """Show a workflow template for humans."""
        url = self._url(instance)
        table = Table(box=rich.box.SIMPLE, show_header=False)
        table.add_column("Name", justify="right")
        table.add_row("ID:", f"#{instance.id}")
        table.add_row("Name:", f"[link={url}]{instance.name}[/]")
        table.add_row("Task name:", instance.task_name)
        table.add_row("Priority:", str(instance.priority))
        table.add_row(
            "Static parameters:",
            rich.json.JSON.from_data(instance.static_parameters),
        )
        table.add_row(
            "Runtime parameters:",
            rich.json.JSON.from_data(instance.runtime_parameters),
        )
        table.add_row(
            "Extra groups:", " ".join(str(g) for g in instance.extra_groups)
        )
        table.add_row("Restricted:", str(instance.restricted))
        rich.print(table)

    @classmethod
    def _parse_input_data(
        cls, input_data: dict[str, Any], optional: bool = False
    ) -> tuple[dict[str, Any], RuntimeParameters, list[int | str], bool]:
        """
        Extract required fields from input_data.

        If optional is set, these values are given reasonable defaults;
        otherwise, they are all required.
        """
        expected_keys = {
            "static_parameters",
            "runtime_parameters",
            "extra_groups",
            "restricted",
        }
        found_keys = set(input_data.keys())
        if optional:
            if not found_keys <= expected_keys:
                cls._fail(
                    f"Error parsing input data: Expecting only"
                    f" {len(expected_keys)} keys: "
                    f"{sorted(expected_keys)}, found: {sorted(found_keys)}"
                )
        else:
            if found_keys != expected_keys:
                cls._fail(
                    f"Error parsing input data: Expecting"
                    f" {len(expected_keys)} keys: "
                    f"{sorted(expected_keys)}, found: {sorted(found_keys)}"
                )
        static_parameters = input_data.get("static_parameters", {})
        runtime_parameters = input_data.get("runtime_parameters", {})
        extra_groups = input_data.get("extra_groups", [])
        restricted = input_data.get("restricted", False)
        if not isinstance(static_parameters, dict):
            cls._fail(
                f"Error: static_parameters must be a dictionary. "
                f"It is: {type(static_parameters).__name__}"
            )
        if (data_type := type(runtime_parameters)) not in (dict, str):
            cls._fail(
                f"Error: runtime_parameters must be a dictionary or string. "
                f"It is: {data_type.__name__}"
            )
        if not isinstance(extra_groups, list):
            cls._fail(
                f"Error: extra_groups must be a list. "
                f"It is: {type(extra_groups).__name__}"
            )
        for g in extra_groups:
            if not isinstance(g, int | str):
                cls._fail(
                    "Error: extra_groups must be a list of integers or strings"
                )
        if not isinstance(restricted, bool):
            cls._fail(
                f"Error: restricted must be a boolean. "
                f"It is: {type(restricted).__name__}"
            )

        return (
            static_parameters,
            cast(RuntimeParameters, runtime_parameters),
            extra_groups,
            restricted,
        )


class InputBase(WorkflowTemplateCommand, abc.ABC):
    """Add arguments to input parts of a WorkflowTemplate."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "--priority",
            type=int,
            default=None,
            help="Base priority for work requests created from this template",
        )

    #: Priority set by the user
    new_priority: int | None

    def __init__(
        self, parser: argparse.ArgumentParser, args: argparse.Namespace
    ) -> None:
        """Populate new_priority."""
        super().__init__(parser, args)
        self.new_priority = self.args.priority


class Create(
    RequiredInputDataCommand,
    InputBase,
    WorkflowTemplateCommand,
    group="workflow-template",
):
    """Create a workflow template."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "task_name",
            type=str,
            help="Name of the workflow task that the template will use",
        )
        parser.add_argument(
            "template_name", type=str, help="Name of the new workflow template"
        )

    @override
    def run(self) -> None:
        """Create the new workflow template and return the server response."""
        static_parameters, runtime_parameters, extra_groups, restricted = (
            self._parse_input_data(self.input_data, optional=True)
        )
        workflow_template = WorkflowTemplateDataNew(
            name=self.args.template_name,
            task_name=self.args.task_name,
            static_parameters=static_parameters,
            runtime_parameters=runtime_parameters,
            priority=self.new_priority or 0,
            extra_groups=extra_groups,
            restricted=restricted,
        )
        with self._api_call_or_fail():
            created = self.debusine.workflow_template_create(
                self.workspace, workflow_template
            )
        self.show(created)


class List(WorkflowTemplateCommand, group="workflow-template"):
    """List workflow templates."""

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            instances = self.debusine.workflow_template_iter(self.workspace)
            self.list(instances)


class Show(WorkflowTemplateCommand, group="workflow-template"):
    """Show an existing workflow template."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "name", type=str, default="_", help="workflow template name or id"
        )

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            instance = self.debusine.workflow_template_get(
                self.workspace, self.args.name
            )
        self.show(instance)


class Manage(OptionalInputDataCommand, InputBase, group="workflow-template"):
    """Configure an existing workflow template."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "name", type=str, default="_", help="workflow template name or id"
        )
        parser.add_argument(
            "--rename", type=str, help="rename workflow template"
        )

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            instance = self.debusine.workflow_template_get(
                self.workspace, self.args.name
            )

        if self.new_priority is not None:
            instance.priority = self.new_priority
        if self.input_data is not None:
            (
                instance.static_parameters,
                instance.runtime_parameters,
                instance.extra_groups,
                instance.restricted,
            ) = self._parse_input_data(self.input_data, optional=True)
        if (name := self.args.rename) is not None:
            instance.name = name

        with self._api_call_or_fail():
            instance = self.debusine.workflow_template_update(
                self.workspace, instance
            )

        self.show(instance)


class Delete(WorkflowTemplateCommand, group="workflow-template"):
    """Delete an existing workflow template."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "name", type=str, default="_", help="workflow template name or id"
        )

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            self.debusine.workflow_template_delete(
                self.workspace, self.args.name
            )
        self.feedback("Workflow template deleted.")


class Edit(WorkflowTemplateCommand, group="workflow-template"):
    """Edit a workflow template data in an editor."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        super().configure(parser)
        parser.add_argument(
            "name", type=str, default="_", help="workflow template name or id"
        )

    def edit_parameters(self, instance: WorkflowTemplateData) -> bool:
        """
        Edit the instance's parameters in an editor.

        :returns: True if the task data was changed, False otherwise
        """
        input_ = {
            "static_parameters": instance.static_parameters,
            "runtime_parameters": instance.runtime_parameters,
            "extra_groups": instance.extra_groups,
            "restricted": instance.restricted,
        }
        editor: YamlEditor[dict[str, Any]] = YamlEditor(
            input_,
            help_text=[
                f"Here you can edit the parameters of the {instance.name}"
                f" workflow template in workspace {self.workspace}."
            ],
        )

        if not editor.edit():
            return False
        if editor.value == input_:
            return False

        (
            instance.static_parameters,
            instance.runtime_parameters,
            instance.extra_groups,
            instance.restricted,
        ) = self._parse_input_data(editor.value)
        return True

    @override
    def run(self) -> None:
        with self._api_call_or_fail():
            instance = self.debusine.workflow_template_get(
                self.workspace, self.args.name
            )

        if self.edit_parameters(instance):
            with self._api_call_or_fail():
                instance = self.debusine.workflow_template_update(
                    self.workspace, instance
                )
        else:
            self.feedback(
                f"Workflow template [i]{rich.markup.escape(instance.name)}[/i]"
                " is left unchanged.",
                highlight=False,
            )

        self.show(instance)
