package autotag

import (
	"context"
	"fmt"

	"yellowjacket/backend/database/sql/sqlcgen"
)

// LocalResolver turns a tagging group's album name into zero-cost
// candidate releases by looking for local release_groups (with
// MBIDs) whose normalized name matches.  The user's own tagged
// albums become free candidates — if they already have another
// library where the same album was tagged correctly, reuse that.
//
// The resolver talks to the DB through the sqlc-generated Queries
// type, not the database package's DB wrapper — keeps the import
// graph acyclic with the database package (which already depends
// on autotag.GroupKey for migration backfills).
type LocalResolver struct {
	q *sqlcgen.Queries
}

// NewLocalResolver returns a resolver bound to the given Queries.
func NewLocalResolver(q *sqlcgen.Queries) *LocalResolver {
	return &LocalResolver{q: q}
}

// LocalTracksForGroup returns the local audio files in the given
// tagging group, projected into the scorer-ready shape.
func (r *LocalResolver) LocalTracksForGroup(
	ctx context.Context, groupKey string,
) ([]LocalTrack, error) {
	rows, err := r.q.ListAudioFilesInTaggingGroup(ctx, groupKey)
	if err != nil {
		return nil, fmt.Errorf("list local tracks: %w", err)
	}

	out := make([]LocalTrack, 0, len(rows))
	for _, row := range rows {
		out = append(out, LocalTrack{
			AudioFileID:    row.ID,
			FilePath:       row.FilePath,
			Title:          row.Title,
			Artist:         row.ArtistName,
			TrackNumber:    int(row.TrackNumber),
			DiscNumber:     int(row.DiscNumber),
			LengthMillis:   row.LengthMilliseconds,
			RecordingMBID:  row.RecordingMbid,
			AlbumTag:       row.AlbumName,
			AlbumArtistTag: row.AlbumArtist,
		})
	}

	return out, nil
}

// ResolveLocal returns candidate releases sourced from the local
// DB's albums (filtered to those carrying an MBID)
// whose normalized name matches the tagging item's album name.
// No network calls.  Candidates carry all tracks flat; caller runs
// AlignTracks on each to produce per-track alignments.
func (r *LocalResolver) ResolveLocal(
	ctx context.Context, albumName string,
) ([]Candidate, error) {
	if albumName == "" {
		return nil, nil
	}

	rows, err := r.q.ListLocalAlbumCandidates(ctx, albumName)
	if err != nil {
		return nil, fmt.Errorf("list local candidates: %w", err)
	}

	normalizedTarget := Normalize(albumName)
	byID := make(map[int64]*Candidate, 4)             //nolint:mnd
	tracksByID := make(map[int64][]CandidateTrack, 4) //nolint:mnd

	for _, row := range rows {
		// Case-insensitive SQL match is a cheap pre-filter; we
		// still apply our full normalization rule in Go to reject
		// false positives like "Greatest Hits" vs "Greatest Hits".
		if Normalize(row.AlbumName) != normalizedTarget {
			continue
		}

		if _, ok := byID[row.AlbumID]; !ok {
			byID[row.AlbumID] = localCandidate(row)
		}

		tracksByID[row.AlbumID] = append(
			tracksByID[row.AlbumID],
			CandidateTrack{
				Position:     int(row.TrackNumber),
				DiscNumber:   int(row.DiscNumber),
				Title:        row.TrackTitle,
				LengthMillis: row.LengthMilliseconds,
				MBID:         row.RecordingMbid,
			},
		)
	}

	out := make([]Candidate, 0, len(byID))
	for id, c := range byID {
		c.Tracks = tracksByID[id]
		c.TrackCount = len(c.Tracks)
		out = append(out, *c)
	}

	return out, nil
}

// localCandidate converts one sqlc row (minus track-level fields)
// into a Candidate shell.  Track fields and alignments are filled
// in by the caller.
func localCandidate(row sqlcgen.ListLocalAlbumCandidatesRow) *Candidate {
	date := ""
	if row.Year > 0 {
		date = fmt.Sprintf("%04d", row.Year)
	}

	mbid := ""
	if row.AlbumMbid.Valid {
		mbid = row.AlbumMbid.String
	}

	return &Candidate{
		ReleaseGroupMBID: mbid,
		Title:            row.AlbumName,
		ArtistCredit:     row.ArtistCredit,
		Date:             date,
		Source:           SourceLocal,
		Provenance:       "local",
	}
}
