package datasource

import (
	"fmt"
	"net/http"

	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
	"github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
	st "github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"

	"github.com/launchdarkly/go-jsonstream/v3/jreader"
)

type httpStatusError struct {
	Message string
	Code    int
}

func (e httpStatusError) Error() string {
	return e.Message
}

// Tests whether an HTTP error status represents a condition that might resolve on its own if we retry,
// or at least should not make us permanently stop sending requests.
func isHTTPErrorRecoverable(statusCode int) bool {
	if statusCode >= 400 && statusCode < 500 {
		switch statusCode {
		case 400: // bad request
			return true
		case 408: // request timeout
			return true
		case 429: // too many requests
			return true
		default:
			return false // all other 4xx errors are unrecoverable
		}
	}
	return true
}

func httpErrorDescription(statusCode int) string {
	message := ""
	if statusCode == 401 || statusCode == 403 {
		message = " (invalid SDK key)"
	}
	return fmt.Sprintf("HTTP error %d%s", statusCode, message)
}

// Logs an HTTP error or network error at the appropriate level and determines whether it is recoverable
// (as defined by isHTTPErrorRecoverable).
func checkIfErrorIsRecoverableAndLog(
	loggers ldlog.Loggers,
	errorDesc, errorContext string,
	statusCode int,
	recoverableMessage string,
) bool {
	if statusCode > 0 && !isHTTPErrorRecoverable(statusCode) {
		loggers.Errorf("Error %s (giving up permanently): %s", errorContext, errorDesc)
		return false
	}
	loggers.Warnf("Error %s (%s): %s", errorContext, recoverableMessage, errorDesc)
	return true
}

func checkForHTTPError(statusCode int, url string) error {
	if statusCode == http.StatusUnauthorized {
		return httpStatusError{
			Message: fmt.Sprintf("Invalid SDK key when accessing URL: %s. Verify that your SDK key is correct.", url),
			Code:    statusCode}
	}

	if statusCode == http.StatusNotFound {
		return httpStatusError{
			Message: fmt.Sprintf("Resource not found when accessing URL: %s. Verify that this resource exists.", url),
			Code:    statusCode}
	}

	if statusCode/100 != 2 {
		return httpStatusError{
			Message: fmt.Sprintf("Unexpected response code: %d when accessing URL: %s", statusCode, url),
			Code:    statusCode}
	}
	return nil
}

// This method parses a JSON data structure representing a full set of SDK data. For example:
//
//	{
//	  "flags": {
//	    "flag1": { "key": "flag1", "version": 1, ...etc. },
//	    "flag2": { "key": "flag2", "version": 1, ...etc. },
//	  },
//	  "segments": {
//	    "segment1": { "key", "segment1", "version": 1, ...etc. }
//	  }
//	}
//
// Even though this is map-like, we don't return the data as a map, because the SDK does not need to
// manipulate it as a map. Our data store API instead expects a list of Collections, each of which has
// a list of data items, so that's what we build here.
//
// This representation makes up the entirety of a polling response for PollingDataSource, and is a
// subset of the stream data for StreamingDataSource.
func parseAllStoreDataFromJSONReader(r *jreader.Reader) []st.Collection {
	var ret []st.Collection
	for dataObj := r.Object(); dataObj.Next(); {
		var dataKind datakinds.DataKindInternal
		switch string(dataObj.Name()) {
		case "flags":
			dataKind = datakinds.Features
		case "segments":
			dataKind = datakinds.Segments
		default: // unrecognized category, skip it
			continue
		}
		coll := st.Collection{Kind: dataKind}
		for keysToItemsObj := r.Object(); keysToItemsObj.Next(); {
			key := string(keysToItemsObj.Name())
			item, err := dataKind.DeserializeFromJSONReader(r)
			if err == nil {
				coll.Items = append(coll.Items, st.KeyedItemDescriptor{Key: key, Item: item})
			}
		}
		ret = append(ret, coll)
	}
	return ret
}
