package subsonic

import (
	"context"
	"encoding/xml"
	"path"
	"strings"

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

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

// IDs on the wire are the bare UUID string. Subsonic endpoints know from
// context (stream.view → track, getArtist → artist) which table to resolve
// against, so we don't need type-prefixed IDs.

// uuidToID renders a pgtype.UUID as the canonical 8-4-4-4-12 string used by
// clients. Returns "" for a zero/invalid UUID so callers can omit the attr.
func uuidToID(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[:j])
}

// parseUUID is the inverse of uuidToID — tolerant of the canonical hyphenated
// form and also raw hex. Callers that fail to parse should emit ErrDataNotFound.
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
}

// MusicFolder matches Subsonic's /rest/getMusicFolders. v1 always reports a
// single folder because Minstrel has no multi-library concept yet.
type MusicFolder struct {
	XMLName xml.Name `json:"-" xml:"musicFolder"`
	ID      int      `json:"id" xml:"id,attr"`
	Name    string   `json:"name" xml:"name,attr"`
}

type MusicFoldersContainer struct {
	XMLName      xml.Name      `json:"-" xml:"musicFolders"`
	MusicFolders []MusicFolder `json:"musicFolder" xml:"musicFolder"`
}

type MusicFoldersResponse struct {
	Envelope
	MusicFolders MusicFoldersContainer `json:"musicFolders" xml:"musicFolders"`
}

// Index groups artists by the first letter of their sort name.
type Index struct {
	XMLName xml.Name    `json:"-" xml:"index"`
	Name    string      `json:"name" xml:"name,attr"`
	Artists []ArtistRef `json:"artist" xml:"artist"`
}

// ArtistsContainer is the body of getArtists (ID3 namespace).
type ArtistsContainer struct {
	XMLName         xml.Name `json:"-" xml:"artists"`
	IgnoredArticles string   `json:"ignoredArticles" xml:"ignoredArticles,attr"`
	Indexes         []Index  `json:"index" xml:"index"`
}

type ArtistsResponse struct {
	Envelope
	Artists ArtistsContainer `json:"artists" xml:"artists"`
}

// IndexesContainer is the body of the legacy getIndexes. Shape mirrors
// ArtistsContainer; some clients poll getIndexes and others getArtists.
type IndexesContainer struct {
	XMLName         xml.Name `json:"-" xml:"indexes"`
	LastModified    int64    `json:"lastModified" xml:"lastModified,attr"`
	IgnoredArticles string   `json:"ignoredArticles" xml:"ignoredArticles,attr"`
	Indexes         []Index  `json:"index" xml:"index"`
}

type IndexesResponse struct {
	Envelope
	Indexes IndexesContainer `json:"indexes" xml:"indexes"`
}

// ArtistRef is the artist projection used in listings and as an album/song
// child reference.
type ArtistRef struct {
	XMLName    xml.Name `json:"-" xml:"artist"`
	ID         string   `json:"id" xml:"id,attr"`
	Name       string   `json:"name" xml:"name,attr"`
	AlbumCount int      `json:"albumCount,omitempty" xml:"albumCount,attr,omitempty"`
}

// ArtistDetail is the body of getArtist — artist attrs plus nested albums.
type ArtistDetail struct {
	XMLName    xml.Name   `json:"-" xml:"artist"`
	ID         string     `json:"id" xml:"id,attr"`
	Name       string     `json:"name" xml:"name,attr"`
	AlbumCount int        `json:"albumCount" xml:"albumCount,attr"`
	Albums     []AlbumRef `json:"album" xml:"album"`
}

type ArtistResponse struct {
	Envelope
	Artist ArtistDetail `json:"artist" xml:"artist"`
}

// AlbumRef projects an album for listings (album list, search results, artist
// detail). Count/duration are optional — not every call populates them.
type AlbumRef struct {
	XMLName   xml.Name `json:"-" xml:"album"`
	ID        string   `json:"id" xml:"id,attr"`
	Name      string   `json:"name" xml:"name,attr"`
	Artist    string   `json:"artist" xml:"artist,attr"`
	ArtistID  string   `json:"artistId" xml:"artistId,attr"`
	CoverArt  string   `json:"coverArt,omitempty" xml:"coverArt,attr,omitempty"`
	SongCount int      `json:"songCount,omitempty" xml:"songCount,attr,omitempty"`
	Duration  int      `json:"duration,omitempty" xml:"duration,attr,omitempty"`
	Created   string   `json:"created,omitempty" xml:"created,attr,omitempty"`
	Year      int      `json:"year,omitempty" xml:"year,attr,omitempty"`
	Genre     string   `json:"genre,omitempty" xml:"genre,attr,omitempty"`
}

