package exec

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

	invopopjsonschema "github.com/invopop/jsonschema"
)

func reflectContractSchema(t reflect.Type) (json.RawMessage, error) {
	reflector := &invopopjsonschema.Reflector{
		Anonymous:      true,
		DoNotReference: true,
		ExpandedStruct: true,
	}
	schema := reflector.ReflectFromType(t)
	if schema != nil && schema.Type == "" && len(schema.OneOf) > 0 {
		schema.Type = "object"
	}
	raw, err := json.Marshal(schema)
	if err != nil {
		return nil, err
	}
	if !json.Valid(raw) {
		return nil, fmt.Errorf("generated invalid JSON")
	}
	return json.RawMessage(raw), nil
}

type contractSchemaPolicy struct {
	rootType reflect.Type
	rootPath string
}

type contractSchemaCursor struct {
	typ           reflect.Type
	semanticPath  string
	oneOf         []contractSchemaCursor
	emptyPageRoot bool
	allowTrue     bool
}

func validateContractSchemaPolicy(raw json.RawMessage, policy contractSchemaPolicy) error {
	document, err := decodeStrict(raw)
	if err != nil {
		return fmt.Errorf("schema is not valid JSON: %w", err)
	}
	if err := rejectContractSchemaRefs(document, "schema"); err != nil {
		return err
	}
	return validateContractSchemaNode(document, contractSchemaCursor{
		typ: policy.rootType, semanticPath: policy.rootPath,
	}, "schema", make(map[reflect.Type]bool))
}

func rejectContractSchemaRefs(node any, schemaPath string) error {
	schema, ok := node.(map[string]any)
	if !ok {
		return nil
	}
	if _, exists := schema["$ref"]; exists {
		return fmt.Errorf("%s.$ref is not supported; contract schemas must be fully expanded", schemaPath)
	}
	if _, exists := schema["$dynamicRef"]; exists {
		return fmt.Errorf("%s.$dynamicRef is not supported; contract schemas must be fully expanded", schemaPath)
	}
	for _, keyword := range schemaArrayKeywords {
		children, _ := schema[keyword].([]any)
		for index, child := range children {
			if err := rejectContractSchemaRefs(child, fmt.Sprintf("%s.%s[%d]", schemaPath, keyword, index)); err != nil {
				return err
			}
		}
	}
	for _, keyword := range singleSchemaKeywords {
		if child, exists := schema[keyword]; exists {
			if err := rejectContractSchemaRefs(child, schemaPath+"."+keyword); err != nil {
				return err
			}
		}
	}
	for _, keyword := range namedSchemaMapKeywords {
		children, _ := schema[keyword].(map[string]any)
		for name, child := range children {
			if err := rejectContractSchemaRefs(child, schemaPath+"."+keyword+"."+name); err != nil {
				return err
			}
		}
	}
	return nil
}

func validateContractSchemaNode(
	node any,
	cursor contractSchemaCursor,
	schemaPath string,
	active map[reflect.Type]bool,
) error {
	if boolean, ok := node.(bool); ok {
		if boolean && !cursor.allowTrue && !approvedOpenContractSchema(cursor.typ, cursor.semanticPath) {
			return fmt.Errorf("%s must not be an open true schema", schemaPath)
		}
		return nil
	}
	schema, ok := node.(map[string]any)
	if !ok {
		return fmt.Errorf("%s must be a schema object", schemaPath)
	}
	t := cursor.typ
	for t.Kind() == reflect.Pointer {
		t = t.Elem()
	}
	cursor.typ = t
	openAllowed := approvedOpenContractSchema(t, cursor.semanticPath)

	isObject := contractSchemaIsObject(schema)
	rootUnion := schemaPath == "schema" && schema["properties"] == nil && schema["oneOf"] != nil &&
		(approvedContractSchemaBranches(t) != nil || t == reflect.TypeOf(workflowPlanRequest{}) ||
			t == reflect.TypeOf(workflowTriggerRequest{}) || t == reflect.TypeOf(workflowRunRequest{}) ||
			t == reflect.TypeOf(workflowStepCommand{}))
	if err := validateContractObjectClosure(schema, schemaPath, isObject, openAllowed || rootUnion); err != nil {
		return err
	}
	if err := validateContractSchemaKind(schema, t, schemaPath, openAllowed); err != nil {
		return err
	}
	if err := validateContractSchemaCollections(schema, cursor, schemaPath, active); err != nil {
		return err
	}
	return validateContractSchemaShape(schema, cursor, schemaPath, active, openAllowed)
}

