package exec

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"

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

type actionMeta struct {
	Domain      string     `json:"domain"`
	Name        string     `json:"name"`
	Summary     string     `json:"summary"`
	Description string     `json:"description,omitempty"`
	Permission  permission `json:"permission"`
}

// actionExample keeps an example type-checked against the action input DTO.
type actionExample[I any] struct {
	Title string `json:"title"`
	Input I      `json:"input"`
}

// actionSpec is the typed, contract-only source for one action. It deliberately
// has no handler, run function, decoder, target resolver, or runtime dependency.
type actionSpec[I, O any] struct {
	Meta     actionMeta         `json:"meta"`
	Examples []actionExample[I] `json:"examples,omitempty"`
}

// actionResult is the stable success envelope returned by every Exec action.
// O is the concrete data DTO for that action.
type actionResult[O any] struct {
	Data     O              `json:"data"`
	Text     string         `json:"text,omitempty"`
	Metadata map[string]any `json:"metadata,omitempty"`
}

// jsonExample is the type-erased, immutable descriptor projection of one
// typed example. It is used only for help/schema/OpenAPI rendering.
type jsonExample struct {
	Title string          `json:"title"`
	Input json.RawMessage `json:"input"`
}

// actionDescriptor is the non-executable projection of actionSpec[I,O].
type actionDescriptor struct {
	Domain           string          `json:"domain"`
	Name             string          `json:"name"`
	Summary          string          `json:"summary"`
	Description      string          `json:"description,omitempty"`
	Permission       permission      `json:"permission"`
	InputSchemaName  string          `json:"inputSchemaName"`
	OutputSchemaName string          `json:"outputSchemaName"`
	InputSchema      json.RawMessage `json:"inputSchema"`
	OutputSchema     json.RawMessage `json:"outputSchema"`
	Examples         []jsonExample   `json:"examples,omitempty"`
}

func (d actionDescriptor) identity() string {
	return d.Domain + " " + d.Name
}

// describe projects a typed contract into immutable JSON schemas and examples.
// The output schema describes the complete actionResult[O] envelope, not bare O.
func describe[I, O any](spec actionSpec[I, O]) (actionDescriptor, error) {
	if err := validateName(spec.Meta.Domain); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: typed contract domain: %w", err)
	}
	if err := validateName(spec.Meta.Name); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: typed contract action: %w", err)
	}
	if strings.TrimSpace(spec.Meta.Summary) == "" {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s: summary required", spec.Meta.Domain, spec.Meta.Name)
	}
	switch spec.Meta.Permission {
	case permissionReadOnly, permissionAgentScopedMutation, permissionExistingGate:
	default:
		return actionDescriptor{}, fmt.Errorf(
			"exec: %s %s: unknown permission %q",
			spec.Meta.Domain,
			spec.Meta.Name,
			spec.Meta.Permission,
		)
	}

	inputType := contractType[I]()
	outputDataType := contractType[O]()
	outputType := contractType[actionResult[O]]()
	if err := validateConcreteContractType(inputType, "input"); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}
	if err := validateConcreteContractType(outputDataType, "output data"); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}

	inputSchema, err := reflectContractSchema(inputType)
	if err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s input schema: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}
	outputSchema, err := reflectContractSchema(outputType)
	if err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s output schema: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}
	if err := validateContractSchemaPolicy(inputSchema, contractSchemaPolicy{
		rootType: inputType,
		rootPath: contractTypeName(inputType),
	}); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s input schema: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}
	if err := validateContractSchemaPolicy(outputSchema, contractSchemaPolicy{
		rootType: outputType,
		rootPath: "actionResult",
	}); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s output schema: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}
	compiledInput, err := compileSchema(inputSchema)
	if err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s input schema: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}
	if _, err := compileSchema(outputSchema); err != nil {
		return actionDescriptor{}, fmt.Errorf("exec: %s %s output schema: %w", spec.Meta.Domain, spec.Meta.Name, err)
	}

	examples := make([]jsonExample, 0, len(spec.Examples))
	for _, example := range spec.Examples {
		title := strings.TrimSpace(example.Title)
		if title == "" {
			return actionDescriptor{}, fmt.Errorf("exec: %s %s: example title required", spec.Meta.Domain, spec.Meta.Name)
		}
		raw, err := json.Marshal(example.Input)
		if err != nil {
			return actionDescriptor{}, fmt.Errorf("exec: %s %s example %q: %w", spec.Meta.Domain, spec.Meta.Name, title, err)
		}
		if err := validateInstance(compiledInput, raw); err != nil {
			return actionDescriptor{}, fmt.Errorf("exec: %s %s example %q: %w", spec.Meta.Domain, spec.Meta.Name, title, err)
		}
		examples = append(examples, jsonExample{Title: title, Input: append(json.RawMessage(nil), raw...)})
	}

	return actionDescriptor{
		Domain:           spec.Meta.Domain,
		Name:             spec.Meta.Name,
		Summary:          spec.Meta.Summary,
		Description:      spec.Meta.Description,
		Permission:       spec.Meta.Permission,
		InputSchemaName:  descriptorSchemaName(spec.Meta.Domain, spec.Meta.Name, "Input"),
		OutputSchemaName: descriptorSchemaName(spec.Meta.Domain, spec.Meta.Name, "Output"),
		InputSchema:      inputSchema,
		OutputSchema:     outputSchema,
		Examples:         examples,
	}, nil
}

