package exec

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

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

// ExtensionSafety is the Agent-visible safety classification used by local
// recovery-mode invocation checks and help rendering.
type ExtensionSafety struct {
	Class string `json:"class"`
}

// ExtensionAction is one fully fetched Extension descriptor. It contains only
// Agent-visible Definition data; provider binding and credentials stay on the
// AgentTeam Server.
type ExtensionAction struct {
	ExtensionName  string
	DisplayName    string
	Name           string
	Summary        string
	Description    string
	Safety         ExtensionSafety
	InputSchema    map[string]any
	OutputSchema   map[string]any
	Example        any
	inputValidator *jsonschema.Schema
}

// ValidateInput checks one JSON value against this immutable Action's input
// schema. A nil schema accepts any single valid JSON value.
func (action ExtensionAction) ValidateInput(raw json.RawMessage) error {
	if _, err := decodeStrict(raw); err != nil {
		return err
	}
	if action.inputValidator == nil {
		return nil
	}
	return validateInstance(action.inputValidator, raw)
}

// ExtensionDomain is one authorized Agent-visible Extension name and its
// complete Action descriptors.
type ExtensionDomain struct {
	ExtensionName string
	DisplayName   string
	Actions       []ExtensionAction
}

// ExtensionSnapshot is an immutable-by-copy view of one verified Server
// Catalog response. The ETag is an opaque HTTP validator for the exact
// authorization-projected response bytes; it is not a runtime identity.
type ExtensionSnapshot struct {
	etag    string
	domains []ExtensionDomain
}

// ETag returns the opaque HTTP validator for this complete snapshot.
func (snapshot ExtensionSnapshot) ETag() string { return snapshot.etag }

// Empty reports whether the verified snapshot authorizes no actions.
func (snapshot ExtensionSnapshot) Empty() bool { return len(snapshot.domains) == 0 }

// Domains returns a deep copy in deterministic Extension-name/Action order.
func (snapshot ExtensionSnapshot) Domains() []ExtensionDomain {
	return cloneExtensionDomains(snapshot.domains)
}

// Action returns one exact Extension-name/Action descriptor as a deep copy.
func (snapshot ExtensionSnapshot) Action(extensionName, name string) (ExtensionAction, bool) {
	index := sort.Search(len(snapshot.domains), func(i int) bool {
		return snapshot.domains[i].ExtensionName >= extensionName
	})
	if index >= len(snapshot.domains) || snapshot.domains[index].ExtensionName != extensionName {
		return ExtensionAction{}, false
	}
	actions := snapshot.domains[index].Actions
	actionIndex := sort.Search(len(actions), func(i int) bool { return actions[i].Name >= name })
	if actionIndex >= len(actions) || actions[actionIndex].Name != name {
		return ExtensionAction{}, false
	}
	return cloneExtensionAction(actions[actionIndex]), true
}

// ExtensionNames returns the exact Agent-visible Extension names.
func (snapshot ExtensionSnapshot) ExtensionNames() []string {
	names := make([]string, len(snapshot.domains))
	for index, domain := range snapshot.domains {
		names[index] = domain.ExtensionName
	}
	return names
}

// ActionNames returns the exact authorized Actions for one Extension name.
func (snapshot ExtensionSnapshot) ActionNames(extensionName string) []string {
	index := sort.Search(len(snapshot.domains), func(i int) bool {
		return snapshot.domains[i].ExtensionName >= extensionName
	})
	if index >= len(snapshot.domains) || snapshot.domains[index].ExtensionName != extensionName {
		return nil
	}
	names := make([]string, len(snapshot.domains[index].Actions))
	for actionIndex, action := range snapshot.domains[index].Actions {
		names[actionIndex] = action.Name
	}
	return names
}

// ValidateInput checks one input against the schema pinned to this snapshot.
func (snapshot ExtensionSnapshot) ValidateInput(
	extensionName string,
	name string,
	raw json.RawMessage,
) error {
	action, ok := snapshot.Action(extensionName, name)
	if !ok {
		return errors.New("extension action not found")
	}
	return action.ValidateInput(raw)
}

// NewExtensionSnapshot builds an immutable snapshot. Callers that publish an
// untrusted candidate must first call ValidateExtensionSnapshot; Server fetch
// does so before returning a snapshot.
func NewExtensionSnapshot(
	etag string,
	domains []ExtensionDomain,
) ExtensionSnapshot {
	sortedDomains := cloneExtensionDomains(domains)
	sort.Slice(sortedDomains, func(i, j int) bool {
		return sortedDomains[i].ExtensionName < sortedDomains[j].ExtensionName
	})
	for domainIndex := range sortedDomains {
		for actionIndex := range sortedDomains[domainIndex].Actions {
			action := &sortedDomains[domainIndex].Actions[actionIndex]
			if action.inputValidator == nil && action.InputSchema != nil {
				action.inputValidator, _ = compileExtensionSchema(action.InputSchema)
			}
		}
		sort.Slice(sortedDomains[domainIndex].Actions, func(i, j int) bool {
			return sortedDomains[domainIndex].Actions[i].Name <
				sortedDomains[domainIndex].Actions[j].Name
		})
	}
	return ExtensionSnapshot{
		etag:    etag,
		domains: sortedDomains,
	}
}