// AlbumDetail is getAlbum — album attrs plus nested song list.
type AlbumDetail struct {
	XMLName   xml.Name  `json:"-" xml:"album"`
	ID        string    `json:"id" xml:"id,attr"`
	Name      string    `json:"name" xml:"name,attr"`
	Artist    string    `json:"artist" xml:"artist,attr"`
	ArtistID  string    `json:"artistId" xml:"artistId,attr"`
	CoverArt  string    `json:"coverArt,omitempty" xml:"coverArt,attr,omitempty"`
	SongCount int       `json:"songCount" xml:"songCount,attr"`
	Duration  int       `json:"duration" xml:"duration,attr"`
	Created   string    `json:"created,omitempty" xml:"created,attr,omitempty"`
	Year      int       `json:"year,omitempty" xml:"year,attr,omitempty"`
	Genre     string    `json:"genre,omitempty" xml:"genre,attr,omitempty"`
	Songs     []SongRef `json:"song" xml:"song"`
}

type AlbumResponse struct {
	Envelope
	Album AlbumDetail `json:"album" xml:"album"`
}

// SongRef is the track projection used in album detail, getSong, search3,
// and later streaming/now-playing responses.
type SongRef struct {
	XMLName     xml.Name `json:"-" xml:"song"`
	ID          string   `json:"id" xml:"id,attr"`
	Parent      string   `json:"parent,omitempty" xml:"parent,attr,omitempty"`
	Title       string   `json:"title" xml:"title,attr"`
	Album       string   `json:"album,omitempty" xml:"album,attr,omitempty"`
	Artist      string   `json:"artist,omitempty" xml:"artist,attr,omitempty"`
	ArtistID    string   `json:"artistId,omitempty" xml:"artistId,attr,omitempty"`
	AlbumID     string   `json:"albumId,omitempty" xml:"albumId,attr,omitempty"`
	Track       int      `json:"track,omitempty" xml:"track,attr,omitempty"`
	DiscNumber  int      `json:"discNumber,omitempty" xml:"discNumber,attr,omitempty"`
	Genre       string   `json:"genre,omitempty" xml:"genre,attr,omitempty"`
	CoverArt    string   `json:"coverArt,omitempty" xml:"coverArt,attr,omitempty"`
	Size        int64    `json:"size,omitempty" xml:"size,attr,omitempty"`
	ContentType string   `json:"contentType,omitempty" xml:"contentType,attr,omitempty"`
	Suffix      string   `json:"suffix,omitempty" xml:"suffix,attr,omitempty"`
	Duration    int      `json:"duration,omitempty" xml:"duration,attr,omitempty"`
	BitRate     int      `json:"bitRate,omitempty" xml:"bitRate,attr,omitempty"`
	IsDir       bool     `json:"isDir" xml:"isDir,attr"`
	Type        string   `json:"type" xml:"type,attr"`
}

type SongResponse struct {
	Envelope
	Song SongRef `json:"song" xml:"song"`
}

// AlbumList2Container is the body of getAlbumList2.
type AlbumList2Container struct {
	XMLName xml.Name   `json:"-" xml:"albumList2"`
	Albums  []AlbumRef `json:"album" xml:"album"`
}

type AlbumList2Response struct {
	Envelope
	AlbumList2 AlbumList2Container `json:"albumList2" xml:"albumList2"`
}

// SearchResult3 is the body of search3.
type SearchResult3Container struct {
	XMLName xml.Name    `json:"-" xml:"searchResult3"`
	Artists []ArtistRef `json:"artist" xml:"artist"`
	Albums  []AlbumRef  `json:"album" xml:"album"`
	Songs   []SongRef   `json:"song" xml:"song"`
}

type SearchResult3Response struct {
	Envelope
	SearchResult3 SearchResult3Container `json:"searchResult3" xml:"searchResult3"`
}

// StarredContainer is the body of getStarred / getStarred2.
type StarredContainer struct {
	XMLName xml.Name    `json:"-" xml:"starred2"`
	Artists []ArtistRef `json:"artist" xml:"artist"`
	Albums  []AlbumRef  `json:"album" xml:"album"`
	Songs   []SongRef   `json:"song" xml:"song"`
}

type GetStarred2Response struct {
	Envelope
	Starred2 StarredContainer `json:"starred2" xml:"starred2"`
}

// GetStarredContainer mirrors getStarred (id3-less variant). Response uses
// the same shape; the only material difference from getStarred2 is the
// outer key name.
type GetStarredContainer struct {
	XMLName xml.Name    `json:"-" xml:"starred"`
	Artists []ArtistRef `json:"artist" xml:"artist"`
	Albums  []AlbumRef  `json:"album" xml:"album"`
	Songs   []SongRef   `json:"song" xml:"song"`
}

type GetStarredResponse struct {
	Envelope
	Starred GetStarredContainer `json:"starred" xml:"starred"`
}

// ---- conversion helpers ----

func artistRef(a dbq.Artist, albumCount int) ArtistRef {
	return ArtistRef{
		ID:         uuidToID(a.ID),
		Name:       a.Name,
		AlbumCount: albumCount,
	}
}

