# 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

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

    def _list_rich(self, instances: Iterable[WorkflowTemplateData]) -> 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("Task name")
        table.add_column("Priority")
        table.add_column("Static parameters")
        table.add_column("Runtime parameters")
        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
                ),
            )
        rich.print(table)

    def _show_rich(self, instance: WorkflowTemplateData) -> None:
        """Show a collection 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),
        )
        rich.print(table)

    @classmethod
    def _parse_input_data(
        cls, input_data: dict[str, Any], optional: bool = False
    ) -> tuple[dict[str, Any], RuntimeParameters]:
        """
        Extract static_parameters and runtime_parameters from input_data.

        If optional is set, these values default to {}, otherwise they are both
        required.
        """
        expected_keys = {"static_parameters", "runtime_parameters"}
        found_keys = set(input_data.keys())
        if optional:
            if not found_keys <= expected_keys:
                cls._fail(
                    f"Error parsing input data: Expecting only 2 keys: "
                    f"{sorted(expected_keys)}, found: {sorted(found_keys)}"
                )
        else:
            if found_keys != expected_keys:
                cls._fail(
                    f"Error parsing input data: Expecting 2 keys: "
                    f"{sorted(expected_keys)}, found: {sorted(found_keys)}"
                )
        static_parameters = input_data.get("static_parameters", {})
        runtime_parameters = input_data.get("runtime_parameters", {})
        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__}"
            )
        return (static_parameters, cast(RuntimeParameters, runtime_parameters))


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

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        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."""

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        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"
        )

    def run(self) -> None:
        """Create the new workflow template and return the server response."""
        static_parameters, runtime_parameters = 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,
        )
        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."""

    def run(self) -> None:
        """Run the command."""
        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."""

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

    def run(self) -> None:
        """Run the command."""
        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 collection."""

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

    def run(self) -> None:
        """Run the command."""
        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 = (
                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 Edit(WorkflowTemplateCommand, group="workflow-template"):
    """Edit a workflow template data in an editor."""

    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        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,
        }
        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 = (
            self._parse_input_data(editor.value)
        )
        return True

    def run(self) -> None:
        """Run the command."""
        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)