// ValidateExtensionSnapshot verifies names, the complete-response ETag, safety
// classes, and schema presence before a candidate can enter runtime.
func ValidateExtensionSnapshot(snapshot ExtensionSnapshot) error {
	if snapshot.ETag() == "" {
		if snapshot.Empty() {
			return nil
		}
		return errors.New("invalid Extension Catalog ETag")
	}
	if !validCatalogETag(snapshot.ETag()) {
		return errors.New("invalid Extension Catalog ETag")
	}

	seenExtensionNames := make(map[string]struct{}, len(snapshot.domains))
	for _, domain := range snapshot.domains {
		if err := validateName(domain.ExtensionName); err != nil {
			return fmt.Errorf("invalid Extension name: %w", err)
		}
		if strings.TrimSpace(domain.DisplayName) == "" ||
			domain.DisplayName != strings.TrimSpace(domain.DisplayName) ||
			len(domain.Actions) == 0 {
			return fmt.Errorf("invalid Extension domain %q", domain.ExtensionName)
		}
		if _, duplicate := seenExtensionNames[domain.ExtensionName]; duplicate {
			return fmt.Errorf("duplicate Extension name %q", domain.ExtensionName)
		}
		seenExtensionNames[domain.ExtensionName] = struct{}{}

		seenActions := make(map[string]struct{}, len(domain.Actions))
		for _, action := range domain.Actions {
			if err := validateName(action.Name); err != nil {
				return fmt.Errorf("invalid Extension Action for %q: %w", domain.ExtensionName, err)
			}
			if action.ExtensionName != domain.ExtensionName ||
				action.DisplayName != domain.DisplayName {
				return fmt.Errorf(
					"Extension Action %q/%q identity does not match its domain",
					domain.ExtensionName,
					action.Name,
				)
			}
			if _, duplicate := seenActions[action.Name]; duplicate {
				return fmt.Errorf(
					"duplicate Extension Action %q for Extension name %q",
					action.Name,
					domain.ExtensionName,
				)
			}
			seenActions[action.Name] = struct{}{}
			switch action.Safety.Class {
			case "read", "mutation", "destructive":
			default:
				return fmt.Errorf(
					"invalid Extension safety class %q for %q/%q",
					action.Safety.Class,
					domain.ExtensionName,
					action.Name,
				)
			}
			// The schema must be structurally present (a map) to enter the
			// runtime authorization. Per-action schema CALLABILITY (does it
			// compile, does the wrapped output schema resolve its $defs) is
			// checked and isolated at RuntimeCatalog construction so one bad
			// action never fails the whole Catalog: it is excluded from
			// lookup/actions/help/HTTP routes while built-ins and other valid
			// extension actions stay publishable.
			if action.InputSchema == nil || action.OutputSchema == nil {
				return fmt.Errorf(
					"Extension input/output schema is required for %q/%q",
					domain.ExtensionName,
					action.Name,
				)
			}
		}
	}
	return nil
}

func compileExtensionSchema(schema map[string]any) (*jsonschema.Schema, error) {
	if schema == nil {
		return nil, nil
	}
	raw, err := json.Marshal(schema)
	if err != nil {
		return nil, err
	}
	return compileSchema(raw)
}

func cloneExtensionDomains(domains []ExtensionDomain) []ExtensionDomain {
	if domains == nil {
		return nil
	}
	out := make([]ExtensionDomain, len(domains))
	for index, domain := range domains {
		out[index] = domain
		if domain.Actions != nil {
			out[index].Actions = make([]ExtensionAction, len(domain.Actions))
			for actionIndex, action := range domain.Actions {
				out[index].Actions[actionIndex] = cloneExtensionAction(action)
			}
		}
	}
	return out
}

func cloneExtensionAction(action ExtensionAction) ExtensionAction {
	action.InputSchema = cloneJSONMap(action.InputSchema)
	action.OutputSchema = cloneJSONMap(action.OutputSchema)
	action.Example = cloneJSONValue(action.Example)
	return action
}

func cloneJSONMap(value map[string]any) map[string]any {
	if value == nil {
		return nil
	}
	cloned, _ := cloneJSONValue(value).(map[string]any)
	return cloned
}

func cloneJSONValue(value any) any {
	if value == nil {
		return nil
	}
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil
	}
	var cloned any
	if err := json.Unmarshal(encoded, &cloned); err != nil {
		return nil
	}
	return cloned
}
