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

"""Useful support code for Debusine views."""

import abc
import json
from dataclasses import dataclass
from typing import Any, override

import pygments
import pygments.formatters
import pygments.lexers
import yaml
from django.core.serializers.json import DjangoJSONEncoder
from django.template.defaultfilters import filesizeformat
from django.utils.html import format_html
from django.utils.safestring import SafeString

from debusine.db.models import FileInArtifact


class UIDumper(yaml.SafeDumper):
    """A YAML dumper that represents multi-line strings in the literal style."""

    @override
    def represent_scalar(
        self, tag: str, value: Any, style: str | None = None
    ) -> yaml.ScalarNode:
        """Represent multi-line strings in the literal style."""
        if style is None and "\n" in value:
            style = "|"
        return super().represent_scalar(tag, value, style=style)


def syntax_highlight(lexer_name: str, data: str) -> SafeString:
    """Return syntax-highlighted HTML for data using Pygments lexer."""
    formatter = pygments.formatters.HtmlFormatter(
        cssclass="formatted-content", linenos=False
    )
    lexer = pygments.lexers.get_lexer_by_name(lexer_name)
    return SafeString(pygments.highlight(data, lexer, formatter))


def highlight_as_yaml(data: Any, sort_keys: bool = True) -> SafeString:
    """Return syntax-highlighted HTML for data serialized as YAML."""
    dump = yaml.dump(data, Dumper=UIDumper, sort_keys=sort_keys)
    return syntax_highlight("yaml", dump)


def highlight_as_json(data: Any) -> SafeString:
    """Return syntax-highlighted HTML for data serialized as JSON."""
    dump = json.dumps(data, cls=DjangoJSONEncoder, indent=2)
    return syntax_highlight("json", dump)


@dataclass(slots=True)
class FileInArtifactError(Exception):
    """Base class for FileInArtifact errors."""

    file_in_artifact: FileInArtifact

    @abc.abstractmethod
    def to_html(self) -> SafeString:
        """Return a user-facing HTML error message."""


@dataclass(slots=True)
class IncompleteFileInArtifactError(FileInArtifactError):
    """Attempted to read an incomplete file."""

    @override
    def to_html(self) -> SafeString:
        """Return an HTML message indicating the file is still uploading."""
        return format_html(
            'File "{path}" has not completed uploading.',
            path=self.file_in_artifact.path,
        )


@dataclass(slots=True)
class TooBigFileInArtifactError(FileInArtifactError):
    """Attempted to read a too big file."""

    max_size: int

    @override
    def to_html(self) -> SafeString:
        """Return an HTML message with links to download or view the file."""
        return format_html(
            'File too big to display (size: {size} maximum size: {max_size}). '
            '<a href="{download_url}">Download</a> or '
            '<a href="{raw_url}">view</a> it raw.',
            download_url=self.file_in_artifact.get_absolute_url_download(),
            raw_url=self.file_in_artifact.get_absolute_url_raw(),
            size=filesizeformat(self.file_in_artifact.file.size),
            max_size=filesizeformat(self.max_size),
        )


def read_file(file_in_artifact: FileInArtifact, *, max_file_size: int) -> bytes:
    """
    Read contents if file is completed and size <= max_file_size.

    Raise exception if not possible to read the file.
    """
    if not file_in_artifact.complete:
        raise IncompleteFileInArtifactError(file_in_artifact=file_in_artifact)

    file = file_in_artifact.file
    if file.size > max_file_size:
        raise TooBigFileInArtifactError(
            file_in_artifact=file_in_artifact,
            max_size=max_file_size,
        )

    scope = file_in_artifact.artifact.workspace.scope

    with (
        scope.download_file_backend(file)
        .get_entry(file)
        .get_temporary_local_path() as path
    ):
        return path.read_bytes()