// albumRef projects an album. artistName must be pre-resolved because Album
// only carries artist_id. songCount/duration are optional; pass 0 to omit.
func albumRef(a dbq.Album, artistName string, songCount, durationSec int) AlbumRef {
	return AlbumRef{
		ID:        uuidToID(a.ID),
		Name:      a.Title,
		Artist:    artistName,
		ArtistID:  uuidToID(a.ArtistID),
		CoverArt:  coverArtID(a),
		SongCount: songCount,
		Duration:  durationSec,
		Created:   timestamptzToRFC3339(a.CreatedAt),
		Year:      yearFromDate(a.ReleaseDate),
	}
}

func albumDetail(a dbq.Album, artistName string, songs []SongRef) AlbumDetail {
	dur := 0
	for _, s := range songs {
		dur += s.Duration
	}
	return AlbumDetail{
		ID:        uuidToID(a.ID),
		Name:      a.Title,
		Artist:    artistName,
		ArtistID:  uuidToID(a.ArtistID),
		CoverArt:  coverArtID(a),
		SongCount: len(songs),
		Duration:  dur,
		Created:   timestamptzToRFC3339(a.CreatedAt),
		Year:      yearFromDate(a.ReleaseDate),
		Songs:     songs,
	}
}

func songRef(t dbq.Track, albumTitle, artistName string) SongRef {
	s := SongRef{
		ID:          uuidToID(t.ID),
		Parent:      uuidToID(t.AlbumID),
		Title:       t.Title,
		Album:       albumTitle,
		Artist:      artistName,
		AlbumID:     uuidToID(t.AlbumID),
		ArtistID:    uuidToID(t.ArtistID),
		CoverArt:    uuidToID(t.AlbumID),
		Size:        t.FileSize,
		ContentType: contentTypeForFormat(t.FileFormat),
		Suffix:      strings.TrimPrefix(strings.ToLower(path.Ext(t.FilePath)), "."),
		Duration:    int((t.DurationMs + 500) / 1000),
		IsDir:       false,
		Type:        "music",
	}
	if t.TrackNumber != nil {
		s.Track = int(*t.TrackNumber)
	}
	if t.DiscNumber != nil {
		s.DiscNumber = int(*t.DiscNumber)
	}
	if t.Bitrate != nil {
		s.BitRate = int(*t.Bitrate)
	}
	if t.Genre != nil {
		s.Genre = *t.Genre
	}
	return s
}

// coverArtID returns the album UUID as the cover-art key. getCoverArt uses
// the album row to find art either in cover_art_path (when the scanner sets
// it) or via sidecar lookup in the album directory, so emitting the id
// unconditionally is safe — worst case getCoverArt returns a Subsonic 70.
func coverArtID(a dbq.Album) string {
	return uuidToID(a.ID)
}

func yearFromDate(d pgtype.Date) int {
	if !d.Valid {
		return 0
	}
	return d.Time.Year()
}

func timestamptzToRFC3339(ts pgtype.Timestamptz) string {
	if !ts.Valid {
		return ""
	}
	return ts.Time.UTC().Format("2006-01-02T15:04:05Z07:00")
}

// contentTypeForFormat maps the short file_format we record (mp3/flac/ogg/...)
// to a MIME type Subsonic clients recognize. Falls back to octet-stream.
func contentTypeForFormat(f string) string {
	switch strings.ToLower(f) {
	case "mp3":
		return "audio/mpeg"
	case "flac":
		return "audio/flac"
	case "ogg", "oga":
		return "audio/ogg"
	case "m4a", "aac":
		return "audio/mp4"
	case "opus":
		return "audio/opus"
	case "wav":
		return "audio/wav"
	}
	return "application/octet-stream"
}

// indexLetter returns the bucket name used by getIndexes/getArtists. Treats
// a leading digit as "#" per Subsonic convention. Empty names bucket to "?".
func indexLetter(sortName string) string {
	sortName = strings.TrimSpace(sortName)
	if sortName == "" {
		return "?"
	}
	r := []rune(sortName)[0]
	switch {
	case r >= '0' && r <= '9':
		return "#"
	case r >= 'a' && r <= 'z':
		return strings.ToUpper(string(r))
	case r >= 'A' && r <= 'Z':
		return string(r)
	}
	return "?"
}

// resolveArtistNames fetches distinct artist rows for the given ids and
// returns a uuid-string → name map. Uses the bulk-per-call lookup pattern
// rather than N+1 per album to keep listing endpoints responsive.
func resolveArtistNames(ctx context.Context, q *dbq.Queries, ids []pgtype.UUID) (map[string]string, error) {
	seen := make(map[string]struct{}, len(ids))
	out := make(map[string]string, len(ids))
	for _, id := range ids {
		key := uuidToID(id)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		a, err := q.GetArtistByID(ctx, id)
		if err != nil {
			return nil, err
		}
		out[key] = a.Name
	}
	return out, nil
}
