package subsonic

import (
	"errors"
	"image"
	"io"
	"math"
	"net/url"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/dweymouth/go-subsonic/subsonic"
	"github.com/dweymouth/supersonic/backend/mediaprovider"
	"github.com/dweymouth/supersonic/sharedutil"
)

const cacheValidDurationSeconds = 60

type subsonicMediaProvider struct {
	client          *subsonic.Client
	prefetchCoverCB func(coverArtID string)

	genresCached   []*mediaprovider.Genre
	genresCachedAt int64 // unix

	playlistsCached   []*mediaprovider.Playlist
	playlistsCachedAt int64 // unix
}

func SubsonicMediaProvider(subsonicClient *subsonic.Client) mediaprovider.MediaProvider {
	return &subsonicMediaProvider{client: subsonicClient}
}

func (s *subsonicMediaProvider) SetPrefetchCoverCallback(cb func(coverArtID string)) {
	s.prefetchCoverCB = cb
}

func (s *subsonicMediaProvider) CreatePlaylist(name string, trackIDs []string) error {
	return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"name": name})
}

func (s *subsonicMediaProvider) DeletePlaylist(id string) error {
	return s.client.DeletePlaylist(id)
}

func (s *subsonicMediaProvider) CanMakePublicPlaylist() bool {
	return true
}

func (s *subsonicMediaProvider) EditPlaylist(id, name, description string, public bool) error {
	return s.client.UpdatePlaylist(id, map[string]string{
		"name":    name,
		"comment": description,
		"public":  strconv.FormatBool(public),
	})
}

func (s *subsonicMediaProvider) AddPlaylistTracks(id string, trackIDsToAdd []string) error {
	return s.client.UpdatePlaylistTracks(id, trackIDsToAdd, nil)
}

func (s *subsonicMediaProvider) RemovePlaylistTracks(id string, removeIdxs []int) error {
	return s.client.UpdatePlaylistTracks(id, nil, removeIdxs)
}

func (s *subsonicMediaProvider) GetTrack(trackID string) (*mediaprovider.Track, error) {
	tr, err := s.client.GetSong(trackID)
	if err != nil {
		return nil, err
	}
	return toTrack(tr), nil
}

func (s *subsonicMediaProvider) GetAlbum(albumID string) (*mediaprovider.AlbumWithTracks, error) {
	al, err := s.client.GetAlbum(albumID)
	if err != nil {
		return nil, err
	}
	album := &mediaprovider.AlbumWithTracks{
		Tracks: sharedutil.MapSlice(al.Song, toTrack),
	}
	fillAlbum(al, &album.Album)
	return album, nil
}

func (s *subsonicMediaProvider) GetAlbumInfo(albumID string) (*mediaprovider.AlbumInfo, error) {
	al, err := s.client.GetAlbumInfo(albumID)
	if err != nil {
		return nil, err
	}
	album := &mediaprovider.AlbumInfo{
		Notes:         al.Notes,
		LastFmUrl:     al.LastFmUrl,
		MusicBrainzID: al.MusicBrainzID,
	}
	return album, nil
}

func (s *subsonicMediaProvider) GetArtist(artistID string) (*mediaprovider.ArtistWithAlbums, error) {
	ar, err := s.client.GetArtist(artistID)
	if err != nil {
		return nil, err
	}
	return &mediaprovider.ArtistWithAlbums{
		Artist: mediaprovider.Artist{
			ID:         ar.ID,
			Name:       ar.Name,
			Favorite:   !ar.Starred.IsZero(),
			AlbumCount: ar.AlbumCount,
		},
		Albums: sharedutil.MapSlice(ar.Album, toAlbum),
	}, nil
}

func (s *subsonicMediaProvider) GetArtistInfo(artistID string) (*mediaprovider.ArtistInfo, error) {
	info, err := s.client.GetArtistInfo2(artistID, map[string]string{})
	if err != nil {
		return nil, err
	}
	if info == nil {
		return nil, errors.New("server returned empty artist info")
	}
	return &mediaprovider.ArtistInfo{
		Biography:      info.Biography,
		LastFMUrl:      info.LastFmUrl,
		ImageURL:       info.LargeImageUrl,
		SimilarArtists: sharedutil.MapSlice(info.SimilarArtist, toArtistFromID3),
	}, nil
}

