#!/usr/bin/env python3
# Copyright: 2026, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

"""Grouping policy as an injected strategy.

How files become copyright paragraphs is an axis decopy is expected to vary
(by license today, by copyright today, perhaps by directory or vendor later).
This module gathers that policy behind a single GroupingStrategy so the
consumers (grouping, file_policy, group, copyright_processor) depend on the
abstraction rather than on scattered group_by/split_* conditionals.

The two concrete strategies differ only in how copyright participates:

  * LicenseGrouping drops the copyright slot from the group key and keeps a
    misplaced file only when its full key matches the group key.
  * CopyrightGrouping keeps the copyright slot and additionally rescues a file
    whose key matches the group key with copyright ignored.

The split_on_license/split_debian rules (splitting_path) and the sort band are
shared, so they live on the base class; a future mode that needs a different
boundary or ordering overrides them.

The strategy is selected once per consumer via grouping_strategy_for(options):
grouping.make_group_key (copyright slot), file_policy.splitting_path (split
rules), group.Group.sort_key (sort band) and copyright_processor's
remove_misplaced_files (keep/drop fallback) all depend on it.
"""

from __future__ import annotations

import abc
import typing as t

from .core_types import UNKNOWN, UNKNOWN_COPYRIGHTED

if t.TYPE_CHECKING:
    from .core_types import Options
    from .grouping import GroupKey
    from .nodes import FileInfo


class GroupingStrategy(abc.ABC):
    """Owns the policy that turns files into copyright-paragraph groups."""

    @abc.abstractmethod
    def key_copyright_slot(
        self, copyright_key: tuple[str, ...]
    ) -> tuple[str, ...] | None:
        """Return the copyright component of a group key, or None to omit it."""

    @abc.abstractmethod
    def keeps_misplaced(
        self,
        file_key: GroupKey,
        group_key: GroupKey,
        group_key_no_copyright: GroupKey,
    ) -> bool:
        """Whether a file already placed in a group still belongs there."""

    def splitting_path(self, node: FileInfo, options: Options) -> str | None:
        """Path identifier used to split files into groups.

        A node that is a split boundary (see FileInfo.is_split_boundary)
        contributes its own path; otherwise the node delegates to its parent. A
        parentless non-boundary node (a file at the root) has no splitting path.
        """
        if node.is_split_boundary(options):
            return node.fullname
        if node.parent:
            return self.splitting_path(node.parent, options)
        return None

    def sort_band(self, license_name: str, path_key: str, options: Options) -> int:
        """Primary sort band for a group: 0 normal, 1 unknown, 2 debian.

        Unknown licenses override the debian band (they share band 1), matching
        the historical precedence in Group.sort_key.
        """
        band = 0
        if options.split_debian and path_key == "debian":
            band = 2
        if license_name in {UNKNOWN, UNKNOWN_COPYRIGHTED}:
            band = 1
        return band


class LicenseGrouping(GroupingStrategy):
    """Group by license: copyright never participates in the key."""

    def key_copyright_slot(
        self, copyright_key: tuple[str, ...]
    ) -> tuple[str, ...] | None:
        _ = copyright_key
        return None

    def keeps_misplaced(
        self,
        file_key: GroupKey,
        group_key: GroupKey,
        group_key_no_copyright: GroupKey,
    ) -> bool:
        _ = group_key_no_copyright
        return file_key == group_key


class CopyrightGrouping(GroupingStrategy):
    """Group by copyright: the copyright slot participates, with a fallback."""

    def key_copyright_slot(
        self, copyright_key: tuple[str, ...]
    ) -> tuple[str, ...] | None:
        return copyright_key

    def keeps_misplaced(
        self,
        file_key: GroupKey,
        group_key: GroupKey,
        group_key_no_copyright: GroupKey,
    ) -> bool:
        return file_key in (group_key, group_key_no_copyright)


_LICENSE_GROUPING = LicenseGrouping()
_COPYRIGHT_GROUPING = CopyrightGrouping()


def grouping_strategy_for(options: Options) -> GroupingStrategy:
    """Select the grouping strategy named by options.group_by."""
    if options.group_by == "copyright":
        return _COPYRIGHT_GROUPING
    return _LICENSE_GROUPING
