package monstersirenfetch

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
)

const (
	APIPrefix = "https://monster-siren.hypergryph.com/api"
)

// InconsistentEnrichError is returned by an Enrich method when a field is inconsistent
// between base data and enrichment data.
type InconsistentEnrichError[T any] struct {
	Field    string
	Value    T
	NewValue T
}

func (e *InconsistentEnrichError[T]) Error() string {
	return "field " + e.Field + " inconsistent: " +
		fmt.Sprintf("%v differs from %v", e.Value, e.NewValue)
}

// Net represents an abstraction over the [net] package.
type Net interface {
	// Get makes a get request to url and returns the response body.
	// The caller must close the body after it finishes reading from it.
	Get(ctx context.Context, url string) (resp *http.Response, err error)
}

// ResponseStatusError is returned when MSR responds with a StatusCode other than [http.StatusOK].
type ResponseStatusError struct {
	// URL is the full uninterpreted url string for this request.
	URL string
	// StatusCode holds the [http.Response] field of the same name.
	StatusCode int
	// CloseErr holds the error returned closing the response body.
	CloseErr error
}

func (e *ResponseStatusError) Error() string {
	return e.URL + " responded with status code " + strconv.Itoa(e.StatusCode)
}

// Metadata represents metadata, often enriched, of the entire website.
// This is only used externally and is never returned by the API.
type Metadata struct {
	Albums []Album `json:"albums"`
	Songs  []Song  `json:"songs"`
}

// Response is a generic API response.
type Response[T any] struct {
	Code    int    `json:"code"`
	Message string `json:"msg"`
	Data    T      `json:"data"`
}

// NullableString is a JSON string where its zero value behaves like null.
type NullableString string

func (s *NullableString) MarshalJSON() ([]byte, error) {
	if *s == "" {
		return []byte("null"), nil
	}
	return json.Marshal(string(*s))
}

func (s *NullableString) UnmarshalJSON(data []byte) (err error) {
	var v *string
	err = json.Unmarshal(data, &v)
	if err == nil {
		if v != nil {
			*s = NullableString(*v)
		} else {
			*s = ""
		}
	}
	return
}

// StringInt is a JSON string representing an integer.
type StringInt int

func (i *StringInt) String() (s string) {
	s = strconv.Itoa(int(*i))
	if len(s) <= 4 {
		s = strings.Repeat("0", 4-len(s)) + s
	} else if len(s) < 6 {
		s = strings.Repeat("0", 6-len(s)) + s
	}
	return
}

func (i *StringInt) MarshalJSON() ([]byte, error) { return json.Marshal(i.String()) }
func (i *StringInt) UnmarshalJSON(data []byte) (err error) {
	var v string
	err = json.Unmarshal(data, &v)
	if err == nil {
		var n int
		n, err = strconv.Atoi(v)
		if err == nil {
			*i = StringInt(n)
		}
	}
	return
}

// NewEncoder returns a new encoder that writes to w, configured to match upstream API behaviour.
func NewEncoder(w io.Writer) *json.Encoder {
	e := json.NewEncoder(w)
	e.SetEscapeHTML(false)
	return e
}
