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

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 lastfm

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

	"github.com/shkh/lastfm-go/lastfm"
	"go.uploadedlobster.com/scotty/internal/auth"
	"go.uploadedlobster.com/scotty/internal/config"
	"go.uploadedlobster.com/scotty/internal/i18n"
	"go.uploadedlobster.com/scotty/internal/models"
	"golang.org/x/oauth2"
)

const (
	MaxItemsPerGet          = 1000
	MaxListensPerGet        = 200
	MaxListensPerSubmission = 50
	MaxPage                 = 1000000
)

type LastfmApiBackend struct {
	client   *lastfm.Api
	username string
}

func (b *LastfmApiBackend) Name() string { return "lastfm" }

func (b *LastfmApiBackend) Options() []models.BackendOption {
	return []models.BackendOption{{
		Name:  "username",
		Label: i18n.Tr("User name"),
		Type:  models.String,
	}, {
		Name:  "client-id",
		Label: i18n.Tr("Client ID"),
		Type:  models.String,
	}, {
		Name:  "client-secret",
		Label: i18n.Tr("Client secret"),
		Type:  models.Secret,
	}}
}

func (b *LastfmApiBackend) FromConfig(config *config.ServiceConfig) models.Backend {
	clientId := config.GetString("client-id")
	clientSecret := config.GetString("client-secret")
	b.client = lastfm.New(clientId, clientSecret)
	b.username = config.GetString("username")
	return b
}

func (b *LastfmApiBackend) StartImport() error  { return nil }
func (b *LastfmApiBackend) FinishImport() error { return nil }

func (b *LastfmApiBackend) OAuth2Strategy(redirectUrl *url.URL) auth.OAuth2Strategy {
	return lastfmStrategy{
		client:      b.client,
		redirectUrl: redirectUrl,
	}
}

func (b *LastfmApiBackend) OAuth2Setup(token oauth2.TokenSource) error {
	t, err := token.Token()
	if err != nil {
		return err
	}
	b.client.SetSession(t.AccessToken)
	return nil
}

