package monstersirenfetch

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"os"
	"slices"
)

// SongResponse is the response of /api/song/%d.
type SongResponse Response[Song]

// Song holds the metadata of a song.
type Song struct {
	CID        StringInt `json:"cid"`
	Name       string    `json:"name"`
	AlbumCID   StringInt `json:"albumCid"`
	SourceURL  string    `json:"sourceUrl,omitempty"`
	LyricURL   string    `json:"lyricUrl,omitempty"`
	MvURL      string    `json:"mvUrl,omitempty"`
	MvCoverURL string    `json:"mvCoverUrl,omitempty"`
	Artists    []string  `json:"artists"`
}

const (
	// SongVariantCurrent copies the current variant as-is.
	SongVariantCurrent = iota
	// SongVariantFull leaves all fields intact in the copy.
	// This variant is returned by /api/song/%d.
	SongVariantFull
	// SongVariantBase zeroes [Song.SourceURL], [Song.LyricURL], [Song.MvURL], [Song.MvCoverURL].
	// This variant is included in /api/songs.
	SongVariantBase
)

// IsFull returns whether [Song] is considered to be its [SongVariantFull] variant.
func (s *Song) IsFull() bool { return s.SourceURL != "" }

// Copy makes a copy of [Song].
// For a [Song] where the IsFull method returns true, Copy zeroes fields to convert the copy into other variants.
// For [Song] where IsFull returns false, any variant other than [SongVariantCurrent] is undefined.
func (s *Song) Copy(v *Song, variant int) bool {
	if v == nil || s == nil || v == s {
		return false
	}

	if variant == SongVariantCurrent {
		*v = *s
		return true
	}
	if !s.IsFull() {
		return false
	}

	switch variant {
	case SongVariantFull:
		*v = *s

	case SongVariantBase:
		*v = *s
		v.SourceURL = ""
		v.LyricURL = ""
		v.MvURL = ""
		v.MvCoverURL = ""

	default:
		return false
	}

	return true
}

// Enrich populates the remaining fields of a non-full [Song].
func (s *Song) Enrich(ctx context.Context, n Net) error {
	if s == nil || s.IsFull() {
		return os.ErrInvalid
	}

	var v SongResponse
	u := APIPrefix + "/song/" + s.CID.String()
	if resp, err := n.Get(ctx, u); err != nil {
		return err
	} else if resp.StatusCode != http.StatusOK {
		return &ResponseStatusError{u, resp.StatusCode, resp.Body.Close()}
	} else {
		if err = json.NewDecoder(resp.Body).Decode(&v); err != nil {
			return errors.Join(resp.Body.Close(), err)
		}
		if err = resp.Body.Close(); err != nil {
			return err
		}
	}

	// these should be unreachable unless the server malfunctions
	if s.CID != v.Data.CID {
		return &InconsistentEnrichError[StringInt]{"cid", s.CID, v.Data.CID}
	}
	if s.Name != v.Data.Name {
		return &InconsistentEnrichError[string]{"name", s.Name, v.Data.Name}
	}
	if s.AlbumCID != v.Data.AlbumCID {
		return &InconsistentEnrichError[StringInt]{"albumCid", s.AlbumCID, v.Data.AlbumCID}
	}
	if !slices.Equal(s.Artists, v.Data.Artists) {
		return &InconsistentEnrichError[[]string]{"artists", s.Artists, v.Data.Artists}
	}

	*s = v.Data
	return nil
}

// songDirect is [Song] without its MarshalJSON method.
type songDirect Song

// songNullable is [Song] with corresponding nullable string fields.
type songNullable struct {
	CID        StringInt      `json:"cid"`
	Name       string         `json:"name"`
	AlbumCID   StringInt      `json:"albumCid"`
	SourceURL  string         `json:"sourceUrl"`
	LyricURL   NullableString `json:"lyricUrl"`
	MvURL      NullableString `json:"mvUrl"`
	MvCoverURL NullableString `json:"mvCoverUrl"`
	Artists    []string       `json:"artists"`
}

func (s *Song) MarshalJSON() (data []byte, err error) {
	buf := new(bytes.Buffer)
	e := NewEncoder(buf)

	if !s.IsFull() {
		err = e.Encode((*songDirect)(s))
		data = buf.Bytes()
		return
	}

	return json.Marshal(&songNullable{s.CID, s.Name, s.AlbumCID, s.SourceURL,
		NullableString(s.LyricURL), NullableString(s.MvURL), NullableString(s.MvCoverURL),
		s.Artists})
}
