package monstersirenfetch

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"os"
)

// AlbumResponse is the response of /api/album/%d/data and /api/album/%d/detail.
type AlbumResponse Response[Album]

// AlbumSong represents a minimal variant of [Song] embedded in the detail variant of [Album].
type AlbumSong struct {
	CID     StringInt `json:"cid"`
	Name    string    `json:"name"`
	Artists []string  `json:"artistes"`
}

// Album represents the metadata of an album.
// Field availability is documented for each individual field.
type Album struct {
	// CID is available for all variants of [Album].
	CID StringInt `json:"cid"`
	// Name is available for all variants of [Album].
	Name string `json:"name"`
	// Intro is available for data and detail variants of [Album].
	Intro string `json:"intro,omitempty"`
	// Belong is available for data and detail variants of [Album].
	Belong string `json:"belong,omitempty"`
	// CoverURL is available for all variants of [Album].
	CoverURL string `json:"coverUrl"`
	// CoverDeURL is available for data and detail variants of [Album].
	CoverDeURL string `json:"coverDeUrl,omitempty"`
	// Songs is available for the detail variant of [Album].
	Songs []AlbumSong `json:"songs,omitempty"`
	// Artists is available for base and data variants of [Album].
	Artists []string `json:"artistes"`
}

const (
	// AlbumVariantCurrent copies the current variant as-is.
	AlbumVariantCurrent = iota
	// AlbumVariantFull leaves all fields intact in the copy.
	// This variant is not returned by any endpoint and is obtained via [Album.Enrich].
	AlbumVariantFull
	// AlbumVariantDetail zeroes [Album.Artists].
	// This variant is returned by /api/album/%s/detail.
	AlbumVariantDetail
	// AlbumVariantData zeroes [Album.Songs].
	// This variant is returned by /api/album/%d/data.
	AlbumVariantData
	// AlbumVariantBase zeroes [Album.Intro], [Album.Belong], [Album.CoverDeURL], [Album.Songs].
	// This variant is included in /api/albums.
	AlbumVariantBase
)

// IsFull returns whether [Album] is considered to be its [AlbumVariantFull] variant.
func (a *Album) IsFull() bool { return a.Belong != "" && len(a.Songs) > 0 }

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

	if variant == AlbumVariantCurrent {
		*v = *a
		return true
	}
	if !a.IsFull() {
		return false
	}

	switch variant {
	case AlbumVariantFull:
		*v = *a

	case AlbumVariantDetail:
		*v = *a
		v.Artists = nil

	case AlbumVariantData:
		*v = *a
		v.Songs = nil

	case AlbumVariantBase:
		*v = *a
		v.Intro = ""
		v.Belong = ""
		v.CoverDeURL = ""
		v.Songs = nil

	default:
		return false
	}

	return true
}

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

	var v AlbumResponse
	if body, _, err := n.Get(ctx, APIPrefix+"/album/"+a.CID.String()+"/detail"); err != nil {
		return err
	} else {
		if err = json.NewDecoder(body).Decode(&v); err != nil {
			return errors.Join(body.Close(), err)
		}
		if err = body.Close(); err != nil {
			return err
		}
	}

	// these should be unreachable unless the server malfunctions
	if a.CID != v.Data.CID {
		return &InconsistentEnrichError[StringInt]{"cid", a.CID, v.Data.CID}
	}
	if a.Name != v.Data.Name {
		return &InconsistentEnrichError[string]{"name", a.Name, v.Data.Name}
	}
	if a.CoverURL != v.Data.CoverURL {
		return &InconsistentEnrichError[string]{"coverUrl", a.CoverURL, v.Data.CoverURL}
	}
	if v.Data.Artists != nil {
		return &InconsistentEnrichError[[]string]{"artists", nil, v.Data.Artists}
	}

	v.Data.Artists = a.Artists
	*a = v.Data
	return nil
}

// albumDirect is [Album] without its MarshalJSON method.
type albumDirect Album

// albumNullable is [Album] with corresponding nullable string fields.
type albumNullable struct {
	CID        StringInt      `json:"cid"`
	Name       string         `json:"name"`
	Intro      NullableString `json:"intro"`
	Belong     NullableString `json:"belong"`
	CoverURL   string         `json:"coverUrl"`
	CoverDeURL NullableString `json:"coverDeUrl"`
	Songs      []AlbumSong    `json:"songs,omitempty"`
	Artists    []string       `json:"artistes"`
}

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

	if a.Belong == "" { // this covers both data and detail variants
		err = e.Encode((*albumDirect)(a))
		data = buf.Bytes()
		return
	}

	return json.Marshal(&albumNullable{a.CID, a.Name,
		NullableString(a.Intro), NullableString(a.Belong),
		a.CoverURL, NullableString(a.CoverDeURL), a.Songs, a.Artists})
}