func validateContractSchemaCollections(
	schema map[string]any,
	cursor contractSchemaCursor,
	schemaPath string,
	active map[reflect.Type]bool,
) error {
	t := cursor.typ
	for keyword := range schema {
		if _, supported := supportedContractSchemaKeywords[keyword]; !supported {
			return fmt.Errorf("%s.%s is an unsupported schema keyword", schemaPath, keyword)
		}
	}
	if rawBranches, exists := schema["oneOf"]; exists {
		branches, ok := rawBranches.([]any)
		if !ok || len(branches) == 0 {
			return fmt.Errorf("%s.oneOf must contain at least one branch", schemaPath)
		}
		cursors := cursor.oneOf
		if cursors == nil {
			cursors = approvedContractSchemaBranches(t)
		}
		if cursors == nil {
			cursors = make([]contractSchemaCursor, len(branches))
			for index := range cursors {
				cursors[index] = cursor
			}
		}
		if len(cursors) != len(branches) {
			return fmt.Errorf("%s.oneOf has %d branches; contract declares %d", schemaPath, len(branches), len(cursors))
		}
		for index, branch := range branches {
			branchCursor := cursor
			if cursors != nil {
				branchCursor = cursors[index]
			}
			if err := validateContractSchemaNode(
				branch,
				branchCursor,
				fmt.Sprintf("%s.oneOf[%d]", schemaPath, index),
				active,
			); err != nil {
				return err
			}
		}
	}
	for _, keyword := range []string{"allOf", "anyOf"} {
		branches, _ := schema[keyword].([]any)
		for index, branch := range branches {
			if err := validateContractSchemaNode(
				branch, cursor, fmt.Sprintf("%s.%s[%d]", schemaPath, keyword, index), active,
			); err != nil {
				return err
			}
		}
	}
	if branches, _ := schema["prefixItems"].([]any); len(branches) > 0 {
		itemCursor := contractSchemaItemCursor(cursor)
		for index, branch := range branches {
			if err := validateContractSchemaNode(
				branch,
				itemCursor,
				fmt.Sprintf("%s.prefixItems[%d]", schemaPath, index),
				active,
			); err != nil {
				return err
			}
		}
	}
	if item, exists := schema["items"]; exists {
		itemCursor := contractSchemaItemCursor(cursor)
		itemCursor.allowTrue = cursor.emptyPageRoot && schema["type"] == "array" && schema["maxItems"] == json.Number("0")
		if err := validateContractSchemaNode(item, itemCursor, schemaPath+".items", active); err != nil {
			return err
		}
	}
	for _, keyword := range []string{"not", "if", "then", "else", "contains", "propertyNames", "contentSchema", "unevaluatedProperties"} {
		if child, exists := schema[keyword]; exists {
			if err := validateContractSchemaNode(child, cursor, schemaPath+"."+keyword, active); err != nil {
				return err
			}
		}
	}
	if child, exists := schema["unevaluatedItems"]; exists {
		if err := validateContractSchemaNode(
			child, contractSchemaItemCursor(cursor), schemaPath+".unevaluatedItems", active,
		); err != nil {
			return err
		}
	}
	for _, keyword := range []string{"$defs", "patternProperties", "dependentSchemas"} {
		children, _ := schema[keyword].(map[string]any)
		for name, child := range children {
			if err := validateContractSchemaNode(child, cursor, schemaPath+"."+keyword+"."+name, active); err != nil {
				return err
			}
		}
	}
	return nil
}

func contractType[T any]() reflect.Type {
	return reflect.TypeOf((*T)(nil)).Elem()
}

func validateConcreteContractType(t reflect.Type, label string) error {
	rootPath := contractTypeName(t)
	return validateConcreteContractValue(t, label, rootPath, rootPath, make(map[reflect.Type]bool))
}

type contractVariantProvider interface {
	contractVariants() []reflect.Type
}

var (
	jsonMarshalerType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
	timeType          = reflect.TypeOf(time.Time{})
)

func validateConcreteContractValue(
	t reflect.Type,
	displayPath string,
	semanticPath string,
	rootPath string,
	active map[reflect.Type]bool,
) error {
	for t.Kind() == reflect.Pointer {
		t = t.Elem()
	}
	if t == timeType {
		return nil
	}
	if t == reflect.TypeOf(json.RawMessage{}) {
		return fmt.Errorf("%s DTO must not be json.RawMessage", displayPath)
	}
	if approvedDynamicContractLeaf(t, semanticPath) {
		return nil
	}
	if implementsContractVariantProvider(t) {
		variants, approved := approvedContractVariants(t)
		if !approved {
			return fmt.Errorf("%s DTO implements contractVariants but is not an approved closed union", displayPath)
		}
		declared, _ := contractVariantsFor(t)
		if !reflect.DeepEqual(declared, variants) {
			return fmt.Errorf("%s contractVariants declaration does not match the approved variants", displayPath)
		}
		if err := validateContractUnionStorage(t, variants, displayPath); err != nil {
			return err
		}
		for _, variant := range variants {
			if variant == nil {
				return fmt.Errorf("%s DTO has a nil contract variant", displayPath)
			}
			variantName := variantName(variant)
			if err := validateConcreteContractValue(
				variant,
				displayPath+"<"+variantName+">",
				contractTypeName(variant),
				variantName,
				active,
			); err != nil {
				return err
			}
		}
		return nil
	}
	if implementsJSONMarshaler(t) {
		return fmt.Errorf("%s DTO must not use opaque JSON marshaling, got %s", displayPath, t)
	}

	switch t.Kind() {
	case reflect.Interface, reflect.Map:
		if approvedBoundedContractMap(t, rootPath) {
			return nil
		}
		return fmt.Errorf("%s DTO must be concrete, got %s", displayPath, t)
	case reflect.Slice:
		if t.Elem().Kind() == reflect.Uint8 {
			return fmt.Errorf("%s DTO must not be raw bytes, got %s", displayPath, t)
		}
		return validateConcreteContractValue(t.Elem(), displayPath+"[]", semanticPath+"[]", rootPath+"[]", active)
	case reflect.Array:
		return validateConcreteContractValue(t.Elem(), displayPath+"[]", semanticPath+"[]", rootPath+"[]", active)
	case reflect.Struct:
		if active[t] {
			return fmt.Errorf("%s DTO must not contain a recursive cycle through %s", displayPath, t)
		}
		active[t] = true
		defer delete(active, t)
		for _, field := range jsonVisibleContractFields(t) {
			if err := validateConcreteContractValue(
				field.typ,
				displayPath+"."+field.goPath,
				semanticPath+"."+field.goPath,
				rootPath+"."+field.goPath,
				active,
			); err != nil {
				return err
			}
		}
	}
	return nil
}
