package invocationmanage

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"time"
)

const (
	// Invocation list cursors are generated by this service and currently encode
	// fewer than 128 bytes. Keep a generous fixed ceiling so an untrusted cursor
	// cannot cause an unbounded base64 decode allocation.
	maxListCursorEncodedBytes = 512
	maxListCursorPayloadBytes = 384
)

type listCursor struct {
	AcceptedAt time.Time `json:"acceptedAt"`
	ID         string    `json:"id"`
}

func encodeListCursor(acceptedAt time.Time, id string) (string, error) {
	id = strings.TrimSpace(id)
	if acceptedAt.IsZero() || id == "" {
		return "", fmt.Errorf("encode Invocation cursor: acceptedAt and id are required")
	}
	payload, err := json.Marshal(listCursor{AcceptedAt: acceptedAt.UTC(), ID: id})
	if err != nil {
		return "", fmt.Errorf("encode Invocation cursor: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(payload), nil
}

func decodeListCursor(value string) (listCursor, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return listCursor{}, nil
	}
	if len(value) > maxListCursorEncodedBytes {
		return listCursor{}, invalidRequest("cursor is invalid")
	}
	payload, err := base64.RawURLEncoding.DecodeString(value)
	if err != nil || len(payload) > maxListCursorPayloadBytes {
		return listCursor{}, invalidRequest("cursor is invalid")
	}
	cursor, err := decodeListCursorPayload(payload)
	if err != nil || cursor.AcceptedAt.IsZero() || strings.TrimSpace(cursor.ID) == "" {
		return listCursor{}, invalidRequest("cursor is invalid")
	}
	cursor.AcceptedAt = cursor.AcceptedAt.UTC()
	cursor.ID = strings.TrimSpace(cursor.ID)
	return cursor, nil
}

func decodeListCursorPayload(payload []byte) (listCursor, error) {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	opening, err := decoder.Token()
	if err != nil || opening != json.Delim('{') {
		return listCursor{}, fmt.Errorf("cursor payload must be an object")
	}

	var cursor listCursor
	seen := make(map[string]struct{}, 2)
	for decoder.More() {
		token, err := decoder.Token()
		if err != nil {
			return listCursor{}, err
		}
		name, ok := token.(string)
		if !ok {
			return listCursor{}, fmt.Errorf("cursor field name must be a string")
		}
		if _, duplicate := seen[name]; duplicate {
			return listCursor{}, fmt.Errorf("cursor field %q is duplicated", name)
		}
		seen[name] = struct{}{}
		switch name {
		case "acceptedAt":
			if err := decoder.Decode(&cursor.AcceptedAt); err != nil {
				return listCursor{}, err
			}
		case "id":
			if err := decoder.Decode(&cursor.ID); err != nil {
				return listCursor{}, err
			}
		default:
			return listCursor{}, fmt.Errorf("cursor field %q is unknown", name)
		}
	}
	closing, err := decoder.Token()
	if err != nil || closing != json.Delim('}') {
		return listCursor{}, fmt.Errorf("cursor payload is incomplete")
	}
	if token, err := decoder.Token(); err != io.EOF {
		if err != nil {
			return listCursor{}, err
		}
		return listCursor{}, fmt.Errorf("cursor payload has trailing token %v", token)
	}
	return cursor, nil
}