func (s *subsonicMediaProvider) GetCoverArt(id string, size int) (image.Image, error) {
	params := map[string]string{}
	if size > 0 {
		params["size"] = strconv.Itoa(size)
	}
	return s.client.GetCoverArt(id, params)
}

func (s *subsonicMediaProvider) GetFavorites() (mediaprovider.Favorites, error) {
	fav, err := s.client.GetStarred2(map[string]string{})
	if err != nil {
		return mediaprovider.Favorites{}, err
	}
	return mediaprovider.Favorites{
		Albums:  sharedutil.MapSlice(fav.Album, toAlbum),
		Artists: sharedutil.MapSlice(fav.Artist, toArtistFromID3),
		Tracks:  sharedutil.MapSlice(fav.Song, toTrack),
	}, nil
}

func (s *subsonicMediaProvider) GetGenres() ([]*mediaprovider.Genre, error) {
	if s.genresCached != nil && time.Now().Unix()-s.genresCachedAt < cacheValidDurationSeconds {
		return s.genresCached, nil
	}

	g, err := s.client.GetGenres()
	if err != nil {
		return nil, err
	}
	s.genresCached = sharedutil.MapSlice(g, func(g *subsonic.Genre) *mediaprovider.Genre {
		return &mediaprovider.Genre{
			Name:       g.Name,
			AlbumCount: g.AlbumCount,
			TrackCount: g.SongCount,
		}
	})
	s.genresCachedAt = time.Now().Unix()
	return s.genresCached, nil
}

func (s *subsonicMediaProvider) GetPlaylist(playlistID string) (*mediaprovider.PlaylistWithTracks, error) {
	pl, err := s.client.GetPlaylist(playlistID)
	if err != nil {
		return nil, err
	}
	playlist := &mediaprovider.PlaylistWithTracks{
		Tracks: sharedutil.MapSlice(pl.Entry, toTrack),
	}
	fillPlaylist(pl, &playlist.Playlist)
	return playlist, nil
}

func (s *subsonicMediaProvider) GetPlaylists() ([]*mediaprovider.Playlist, error) {
	if s.playlistsCached != nil && time.Now().Unix()-s.playlistsCachedAt < cacheValidDurationSeconds {
		return s.playlistsCached, nil
	}

	pl, err := s.client.GetPlaylists(map[string]string{})
	if err != nil {
		return nil, err
	}
	s.playlistsCached = sharedutil.MapSlice(pl, toPlaylist)
	s.playlistsCachedAt = time.Now().Unix()
	return s.playlistsCached, nil
}

func (s *subsonicMediaProvider) GetRandomTracks(genreName string, count int) ([]*mediaprovider.Track, error) {
	opts := map[string]string{"size": strconv.Itoa(count)}
	if genreName != "" {
		opts["genre"] = genreName
	}
	tr, err := s.client.GetRandomSongs(opts)
	if err != nil {
		return nil, err
	}
	return sharedutil.MapSlice(tr, toTrack), nil
}

func (s *subsonicMediaProvider) GetSimilarTracks(artistID string, count int) ([]*mediaprovider.Track, error) {
	tr, err := s.client.GetSimilarSongs2(artistID, map[string]string{"count": strconv.Itoa(count)})
	if err != nil {
		return nil, err
	}
	return sharedutil.MapSlice(tr, toTrack), nil
}

func (s *subsonicMediaProvider) GetStreamURL(trackID string, forceRaw bool) (string, error) {
	m := make(map[string]string)
	if forceRaw {
		m["format"] = "raw"
	}
	u, err := s.client.GetStreamURL(trackID, m)
	if err != nil {
		return "", err
	}
	return u.String(), nil
}

func (s *subsonicMediaProvider) GetTopTracks(artist mediaprovider.Artist, count int) ([]*mediaprovider.Track, error) {
	params := map[string]string{}
	if count > 0 {
		params["count"] = strconv.Itoa(count)
	}
	tr, err := s.client.GetTopSongs(artist.Name, params)
	if err != nil {
		return nil, err
	}
	return sharedutil.MapSlice(tr, toTrack), nil
}

func (s *subsonicMediaProvider) ReplacePlaylistTracks(playlistID string, trackIDs []string) error {
	return s.client.CreatePlaylistWithTracks(trackIDs, map[string]string{"playlistId": playlistID})
}

