package datasource

import (
	"errors"
	"strings"

	"github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"

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

var (
	putDataRequiredProperties    = []string{"data"}            //nolint:gochecknoglobals
	patchDataRequiredProperties  = []string{"path", "data"}    //nolint:gochecknoglobals
	deleteDataRequiredProperties = []string{"path", "version"} //nolint:gochecknoglobals
)

// This is the logical representation of the data in the "put" event. In the JSON representation,
// the "data" property is actually a map of maps, but the schema we use internally is a list of
// lists instead.
//
// The "path" property is normally always "/"; the LD streaming service sends this property, but
// some versions of Relay do not, so we do not require it.
//
// Example JSON representation:
//
//	{
//	  "path": "/",
//	  "data": {
//	    "flags": {
//	      "flag1": { "key": "flag1", "version": 1, ...etc. },
//	      "flag2": { "key": "flag2", "version": 1, ...etc. },
//	    },
//	    "segments": {
//	      "segment1": { "key", "segment1", "version": 1, ...etc. }
//	    }
//	  }
//	}
type putData struct {
	Path string // we don't currently do anything with this
	Data []ldstoretypes.Collection
}

// This is the logical representation of the data in the "patch" event. In the JSON representation,
// there is a "path" property in the format "/flags/key" or "/segments/key", which we convert into
// Kind and Key when we parse it. The "data" property is the JSON representation of the flag or
// segment, which we deserialize into an ItemDescriptor.
//
// Example JSON representation:
//
//	{
//	  "path": "/flags/flagkey",
//	  "data": {
//	    "key": "flagkey",
//	    "version": 2, ...etc.
//	  }
//	}
type patchData struct {
	Kind ldstoretypes.DataKind
	Key  string
	Data ldstoretypes.ItemDescriptor
}

// This is the logical representation of the data in the "delete" event. In the JSON representation,
// there is a "path" property in the format "/flags/key" or "/segments/key", which we convert into
// Kind and Key when we parse it.
//
// Example JSON representation:
//
//	{
//	  "path": "/flags/flagkey",
//	  "version": 3
//	}
type deleteData struct {
	Kind    ldstoretypes.DataKind
	Key     string
	Version int
}

func parsePutData(data []byte) (putData, error) {
	var ret putData
	r := jreader.NewReader(data)
	for obj := r.Object().WithRequiredProperties(putDataRequiredProperties); obj.Next(); {
		switch string(obj.Name()) {
		case "path": //nolint:goconst // linter wants us to define constants, but that makes code like this less clear
			ret.Path = r.String()
		case "data": //nolint:goconst
			ret.Data = parseAllStoreDataFromJSONReader(&r)
		}
	}
	return ret, r.Error()
}

func parsePatchData(data []byte) (patchData, error) {
	var ret patchData
	r := jreader.NewReader(data)
	var kind datakinds.DataKindInternal
	var key string
	parseItem := func() (patchData, error) {
		item, err := kind.DeserializeFromJSONReader(&r)
		if err != nil {
			return patchData{}, err
		}
		ret.Data = item
		return ret, nil
	}
	for obj := r.Object().WithRequiredProperties(patchDataRequiredProperties); obj.Next(); {
		switch string(obj.Name()) {
		case "path":
			path := r.String()
			kind, key = parsePath(path)
			ret.Kind, ret.Key = kind, key
			if kind == nil {
				// An unrecognized path isn't considered an error; we'll just return a nil kind,
				// indicating that we should ignore this event.
				return ret, nil
			}
		case "data":
			if kind != nil {
				// If kind is nil, it means we happened to read the "data" property before the
				// "path" property, so we don't yet know what kind of data model object this is,
				// so we can't parse it yet and we'll have to do a second pass.
				return parseItem()
			}
		}
	}
	if err := r.Error(); err != nil {
		return patchData{}, err
	}
	// If we got here, it means we couldn't parse the data model object yet because we saw the
	// "data" property first. But we definitely saw both properties (otherwise we would've got
	// an error due to using WithRequiredProperties) so kind is now non-nil.
	r = jreader.NewReader(data)
	for obj := r.Object(); obj.Next(); {
		if string(obj.Name()) == "data" {
			return parseItem()
		}
	}
	if r.Error() != nil {
		return patchData{}, r.Error()
	}
	return patchData{}, errors.New("patch event had no data property")
}

func parseDeleteData(data []byte) (deleteData, error) {
	var ret deleteData
	r := jreader.NewReader(data)
	for obj := r.Object().WithRequiredProperties(deleteDataRequiredProperties); obj.Next(); {
		switch string(obj.Name()) {
		case "path":
			path := r.String()
			ret.Kind, ret.Key = parsePath(path)
			if ret.Kind == nil {
				// An unrecognized path isn't considered an error; we'll just return a nil kind,
				// indicating that we should ignore this event.
				return ret, nil
			}
		case "version":
			ret.Version = r.Int()
		}
	}
	if r.Error() != nil {
		return deleteData{}, r.Error()
	}
	return ret, nil
}

func parsePath(path string) (datakinds.DataKindInternal, string) {
	switch {
	case strings.HasPrefix(path, "/segments/"):
		return datakinds.Segments, strings.TrimPrefix(path, "/segments/")
	case strings.HasPrefix(path, "/flags/"):
		return datakinds.Features, strings.TrimPrefix(path, "/flags/")
	default:
		return nil, ""
	}
}
