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

"""Core data types for decopy"""

from __future__ import annotations

import dataclasses
import typing as t

from debian.copyright import LicenseParagraph

from decopy.common._regex import rex

# Constants for license identification
UNKNOWN = "Unknown"
UNKNOWN_COPYRIGHTED = "UnknownCopyrighted"


@t.final
class License:
    """License representation for copyright files."""

    __slots__ = ("name", "stored")

    def __init__(self, name: str) -> None:
        self.name: str = name
        self.stored: LicenseParagraph | None = None

    @t.override
    def __str__(self) -> str:
        return self.name


class LicenseRegistry:
    """A per-run store of License instances, keyed by name.

    One registry is created per run and shared between the consumers that read
    or write a license's stored text.
    """

    def __init__(self) -> None:
        self._licenses: dict[str, License] = {}

    def get(self, name: str) -> License:
        license_ = self._licenses.get(name)
        if license_ is None:
            license_ = License(name)
            self._licenses[name] = license_
        return license_


@t.final
class YearRange:
    """Year range representation for copyright dates."""

    __slots__ = ("high", "low")

    def __init__(
        self,
        low: str | int | None = None,
        high: str | int | None = None,
    ) -> None:
        low_value = 0 if low is None else int(low)
        high_value = low_value if high is None else int(high)

        if low_value > high_value:
            low_value, high_value = high_value, low_value
        self.low: int = low_value
        self.high: int = high_value

    def __contains__(self, year: str | int) -> bool:
        value = int(year)
        return self.low <= value <= self.high

    def add(self, year: str | int | None) -> YearRange:
        value = int(year if year is not None else 0)
        if not value:
            return self
        if not self.low or value < self.low:
            self.low = value
        if not self.high or self.high < value:
            self.high = value
        return self

    def newer(self, other: YearRange) -> bool:
        if self.high and other.high:
            return other.high > self.high
        return bool(not self.high and other.high)

    def merge(self, other: YearRange) -> YearRange:
        _ = self.add(other.low)
        _ = self.add(other.high)
        return self

    def __bool__(self) -> bool:
        return bool(self.low)

    @t.override
    def __str__(self) -> str:
        if not self.low:
            return ""
        if self.low == self.high:
            return str(self.low)
        return f"{self.low}-{self.high}"


@t.final
class CopyrightHolder:
    """Copyright holder representation for copyright files."""

    __slots__ = ("email", "name", "years")

    def __init__(self, name: str, email: str, years: YearRange | None = None) -> None:
        self.name = name
        self.email = email
        self.years = years if years is not None else YearRange()

    def merge(self, other: CopyrightHolder) -> t.Self:
        if other.name and self.years.newer(other.years):
            self.name = other.name
        if not self.email and other.email:
            self.email = other.email
        _ = self.years.merge(other.years)
        return self

    @property
    def person(self) -> str:
        result = self.name
        if self.name and self.email:
            result += " "
        if self.email:
            result += f"<{self.email}>"
        return result

    @t.override
    def __str__(self) -> str:
        result = str(self.years)
        result += ", " if result else ""
        result += self.person
        return result

    @t.override
    def __repr__(self) -> str:
        return str(self)

    def as_tuple(self) -> tuple[str, str, str]:
        """Return a tuple representation of the CopyrightHolder."""
        years_str = str(self.years) if self.years else ""
        return (self.name, self.email, years_str)


@dataclasses.dataclass(frozen=True)
class Options:
    """Options dataclass for decopy configuration.

    Frozen: the run's settings are resolved once (decopy._resolve_run_options
    settles the git-changed scope and Files-Excluded patterns into a fresh
    Options) and then read as an invariant by every downstream consumer. Build a
    modified copy with dataclasses.replace rather than mutating in place.
    """

    mode: t.Literal["full", "partial", "changed"]
    copyright_file: str
    debug: bool
    text: bool
    group_by: t.Literal["license", "copyright"]
    split_on_license: bool
    split_debian: bool
    glob: bool
    progress: bool
    jobs: int
    output: str
    root: str
    files: list[str]
    exclude_fullname_re: rex.Pattern
    exclude_special_re: rex.Pattern
    exclude_file_re: rex.Pattern
    exclude_directory_re: rex.Pattern
    spdx: bool = False
    explain: bool = False
    since: str = ""
    expand_archives: bool = True
    max_archive_depth: int = 1
    max_archive_member_size: int = 10 * 1024 * 1024
    use_gitignore: bool = True
    files_excluded: tuple[str, ...] = ()
    fill_licenses: bool = True

    @property
    def scoped_to_subset(self) -> bool:
        """Whether the run is limited to a file subset rather than the whole tree.

        Partial and changed modes both restrict processing and removal to the
        included files; only full mode walks and reports over everything.
        """
        return self.mode in ("partial", "changed")
