# -*- coding: utf-8; -*-
################################################################################
#
#  wuttaweb -- Web App for Wutta Framework
#  Copyright © 2024-2026 Lance Edgar
#
#  This file is part of Wutta Framework.
#
#  Wutta Framework is free software: you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the Free
#  Software Foundation, either version 3 of the License, or (at your option) any
#  later version.
#
#  Wutta Framework is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
#  more details.
#
#  You should have received a copy of the GNU General Public License along with
#  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tools for displaying simple data diffs
"""

import sqlalchemy as sa
from sqlalchemy import orm

from pyramid.renderers import render
from webhelpers2.html import HTML

from wuttjamaican.diffs import Diff


class WebDiff(Diff):
    """
    Simple diff class for the web app.

    This is based on the
    :class:`~wuttjamaican:wuttjamaican.diffs.Diff` class; it just
    tweaks :meth:`render_html()` to use the web template lookup
    engine.
    """

    cell_padding = None

    def render_html(self, template="/diff.mako", **kwargs):
        """
        Render the diff as HTML table.

        :param template: Name of template to render, if you need to
           override the default.

        :param \\**kwargs: Remaining kwargs are passed as context to
           the template renderer.

        :returns: HTML literal string
        """
        context = kwargs
        context["diff"] = self
        html = render(template, context)
        return HTML.literal(html)


class MergeDiff(WebDiff):
    """
    Special diff class for use when merging 2 records.  While based on
    :class:`WebDiff`, this class uses a different signature for the
    constructor.

    It shows the "removing" record, the "keeping" record, and also the
    "final" record showing the calculated result of the merge, with
    special highlighting where values would change on the kept record.

    :param config: The app :term:`config object`.

    :param removing_data: Dict of data for the "removing" record.

    :param new_data: Dict of data for the "keeping" record.

    :param new_data: Dict of "final" data for the kept record.

    :param \\**kwargs: Remaining kwargs are passed as-is to the
       :class:`WebDiff` constructor.
    """

    def __init__(self, config, removing_data, keeping_data, final_data, **kwargs):
        super().__init__(config, removing_data, keeping_data, **kwargs)
        self.removing_data = removing_data
        self.keeping_data = keeping_data
        self.final_data = final_data
        self.columns = ["field name", "removing", "keeping", "final"]

    def render_field_row(self, field):
        keep_diff = self.keeping_data.get(field) != self.removing_data.get(field)
        final_diff = self.final_data.get(field) != self.keeping_data.get(field)

        # TODO: there is a fair bit of duplication here, compared to
        # base class.  should maybe clean that up someday..

        kw = {}
        if self.cell_padding:
            kw["style"] = f"padding: {self.cell_padding}"
        td_field = HTML.tag("td", class_="field", c=field, **kw)

        td_old_value = HTML.tag(
            "td",
            c=self.render_old_value(field),
            **self.get_old_value_attrs(keep_diff),
        )

        td_new_value = HTML.tag(
            "td",
            c=self.render_new_value(field),
            **self.get_new_value_attrs(keep_diff),
        )

        td_final_value = HTML.tag(
            "td",
            c=self.render_final_value(field),
            **self.get_final_value_attrs(final_diff),
        )

        return HTML.tag("tr", c=[td_field, td_old_value, td_new_value, td_final_value])

    def get_final_value_attrs(
        self, is_diff
    ):  # pylint: disable=missing-function-docstring
        attrs = {}
        if is_diff:
            attrs["class_"] = "has-background-warning"
        return self.get_cell_attrs(**attrs)

    def render_final_value(self, field):  # pylint: disable=missing-function-docstring
        value = repr(self.final_data.get(field))
        return self.render_cell_value(value)


class VersionDiff(WebDiff):
    """
    Special diff class for use with version history views.  While
    based on :class:`WebDiff`, this class uses a different signature
    for the constructor.

    :param config: The app :term:`config object`.

    :param version: Reference to a Continuum version record object.

    :param \\**kwargs: Remaining kwargs are passed as-is to the
       :class:`WebDiff` constructor.
    """

    def __init__(self, config, version, **kwargs):
        import sqlalchemy_continuum as continuum  # pylint: disable=import-outside-toplevel
        from wutta_continuum.util import (  # pylint: disable=import-outside-toplevel
            render_operation_type,
        )

        self.version = version
        self.model_class = continuum.parent_class(type(self.version))
        self.mapper = sa.inspect(self.model_class)
        self.version_mapper = sa.inspect(type(self.version))
        self.title = kwargs.pop("title", self.model_class.__name__)

        self.operation_title = render_operation_type(self.version.operation_type)

        if "nature" not in kwargs:
            if (
                version.previous
                and version.operation_type == continuum.Operation.DELETE
            ):
                kwargs["nature"] = "delete"
            elif version.previous:
                kwargs["nature"] = "update"
            else:
                kwargs["nature"] = "create"

        if "fields" not in kwargs:
            kwargs["fields"] = self.get_default_fields()

        old_data = {}
        new_data = {}
        for field in kwargs["fields"]:
            if version.previous:
                old_data[field] = getattr(version.previous, field)
            new_data[field] = getattr(version, field)

        super().__init__(config, old_data, new_data, **kwargs)

    def get_default_fields(self):  # pylint: disable=missing-function-docstring
        fields = sorted(self.version_mapper.columns.keys())

        unwanted = [
            "transaction_id",
            "end_transaction_id",
            "operation_type",
        ]

        return [field for field in fields if field not in unwanted]

    def render_version_value(self, version, field, value):
        """
        Render the cell value HTML for a given version + field.

        This method is used to render both sides of the diff (old +
        new values).  It will just render the field value using a
        monospace font by default.  However:

        If the field is involved in a mapper relationship (i.e. it is
        the "foreign key" to a related table), the logic here will
        also (try to) traverse that show display text for the related
        object (if found).

        :param version: Reference to the Continuum version object.

        :param field: Name of the field, as string.

        :param value: Raw value for the field, as obtained from the
           version object.

        :returns: Rendered cell value as HTML literal
        """
        # first render normal span; this is our fallback but also may
        # be embedded within a more complex result.
        text = HTML.tag("span", c=[repr(value)], style="font-family: monospace;")

        # style to apply for bold human-friendly text (if applicable)
        bold = "margin-left: 2rem; font-style: italic; font-weight: bold;"

        # check for standard datetime field
        if self.mapper.has_property(field):
            prop = self.mapper.get_property(field)
            if isinstance(prop, orm.ColumnProperty):
                if len(prop.columns) == 1:
                    col = prop.columns[0]
                    if isinstance(col.type, sa.DateTime):
                        if value:
                            # render as local datetime w/ "time since" tooltip
                            display = HTML.tag(
                                "span",
                                c=self.app.render_datetime(value, html=True),
                                style=bold,
                            )
                            return HTML.tag("span", c=[text, display])

        # loop thru all mapped relationship props
        for prop in self.mapper.relationships:

            # we only want singletons
            if prop.uselist:
                continue

            # loop thru columns for prop
            # nb. there should always be just one colum for a
            # singleton prop, but technically a list is used, so no
            # harm in looping i assume..
            for col in prop.local_columns:

                # we only want the matching column
                if col.name != field:
                    continue

                # grab "related version" reference via prop key.  this
                # would be like a UserVersion for instance.
                if ref := getattr(version, prop.key):

                    # grab "related object" reference.  this would be
                    # like a User for instance.
                    if ref := getattr(ref, "version_parent", None):

                        # render text w/ related object as bold string
                        return HTML.tag(
                            "span",
                            c=[text, HTML.tag("span", c=[str(ref)], style=bold)],
                        )

        return text

    def render_old_value(self, field):
        if self.nature == "create":
            return ""
        value = self.old_value(field)
        return self.render_version_value(self.version.previous, field, value)

    def render_new_value(self, field):
        if self.nature == "delete":
            return ""
        value = self.new_value(field)
        return self.render_version_value(self.version, field, value)