func (s *subsonicMediaProvider) ClientDecidesScrobble() bool { return true }

func (s *subsonicMediaProvider) TrackBeganPlayback(trackID string) error {
	return s.client.Scrobble(trackID, map[string]string{
		"time":       strconv.FormatInt(time.Now().UnixMilli(), 10),
		"submission": "false"})
}

func (s *subsonicMediaProvider) TrackEndedPlayback(trackID string, _ int, submission bool) error {
	if !submission {
		return nil
	}
	return s.client.Scrobble(trackID, map[string]string{
		"time":       strconv.FormatInt(time.Now().UnixMilli(), 10),
		"submission": "true"})
}

func (s *subsonicMediaProvider) SetFavorite(params mediaprovider.RatingFavoriteParameters, favorite bool) error {
	subParams := subsonic.StarParameters{
		AlbumIDs:  params.AlbumIDs,
		ArtistIDs: params.ArtistIDs,
		SongIDs:   params.TrackIDs,
	}
	if favorite {
		return s.client.Star(subParams)
	}
	return s.client.Unstar(subParams)
}

func (s *subsonicMediaProvider) SetRating(params mediaprovider.RatingFavoriteParameters, rating int) error {
	// Subsonic doesn't allow bulk setting ratings.
	// To not overwhelm the server with requests, set rating for
	// only 5 tracks at a time concurrently
	batchSize := 5
	var err error
	batchSetRating := func(offs int, wg *sync.WaitGroup) {
		for i := 0; i < batchSize && offs+i < len(params.TrackIDs); i++ {
			wg.Add(1)
			go func(idx int) {
				newErr := s.client.SetRating(params.TrackIDs[idx], rating)
				if err == nil && newErr != nil {
					err = newErr
				}
				wg.Done()
			}(offs + i)
		}
	}

	numBatches := int(math.Ceil(float64(len(params.TrackIDs)) / float64(batchSize)))
	for i := 0; i < numBatches; i++ {
		var wg sync.WaitGroup
		batchSetRating(i*batchSize, &wg)
		wg.Wait()
	}

	return err
}

func (s *subsonicMediaProvider) CreateShareURL(id string) (*url.URL, error) {
	share, err := s.client.CreateShare(id, nil)
	if err != nil {
		return nil, err
	}

	shareUrl, err := url.Parse(share.Url)
	if err != nil {
		return nil, err
	}

	return shareUrl, nil
}

func (s *subsonicMediaProvider) CanShareArtists() bool {
	// TODO: Change to true when we decide to allow sharing artists, in case an OpenSubsonic extension
	//       is approved to share artists in addition to albums and tracks.
	return false
}

func (s *subsonicMediaProvider) DownloadTrack(trackID string) (io.Reader, error) {
	return s.client.Download(trackID)
}

func (s *subsonicMediaProvider) RescanLibrary() error {
	_, err := s.client.StartScan()
	return err
}

// LyricsProvider interface
var _ mediaprovider.LyricsProvider = (*subsonicMediaProvider)(nil)

func (s *subsonicMediaProvider) GetLyrics(track *mediaprovider.Track) (*mediaprovider.Lyrics, error) {
	ext, err := s.client.GetOpenSubsonicExtensions()
	supportsSynced := err == nil &&
		slices.ContainsFunc(ext, func(ext *subsonic.OpenSubsonicExtension) bool {
			return ext.Name == subsonic.SongLyricsExtension
		})
	if supportsSynced {
		lyrics, err := s.client.GetLyricsBySongId(track.ID)
		if err != nil || len(lyrics.StructuredLyrics) == 0 {
			return nil, err
		}
		lyric := lyrics.StructuredLyrics[0]
		mpLyrics := &mediaprovider.Lyrics{
			Title:  lyric.DisplayTitle,
			Artist: lyric.DisplayArtist,
			Synced: lyric.Synced,
		}
		for _, line := range lyric.Lines {
			mpLyrics.Lines = append(mpLyrics.Lines, mediaprovider.LyricLine{
				Text:  line.Text,
				Start: float64(line.Start) / 1000,
			})
		}
		return mpLyrics, nil
	}
	// fallback to legacy getLyrics endpoint
	lyrics, err := s.client.GetLyrics(track.Name, track.ArtistNames[0])
	if err != nil || lyrics == nil || lyrics.Text == "" {
		return nil, err
	}
	mpLyrics := &mediaprovider.Lyrics{
		Title:  lyrics.Title,
		Artist: lyrics.Artist,
		Synced: false,
	}
	lines := strings.Split(lyrics.Text, "\n")
	for _, line := range lines {
		mpLyrics.Lines = append(mpLyrics.Lines, mediaprovider.LyricLine{
			Text: line,
		})
	}
	return mpLyrics, nil
}