// Catalog is a read-only heterogeneous collection of descriptor
// projections. It stores neither typed handlers nor runtime execution hooks.
type Catalog struct {
	actions  []actionDescriptor
	byKey    map[string]actionDescriptor
	compiled map[string]*jsonschema.Schema
}

func newDescriptorCatalog(descriptors ...actionDescriptor) (*Catalog, error) {
	if len(descriptors) == 0 {
		return nil, fmt.Errorf("exec: descriptor catalog is empty")
	}
	catalog := &Catalog{
		byKey:    make(map[string]actionDescriptor, len(descriptors)),
		compiled: make(map[string]*jsonschema.Schema, len(descriptors)),
	}
	for _, descriptor := range descriptors {
		key := descriptor.identity()
		if _, exists := catalog.byKey[key]; exists {
			return nil, fmt.Errorf("exec: duplicate action descriptor %q", key)
		}
		if !json.Valid(descriptor.InputSchema) || !json.Valid(descriptor.OutputSchema) {
			return nil, fmt.Errorf("exec: %s: invalid descriptor schema", key)
		}
		compiled, err := compileSchema(descriptor.InputSchema)
		if err != nil {
			return nil, fmt.Errorf("exec: %s input schema: %w", key, err)
		}
		if _, err := compileSchema(descriptor.OutputSchema); err != nil {
			return nil, fmt.Errorf("exec: %s output schema: %w", key, err)
		}
		descriptor = cloneActionDescriptor(descriptor)
		catalog.byKey[key] = descriptor
		catalog.compiled[key] = compiled
		catalog.actions = append(catalog.actions, descriptor)
	}
	sort.Slice(catalog.actions, func(i, j int) bool {
		return catalog.actions[i].identity() < catalog.actions[j].identity()
	})
	return catalog, nil
}

func (c *Catalog) lookup(domain, name string) (actionDescriptor, bool) {
	if c == nil {
		return actionDescriptor{}, false
	}
	descriptor, ok := c.byKey[domain+" "+name]
	if !ok {
		return actionDescriptor{}, false
	}
	return cloneActionDescriptor(descriptor), true
}

func (c *Catalog) actionsSnapshot() []actionDescriptor {
	if c == nil {
		return nil
	}
	out := make([]actionDescriptor, 0, len(c.actions))
	for _, descriptor := range c.actions {
		out = append(out, cloneActionDescriptor(descriptor))
	}
	return out
}

func (c *Catalog) len() int {
	if c == nil {
		return 0
	}
	return len(c.actions)
}

// Has reports whether a literal domain/action pair is registered.
func (c *Catalog) Has(domain, name string) bool {
	_, ok := c.lookup(domain, name)
	return ok
}

// ValidateInput checks one input against the catalog's compiled schema.
func (c *Catalog) ValidateInput(domain, name string, input json.RawMessage) error {
	if c == nil {
		return fmt.Errorf("exec: descriptor catalog is unavailable")
	}
	schema, ok := c.compiled[domain+" "+name]
	if !ok {
		return fmt.Errorf("exec: unknown action %s %s", domain, name)
	}
	return validateInstance(schema, input)
}

// RenderActionInputSchemaJSON returns the standalone input contract used by
// `agentteam exec <domain> <action> --input-schema`.
func (c *Catalog) RenderActionInputSchemaJSON(domain, name string) (string, bool) {
	descriptor, ok := c.lookup(domain, name)
	if !ok {
		return "", false
	}
	return standaloneSchemaJSON(descriptor.InputSchema, false)
}

// RenderActionOutputSchemaJSON returns the standalone success contract used by
// `agentteam exec <domain> <action> --output-schema`.
func (c *Catalog) RenderActionOutputSchemaJSON(domain, name string) (string, bool) {
	descriptor, ok := c.lookup(domain, name)
	if !ok {
		return "", false
	}
	return standaloneSchemaJSON(descriptor.OutputSchema, true)
}

// RenderErrorSchemaJSON returns the shared Exec failure contract used by
// `agentteam exec --error-schema`.
func (c *Catalog) RenderErrorSchemaJSON() (string, bool) {
	raw, err := reflectContractSchema(contractType[actionErrorResponse]())
	if err != nil {
		return "", false
	}
	return standaloneSchemaJSON(raw, true)
}

func standaloneSchemaJSON(schema json.RawMessage, output bool) (string, bool) {
	schema = compactModelSchema(schema, output)
	projected, err := compactProjectionSchema(schema)
	if err != nil || !json.Valid(projected) {
		return "", false
	}
	if _, err := compileSchema(projected); err != nil {
		return "", false
	}
	return string(projected) + "\n", true
}

func descriptorSchemaName(domain, action, suffix string) string {
	var b strings.Builder
	for _, part := range strings.FieldsFunc(domain+"-"+action, func(r rune) bool {
		return r == '-' || r == '_' || r == ' '
	}) {
		if part == "" {
			continue
		}
		b.WriteString(strings.ToUpper(part[:1]))
		b.WriteString(part[1:])
	}
	b.WriteString(suffix)
	return b.String()
}

func cloneActionDescriptor(descriptor actionDescriptor) actionDescriptor {
	descriptor.InputSchema = append(json.RawMessage(nil), descriptor.InputSchema...)
	descriptor.OutputSchema = append(json.RawMessage(nil), descriptor.OutputSchema...)
	descriptor.Examples = append([]jsonExample(nil), descriptor.Examples...)
	for i := range descriptor.Examples {
		descriptor.Examples[i].Input = append(json.RawMessage(nil), descriptor.Examples[i].Input...)
	}
	return descriptor
}
