package monstersirenfetch

import (
	"maps"
	"os"
	"slices"
	"strconv"
	"strings"
)

// AlbumConflictError is returned by [Flatten] if two or more albums share the same CID.
type AlbumConflictError struct {
	Index    int
	Previous Album
	Current  Album
}

func (e *AlbumConflictError) Error() string {
	return "album CID conflict on index " + strconv.Itoa(e.Index) + ": " +
		"previous album " + e.Previous.Name + ", " +
		"current album " + e.Current.Name
}

// SongConflictError is returned by [Flatten] if two or more songs belonging to the same [Album] share the same CID.
type SongConflictError struct {
	Index    int
	Previous *Song
	Current  Song
}

func (e *SongConflictError) Error() string {
	return "song CID conflict on index " + strconv.Itoa(e.Index) + ": " +
		"previous song " + e.Previous.Name + ", " +
		"current song " + e.Current.Name
}

// CompositeAlbum represents an [Album] with a collection of its associated [Song].
type CompositeAlbum struct {
	// Songs is a map of [Song.CID] to [Song].
	Songs SongsMap `json:"songs"`

	// Album is the underlying [Album].
	Album *Album `json:"data"`
}

type (
	// CompositeAlbumsMap is a map of [Album.CID] to [CompositeAlbum].
	CompositeAlbumsMap map[StringInt]CompositeAlbum

	// SongsMap is a map of [Song.CID] to the address of [Song].
	SongsMap map[StringInt]*Song
)

func (m CompositeAlbumsMap) String() string {
	if len(m) == 0 {
		return "<empty>"
	}

	compAlbums := slices.Collect(maps.Values(m))
	slices.SortFunc(compAlbums, func(a, b CompositeAlbum) int { return int(a.Album.CID - b.Album.CID) })

	s := make([]string, len(compAlbums))
	var buf strings.Builder
	for i, ca := range compAlbums {
		buf.WriteString(
			"Album: " + ca.Album.Name + " (" + ca.Album.CID.String() + ")\n" +
				"Cover: " + ca.Album.CoverURL + "\n" +
				"Artist(s): " + strings.Join(ca.Album.Artists, ", ") + "\n" +
				"Songs:\n")

		albumSongs := slices.Collect(maps.Values(ca.Songs))
		slices.SortFunc(albumSongs, func(a, b *Song) int { return int(a.CID - b.CID) })
		for _, cs := range albumSongs {
			buf.WriteString(
				"  " + strings.Join(cs.Artists, ", ") + " ─ " + cs.Name + "\n")
		}

		s[i] = buf.String()
		buf.Reset()
	}

	return strings.Join(s, "\n")
}

// Flatten flattens [AlbumsData] and [SongsData] into a [CompositeAlbumsMap].
// All values in [CompositeAlbum.Songs] and the [CompositeAlbum.Album] field are guaranteed to be non-nil.
func Flatten(albumData AlbumsData, songsData SongsData) (CompositeAlbumsMap, error) {
	m := make(CompositeAlbumsMap, len(albumData))
	for i, a := range albumData {
		if c, ok := m[a.CID]; ok {
			return nil, &AlbumConflictError{Index: i, Previous: *c.Album, Current: a}
		}
		m[a.CID] = CompositeAlbum{Songs: make(map[StringInt]*Song), Album: &a}
	}

	for i, s := range songsData.List {
		var c *Song
		if a, ok := m[s.AlbumCID]; !ok {
			return nil, os.ErrNotExist
		} else if c, ok = a.Songs[s.CID]; ok {
			return nil, &SongConflictError{Index: i, Previous: c, Current: s}
		} else {
			a.Songs[s.CID] = &s
		}
	}

	return m, nil
}
