package engine

import (
	"errors"
	"fmt"
	"net/http"
	"net/textproto"
	"slices"
	"sort"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/event"
	starjson "go.starlark.net/lib/json"
	startime "go.starlark.net/lib/time"
	"go.starlark.net/starlark"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const (
	keyAttributes starlark.String = "attributes"
	keyData       starlark.String = "data"

	// https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#datacontenttype
	attrDataContentType = "datacontenttype"
	dataContentTypeJSON = "application/json"
)

func EventToDict(e *event.CloudEvent) (*starlark.Dict, error) {
	eOuter := proto.CloneOf(e)
	eOuter.Data = nil

	eOuterVal, err := protoMessageToStarlarkValue(eOuter)
	if err != nil {
		return nil, err
	}

	eOuterDict, ok := eOuterVal.(*starlark.Dict)
	if !ok {
		return nil, fmt.Errorf("internal error: expecting a dictionary, got %T", eOuterVal)
	}

	_, found, err := eOuterDict.Get(keyAttributes)
	if err != nil {
		return nil, err
	}
	if !found {
		err = eOuterDict.SetKey(keyAttributes, starlark.NewDict(0))
		if err != nil {
			return nil, err
		}
	}
	var dataVal starlark.Value

	switch data := e.Data.(type) {
	case *event.CloudEvent_ProtoData:
		dataVal, err = protoMessageToStarlarkValue(data.ProtoData)
		if err != nil {
			return nil, err
		}
	case *event.CloudEvent_BinaryData:
		attr := e.Attributes[attrDataContentType]
		if attr == nil { // no datacontenttype attribute set
			dataVal = starlark.Bytes(data.BinaryData)
		} else {
			switch ct := attr.Attr.(type) {
			case *event.CloudEvent_CloudEventAttributeValue_CeString:
				switch ct.CeString {
				case dataContentTypeJSON:
					dataVal, err = unmarshalJSON(starlark.String(data.BinaryData))
					if err != nil {
						return nil, err
					}
				default:
					return nil, fmt.Errorf("unsupported event data content type: %s", ct.CeString)
				}
			default:
				return nil, fmt.Errorf("expecting a string for %s attribute, got %T", attrDataContentType, attr.Attr)
			}
		}
	case *event.CloudEvent_TextData:
		attr := e.Attributes[attrDataContentType]
		if attr == nil { // no datacontenttype attribute set
			dataVal = starlark.String(data.TextData)
		} else {
			switch ct := attr.Attr.(type) {
			case *event.CloudEvent_CloudEventAttributeValue_CeString:
				switch ct.CeString {
				case dataContentTypeJSON:
					dataVal, err = unmarshalJSON(starlark.String(data.TextData))
					if err != nil {
						return nil, err
					}
				default:
					return nil, fmt.Errorf("unsupported event data content type: %s", ct.CeString)
				}
			default:
				return nil, fmt.Errorf("expecting a string for %s attribute, got %T", attrDataContentType, attr.Attr)
			}
		}
	default:
		return nil, fmt.Errorf("unsupported event type: %T", e.Data)
	}

	err = eOuterDict.SetKey(keyData, dataVal)
	if err != nil {
		return nil, err
	}

	eOuterDict.Freeze()
	return eOuterDict, nil
}

func unmarshalJSON(data starlark.String) (starlark.Value, error) {
	thread := &starlark.Thread{
		Name: "unmarshalJSON",
		Print: func(thread *starlark.Thread, msg string) {
			// noop
		},
		Load: func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
			// load() can only be a top-level statement so cannot occur while running a Starlark function.
			return nil, fmt.Errorf("unexpected load(%s) call", module)
		},
	}
	startime.SetNow(thread, func() (time.Time, error) {
		return time.Time{}, errors.New("time.now() is not available")
	})
	// This call to starlark.Call is "safe" to treat as deterministic,
	// because that is what Starlark guarantees us.
	//workflowcheck:ignore
	v, err := starlark.Call(thread, starjson.Module.Members["decode"], starlark.Tuple{data}, nil)
	if err != nil {
		return nil, fmt.Errorf("unmarshalJSON: %w", err)
	}
	return v, nil
}

type kv struct {
	key starlark.String
	val starlark.Value
}

type mapEntry struct {
	key, val protoreflect.Value
}

func protoMessageToStarlarkValue(m proto.Message) (starlark.Value, error) {
	switch km := m.(type) {
	case *anypb.Any: // TODO support other WKT
		unwrapped, err := km.UnmarshalNew()
		if err != nil {
			return nil, err
		}
		return protoMessageToStarlarkValue(unwrapped)
	case *timestamppb.Timestamp:
		return startime.Time(km.AsTime()), nil
	default:
		return protoToDictInternal(m.ProtoReflect())
	}
}

