package exec

import (
	"encoding/json"
	"fmt"
	"reflect"

	invopopjsonschema "github.com/invopop/jsonschema"

	orderedmap "github.com/wk8/go-ordered-map/v2"
)

type workflowPlanResult struct {
	create     *workflowPlanCreateResponse
	list       *workflowPlanListResponse
	plan       *workflowPlanResponse
	validation *workflowPlanValidationResponse
	draft      *workflowPlanDraftResponse
	publish    *workflowPlanPublishResponse
	agentView  *workflowPlanAgentViewResponse
	version    *workflowPlanVersionResponse
}

type workflowTriggerResult struct {
	mutation   *workflowTriggerMutationResponse
	list       *workflowTriggerListResponse
	trigger    *workflowTriggerResponse
	show       *workflowTriggerShowResponse
	validation *workflowTriggerValidationResponse
}

type workflowRunResult struct {
	start      *workflowRunStartResponse
	list       *workflowRunListResponse
	show       *workflowRunShowResponse
	logs       *workflowRunLogsResponse
	logDetails *workflowRunLogsDetailsResponse
	cancel     *workflowRunCancelResponse
}

type workflowStepOutcome struct {
	step *workflowStepView
	list *workflowStepListResponse
}

func (r workflowPlanResult) MarshalJSON() ([]byte, error) {
	return marshalWorkflowResult(
		r.create, r.list, r.plan, r.validation, r.draft, r.publish, r.agentView, r.version,
	)
}

func (r workflowTriggerResult) MarshalJSON() ([]byte, error) {
	return marshalWorkflowResult(
		r.mutation, r.list, r.trigger, r.show, r.validation,
	)
}

func (r workflowRunResult) MarshalJSON() ([]byte, error) {
	variants := []any{r.start, r.list, r.show, r.logs, r.logDetails, r.cancel}
	return marshalWorkflowResult(variants...)
}

func (r workflowStepOutcome) MarshalJSON() ([]byte, error) {
	return marshalWorkflowResult(r.step, r.list)
}

func marshalWorkflowResult(variants ...any) ([]byte, error) {
	var selected any
	for _, variant := range variants {
		value := reflect.ValueOf(variant)
		if !value.IsValid() || (value.Kind() == reflect.Pointer && value.IsNil()) {
			continue
		}
		if selected != nil {
			return nil, fmt.Errorf("workflow result contains multiple variants")
		}
		selected = variant
	}
	if selected == nil {
		return nil, fmt.Errorf("workflow result does not contain a variant")
	}
	return json.Marshal(selected)
}

func (workflowPlanResult) contractVariants() []reflect.Type {
	return contractTypes(
		workflowPlanCreateResponse{}, workflowPlanListResponse{}, workflowPlanResponse{},
		workflowPlanValidationResponse{}, workflowPlanDraftResponse{}, workflowPlanPublishResponse{},
		workflowPlanAgentViewResponse{}, workflowPlanVersionResponse{},
	)
}

func (workflowTriggerResult) contractVariants() []reflect.Type {
	return contractTypes(
		workflowTriggerMutationResponse{}, workflowTriggerListResponse{}, workflowTriggerResponse{}, workflowTriggerShowResponse{},
		workflowTriggerValidationResponse{},
	)
}

func (workflowRunResult) contractVariants() []reflect.Type {
	return contractTypes(workflowRunStartResponse{}, workflowRunListResponse{}, workflowRunShowResponse{}, workflowRunLogsResponse{}, workflowRunLogsDetailsResponse{}, workflowRunCancelResponse{})
}

func (workflowStepOutcome) contractVariants() []reflect.Type {
	return contractTypes(workflowStepView{}, workflowStepListResponse{})
}

func (workflowPlanResult) JSONSchema() *invopopjsonschema.Schema {
	return closedWorkflowResultSchema([]any{
		workflowPlanCreateResponse{}, workflowPlanResponse{}, workflowPlanValidationResponse{},
		workflowPlanDraftResponse{}, workflowPlanPublishResponse{},
		workflowPlanAgentViewResponse{}, workflowPlanVersionResponse{},
	}, [][]any{{workflowPlanResponse{}}})
}

func (workflowTriggerResult) JSONSchema() *invopopjsonschema.Schema {
	return closedWorkflowResultSchema([]any{
		workflowTriggerMutationResponse{}, workflowTriggerResponse{}, workflowTriggerShowResponse{}, workflowTriggerValidationResponse{},
	}, [][]any{{workflowTriggerResponse{}}})
}

func (workflowRunResult) JSONSchema() *invopopjsonschema.Schema {
	return closedWorkflowResultSchema([]any{
		workflowRunStartResponse{}, workflowRunShowResponse{}, workflowRunCancelResponse{}, workflowRunLogsDetailsResponse{},
	}, [][]any{{workflowRunResponse{}, workflowRunLogEntryResponse{}}})
}

func (workflowStepOutcome) JSONSchema() *invopopjsonschema.Schema {
	return closedWorkflowResultSchema([]any{workflowStepView{}, workflowStepListResponse{}}, nil)
}

func contractTypes(values ...any) []reflect.Type {
	types := make([]reflect.Type, 0, len(values))
	for _, value := range values {
		types = append(types, reflect.TypeOf(value))
	}
	return types
}

func closedWorkflowResultSchema(direct []any, pageClasses [][]any) *invopopjsonschema.Schema {
	oneOf := make([]*invopopjsonschema.Schema, 0, len(direct)+len(pageClasses))
	for _, value := range direct {
		oneOf = append(oneOf, reflectedWorkflowSchema(value))
	}
	for _, items := range pageClasses {
		oneOf = append(oneOf, workflowPageClassSchema(items))
	}
	return &invopopjsonschema.Schema{OneOf: oneOf}
}

func workflowPageClassSchema(items []any) *invopopjsonschema.Schema {
	empty := closedWorkflowPageSchema(invopopjsonschema.TrueSchema, uint64Pointer(0), nil)
	variants := []*invopopjsonschema.Schema{empty}
	for _, item := range items {
		variants = append(variants, closedWorkflowPageSchema(reflectedWorkflowSchema(item), nil, uint64Pointer(1)))
	}
	return &invopopjsonschema.Schema{OneOf: variants}
}

func closedWorkflowPageSchema(item *invopopjsonschema.Schema, maxItems, minItems *uint64) *invopopjsonschema.Schema {
	items := &invopopjsonschema.Schema{Type: "array", Items: item, MaxItems: maxItems, MinItems: minItems}
	properties := orderedmap.New[string, *invopopjsonschema.Schema]()
	properties.Set("items", items)
	properties.Set("nextCursor", &invopopjsonschema.Schema{Type: "string"})
	return &invopopjsonschema.Schema{
		Type: "object", Properties: properties, Required: []string{"items"},
		AdditionalProperties: invopopjsonschema.FalseSchema,
	}
}

func reflectedWorkflowSchema(value any) *invopopjsonschema.Schema {
	reflector := &invopopjsonschema.Reflector{Anonymous: true, DoNotReference: true, ExpandedStruct: true}
	return reflector.Reflect(value)
}

func uint64Pointer(value uint64) *uint64 { return &value }
