/*
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>

This file is part of Scotty.

Scotty is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later version.

Scotty is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
Scotty. If not, see <https://www.gnu.org/licenses/>.
*/

package spotify

import (
	"math"
	"net/url"
	"sort"
	"strconv"
	"time"

	"github.com/spf13/viper"
	"go.uploadedlobster.com/scotty/internal/auth"
	"go.uploadedlobster.com/scotty/models"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/spotify"
)

type SpotifyApiBackend struct {
	client       Client
	clientId     string
	clientSecret string
}

func (b *SpotifyApiBackend) Name() string { return "spotify" }

func (b *SpotifyApiBackend) FromConfig(config *viper.Viper) models.Backend {
	b.clientId = config.GetString("client-id")
	b.clientSecret = config.GetString("client-secret")
	return b
}

func (b *SpotifyApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy {
	conf := oauth2.Config{
		ClientID:     b.clientId,
		ClientSecret: b.clientSecret,
		Scopes: []string{
			"user-read-currently-playing",
			"user-read-recently-played",
			"user-library-read",
			"user-library-modify",
		},
		RedirectURL: redirectUrl.String(),
		Endpoint:    spotify.Endpoint,
	}

	return auth.NewStandardStrategy(conf)
}

func (b *SpotifyApiBackend) OAuth2Config(redirectUrl *url.URL) oauth2.Config {
	return oauth2.Config{
		ClientID:     b.clientId,
		ClientSecret: b.clientSecret,
		Scopes: []string{
			"user-read-currently-playing",
			"user-read-recently-played",
			"user-library-read",
			"user-library-modify",
		},
		RedirectURL: redirectUrl.String(),
		Endpoint:    spotify.Endpoint,
	}
}

func (b *SpotifyApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
	b.client = NewClient(token)
	return nil
}

func (b *SpotifyApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) {
	startTime := time.Now()
	minTime := oldestTimestamp

	totalDuration := startTime.Sub(oldestTimestamp)

	defer close(results)

	p := models.Progress{Total: int64(totalDuration.Seconds())}

	for {
		result, err := b.client.RecentlyPlayedAfter(minTime, MaxItemsPerGet)
		if err != nil {
			progress <- p.Complete()
			results <- models.ListensResult{Error: err}
			return
		}

		if result.Cursors.After == "" {
			break
		}

		// Set minTime to the newest returned listen
		after, err := strconv.ParseInt(result.Cursors.After, 10, 64)
		if err != nil {
			progress <- p.Complete()
			results <- models.ListensResult{Error: err}
			return
		} else if after <= minTime.Unix() {
			// new cursor timestamp did not progress
			break
		}
		minTime = time.Unix(after, 0)
		remainingTime := startTime.Sub(minTime)

		count := len(result.Items)
		if count == 0 {
			break
		}

		listens := make(models.ListensList, 0, len(result.Items))

		for _, listen := range result.Items {
			l := listen.AsListen()
			if l.ListenedAt.Unix() > oldestTimestamp.Unix() {
				listens = append(listens, l)
			} else {
				// result contains listens older then oldestTimestamp,
				// we can stop requesting more
				break
			}
		}

		sort.Sort(listens)
		p.Elapsed = int64(totalDuration.Seconds() - remainingTime.Seconds())
		progress <- p
		results <- models.ListensResult{Listens: listens, OldestTimestamp: minTime}
	}

	results <- models.ListensResult{OldestTimestamp: minTime}
	progress <- p.Complete()
}

func (b *SpotifyApiBackend) ExportLoves(oldestTimestamp time.Time, results chan models.LovesResult, progress chan models.Progress) {
	// Choose a high offset, we attempt to search the loves backwards starting
	// at the oldest one.
	offset := math.MaxInt32
	perPage := MaxItemsPerGet

	defer close(results)

	p := models.Progress{Total: int64(perPage)}
	var totalCount int

out:
	for {
		result, err := b.client.UserTracks(offset, perPage)
		if err != nil {
			progress <- p.Complete()
			results <- models.LovesResult{Error: err}
			return
		}

		// The offset was higher then the actual number of tracks. Adjust the offset
		// and continue.
		if offset >= result.Total {
			p.Total = int64(result.Total)
			totalCount = result.Total
			offset = result.Total - perPage
			if offset < 0 {
				offset = 0
			}
			continue
		}

		count := len(result.Items)
		if count == 0 {
			break out
		}

		loves := make(models.LovesList, 0, perPage)
		for _, track := range result.Items {
			love := track.AsLove()
			if love.Created.Unix() > oldestTimestamp.Unix() {
				loves = append(loves, love)
			} else {
				totalCount -= 1
				break
			}
		}

		sort.Sort(loves)
		results <- models.LovesResult{Loves: loves, Total: totalCount}
		p.Elapsed += int64(count)
		progress <- p

		if offset <= 0 {
			// This was the last request, no further results
			break out
		}

		offset -= perPage
		if offset < 0 {
			offset = 0
		}
	}

	progress <- p.Complete()
}

func (l Listen) AsListen() models.Listen {
	listenedAt, _ := time.Parse(time.RFC3339, l.PlayedAt)
	listen := models.Listen{
		ListenedAt: listenedAt,
		Track:      l.Track.AsTrack(),
	}

	return listen
}

func (t SavedTrack) AsLove() models.Love {
	addedAt, _ := time.Parse(time.RFC3339, t.AddedAt)
	love := models.Love{
		Created: addedAt,
		Track:   t.Track.AsTrack(),
	}

	return love
}

func (t Track) AsTrack() models.Track {
	track := models.Track{
		TrackName:      t.Name,
		ReleaseName:    t.Album.Name,
		ArtistNames:    make([]string, 0, len(t.Artists)),
		Duration:       time.Duration(t.DurationMs * int(time.Millisecond)),
		TrackNumber:    t.TrackNumber,
		DiscNumber:     t.DiscNumber,
		ISRC:           t.ExternalIds.ISRC,
		AdditionalInfo: map[string]any{},
	}

	for _, artist := range t.Artists {
		track.ArtistNames = append(track.ArtistNames, artist.Name)
	}

	info := track.AdditionalInfo
	if !t.IsLocal {
		info["music_service"] = "spotify.com"
	}

	if t.ExternalUrls.Spotify != "" {
		info["origin_url"] = t.ExternalUrls.Spotify
		info["spotify_id"] = t.ExternalUrls.Spotify
	}

	if t.Album.ExternalUrls.Spotify != "" {
		info["spotify_album_id"] = t.Album.ExternalUrls.Spotify
	}

	if len(t.Artists) > 0 {
		info["spotify_artist_ids"] = extractArtistIds(t.Artists)
	}

	if len(t.Album.Artists) > 0 {
		info["spotify_album_artist_ids"] = extractArtistIds(t.Album.Artists)
	}

	return track
}

func extractArtistIds(artists []Artist) []string {
	artistIds := make([]string, len(artists))
	for i, artist := range artists {
		artistIds[i] = artist.ExternalUrls.Spotify
	}
	return artistIds
}