"""Settings used to manage the processing and render of diffs.

Version Added:
    5.0.2
"""

from __future__ import annotations

import json
from dataclasses import dataclass, fields as dataclass_fields
from hashlib import sha256
from typing import Any, ClassVar, Dict, List, Optional, TYPE_CHECKING, cast

from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.utils.functional import cached_property
from djblets.siteconfig.models import SiteConfiguration

if TYPE_CHECKING:
    from django.http import HttpRequest
    from typelets.json import JSONDict

    from reviewboard.site.models import LocalSite


@dataclass
class DiffSettings:
    """Settings used to render a diff.

    These settings represent a combination of site configuration settings for
    the diff viewer and user-configured settings. They're used to control how
    the diff viewer displays diffs, covering syntax highlighting options,
    code safety checks, whitespace handling, and more.

    Version Added:
        5.0.2
    """

    #: The default for tabstop widths.
    #:
    #: Version Added:
    #:     7.0.4
    DEFAULT_TAB_SIZE: ClassVar[int] = 8

    ######################
    # Instance variables #
    ######################

    #: A mapping of code safety checker IDs to configurations.
    #:
    #: Type:
    #:     dict
    code_safety_configs: dict[str, dict[str, Any]]

    #: The number of lines of context to show around modifications in a chunk.
    #:
    #: Type:
    #:     int
    context_num_lines: int

    #: A custom mapping of file extensions to Pygments lexers.
    #:
    #: Type:
    #:     dict
    custom_pygments_lexers: dict[str, str]

    #: A list of file globs for which legacy whitespace rules should be used.
    #:
    #: Any file matching a pattern in this list will treat all whitespace as
    #: modifications to lines. Smart indentation handling and other features
    #: may behave differently.
    #:
    #: This is considered a legacy feature.
    #:
    #: Type:
    #:     list of str
    include_space_patterns: list[str]

    #: Whether to filter upstream changes out of an interdiff.
    #:
    #: When enabled, this will filter out upstream changes that may be
    #: introduced between two revisions of any given file in an interdiff.
    #:
    #: This defaults to ``True``.
    #:
    #: Version Added:
    #:     8.0
    #:
    #: Type:
    #:     bool
    interdiff_filtering: bool

    #: The number of files to include in each page of a diff.
    #:
    #: Type:
    #:     int
    paginate_by: int

    #: The maximum number of orphans to include on the last page of a diff.
    #:
    #: Orphans are extra files that would make up less than a full page. If
    #: there aren't more than this number of orphans, they'll be rolled up
    #: into the final page of the diff viewer.
    #:
    #: Type:
    #:     int
    paginate_orphans: int

    #: Whether to enable syntax highlighting.
    #:
    #: Type:
    #:     bool
    syntax_highlighting: bool

    #: The maximum number of lines in a file to allow syntax highlighting.
    #:
    #: If a file has lines beyond this threshold, syntax highlighting will
    #: be forcefully disabled.
    #:
    #: Type:
    #:     int
    syntax_highlighting_threshold: int

    #: The default tabstop width for diffs.
    #:
    #: Version Added:
    #:     7.0.4
    tab_size: int

    @classmethod
    def create(
        cls,
        *,
        user: (User | None) = None,
        local_site: (LocalSite | None) = None,
        request: (HttpRequest | None) = None,
        interdiff_filtering: bool = True,
        syntax_highlighting: (bool | None) = None,
    ) -> DiffSettings:
        """Create diff settings based on the provided arguments.

        This will compute settings based on any HTTP request, Local Site, or
        user provided. The rest will be filled out from the site configuration.

        Syntax highlighting will be enabled if it's both enabled in the site
        configuration and in the user's profile.

        If ``request`` is provided, its associated user and Local Site will
        be used as defaults if ``user`` or ``local_site`` are not provided.

        Version Added:
            8.0:
            Added the ``interdiff_filtering`` argument.

        Args:
            user (django.contrib.auth.models.User, optional):
                The user the settings should pertain to.

            local_site (reviewboard.site.models.LocalSite, optional):
                The Local Site that the settings should pertain to.

            request (django.http.HttpRequest, optional):
                The HTTP request from the client.

            interdiff_filtering (bool, optional):
                Whether to filter upstream changes out of an interdiff.

                When enabled, this will filter out upstream changes that may
                be introduced between two revisions of any given file in an
                interdiff.

                This defaults to ``True``.

                Version Added:
                    8.0

            syntax_highlighting (bool, optional):
                An explicit value for :py:attr:`syntax_highlighting`. If
                provided, the user and site settings will be ignored.

        Returns:
            DiffSettings:
            The settings for rendering the diff.
        """
        siteconfig = SiteConfiguration.objects.get_current()

        # If a request is passed, see if we need to extract information from
        # it to fill in missing attributes.
        if request is not None:
            if user is None and request.user.is_authenticated:
                # Satisfy the type checker.
                assert isinstance(request.user, User)

                user = request.user

            if local_site is None:
                local_site = getattr(request, 'local_site', None)

        # Set up config layers to allow Local Sites to override the global
        # settings. We may eventually want to add repository- and user-level
        # settings as well.
        config_layers: list[JSONDict] = []

        if (local_site and
            (local_site_config := local_site.extra_data.get('siteconfig'))):
            config_layers.append(local_site_config)

        # Figure out the default for syntax highlighting.
        if syntax_highlighting is None:
            syntax_highlighting = cast(
                bool,
                siteconfig.get('diffviewer_syntax_highlighting',
                               layers=config_layers))

            if syntax_highlighting and user and user.is_authenticated:
                # The server enables syntax highlighting. See if the user has
                # enabled or disabled it.
                try:
                    profile = user.get_profile()  # type: ignore
                    syntax_highlighting = profile.syntax_highlighting
                except ObjectDoesNotExist:
                    pass

            assert syntax_highlighting is not None

        tab_size = cast(
            Optional[int],
            siteconfig.get('diffviewer_default_tab_size',
                           layers=config_layers))

        # If the tab size is zero or unset, set to default.
        if not tab_size:
            tab_size = cls.DEFAULT_TAB_SIZE

        return cls(
            code_safety_configs=cast(
                Dict[str, Dict[str, Any]],
                siteconfig.get('code_safety_checkers',
                               layers=config_layers)
            ),
            context_num_lines=cast(
                int,
                siteconfig.get('diffviewer_context_num_lines',
                               layers=config_layers)),
            custom_pygments_lexers=cast(
                Dict[str, str],
                siteconfig.get('diffviewer_custom_pygments_lexers',
                               layers=config_layers)),
            include_space_patterns=cast(
                List[str],
                siteconfig.get('diffviewer_include_space_patterns',
                               layers=config_layers)),
            interdiff_filtering=interdiff_filtering,
            paginate_by=cast(
                int,
                siteconfig.get('diffviewer_paginate_by',
                               layers=config_layers)),
            paginate_orphans=cast(
                int,
                siteconfig.get('diffviewer_paginate_orphans',
                               layers=config_layers)),
            syntax_highlighting=syntax_highlighting,
            syntax_highlighting_threshold=cast(
                int,
                siteconfig.get('diffviewer_syntax_highlighting_threshold',
                               layers=config_layers)),
            tab_size=tab_size,
        )

    @cached_property
    def state_hash(self) -> str:
        """Return a hash of the current settings.

        This can be used as a component in cache keys and ETags to ensure
        that changes to diff settings trigger re-generations of diffs.

        This is calculated only once per instance. It won't take into account
        any setting changes since the first access.

        Type:
            str
        """
        return sha256(
            json.dumps(
                {
                    field.name: getattr(self, field.name)
                    for field in dataclass_fields(self)
                },
                sort_keys=True)
            .encode('utf-8')
        ).hexdigest()
