# 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.
"""Functions for rendering input fields."""

from collections.abc import Generator
from typing import Any, override

from django.template.context import Context
from django.utils.html import format_html
from django.utils.safestring import SafeString

import debusine.db.tasks.inputs as dbinputs
from debusine.db.models import Artifact, Collection
from debusine.db.tasks import DBTask
from debusine.tasks.inputs import inputs
from debusine.tasks.models import (
    ArtifactInfo,
    BaseTaskInputArtifact,
    ExtraExternalRepository,
    InputArtifactMultiple,
    InputCollectionSingle,
    LookupMultiple,
    LookupSingle,
)
from debusine.web.views.base import TemplateWidget, Widget
from debusine.web.views.view_utils import highlight_as_yaml


def render_artifact(
    value: BaseTaskInputArtifact, context: Context
) -> SafeString:
    """Render a BaseInputArtifactSingle."""
    # Try to access a cached version of the artifact
    artifact: Artifact | None
    if (artifact := getattr(value, "_artifact", None)) is None:
        # If it fails, try to look it up
        try:
            artifact = Artifact.objects.get(pk=value.artifact_id)
            if hasattr(value, "_artifact"):
                value._artifact = artifact
        except Artifact.DoesNotExist:
            artifact = None

    assert context.template is not None
    template = context.template.engine.get_template(
        "web/_input-field-row-artifact.html"
    )
    with context.update({"value": value, "artifact": artifact}):
        return template.render(context)


def render_collection(
    value: InputCollectionSingle, context: Context
) -> SafeString:
    """Render an InputCollectionSingle."""
    # Try to access a cached version of the collection
    collection: Collection | None
    if (collection := getattr(value, "_collection", None)) is None:
        # If it fails, try to look it up
        try:
            collection = Collection.objects.get(pk=value.collection_id)
            if hasattr(value, "_collection"):
                value._collection = collection
        except Collection.DoesNotExist:
            collection = None

    assert context.template is not None
    template = context.template.engine.get_template(
        "web/_input-field-row-collection.html"
    )
    with context.update({"value": value, "collection": collection}):
        return template.render(context)


def render_result_label(label: str) -> SafeString:
    """Render a label from a lookup result."""
    return format_html("<code>{label}</code>", label=label)


def render_lookup(lookup: LookupSingle | LookupMultiple) -> SafeString:
    """Render a lookup."""
    match lookup:
        case int() | str():
            return format_html("<code>{value}</code>", value=lookup)
        case LookupMultiple():
            return highlight_as_yaml(lookup.export())
        case _:
            return highlight_as_yaml(lookup)


class InputFieldWidget[ValueType](Widget):
    """Base for widgets rendering the value of an input field."""

    def __init__(self, field: inputs.TaskInput[Any], value: ValueType) -> None:
        """
        Initialize common members.

        :param field: the input field
        :param value: the current value of the input field
        """
        self.name = field.name
        self.verbose_name = (
            field.verbose_name or field.name.replace("_", " ").capitalize()
        )
        self.help_text = field.help_text
        self.field = field
        self.value = value

    def render_details(self) -> SafeString:
        """Get the text for the tooltip on the field name."""
        result = SafeString()

        if self.help_text and (help_text := self.help_text.rstrip()):
            if help_text[-1] != ".":
                help_text += "."
            result += format_html(
                """<p class="mb-3 fst-italic">{description}</p>""",
                description=help_text,
            )

        result += SafeString("<dl>")
        for label, content in self.render_details_rows():
            result += format_html(
                "<dt>{label}:</dt><dd>{content}</dd>",
                label=label,
                content=content,
            )
        result += SafeString("</dl>")
        return result

    def render_row_title(self, context: Context) -> SafeString:
        """Render the title cell of the result row."""
        assert context.template is not None
        template = context.template.engine.get_template(
            "web/_input-field-row-title.html"
        )
        with context.update({"widget": self}):
            return template.render(context)

    def render_details_rows(self) -> Generator[tuple[SafeString, SafeString]]:
        """Render term, description pairs for the details DL element."""
        yield (
            SafeString("Defined as"),
            format_html("<code>{name}</code>", name=self.name),
        )

    def render_value_rows(self, _: Context) -> Generator[SafeString]:
        """Render the rows for the field value."""
        serialized = self.field.serialize_value(self.value)
        yield highlight_as_yaml(serialized)

    @override
    def render(self, context: Context) -> str:
        if not (rows := list(self.render_value_rows(context))):
            return SafeString()
        with context.update({"rows": rows}):
            result = format_html(
                "<tr>{title}<td>{row}</td></tr>",
                title=self.render_row_title(context),
                row=rows[0],
            )
            for row in rows[1:]:
                result += format_html("<tr><td>{row}</td></tr>", row=row)
        return result

    @classmethod
    def create(
        cls, container: inputs.TaskFieldContainer, field: inputs.TaskInput[Any]
    ) -> "InputFieldWidget[Any]":
        """Get the widget for an input field."""
        if not container.field_is_resolved(field.name):
            return UnresolvedFieldWidget(field, None)

        value = field.get_value(container)
        match field:
            case (
                inputs.SingleInput()
                | inputs.OptionalSingleInput()
                | inputs.EnvironmentInput()
                | inputs.OptionalEnvironmentInput()
                | dbinputs.ArtifactInput()
            ):
                return ArtifactInputFieldWidget(field, value)

            case inputs.SingleInputList():
                return ArtifactInputListFieldWidget(field, value)

            case inputs.MultiInput() | inputs.UploadArtifactsInput():
                return InputArtifactMultipleFieldWidget(field, value)

            # TODO: list[InputArtifactMultiple]
            # case inputs.MultiInputList():
            #    return InputFieldWidget(field, value)

            # TODO: tuple[InputCollectionSingle, InputCollectionSingle | None]
            # case inputs.SuiteArchiveInput():
            #    return InputFieldWidget(field, value)

            case inputs.ExtraRepositoriesInput():
                return ExtraExternalRepositoryFieldWidget(field, value)

            case inputs.DebusineFQDNInput() | inputs.BuildArchitectureInput():
                return CodeStringInputFieldWidget(field, value)

            case (
                dbinputs.CollectionInput()
                | dbinputs.TaskConfigurationCollection()
            ):
                return CollectionInputFieldWidget(field, value)

            case _:
                return InputFieldWidget(field, value)


