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

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package models

import (
	"strings"
	"time"
)

type MBID string
type Entity string

const (
	Listens Entity = "listens"
	Loves   Entity = "loves"
)

type AdditionalInfo map[string]any

type Track struct {
	TrackName        string
	ReleaseName      string
	ArtistNames      []string
	TrackNumber      int
	DiscNumber       int
	Duration         time.Duration
	ISRC             string
	RecordingMbid    MBID
	ReleaseMbid      MBID
	ReleaseGroupMbid MBID
	ArtistMbids      []MBID
	WorkMbids        []MBID
	Tags             []string
	AdditionalInfo   AdditionalInfo
}

func (t Track) ArtistName() string {
	return strings.Join(t.ArtistNames, ", ")
}

// Updates AdditionalInfo to have standard fields as defined by ListenBrainz
func (t *Track) FillAdditionalInfo() {
	if t.AdditionalInfo == nil {
		t.AdditionalInfo = make(AdditionalInfo, 5)
	}
	if t.RecordingMbid != "" {
		t.AdditionalInfo["recording_mbid"] = t.RecordingMbid
	}
	if t.ReleaseGroupMbid != "" {
		t.AdditionalInfo["release_group_mbid"] = t.ReleaseGroupMbid
	}
	if t.ReleaseMbid != "" {
		t.AdditionalInfo["release_mbid"] = t.ReleaseMbid
	}
	if len(t.ArtistMbids) > 0 {
		t.AdditionalInfo["artist_mbids"] = t.ArtistMbids
	}
	if len(t.WorkMbids) > 0 {
		t.AdditionalInfo["work_mbids"] = t.WorkMbids
	}
	if t.ISRC != "" {
		t.AdditionalInfo["isrc"] = t.ISRC
	}
	if t.TrackNumber != 0 {
		t.AdditionalInfo["tracknumber"] = t.TrackNumber
	}
	if t.DiscNumber != 0 {
		t.AdditionalInfo["discnumber"] = t.DiscNumber
	}
	if t.Duration != 0 {
		rounded := t.Duration.Round(time.Second)
		if t.Duration == rounded {
			t.AdditionalInfo["duration"] = int64(t.Duration.Seconds())
		} else {
			t.AdditionalInfo["duration_ms"] = t.Duration.Milliseconds()
		}
	}
	if len(t.Tags) > 0 {
		t.AdditionalInfo["tags"] = t.Tags
	}
}

type Listen struct {
	Track
	ListenedAt       time.Time
	PlaybackDuration time.Duration
	UserName         string
}

type Love struct {
	Track
	Created       time.Time
	UserName      string
	RecordingMbid MBID
	RecordingMsid MBID
}

type ListensList []Listen

// Returns a new ListensList with only elements that are newer than t.
func (l ListensList) NewerThan(t time.Time) ListensList {
	result := make(ListensList, 0, len(l))
	for _, item := range l {
		if item.ListenedAt.Unix() > t.Unix() {
			result = append(result, item)
		}
	}
	return result
}

func (l ListensList) Len() int {
	return len(l)
}

func (l ListensList) Less(i, j int) bool {
	return l[i].ListenedAt.Unix() < l[j].ListenedAt.Unix()
}

func (l ListensList) Swap(i, j int) {
	l[i], l[j] = l[j], l[i]
}

type LovesList []Love

func (l LovesList) Len() int {
	return len(l)
}

func (l LovesList) Less(i, j int) bool {
	return l[i].Created.Unix() < l[j].Created.Unix()
}

func (l LovesList) Swap(i, j int) {
	l[i], l[j] = l[j], l[i]
}

type ExportResult[T LovesList | ListensList] struct {
	Items           T
	Total           int
	OldestTimestamp time.Time
	Error           error
}

type ListensResult ExportResult[ListensList]

type LovesResult ExportResult[LovesList]

type LogEntryType string

const (
	Info    LogEntryType = "Info"
	Warning LogEntryType = "Warning"
	Error   LogEntryType = "Error"
)

type LogEntry struct {
	Type    LogEntryType
	Message string
}

type ImportResult struct {
	TotalCount    int
	ImportCount   int
	LastTimestamp time.Time
	ImportLog     []LogEntry

	// Error is only set if an unrecoverable import error occurred
	Error error
}

// Sets LastTimestamp to newTime, if newTime is newer than LastTimestamp
func (i *ImportResult) UpdateTimestamp(newTime time.Time) {
	if newTime.Unix() > i.LastTimestamp.Unix() {
		i.LastTimestamp = newTime
	}
}

func (i *ImportResult) Update(from ImportResult) {
	i.TotalCount = from.TotalCount
	i.ImportCount = from.ImportCount
	i.UpdateTimestamp(from.LastTimestamp)
	i.ImportLog = append(i.ImportLog, from.ImportLog...)
}

func (i *ImportResult) Log(t LogEntryType, msg string) {
	i.ImportLog = append(i.ImportLog, LogEntry{
		Type:    t,
		Message: msg,
	})
}

type Progress struct {
	Total     int64
	Elapsed   int64
	Completed bool
}

func (p Progress) FromImportResult(result ImportResult) Progress {
	p.Total = int64(result.TotalCount)
	p.Elapsed = int64(result.ImportCount)
	return p
}

func (p Progress) Complete() Progress {
	p.Elapsed = p.Total
	p.Completed = true
	return p
}