package exec

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

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

// RuntimeCatalogView is the immutable, bearer-bound discovery projection
// consumed by agentteam exec. It contains descriptors only, never handlers,
// Definition digests, Server clients, or invocation credentials.
type RuntimeCatalogView struct {
	actions    []RuntimeAction
	byKey      map[string]RuntimeAction
	validators map[string]*jsonschema.Schema
}

type runtimeCatalogDocument struct {
	Actions []runtimeActionDocument `json:"actions"`
}

type runtimeActionDocument struct {
	Source       RuntimeActionSource `json:"source"`
	Domain       string              `json:"domain"`
	Name         string              `json:"name"`
	Summary      string              `json:"summary"`
	Description  string              `json:"description,omitempty"`
	InputSchema  json.RawMessage     `json:"inputSchema"`
	OutputSchema json.RawMessage     `json:"outputSchema"`
	Example      json.RawMessage     `json:"example,omitempty"`
	Permission   string              `json:"permission,omitempty"`
	Safety       *ExtensionSafety    `json:"safety,omitempty"`
}

// View returns a descriptor-only deep copy suitable for local discovery.
func (catalog *RuntimeCatalog) View() (*RuntimeCatalogView, error) {
	if catalog == nil {
		return nil, errors.New("Exec Runtime Catalog is unavailable")
	}
	return newRuntimeCatalogView(catalog.Actions())
}

func newRuntimeCatalogView(actions []RuntimeAction) (*RuntimeCatalogView, error) {
	view := &RuntimeCatalogView{
		byKey:      make(map[string]RuntimeAction, len(actions)),
		validators: make(map[string]*jsonschema.Schema, len(actions)),
	}
	for _, candidate := range actions {
		action := cloneRuntimeAction(candidate)
		if err := validateName(action.Domain); err != nil {
			return nil, fmt.Errorf("invalid Runtime Catalog domain: %w", err)
		}
		if err := validateName(action.Name); err != nil {
			return nil, fmt.Errorf("invalid Runtime Catalog action: %w", err)
		}
		switch action.Source {
		case RuntimeActionBuiltin:
			if strings.TrimSpace(action.Permission) == "" {
				return nil, fmt.Errorf("built-in action %q lacks permission", action.Domain+" "+action.Name)
			}
		case RuntimeActionExtension:
			if action.Safety.Class == "" {
				return nil, fmt.Errorf("Extension action %q lacks safety", action.Domain+" "+action.Name)
			}
		default:
			return nil, fmt.Errorf("invalid Runtime Catalog action source %q", action.Source)
		}
		if !json.Valid(action.InputSchema) || !json.Valid(action.OutputSchema) {
			return nil, fmt.Errorf("invalid Runtime Catalog schema for %q", action.Domain+" "+action.Name)
		}
		if int64(len(action.InputSchema)) > maxRuntimeActionSchemaBytes ||
			int64(len(action.OutputSchema)) > maxRuntimeActionSchemaBytes ||
			int64(len(action.Example)) > maxRuntimeActionExampleBytes {
			return nil, fmt.Errorf("Runtime Catalog descriptor exceeded its size budget for %q", action.Domain+" "+action.Name)
		}
		validator, err := compileSchema(action.InputSchema)
		if err != nil {
			return nil, fmt.Errorf("compile Runtime Catalog input schema for %q: %w", action.Domain+" "+action.Name, err)
		}
		if _, err := compileSchema(action.OutputSchema); err != nil {
			return nil, fmt.Errorf("compile Runtime Catalog output schema for %q: %w", action.Domain+" "+action.Name, err)
		}
		key := action.Domain + " " + action.Name
		if _, duplicate := view.byKey[key]; duplicate {
			return nil, fmt.Errorf("duplicate Runtime Catalog action %q", key)
		}
		view.actions = append(view.actions, action)
		view.byKey[key] = action
		view.validators[key] = validator
	}
	sort.Slice(view.actions, func(i, j int) bool {
		return view.actions[i].Domain+" "+view.actions[i].Name <
			view.actions[j].Domain+" "+view.actions[j].Name
	})
	if err := validateRuntimeCatalogBuiltins(view); err != nil {
		return nil, err
	}
	return view, nil
}

