from django.conf import settings
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import gettext as _

from wagtail.admin.utils import get_latest_str, get_user_display_name
from wagtail.utils.timestamps import render_timestamp


class BaseLock:
    """
    Holds information about a lock on an object.

    Returned by LockableMixin.get_lock() (or Page.get_lock()).
    """

    def __init__(self, object):
        from wagtail.models import Page

        self.object = object
        self.is_page = isinstance(object, Page)
        # Use the base page's model name instead of the specific type for brevity
        self.model_name = (Page if self.is_page else object)._meta.verbose_name

    def for_user(self, user):
        """
        Returns True if the lock applies to the given user.
        """
        return NotImplemented

    def get_message(self, user):
        """
        Returns a message to display to the given user describing the lock.
        """
        return None

    def get_icon(self, user):
        """
        Returns the name of the icon to use for the lock.
        """
        return "lock"

    def get_locked_by(self, user):
        """
        Returns a string that represents the user or mechanism that locked the object.
        """
        return _("Locked")

    def get_description(self, user):
        """
        Returns a description of the lock to display to the given user.
        """
        return capfirst(
            _("No one can make changes while the %(model_name)s is locked")
            % {"model_name": self.model_name}
        )

    def get_context_for_user(self, user, parent_context=None):
        """
        Returns a context dictionary to use in templates for the given user.
        """
        return {
            "locked": self.for_user(user),
            "message": self.get_message(user),
            "icon": self.get_icon(user),
            "locked_by": self.get_locked_by(user),
            "description": self.get_description(user),
        }


class BasicLock(BaseLock):
    """
    A lock that is enabled when the "locked" attribute of an object is True.

    The object may be editable by a user depending on whether the locked_by field is set
    and if WAGTAILADMIN_GLOBAL_EDIT_LOCK is not set to True.
    """

    def for_user(self, user):
        global_edit_lock = getattr(settings, "WAGTAILADMIN_GLOBAL_EDIT_LOCK", None)
        return global_edit_lock or user.pk != self.object.locked_by_id

    def get_message(self, user):
        title = get_latest_str(self.object)

        if self.object.locked_by_id == user.pk:
            if self.object.locked_at:
                return format_html(
                    # nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
                    _(
                        "<b>'{title}' was locked</b> by <b>you</b> on <b>{datetime}</b>."
                    ),
                    title=title,
                    datetime=render_timestamp(self.object.locked_at),
                )

            else:
                return format_html(
                    # nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
                    _("<b>'{title}' is locked</b> by <b>you</b>."),
                    title=title,
                )
        else:
            if self.object.locked_by and self.object.locked_at:
                return format_html(
                    # nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
                    _(
                        "<b>'{title}' was locked</b> by <b>{user}</b> on <b>{datetime}</b>."
                    ),
                    title=title,
                    user=get_user_display_name(self.object.locked_by),
                    datetime=render_timestamp(self.object.locked_at),
                )
            else:
                # Object was probably locked with an old version of Wagtail, or a script
                return format_html(
                    # nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
                    _("<b>'{title}' is locked</b>."),
                    title=title,
                )

    def get_locked_by(self, user):
        if self.object.locked_by_id == user.pk:
            return _("Locked by you")
        if self.object.locked_by_id:
            return _("Locked by another user")
        return super().get_locked_by(user)

    def get_description(self, user):
        if self.object.locked_by_id == user.pk:
            return capfirst(
                _("Only you can make changes while the %(model_name)s is locked")
                % {"model_name": self.model_name}
            )
        if self.object.locked_by_id:
            return capfirst(
                _("Only %(user)s can make changes while the %(model_name)s is locked")
                % {
                    "user": get_user_display_name(self.object.locked_by),
                    "model_name": self.model_name,
                }
            )
        return super().get_description(user)


class WorkflowLock(BaseLock):
    """
    A lock that requires the user to pass the Task.locked_for_user test on the given workflow task.
    """

    def __init__(self, object, task):
        super().__init__(object)
        self.task = task

    def for_user(self, user):
        return self.task.locked_for_user(self.object, user)

    def get_message(self, user):
        if self.for_user(user):
            current_workflow_state = self.object.current_workflow_state
            if (
                current_workflow_state
                and len(current_workflow_state.all_tasks_with_status()) == 1
            ):
                # If only one task in workflow, show simple message
                workflow_info = capfirst(
                    _("This %(model_name)s is currently awaiting moderation.")
                    % {"model_name": self.model_name}
                )
            else:
                workflow_info = format_html(
                    # nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
                    _(
                        "This {model_name} is awaiting <b>'{task_name}'</b> in the <b>'{workflow_name}'</b> workflow."
                    ),
                    model_name=self.model_name,
                    task_name=self.task.name,
                    workflow_name=current_workflow_state.workflow.name,
                )
                # Make sure message is correctly capitalised even if it
                # starts with model_name.
                workflow_info = mark_safe(capfirst(workflow_info))

            reviewers_info = capfirst(
                _("Only reviewers for this task can edit the %(model_name)s.")
                % {"model_name": self.model_name}
            )

            return mark_safe(workflow_info + " " + reviewers_info)

    def get_icon(self, user, can_lock=False):
        if can_lock:
            return "lock-open"
        return super().get_icon(user)

    def get_locked_by(self, user, can_lock=False):
        if can_lock:
            return _("Unlocked")
        return _("Locked by workflow")

    def get_description(self, user, can_lock=False):
        if can_lock:
            return capfirst(
                _(
                    "Reviewers can edit this %(model_name)s – lock it to prevent other reviewers from editing"
                )
                % {"model_name": self.model_name}
            )
        return capfirst(
            _("Only reviewers can edit and approve the %(model_name)s")
            % {"model_name": self.model_name}
        )

    def get_context_for_user(self, user, parent_context=None):
        context = super().get_context_for_user(user, parent_context)
        # BasicLock can still be applied on top of WorkflowLock, so we need to
        # check if the user can lock the object based on the parent context.
        # We're utilising the parent context instead of self.task.user_can_lock()
        # because the latter does not take into account the user's permissions,
        # while the parent context does and also checks self.task.user_can_lock().
        if parent_context and "user_can_lock" in parent_context:
            can_lock = parent_context.get("user_can_lock", False)
            context.update(
                {
                    "icon": self.get_icon(user, can_lock),
                    "locked_by": self.get_locked_by(user, can_lock),
                    "description": self.get_description(user, can_lock),
                }
            )
        return context


class ScheduledForPublishLock(BaseLock):
    """
    A lock that occurs when something is scheduled to be published.

    This prevents it becoming difficult for users to see which version is going to be published.
    Nobody can edit something that's scheduled for publish.
    """

    def for_user(self, user):
        return True

    def get_message(self, user):
        scheduled_revision = self.object.scheduled_revision

        message = format_html(
            # nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
            _(
                "{model_name} '{title}' is locked and has been scheduled to go live at {datetime}"
            ),
            model_name=self.model_name,
            title=scheduled_revision.object_str,
            datetime=render_timestamp(scheduled_revision.approved_go_live_at),
        )
        return mark_safe(capfirst(message))

    def get_locked_by(self, user):
        return _("Locked by schedule")

    def get_description(self, user):
        return _("Currently locked and will go live on the scheduled date")
