package spec

import (
	"fmt"

	jsonschema "github.com/santhosh-tekuri/jsonschema/v6"
)

const draft2020URI = "https://json-schema.org/draft/2020-12/schema"

// JSON Schema keywords grouped by value shape so walkDraft2020 descends only
// into schema-valued positions, never into data keywords (const/enum/examples/
// default) where a literal "$schema" string is ordinary application data.
var (
	namedSchemaMapKeywords = []string{"$defs", "properties", "patternProperties", "dependentSchemas"}
	singleSchemaKeywords   = []string{
		"additionalProperties", "items", "contains", "unevaluatedItems",
		"if", "then", "else", "not", "propertyNames", "unevaluatedProperties",
		"contentSchema",
	}
	schemaArrayKeywords = []string{"oneOf", "allOf", "anyOf", "prefixItems"}
)

// compileInputSchema validates and compiles a catalog input/output schema as
// Draft 2020-12. A schema must declare the 2020-12 dialect and must not use
// pre-2020 keywords that would silently switch dialect.
func compileInputSchema(name string, schema map[string]any) (*jsonschema.Schema, error) {
	if schema == nil {
		return nil, nil
	}
	if err := assertDraft2020(schema); err != nil {
		return nil, fmt.Errorf("action %s: %w", name, err)
	}
	compiler := jsonschema.NewCompiler()
	compiler.DefaultDraft(jsonschema.Draft2020)
	if err := compiler.AddResource("schema.json", schema); err != nil {
		return nil, fmt.Errorf("action %s: add schema resource: %w", name, err)
	}
	compiled, err := compiler.Compile("schema.json")
	if err != nil {
		return nil, fmt.Errorf("action %s: compile schema: %w", name, err)
	}
	return compiled, nil
}

// validateExample confirms the catalog example (if any) satisfies the input
// schema. Examples are authoring aids; an invalid one is an authoring error.
func validateExample(actionName string, compiled *jsonschema.Schema, example any) error {
	if compiled == nil || example == nil {
		return nil
	}
	if err := compiled.Validate(example); err != nil {
		return fmt.Errorf("action %s: example fails input schema: %w", actionName, err)
	}
	return nil
}

func assertDraft2020(node any) error {
	// The document root MUST declare the Draft 2020-12 dialect. Nested
	// subschemas inherit the root dialect and only need re-checking when they
	// carry their own $schema (dialect scoping).
	m, ok := node.(map[string]any)
	if !ok {
		return fmt.Errorf("schema must be a JSON object")
	}
	if raw, ok := m["$schema"]; ok {
		s, isStr := raw.(string)
		if !isStr || s != draft2020URI {
			return fmt.Errorf("schema declares $schema %q; only Draft 2020-12 (%s) is allowed", raw, draft2020URI)
		}
	} else {
		return fmt.Errorf("schema is missing required $schema (only Draft 2020-12 %s is allowed)", draft2020URI)
	}
	return walkDraft2020(node)
}

func walkDraft2020(node any) error {
	m, ok := node.(map[string]any)
	if !ok {
		return nil // bool schemas and non-object values have no $schema
	}
	if raw, ok := m["$schema"]; ok {
		s, isStr := raw.(string)
		if !isStr {
			return fmt.Errorf("$schema must be a string")
		}
		if s != draft2020URI {
			return fmt.Errorf("schema declares $schema %q; only Draft 2020-12 (%s) is allowed", s, draft2020URI)
		}
	}
	for _, keyword := range []string{"definitions", "dependencies", "additionalItems"} {
		if _, ok := m[keyword]; ok {
			return fmt.Errorf("pre-2020 dialect keyword %q not allowed in Draft 2020-12 (use $defs/dependentSchemas/prefixItems)", keyword)
		}
	}
	for _, kw := range namedSchemaMapKeywords {
		if v, ok := m[kw].(map[string]any); ok {
			for _, sub := range v {
				if err := walkDraft2020(sub); err != nil {
					return err
				}
			}
		}
	}
	for _, kw := range singleSchemaKeywords {
		if v, ok := m[kw]; ok {
			if err := walkDraft2020(v); err != nil {
				return err
			}
		}
	}
	for _, kw := range schemaArrayKeywords {
		if arr, ok := m[kw].([]any); ok {
			for _, sub := range arr {
				if err := walkDraft2020(sub); err != nil {
					return err
				}
			}
		}
	}
	return nil
}