// CanSavePlayQueue interface

func (s *subsonicMediaProvider) SavePlayQueue(trackIDs []string, currentTrackPos int, timeSeconds int) error {
	if len(trackIDs) == 0 {
		return nil // don't save an empty queue
	}
	return s.client.SavePlayQueue(trackIDs, map[string]string{
		"current":  trackIDs[currentTrackPos],
		"position": strconv.Itoa(timeSeconds * 1000),
	})
}

func (s *subsonicMediaProvider) GetPlayQueue() (*mediaprovider.SavedPlayQueue, error) {
	pq, err := s.client.GetPlayQueue()
	if err != nil {
		return nil, err
	}
	savedQueue := &mediaprovider.SavedPlayQueue{}
	savedQueue.Tracks = sharedutil.MapSlice(pq.Entries, toTrack)
	savedQueue.TrackPos = slices.IndexFunc(pq.Entries, func(e *subsonic.Child) bool {
		return e.ID == pq.Current
	})
	savedQueue.TimePos = int(pq.Position / 1000)
	return savedQueue, nil
}

func toTrack(ch *subsonic.Child) *mediaprovider.Track {
	if ch == nil {
		return nil
	}
	var artistNames, artistIDs []string
	if len(ch.Artists) > 0 {
		// OpenSubsonic extension
		for _, a := range ch.Artists {
			artistIDs = append(artistIDs, a.ID)
			artistNames = append(artistNames, a.Name)
		}
	} else {
		artistNames = append(artistNames, ch.Artist)
		artistIDs = append(artistIDs, ch.ArtistID)
	}

	return &mediaprovider.Track{
		ID:          ch.ID,
		CoverArtID:  ch.CoverArt,
		ParentID:    ch.Parent,
		Name:        ch.Title,
		Duration:    ch.Duration,
		TrackNumber: ch.Track,
		DiscNumber:  ch.DiscNumber,
		Genre:       ch.Genre,
		ArtistIDs:   artistIDs,
		ArtistNames: artistNames,
		Album:       ch.Album,
		AlbumID:     ch.AlbumID,
		Year:        ch.Year,
		Rating:      ch.UserRating,
		Favorite:    !ch.Starred.IsZero(),
		PlayCount:   int(ch.PlayCount),
		FilePath:    ch.Path,
		Size:        ch.Size,
		BitRate:     ch.BitRate,
		Comment:     ch.Comment,
	}
}

func toAlbum(al *subsonic.AlbumID3) *mediaprovider.Album {
	if al == nil {
		return nil
	}
	album := &mediaprovider.Album{}
	fillAlbum(al, album)
	return album
}

func fillAlbum(subAlbum *subsonic.AlbumID3, album *mediaprovider.Album) {
	var artistNames, artistIDs []string
	if len(subAlbum.Artists) > 0 {
		// OpenSubsonic extension
		for _, a := range subAlbum.Artists {
			artistIDs = append(artistIDs, a.ID)
			artistNames = append(artistNames, a.Name)
		}
	} else {
		artistNames = append(artistNames, subAlbum.Artist)
		artistIDs = append(artistIDs, subAlbum.ArtistID)
	}

	var genres []string
	if len(subAlbum.Genres) > 0 {
		// OpenSubsonic extension
		for _, g := range subAlbum.Genres {
			genres = append(genres, g.Name)
		}
	} else {
		genres = append(genres, subAlbum.Genre)
	}

	album.Year = subAlbum.Year
	if subAlbum.OriginalReleaseDate != nil &&
		subAlbum.OriginalReleaseDate.Year != nil &&
		*subAlbum.OriginalReleaseDate.Year < subAlbum.Year {
		album.Year = *subAlbum.OriginalReleaseDate.Year
	}
	if subAlbum.ReleaseDate != nil && subAlbum.ReleaseDate.Year != nil {
		album.ReissueYear = *subAlbum.ReleaseDate.Year
	}

	album.ID = subAlbum.ID
	album.CoverArtID = subAlbum.CoverArt
	album.Name = subAlbum.Name
	album.Duration = subAlbum.Duration
	album.ArtistIDs = artistIDs
	album.ArtistNames = artistNames
	album.TrackCount = subAlbum.SongCount
	album.Genres = genres
	album.Favorite = !subAlbum.Starred.IsZero()
	album.ReleaseTypes = normalizeReleaseTypes(subAlbum.ReleaseTypes)
	if subAlbum.IsCompilation {
		album.ReleaseTypes |= mediaprovider.ReleaseTypeCompilation
	}
}