func (b *LastfmApiBackend) ExportListens(oldestTimestamp time.Time, results chan models.ListensResult, progress chan models.Progress) {
	page := MaxPage
	minTime := oldestTimestamp
	perPage := MaxItemsPerGet

	defer close(results)

	// We need to gather the full list of listens in order to sort them
	p := models.Progress{Total: int64(page)}

out:
	for page > 0 {
		args := lastfm.P{
			"user":  b.username,
			"limit": MaxListensPerGet,
			// last.fm includes the listen with the exact timestamp in the result
			"from": oldestTimestamp.Add(time.Second).Unix(),
			"page": page,
		}
		result, err := b.client.User.GetRecentTracks(args)
		if err != nil {
			results <- models.ListensResult{Error: err}
			progress <- p.Complete()
			return
		}

		count := len(result.Tracks)
		if count == 0 {
			// The page was outside of the result range, adjust and request again
			if page > result.TotalPages {
				page = result.TotalPages
				continue
			}
			break
		}

		listens := make(models.ListensList, 0, 2*perPage)
		for _, scrobble := range result.Tracks {
			timestamp, err := strconv.ParseInt(scrobble.Date.Uts, 10, 64)
			if err != nil {
				results <- models.ListensResult{Error: err}
				progress <- p.Complete()
				break out
			}
			if timestamp > oldestTimestamp.Unix() {
				p.Elapsed += 1
				listen := models.Listen{
					ListenedAt: time.Unix(timestamp, 0),
					UserName:   b.username,
					Track: models.Track{
						TrackName:     scrobble.Name,
						ArtistNames:   []string{},
						ReleaseName:   scrobble.Album.Name,
						RecordingMbid: models.MBID(scrobble.Mbid),
						ArtistMbids:   []models.MBID{},
						ReleaseMbid:   models.MBID(scrobble.Album.Mbid),
					},
				}
				if scrobble.Artist.Name != "" {
					listen.Track.ArtistNames = []string{scrobble.Artist.Name}
				}
				if scrobble.Artist.Mbid != "" {
					listen.Track.ArtistMbids = []models.MBID{models.MBID(scrobble.Artist.Mbid)}
				}
				listens = append(listens, listen)
			} else {
				break out
			}
		}

		sort.Sort(listens)
		minTime = listens[len(listens)-1].ListenedAt
		page -= 1

		results <- models.ListensResult{
			Items:           listens,
			Total:           result.Total,
			OldestTimestamp: minTime,
		}
		p.Total = int64(result.TotalPages)
		p.Elapsed = int64(result.TotalPages - page)
		progress <- p
	}

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

func (b *LastfmApiBackend) ImportListens(export models.ListensResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
	total := len(export.Items)
	for i := 0; i < total; i += MaxListensPerSubmission {
		listens := export.Items[i:min(i+MaxListensPerSubmission, total)]
		count := len(listens)
		if count == 0 {
			break
		}

		artists := make([]string, count)
		tracks := make([]string, count)
		timestamps := make([]string, count)
		albums := make([]string, count)
		trackNumbers := make([]string, count)
		mbids := make([]string, count)
		// albumArtists := make([]string, count)
		durations := make([]int64, count)

		for _, l := range listens {
			artists = append(artists, l.ArtistName())
			tracks = append(tracks, l.TrackName)
			timestamps = append(timestamps, strconv.FormatInt(l.ListenedAt.Unix(), 10))
			if l.ReleaseName != "" {
				albums = append(albums, l.ReleaseName)
			}
			if l.TrackNumber > 0 {
				trackNumbers = append(trackNumbers, strconv.Itoa(l.TrackNumber))
			}
			if l.RecordingMbid != "" {
				mbids = append(mbids, string(l.RecordingMbid))
			}
			// if l.ReleaseArtist != "" {
			//	albumArtists = append(albums, l.ReleaseArtist)
			// }
			if l.Duration > 0 {
				durations = append(durations, int64(l.Duration.Seconds()))
			}
		}

		result, err := b.client.Track.Scrobble(lastfm.P{
			"artist":      artists,
			"track":       tracks,
			"timestamp":   timestamps,
			"album":       albums,
			"trackNumber": trackNumbers,
			"mbid":        mbids,
			"duration":    durations,
		})
		if err != nil {
			return importResult, err
		}

		accepted, err := strconv.Atoi(result.Accepted)
		if err != nil {
			return importResult, err
		}

		if accepted < count {
			for _, s := range result.Scrobbles {
				ignoreMsg := s.IgnoredMessage.Body
				if ignoreMsg != "" {
					importResult.Log(models.Warning, ignoreMsg)
				}
			}
			err := fmt.Errorf("last.fm import ignored %v scrobbles", count-accepted)
			return importResult, err
		}

		importResult.UpdateTimestamp(listens[count-1].ListenedAt)
		importResult.ImportCount += accepted
		progress <- models.Progress{}.FromImportResult(importResult)
	}

	return importResult, nil
}

func (b *LastfmApiBackend) 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.
	page := 1
	perPage := MaxItemsPerGet

	defer close(results)

	loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
	p := models.Progress{Total: int64(perPage)}
	var totalCount int

out:
	for {
		result, err := b.client.User.GetLovedTracks(lastfm.P{
			"user":  b.username,
			"limit": MaxItemsPerGet,
			"page":  page,
		})
		if err != nil {
			progress <- p.Complete()
			results <- models.LovesResult{Error: err}
			return
		}

		p.Total = int64(result.Total)
		count := len(result.Tracks)
		if count == 0 {
			break out
		}

		for _, track := range result.Tracks {
			timestamp, err := strconv.ParseInt(track.Date.Uts, 10, 64)
			if err != nil {
				progress <- p.Complete()
				results <- models.LovesResult{Error: err}
				return
			}
			if timestamp > oldestTimestamp.Unix() {
				totalCount += 1
				love := models.Love{
					Created:       time.Unix(timestamp, 0),
					UserName:      result.User,
					RecordingMbid: models.MBID(track.Mbid),
					Track: models.Track{
						TrackName:     track.Name,
						ArtistNames:   []string{track.Artist.Name},
						RecordingMbid: models.MBID(track.Mbid),
						ArtistMbids:   []models.MBID{models.MBID(track.Artist.Mbid)},
						AdditionalInfo: models.AdditionalInfo{
							"lastfm_url": track.Url,
						},
					},
				}
				loves = append(loves, love)
			} else {
				break out
			}
		}

		p.Elapsed += int64(count)
		progress <- p

		page += 1
	}

	sort.Sort(loves)
	results <- models.LovesResult{Items: loves, Total: totalCount}
	progress <- p.Complete()
}

func (b *LastfmApiBackend) ImportLoves(export models.LovesResult, importResult models.ImportResult, progress chan models.Progress) (models.ImportResult, error) {
	for _, love := range export.Items {
		err := b.client.Track.Love(lastfm.P{
			"track":  love.TrackName,
			"artist": love.ArtistName(),
		})

		if err == nil {
			importResult.UpdateTimestamp(love.Created)
			importResult.ImportCount += 1
		} else {
			msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v",
				love.TrackName, love.ArtistName(), err.Error())
			importResult.Log(models.Error, msg)
		}

		progress <- models.Progress{}.FromImportResult(importResult)
	}

	return importResult, nil
}