// Package autotag provides the shared primitives used by the
// MusicBrainz autotagger: deterministic album-group keys, scoring,
// and (in later phases) MB orchestration.
package autotag

import (
	"crypto/sha1" //nolint:gosec // non-crypto deterministic grouping key.
	"encoding/hex"
	"path/filepath"
	"strconv"
	"strings"
)

// GroupKey returns a deterministic album-group identifier for the
// file at filePath belonging to libraryID, carrying the given disc
// number.
//
// The key is a lower-case hex SHA-1 over
//
//	libraryID || 0 || normalized_parent_dir || 0 || disc_number
//
// where the parent directory is lower-cased and disc_number is
// normalized so an untagged disc (0) folds into disc 1 — see
// normalizeDiscNumber.  The folder is taken as the album boundary —
// including the album tag string would fragment albums whose tracks
// carry slightly different tags (`Abbey Road` vs `Abbey Road
// (Remastered 2009)`, etc.).  The album name is still surfaced in
// `tagging_items.album_name` for the review UI; it just doesn't
// decide grouping.
//
// Using SHA-1 matches the codebase's existing non-crypto
// deterministic-key convention; collision risk at album-group
// cardinality is irrelevant.
func GroupKey(
	libraryID int64,
	filePath string,
	discNumber int,
) string {
	parentDir := strings.ToLower(filepath.Dir(filePath))

	h := sha1.New() //nolint:gosec // see package doc — grouping only.
	h.Write([]byte(strconv.FormatInt(libraryID, 10)))
	h.Write([]byte{0})
	h.Write([]byte(parentDir))
	h.Write([]byte{0})
	h.Write([]byte(strconv.Itoa(normalizeDiscNumber(discNumber))))

	return hex.EncodeToString(h.Sum(nil))
}

// SyntheticGroupKey returns a deterministic identifier for a
// tag-clustered sub-group carved out of parentGroupKey by
// SplitMixedFolder — same SHA-1-over-null-separated-fields shape as
// GroupKey, but keyed on the cluster's (album, album-artist) tags
// instead of a directory, since a synthetic group's tracks don't
// share a directory boundary distinct from their siblings left
// behind in the parent folder.
func SyntheticGroupKey(parentGroupKey, albumName, albumArtist string) string {
	h := sha1.New() //nolint:gosec // see package doc — grouping only.
	h.Write([]byte(parentGroupKey))
	h.Write([]byte{0})
	h.Write([]byte(Normalize(albumName)))
	h.Write([]byte{0})
	h.Write([]byte(Normalize(albumArtist)))

	return hex.EncodeToString(h.Sum(nil))
}

// SyntheticTrackGroupKey returns a deterministic identifier for a
// single leftover track carved out of a mixed-bag folder by
// SplitMixedFolder's singleton fallback (autotag.SplitPlan). Keyed on
// the track's own audio_files id rather than its tags — two
// untagged leftover tracks would otherwise both normalize to the
// same empty (album, album-artist) pair and collide under
// SyntheticGroupKey.
func SyntheticTrackGroupKey(parentGroupKey string, audioFileID int64) string {
	h := sha1.New() //nolint:gosec // see package doc — grouping only.
	h.Write([]byte(parentGroupKey))
	h.Write([]byte{0})
	h.Write([]byte("track"))
	h.Write([]byte{0})
	h.Write([]byte(strconv.FormatInt(audioFileID, 10)))

	return hex.EncodeToString(h.Sum(nil))
}

// normalizeDiscNumber folds a missing/invalid disc tag (<= 0) into
// disc 1 for grouping purposes.  Without this, a folder where only
// some tracks carry an explicit "disc 1 of 1" tag — common when
// files were ripped or re-tagged at different times — splits into
// two tagging groups for what is really one single-disc album: the
// untagged tracks hash to disc 0, the tagged ones to disc 1.  A
// genuine multi-disc release still separates correctly, since its
// disc-2-and-up tracks carry an explicit non-zero, non-one disc
// number.
//
// This is the single-file fallback used where a whole directory's
// disc tags aren't available (e.g. maybeRebindTaggingGroup, which
// rebinds one changed file at a time).  Where a directory's full set
// of raw disc numbers IS available, prefer ResolveDirectoryDiscNumbers
// instead — a hardcoded "1" is the wrong guess for an untagged track
// sitting alongside siblings that all agree on disc 2.
func normalizeDiscNumber(discNumber int) int {
	if discNumber <= 0 {
		return 1
	}

	return discNumber
}

// ResolveDirectoryDiscNumbers returns, for one directory's files, the
// disc number each should use when computing its GroupKey.
//
// normalizeDiscNumber's fixed "fold untagged to disc 1" is only a
// safe guess when the caller has no other evidence. Given the whole
// directory's raw disc tags at once, a better guess is available: if
// every file that DOES carry an explicit disc number agrees on the
// same value, an untagged sibling is almost certainly the same disc
// — a partially re-tagged rip, not a stray track from a different
// one — so it folds to that value instead of a hardcoded 1. If the
// directory's explicit disc numbers disagree, it's a genuine
// multi-disc release with no per-disc subfolders, and there's no
// single disc to guess for the untagged ones, so they fall back to
// normalizeDiscNumber's default.
//
// rawDiscNumbers must be in the same order as the files they belong
// to; the returned slice mirrors that order 1:1.
func ResolveDirectoryDiscNumbers(rawDiscNumbers []int) []int {
	consensus := 0
	ambiguous := false

	for _, d := range rawDiscNumbers {
		if d <= 0 {
			continue
		}

		switch {
		case consensus == 0:
			consensus = d
		case consensus != d:
			ambiguous = true
		}
	}

	fallback := 1
	if consensus > 0 && !ambiguous {
		fallback = consensus
	}

	out := make([]int, len(rawDiscNumbers))

	for i, d := range rawDiscNumbers {
		if d <= 0 {
			out[i] = fallback
		} else {
			out[i] = d
		}
	}

	return out
}