func validateRuntimeCatalogBuiltins(view *RuntimeCatalogView) error {
	builtin, err := DefaultCatalog()
	if err != nil {
		return err
	}
	expected, err := NewRuntimeCatalog(builtin, ExtensionSnapshot{})
	if err != nil {
		return err
	}
	expectedActions := expected.Actions()
	actual := make([]RuntimeAction, 0, len(expectedActions))
	for _, action := range view.actions {
		if action.Source == RuntimeActionBuiltin {
			actual = append(actual, action)
		}
	}
	if len(actual) != len(expectedActions) {
		return errors.New("Runtime Catalog built-in action set mismatch")
	}
	for index := range expectedActions {
		if !runtimeActionDocumentsEqual(
			runtimeActionDocumentFromAction(expectedActions[index]),
			runtimeActionDocumentFromAction(actual[index]),
		) {
			return fmt.Errorf(
				"Runtime Catalog built-in descriptor mismatch for %q",
				expectedActions[index].Domain+" "+expectedActions[index].Name,
			)
		}
	}
	return nil
}

func runtimeActionDocumentsEqual(left, right runtimeActionDocument) bool {
	leftRaw, leftErr := json.Marshal(left)
	rightRaw, rightErr := json.Marshal(right)
	return leftErr == nil && rightErr == nil && bytes.Equal(leftRaw, rightRaw)
}

func runtimeActionDocumentFromAction(action RuntimeAction) runtimeActionDocument {
	document := runtimeActionDocument{
		Source: action.Source, Domain: action.Domain, Name: action.Name,
		Summary: action.Summary, Description: action.Description,
		InputSchema:  append(json.RawMessage(nil), action.InputSchema...),
		OutputSchema: append(json.RawMessage(nil), action.OutputSchema...),
		Example:      append(json.RawMessage(nil), action.Example...),
		Permission:   action.Permission,
	}
	if action.Source == RuntimeActionExtension {
		safety := action.Safety
		document.Safety = &safety
	}
	return document
}

func runtimeCatalogDocumentFromView(view *RuntimeCatalogView) runtimeCatalogDocument {
	document := runtimeCatalogDocument{
		Actions: make([]runtimeActionDocument, 0, len(view.actions)),
	}
	for _, action := range view.actions {
		document.Actions = append(document.Actions, runtimeActionDocumentFromAction(action))
	}
	return document
}

func (document runtimeCatalogDocument) view() (*RuntimeCatalogView, error) {
	actions := make([]RuntimeAction, 0, len(document.Actions))
	for _, item := range document.Actions {
		action := RuntimeAction{
			Source: item.Source, Domain: item.Domain, Name: item.Name,
			Summary: item.Summary, Description: item.Description,
			InputSchema:  append(json.RawMessage(nil), item.InputSchema...),
			OutputSchema: append(json.RawMessage(nil), item.OutputSchema...),
			Example:      append(json.RawMessage(nil), item.Example...),
			Permission:   item.Permission,
		}
		if item.Safety != nil {
			action.Safety = *item.Safety
		}
		actions = append(actions, action)
	}
	return newRuntimeCatalogView(actions)
}

// Actions returns all merged descriptors in deterministic order.
func (view *RuntimeCatalogView) Actions() []RuntimeAction {
	if view == nil {
		return nil
	}
	result := make([]RuntimeAction, 0, len(view.actions))
	for _, action := range view.actions {
		result = append(result, cloneRuntimeAction(action))
	}
	return result
}

// Lookup returns one exact merged descriptor.
func (view *RuntimeCatalogView) Lookup(domain, action string) (RuntimeAction, bool) {
	if view == nil {
		return RuntimeAction{}, false
	}
	item, ok := view.byKey[domain+" "+action]
	return cloneRuntimeAction(item), ok
}

// ValidateInput validates one JSON input against its bearer-bound descriptor.
func (view *RuntimeCatalogView) ValidateInput(domain, action string, input json.RawMessage) error {
	if _, err := decodeStrict(input); err != nil {
		return err
	}
	validator, ok := view.validators[domain+" "+action]
	if !ok {
		return errors.New("Exec action not found")
	}
	if validator == nil {
		return nil
	}
	return validateInstance(validator, input)
}
