package api

import (
	"errors"
	"net/url"
	"strconv"
	"strings"

	"github.com/jackc/pgx/v5/pgtype"

	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

var errBadPaging = errors.New("invalid limit or offset")

// uuidToString renders a pgtype.UUID as a canonical hyphenated string.
// Returns "" when the UUID is invalid (unset). Duplicated from the subsonic
// package intentionally — the two packages must not depend on each other.
func uuidToString(u pgtype.UUID) string {
	if !u.Valid {
		return ""
	}
	b := u.Bytes
	const hex = "0123456789abcdef"
	out := make([]byte, 36)
	j := 0
	for i, x := range b {
		if i == 4 || i == 6 || i == 8 || i == 10 {
			out[j] = '-'
			j++
		}
		out[j] = hex[x>>4]
		out[j+1] = hex[x&0x0f]
		j += 2
	}
	return string(out)
}

// parseUUID accepts the canonical hyphenated form produced by uuidToString.
// Returns ok=false on any malformed input so handlers can 400 cleanly.
func parseUUID(s string) (pgtype.UUID, bool) {
	var u pgtype.UUID
	if err := u.Scan(s); err != nil {
		return pgtype.UUID{}, false
	}
	return u, u.Valid
}

// yearFromDate returns the 4-digit year from a pgtype.Date, or 0 when the
// date is unset. Album release years are optional at scan time.
func yearFromDate(d pgtype.Date) int {
	if !d.Valid {
		return 0
	}
	return d.Time.Year()
}

// durationMsToSec converts millisecond durations (as stored in tracks.duration_ms)
// to seconds, rounding to nearest. Callers display seconds; sub-second precision
// is not useful in UI.
func durationMsToSec(ms int32) int {
	return int((ms + 500) / 1000)
}

// coverURL returns the /api relative URL for an album's cover art. The
// endpoint ships in Plan 3; until then it 404s, and the SPA does not wire
// <img src> to it.
func coverURL(albumID pgtype.UUID) string {
	return "/api/albums/" + uuidToString(albumID) + "/cover"
}

// streamURL returns the /api relative URL for a track's audio stream. The
// endpoint ships in Plan 3.
func streamURL(trackID pgtype.UUID) string {
	return "/api/tracks/" + uuidToString(trackID) + "/stream"
}

// streamURLWithExt returns the extension-bearing stream URL used by UPnP
// cast tokens. Sonos's URL probe gates duration detection on a recognizable
// audio file extension; the bare `/stream` shape reports TrackDuration=0
// and breaks seek/auto-advance. The bare /stream route stays mounted as an
// alias for legacy / web / Subsonic clients. See task #610.
func streamURLWithExt(trackID pgtype.UUID, ext string) string {
	return streamURL(trackID) + "." + ext
}

// artistRefFrom projects a dbq.Artist into an ArtistRef without cover.
// albumCount must be pre-computed by the caller. Used by code paths that
// don't have a representative-album lookup at hand (artist detail, search,
// liked-artists list).
func artistRefFrom(a dbq.Artist, albumCount int) ArtistRef {
	return ArtistRef{
		ID:         uuidToString(a.ID),
		Name:       a.Name,
		SortName:   a.SortName,
		AlbumCount: albumCount,
	}
}

// artistRefFromCovered projects a dbq.Artist into an ArtistRef with the
// derived CoverURL populated from a representative album id (nullable).
// Used by /api/home and /api/artists?sort=alpha which carry the lookup
// in the same query.
func artistRefFromCovered(a dbq.Artist, albumCount int, coverAlbumID pgtype.UUID) ArtistRef {
	ref := artistRefFrom(a, albumCount)
	if coverAlbumID.Valid {
		ref.CoverURL = coverURL(coverAlbumID)
	}
	return ref
}

// albumRefFrom projects a dbq.Album into an AlbumRef. artistName must be
// pre-resolved because Album only carries artist_id. trackCount and
// durationSec may be 0 when the caller does not compute them.
func albumRefFrom(a dbq.Album, artistName string, trackCount, durationSec int) AlbumRef {
	return AlbumRef{
		ID:             uuidToString(a.ID),
		Title:          a.Title,
		SortTitle:      a.SortTitle,
		ArtistID:       uuidToString(a.ArtistID),
		ArtistName:     artistName,
		Year:           yearFromDate(a.ReleaseDate),
		TrackCount:     trackCount,
		DurationSec:    durationSec,
		CoverURL:       coverURL(a.ID),
		CoverArtSource: a.CoverArtSource,
	}
}

// trackRefFrom projects a dbq.Track into a TrackRef. Parent names must be
// pre-resolved because Track only carries album_id and artist_id.
func trackRefFrom(t dbq.Track, albumTitle, artistName string) TrackRef {
	ref := TrackRef{
		ID:          uuidToString(t.ID),
		Title:       t.Title,
		AlbumID:     uuidToString(t.AlbumID),
		AlbumTitle:  albumTitle,
		ArtistID:    uuidToString(t.ArtistID),
		ArtistName:  artistName,
		DurationSec: durationMsToSec(t.DurationMs),
		StreamURL:   streamURL(t.ID),
	}
	if t.TrackNumber != nil {
		ref.TrackNumber = int(*t.TrackNumber)
	}
	if t.DiscNumber != nil {
		ref.DiscNumber = int(*t.DiscNumber)
	}
	return ref
}

// parsePaging reads limit/offset from the query string, applying defaults
// and clamping. Returns a 400-worthy error when the values are non-numeric;
// out-of-range values silently clamp (deliberate — UX).
func parsePaging(raw url.Values) (limit, offset int, err error) {
	const (
		defLimit = 50
		maxLimit = 200
	)
	limit = defLimit
	if s := strings.TrimSpace(raw.Get("limit")); s != "" {
		n, perr := strconv.Atoi(s)
		if perr != nil {
			return 0, 0, errBadPaging
		}
		if n < 1 {
			n = 1
		}
		if n > maxLimit {
			n = maxLimit
		}
		limit = n
	}
	if s := strings.TrimSpace(raw.Get("offset")); s != "" {
		n, perr := strconv.Atoi(s)
		if perr != nil {
			return 0, 0, errBadPaging
		}
		if n < 0 {
			n = 0
		}
		offset = n
	}
	return limit, offset, nil
}

// nonNilStrings guarantees a JSON array rather than null. The clients iterate
// these without a null check, matching how every other list field in this
// package is emitted.
func nonNilStrings(in []string) []string {
	if in == nil {
		return []string{}
	}
	return in
}
