# This file is part of beets.
# Copyright 2025, Sarunas Nejus, Henry Oberholtzer.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""Dataclasses for managing artist credits and tracklists from Discogs."""

from __future__ import annotations

import re
from dataclasses import asdict, dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, NamedTuple

from beets import config

from .types import ArtistInfo

if TYPE_CHECKING:
    from confuse import ConfigView

    from beets.autotag.hooks import TrackInfo

    from . import DiscogsPlugin
    from .types import Artist, Track, TracklistInfo

DISAMBIGUATION_RE = re.compile(r" \(\d+\)")


@dataclass
class ArtistState:
    """Represent Discogs artist credits.

    This object centralizes the plugin's policy for which Discogs artist fields
    to prefer (name vs. ANV), how to treat 'Various', how to format join
    phrases, and how to separate featured artists. It exposes both per-artist
    components and fully joined strings for common tag targets like 'artist' and
    'artist_credit'.
    """

    class ValidArtist(NamedTuple):
        """A normalized, render-ready artist entry extracted from Discogs data.

        Instances represent the subset of Discogs artist information needed for
        tagging, including the join token following the artist and whether the
        entry is considered a featured appearance.
        """

        id: str
        name: str
        credit: str
        join: str
        is_feat: bool

        def get_artist(self, property_name: str) -> str:
            """Return the requested display field with its trailing join token.

            The join token is normalized so commas become ', ' and other join
            phrases are surrounded with spaces, producing a single fragment that
            can be concatenated to form a full artist string.
            """
            join = {",": ", ", "": ""}.get(self.join, f" {self.join} ")
            return f"{getattr(self, property_name)}{join}"

    raw_artists: list[Artist]
    use_anv: bool
    use_credit_anv: bool
    featured_string: str
    should_strip_disambiguation: bool

    @property
    def info(self) -> ArtistInfo:
        """Expose the state in the shape expected by downstream tag mapping."""
        return {k: getattr(self, k) for k in ArtistInfo.__annotations__}  # type: ignore[return-value]

    def strip_disambiguation(self, text: str) -> str:
        """Strip Discogs disambiguation suffixes from an artist or label string.

        This removes Discogs-specific numeric suffixes like 'Name (5)' and can
        be applied to multi-artist strings as well (e.g., 'A (1) & B (2)'). When
        the feature is disabled, the input is returned unchanged.
        """
        if self.should_strip_disambiguation:
            return DISAMBIGUATION_RE.sub("", text)
        return text

    @cached_property
    def valid_artists(self) -> list[ValidArtist]:
        """Build the ordered, filtered list of artists used for rendering.

        The resulting list normalizes Discogs entries by:
        - substituting the configured 'Various Artists' name when Discogs uses
          'Various'
        - choosing between name and ANV according to plugin settings
        - excluding non-empty roles unless they indicate a featured appearance
        - capturing join tokens so the original credit formatting is preserved
        """
        va_name = config["va_name"].as_str()
        return [
            self.ValidArtist(
                str(a["id"]),
                self.strip_disambiguation(anv if self.use_anv else name),
                self.strip_disambiguation(anv if self.use_credit_anv else name),
                a["join"].strip(),
                is_feat,
            )
            for a in self.raw_artists
            if (
                (name := va_name if a["name"] == "Various" else a["name"])
                and (anv := a["anv"] or name)
                and (
                    (is_feat := ("featuring" in a["role"].lower()))
                    or not a["role"]
                )
            )
        ]

    @property
    def artists_ids(self) -> list[str]:
        """Return Discogs artist IDs for all valid artists, preserving order."""
        return [a.id for a in self.valid_artists]

    @property
    def artist_id(self) -> str:
        """Return the primary Discogs artist ID."""
        return self.artists_ids[0]

    @property
    def artists(self) -> list[str]:
        """Return the per-artist display names used for the 'artist' field."""
        return [a.name for a in self.valid_artists]

    @property
    def artists_credit(self) -> list[str]:
        """Return the per-artist display names used for the credit field."""
        return [a.credit for a in self.valid_artists]

    @property
    def artist(self) -> str:
        """Return the fully rendered artist string using display names."""
        return self.join_artists("name")

    @property
    def artist_credit(self) -> str:
        """Return the fully rendered artist credit string."""
        return self.join_artists("credit")

    def join_artists(self, property_name: str) -> str:
        """Render a single artist string with join phrases and featured artists.

        Non-featured artists are concatenated using their join tokens. Featured
        artists are appended after the configured 'featured' marker, preserving
        Discogs order while keeping featured credits separate from the main
        artist string.
        """
        non_featured = [a for a in self.valid_artists if not a.is_feat]
        featured = [a for a in self.valid_artists if a.is_feat]

        artist = "".join(a.get_artist(property_name) for a in non_featured)
        if featured:
            if "feat" not in artist:
                artist += f" {self.featured_string} "

            artist += ", ".join(a.get_artist(property_name) for a in featured)

        return artist

    @classmethod
    def from_config(
        cls,
        config: ConfigView,
        artists: list[Artist],
        for_album_artist: bool = False,
    ) -> ArtistState:
        return cls(
            artists,
            config["anv"]["album_artist" if for_album_artist else "artist"].get(
                bool
            ),
            config["anv"]["artist_credit"].get(bool),
            config["featured_string"].as_str(),
            config["strip_disambiguation"].get(bool),
        )


@dataclass
class TracklistState:
    index: int = 0
    index_tracks: dict[int, str] = field(default_factory=dict)
    tracks: list[TrackInfo] = field(default_factory=list)
    divisions: list[str] = field(default_factory=list)
    next_divisions: list[str] = field(default_factory=list)
    mediums: list[str | None] = field(default_factory=list)
    medium_indices: list[str | None] = field(default_factory=list)

    @property
    def info(self) -> TracklistInfo:
        return asdict(self)  # type: ignore[return-value]

    @classmethod
    def build(
        cls,
        plugin: DiscogsPlugin,
        clean_tracklist: list[Track],
        albumartistinfo: ArtistState,
    ) -> TracklistState:
        state = cls()
        for track in clean_tracklist:
            if track["position"]:
                state.index += 1
                if state.next_divisions:
                    state.divisions += state.next_divisions
                    state.next_divisions.clear()
                track_info, medium, medium_index = plugin.get_track_info(
                    track, state.index, state.divisions, albumartistinfo
                )
                track_info.track_alt = track["position"]
                state.tracks.append(track_info)
                state.mediums.append(medium or None)
                state.medium_indices.append(medium_index or None)
            else:
                state.next_divisions.append(track["title"])
                try:
                    state.divisions.pop()
                except IndexError:
                    pass
                state.index_tracks[state.index + 1] = track["title"]
        return state
