package runmanage

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

const (
	runCursorVersion      = 3
	cursorVersion         = 1
	maxCursorEncodedBytes = 2048
	maxCursorPayloadBytes = 1536
	cursorKindRuns        = "runs"
	cursorKindActivations = "activations"
	cursorKindEffects     = "effects"
	cursorKindEvents      = "events"
)

// pageCursor is opaque on the wire. Scope and filters are embedded so a
// cursor issued for one workspace/Run/status cannot silently be reused for a
// different query.
type pageCursor struct {
	Version         int        `json:"v"`
	Kind            string     `json:"kind"`
	WorkspaceID     string     `json:"workspaceId"`
	PlanID          string     `json:"planId,omitempty"`
	RunID           string     `json:"runId,omitempty"`
	Status          string     `json:"status,omitempty"`
	AfterID         string     `json:"afterId,omitempty"`
	AfterSeq        int64      `json:"afterSeq,omitempty"`
	BeforeStartedAt *time.Time `json:"beforeStartedAt,omitempty"`
}

func encodeCursor(value pageCursor) (string, error) {
	if value.Kind == cursorKindRuns {
		value.Version = runCursorVersion
	} else {
		value.Version = cursorVersion
	}
	payload, err := json.Marshal(value)
	if err != nil {
		return "", fmt.Errorf("encode Workflow Run cursor: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(payload), nil
}

func decodeCursor(raw, kind, workspaceID, planID, runID, status string) (pageCursor, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return pageCursor{}, nil
	}
	if len(raw) > maxCursorEncodedBytes {
		return pageCursor{}, invalidRequest("cursor is invalid")
	}
	payload, err := base64.RawURLEncoding.DecodeString(raw)
	if err != nil || len(payload) > maxCursorPayloadBytes {
		return pageCursor{}, invalidRequest("cursor is invalid")
	}
	value, err := decodeCursorPayload(payload)
	expectedVersion := cursorVersion
	if kind == cursorKindRuns {
		expectedVersion = runCursorVersion
	}
	if err != nil || value.Version != expectedVersion || value.Kind != kind ||
		value.WorkspaceID != workspaceID || value.PlanID != planID ||
		value.RunID != runID || value.Status != status {
		return pageCursor{}, invalidRequest("cursor does not belong to this query")
	}
	if kind == cursorKindEvents {
		if value.AfterSeq < 1 || value.AfterID != "" || value.BeforeStartedAt != nil {
			return pageCursor{}, invalidRequest("cursor is invalid")
		}
	} else if strings.TrimSpace(value.AfterID) == "" || value.AfterSeq != 0 ||
		(kind != cursorKindRuns && value.BeforeStartedAt != nil) {
		return pageCursor{}, invalidRequest("cursor is invalid")
	}
	return value, nil
}

func decodeCursorPayload(payload []byte) (pageCursor, error) {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	opening, err := decoder.Token()
	if err != nil || opening != json.Delim('{') {
		return pageCursor{}, fmt.Errorf("cursor payload must be an object")
	}
	var value pageCursor
	seen := map[string]struct{}{}
	for decoder.More() {
		token, err := decoder.Token()
		if err != nil {
			return pageCursor{}, err
		}
		name, ok := token.(string)
		if !ok {
			return pageCursor{}, fmt.Errorf("cursor field name must be a string")
		}
		if _, duplicate := seen[name]; duplicate {
			return pageCursor{}, fmt.Errorf("cursor field %q is duplicated", name)
		}
		seen[name] = struct{}{}
		switch name {
		case "v":
			err = decoder.Decode(&value.Version)
		case "kind":
			err = decoder.Decode(&value.Kind)
		case "workspaceId":
			err = decoder.Decode(&value.WorkspaceID)
		case "planId":
			err = decoder.Decode(&value.PlanID)
		case "runId":
			err = decoder.Decode(&value.RunID)
		case "status":
			err = decoder.Decode(&value.Status)
		case "afterId":
			err = decoder.Decode(&value.AfterID)
		case "afterSeq":
			err = decoder.Decode(&value.AfterSeq)
		case "beforeStartedAt":
			err = decoder.Decode(&value.BeforeStartedAt)
		default:
			return pageCursor{}, fmt.Errorf("cursor field %q is unknown", name)
		}
		if err != nil {
			return pageCursor{}, err
		}
	}
	closing, err := decoder.Token()
	if err != nil || closing != json.Delim('}') {
		return pageCursor{}, fmt.Errorf("cursor payload is incomplete")
	}
	if token, err := decoder.Token(); err != io.EOF {
		if err != nil {
			return pageCursor{}, err
		}
		return pageCursor{}, fmt.Errorf("cursor payload has trailing token %v", token)
	}
	return value, nil
}