func normalizeReleaseTypes(releaseTypes []string) mediaprovider.ReleaseTypes {
	var mpReleaseTypes mediaprovider.ReleaseTypes
	for _, t := range releaseTypes {
		switch strings.ToLower(strings.ReplaceAll(t, " ", "")) {
		case "album":
			mpReleaseTypes |= mediaprovider.ReleaseTypeAlbum
		case "audiobook":
			mpReleaseTypes |= mediaprovider.ReleaseTypeAudiobook
		case "audiodrama":
			mpReleaseTypes |= mediaprovider.ReleaseTypeAudioDrama
		case "broadcast":
			mpReleaseTypes |= mediaprovider.ReleaseTypeBroadcast
		case "compilation":
			mpReleaseTypes |= mediaprovider.ReleaseTypeCompilation
		case "demo":
			mpReleaseTypes |= mediaprovider.ReleaseTypeDemo
		case "djmix":
			mpReleaseTypes |= mediaprovider.ReleaseTypeDJMix
		case "ep":
			mpReleaseTypes |= mediaprovider.ReleaseTypeEP
		case "fieldrecording":
			mpReleaseTypes |= mediaprovider.ReleaseTypeFieldRecording
		case "interview":
			mpReleaseTypes |= mediaprovider.ReleaseTypeInterview
		case "live":
			mpReleaseTypes |= mediaprovider.ReleaseTypeLive
		case "mixtape":
			mpReleaseTypes |= mediaprovider.ReleaseTypeMixtape
		case "remix":
			mpReleaseTypes |= mediaprovider.ReleaseTypeRemix
		case "single":
			mpReleaseTypes |= mediaprovider.ReleaseTypeSingle
		case "soundtrack":
			mpReleaseTypes |= mediaprovider.ReleaseTypeSoundtrack
		case "spokenword":
			mpReleaseTypes |= mediaprovider.ReleaseTypeSpokenWord
		}
	}
	if mpReleaseTypes == 0 {
		return mediaprovider.ReleaseTypeAlbum
	}
	return mpReleaseTypes
}

func toArtistFromID3(ar *subsonic.ArtistID3) *mediaprovider.Artist {
	if ar == nil {
		return nil
	}
	return &mediaprovider.Artist{
		ID:         ar.ID,
		CoverArtID: ar.CoverArt,
		Name:       ar.Name,
		Favorite:   !ar.Starred.IsZero(),
		AlbumCount: ar.AlbumCount,
	}
}

func toPlaylist(pl *subsonic.Playlist) *mediaprovider.Playlist {
	if pl == nil {
		return nil
	}
	playlist := &mediaprovider.Playlist{}
	fillPlaylist(pl, playlist)
	return playlist
}

func fillPlaylist(pl *subsonic.Playlist, playlist *mediaprovider.Playlist) {
	playlist.Name = pl.Name
	playlist.ID = pl.ID
	playlist.CoverArtID = pl.CoverArt
	playlist.Description = pl.Comment
	playlist.Owner = pl.Owner
	playlist.Public = pl.Public
	playlist.TrackCount = pl.SongCount
	playlist.Duration = pl.Duration
}

func (s *subsonicMediaProvider) GetSongRadio(trackID string, count int) ([]*mediaprovider.Track, error) {
	tr, err := s.client.GetSimilarSongs(trackID, map[string]string{"count": strconv.Itoa(count)})
	if err != nil {
		return nil, err
	}
	return sharedutil.MapSlice(tr, toTrack), nil
}