class CodeStringInputFieldWidget(InputFieldWidget[str | None]):
    """Widget for string values rendered inside ``<code>`` tags."""

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        yield format_html("<code>{value}</code>", value=self.value)


class ArtifactInputFieldWidget(InputFieldWidget[ArtifactInfo | None]):
    """Widget for ArtifactInfo values."""

    @override
    def render_details_rows(self) -> Generator[tuple[SafeString, SafeString]]:
        yield from super().render_details_rows()
        if self.value is None:
            return
        yield (
            SafeString("Task input field"),
            render_result_label(self.value.label),
        )
        yield SafeString("Lookup"), render_lookup(self.value.lookup)
        # TODO: highlight value.data as YAML in a tooltip or popup?

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        if self.value is None:
            return
        yield render_artifact(self.value, context)


class ArtifactInputListFieldWidget(InputFieldWidget[list[ArtifactInfo]]):
    """Widget for list[ArtifactInfo] values."""

    @override
    def render_details_rows(self) -> Generator[tuple[SafeString, SafeString]]:
        yield from super().render_details_rows()
        if not self.value:
            return
        for value in self.value:
            yield (
                format_html(
                    "Task input field {label}",
                    label=render_result_label(value.label),
                ),
                render_lookup(value.lookup),
            )

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        if not self.value:
            return
        for value in self.value:
            yield render_artifact(value, context)


class InputArtifactMultipleFieldWidget(InputFieldWidget[InputArtifactMultiple]):
    """Widget for InputArtifactMultiple values."""

    @override
    def render_details_rows(self) -> Generator[tuple[SafeString, SafeString]]:
        yield from super().render_details_rows()
        yield (
            SafeString("Task input field"),
            render_result_label(self.value.label),
        )
        yield SafeString("Lookup"), render_lookup(self.value.lookup)

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        if not self.value.artifacts:
            return
        for value in self.value.artifacts:
            yield render_artifact(value, context)


class CollectionInputFieldWidget(
    InputFieldWidget[InputCollectionSingle | None]
):
    """Widget for InputCollectionSingle values."""

    @override
    def render_details_rows(self) -> Generator[tuple[SafeString, SafeString]]:
        yield from super().render_details_rows()
        if self.value is None:
            return
        yield (
            SafeString("Task input field"),
            render_result_label(self.value.label),
        )
        yield SafeString("Lookup"), render_lookup(self.value.lookup)
        # TODO: highlight value.data as YAML in a tooltip or popup?

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        if self.value is None:
            return
        yield render_collection(self.value, context)


class ExtraExternalRepositoryFieldWidget(
    InputFieldWidget[list[ExtraExternalRepository] | None]
):
    """Widget for ExtraExternalRepositoryFieldWidget values."""

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        if self.value is None:
            return
        for repo in self.value:
            yield format_html(
                """
                <details>
                <summary>deb {url} {suite} {components}</summary>
                <pre>{repo}</pre>
                </details>
                """,
                url=repo.url,
                suite=repo.suite,
                components=" ".join(repo.components) if repo.components else "",
                repo=repo.as_deb822_source(),
            )


class UnresolvedFieldWidget(InputFieldWidget[None]):
    """Widget for unresolved fields."""

    @override
    def render_value_rows(self, context: Context) -> Generator[SafeString]:
        yield SafeString("unresolved")


class TaskInputsWidget(TemplateWidget):
    """Render input fields for a task."""

    template_name: str = "web/_input-fields.html"

    def __init__(
        self,
        task: DBTask[Any, Any],
        element_id: str = "task-input-fields",
        internal: bool = False,
    ) -> None:
        """
        Initialize a TaskInputsWidget.

        :param task: task with the input fields to render
        :param class_name: class name of the toplevel element
        """
        self.task = task
        self.element_id = element_id
        self.internal = internal
        self.fields: list[InputFieldWidget[Any]] = []
        for cont, field in sorted(
            self.task.iter_all_fields(), key=lambda x: x[1].name
        ):
            if field.hidden:
                continue
            if self.internal != field.internal:
                continue
            self.fields.append(InputFieldWidget.create(cont, field))

    def __bool__(self) -> bool:
        """Check if there are input fields to display."""
        return bool(self.fields)
