package gobackend

import (
	"errors"
	"fmt"
	"net/http"
	"strings"
)

// Sentinel classifications for lyrics provider failures. Providers wrap their
// errors with the typed constructors below at the point where the failure kind
// is known, so isLyricsProviderUnavailableError can classify with errors.Is
// instead of parsing error text.
var (
	errLyricsNotFound           = errors.New("lyrics not found")
	errLyricsServiceUnavailable = errors.New("lyrics service unavailable")
)

// lyricsNotFoundError: the provider responded but has nothing for this track
// (including "nothing to search for" inputs). Never disables the provider.
type lyricsNotFoundError struct{ err error }

func (e *lyricsNotFoundError) Error() string        { return e.err.Error() }
func (e *lyricsNotFoundError) Unwrap() error        { return e.err }
func (e *lyricsNotFoundError) Is(target error) bool { return target == errLyricsNotFound }

func lyricsNotFoundErrorf(format string, args ...any) error {
	return &lyricsNotFoundError{err: fmt.Errorf(format, args...)}
}

// lyricsServiceUnavailableError: a provider/API-level failure that should
// temporarily disable the lyrics source (see markLyricsProviderUnavailable).
type lyricsServiceUnavailableError struct{ err error }

func (e *lyricsServiceUnavailableError) Error() string { return e.err.Error() }
func (e *lyricsServiceUnavailableError) Unwrap() error { return e.err }
func (e *lyricsServiceUnavailableError) Is(target error) bool {
	return target == errLyricsServiceUnavailable
}

func lyricsServiceUnavailableErrorf(format string, args ...any) error {
	return &lyricsServiceUnavailableError{err: fmt.Errorf(format, args...)}
}

// lyricsHTTPStatusError classifies an upstream HTTP status at the response
// site: 429 and 5xx mean the service is temporarily unusable; anything else
// stays a plain error.
func lyricsHTTPStatusError(statusCode int, format string, args ...any) error {
	if statusCode == http.StatusTooManyRequests || statusCode >= 500 {
		return lyricsServiceUnavailableErrorf(format, args...)
	}
	return fmt.Errorf(format, args...)
}

// Error payloads from third-party lyrics proxies arrive as free text, often
// with HTTP 200 (see detectLyricsErrorPayload). Their meaning can only be
// recognized from the message, so this is the one place lyrics failures are
// still classified by substring. Errors generated by our own code must use
// the typed constructors above instead.
var (
	lyricsPayloadNotFoundSignals = []string{
		"lyrics not found",
		"no lyrics found",
		"no songs found",
		"not found",
	}
	lyricsPayloadUnavailableSignals = []string{
		"rate limit",
		"too many requests",
		"operation too frequent",
		"操作频繁",
		"missing required parameters",
	}
)

func lyricsPayloadIndicatesNotFound(payloadMsg string) bool {
	msg := strings.ToLower(payloadMsg)
	for _, signal := range lyricsPayloadNotFoundSignals {
		if strings.Contains(msg, signal) {
			return true
		}
	}
	return false
}

// classifyLyricsPayloadError builds a provider error from an upstream error
// payload, typed by what the payload message describes. statusCode is the
// HTTP status of the response (0 when it arrived with HTTP 200).
func classifyLyricsPayloadError(statusCode int, payloadMsg string, format string, args ...any) error {
	if lyricsPayloadIndicatesNotFound(payloadMsg) {
		return lyricsNotFoundErrorf(format, args...)
	}
	msg := strings.ToLower(payloadMsg)
	for _, signal := range lyricsPayloadUnavailableSignals {
		if strings.Contains(msg, signal) {
			return lyricsServiceUnavailableErrorf(format, args...)
		}
	}
	return lyricsHTTPStatusError(statusCode, format, args...)
}