func protoToDictInternal(r protoreflect.Message) (*starlark.Dict, error) {
	l := r.Descriptor().Fields().Len()
	numbers := make([]protoreflect.FieldNumber, 0, l)
	kvs := make(map[protoreflect.FieldNumber]kv, l)
	var rangeErr error
	r.Range(func(desc protoreflect.FieldDescriptor, value protoreflect.Value) bool {
		var key starlark.String
		if desc.IsExtension() {
			key = starlark.String(desc.FullName())
		} else {
			key = starlark.String(desc.Name())
		}
		v, err := protoFieldToStarlarkValue(desc, value)
		if err != nil {
			rangeErr = err
			return false
		}
		number := desc.Number()
		numbers = append(numbers, number)
		kvs[number] = kv{
			key: key,
			val: v,
		}
		return true
	})
	if rangeErr != nil {
		return nil, rangeErr
	}

	// Sort fields by their number to ensure deterministic insertion and iteration order.
	slices.Sort(numbers)

	d := starlark.NewDict(len(numbers))

	for _, n := range numbers {
		x := kvs[n]
		err := d.SetKey(x.key, x.val)
		if err != nil {
			return nil, err
		}
	}

	return d, nil
}

func protoFieldToStarlarkValue(desc protoreflect.FieldDescriptor, v protoreflect.Value) (starlark.Value, error) {
	switch {
	case desc.IsList():
		val := v.List()
		l := val.Len()
		elems := make([]starlark.Value, 0, l)
		for i := range l {
			elem, err := scalarProtoFieldToStarlarkValue(desc, val.Get(i))
			if err != nil {
				return nil, err
			}
			elems = append(elems, elem)
		}
		return starlark.NewList(elems), nil
	case desc.IsMap():
		val := v.Map()
		mapKeyDesc := desc.MapKey()
		mapValueDesc := desc.MapValue()
		keyKind := mapKeyDesc.Kind()

		entries := make([]mapEntry, 0, val.Len())
		val.Range(func(key protoreflect.MapKey, value protoreflect.Value) bool {
			entries = append(entries, mapEntry{key: key.Value(), val: value})
			return true
		})
		sort.Slice(entries, func(i, j int) bool {
			switch keyKind { //nolint:exhaustive
			case protoreflect.BoolKind:
				return !entries[i].key.Bool() && entries[j].key.Bool()
			case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
				return entries[i].key.Int() < entries[j].key.Int()
			case protoreflect.Uint32Kind, protoreflect.Fixed32Kind, protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
				return entries[i].key.Uint() < entries[j].key.Uint()
			case protoreflect.StringKind:
				return entries[i].key.String() < entries[j].key.String()
			default:
				panic("invalid key kind")
			}
		})

		d := starlark.NewDict(len(entries))
		for _, e := range entries {
			sk, err := scalarProtoFieldToStarlarkValue(mapKeyDesc, e.key)
			if err != nil {
				return nil, err
			}
			sv, err := scalarProtoFieldToStarlarkValue(mapValueDesc, e.val)
			if err != nil {
				return nil, err
			}
			err = d.SetKey(sk, sv)
			if err != nil {
				return nil, err
			}
		}
		return d, nil
	default:
		return scalarProtoFieldToStarlarkValue(desc, v)
	}
}

func scalarProtoFieldToStarlarkValue(desc protoreflect.FieldDescriptor, v protoreflect.Value) (starlark.Value, error) {
	switch desc.Kind() {
	case protoreflect.BoolKind:
		return starlark.Bool(v.Bool()), nil

	case protoreflect.Fixed32Kind,
		protoreflect.Uint32Kind,
		protoreflect.Uint64Kind,
		protoreflect.Fixed64Kind:
		return starlark.MakeUint64(v.Uint()), nil

	case protoreflect.Int32Kind,
		protoreflect.Sfixed32Kind,
		protoreflect.Sint32Kind,
		protoreflect.Int64Kind,
		protoreflect.Sfixed64Kind,
		protoreflect.Sint64Kind:
		return starlark.MakeInt64(v.Int()), nil

	case protoreflect.StringKind:
		return starlark.String(v.String()), nil

	case protoreflect.BytesKind:
		return starlark.Bytes(v.Bytes()), nil

	case protoreflect.DoubleKind, protoreflect.FloatKind:
		return starlark.Float(v.Float()), nil

	case protoreflect.MessageKind:
		return protoMessageToStarlarkValue(v.Message().Interface())

	case protoreflect.EnumKind:
		return nil, fmt.Errorf("enums are not supported: %s", desc.FullName()) // TODO

	case protoreflect.GroupKind:
		// https://protobuf.dev/programming-guides/encoding/#groups
		return nil, fmt.Errorf("groups are not supported: %s", desc.FullName())

	default:
		return nil, fmt.Errorf("unknown field kind %d for field %s", desc.Kind(), desc.FullName())
	}
}

func valuesMapToHTTPHeader(from map[string]*Values) http.Header {
	res := make(http.Header, len(from))
	for key, val := range from {
		res[textproto.CanonicalMIMEHeaderKey(key)] = val.Value // normalize header names
	}
	return res
}

func httpHeaderToValuesMap(from http.Header) map[string]*Values {
	res := make(map[string]*Values, len(from))
	for key, val := range from {
		res[key] = &Values{
			Value: val,
		}
	}
	return res
}
