package openai

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"strings"

	"iop/packages/go/config"
)

// workspaceOperationKind enumerates the canonical workspace operations the
// binding compiler can encode. The Edge never executes these; it only produces
// deterministic, caller-executed payloads from the preset-declared contract.
type workspaceOperationKind string

const (
	opKindPrepare workspaceOperationKind = "prepare"
	opKindRead    workspaceOperationKind = "read"
	opKindWrite   workspaceOperationKind = "write"
	opKindDelete  workspaceOperationKind = "delete"
)

// canonicalOperationOrder is the deterministic order in which an alternative's
// operations are compiled and fingerprinted. It never depends on Go map
// iteration order.
var canonicalOperationOrder = []workspaceOperationKind{opKindPrepare, opKindRead, opKindWrite, opKindDelete}

// workspaceBindingMode selects how a compiled operation maps tool arguments.
//
// structured: the actual tool exposes the workspace fields by name; the codec
// maps the configured argument fields directly and preserves typed values.
//
// command: the actual tool takes a synthesized command; the codec builds a
// deterministic, shell-safe command from a fixed argv template.
type workspaceBindingMode string

const (
	modeStructured workspaceBindingMode = "structured"
	modeCommand    workspaceBindingMode = "command"
)

// workspaceToolSchema is the normalized view of one decoded tool definition. It
// accepts OpenAI Chat function wrappers, flat OpenAI parameters, and Anthropic
// input_schema shapes and exposes a single JSON Schema object for matching.
type workspaceToolSchema struct {
	name        string
	description string
	// schema is the full JSON Schema object (function.parameters / parameters /
	// input_schema). It is matched against the configured schema_matcher.
	schema map[string]any
	// properties is the resolved property set (oneOf/anyOf/allOf aware) used to
	// validate that mapped argument fields are actually declared by the tool.
	properties map[string]any
}

// workspaceOperationBinding is the immutable compiled mapping for one canonical
// operation of a selected alternative.
type workspaceOperationBinding struct {
	op       workspaceOperationKind
	toolName string
	mode     workspaceBindingMode
	// Structured-mode actual argument field names (dot paths permitted).
	pathField    string
	contentField string
	modeField    string
	// Command-mode encoding.
	commandField string
	argvTemplate []string
	// Immutable copies of the configured contract for this operation.
	schemaMatcher  map[string]any
	argumentMap    map[string]any
	resultMatcher  map[string]any
	createsParents bool
	// normalizedSchema is the actual tool schema this operation bound to.
	normalizedSchema *workspaceToolSchema
}

// workspaceBinding is the immutable, fingerprinted selection of exactly one
// complete configured alternative. It carries every operation mapping and the
// parent-creation capability, and the Edge never mutates it after selection.
type workspaceBinding struct {
	alternativeName string
	operations      map[workspaceOperationKind]*workspaceOperationBinding
	// fingerprint is a sha256 of the canonical selected configuration plus the
	// normalized actual schemas. It correlates results back to this binding.
	fingerprint string
}

func (b *workspaceBinding) requiresCollectedProjection() bool {
	if b == nil {
		return false
	}
	for _, operation := range b.operations {
		if operation != nil && operation.mode == modeCommand {
			return true
		}
	}
	return false
}

// compileWorkspaceBinding selects the first configured alternative whose every
// declared operation matches an actual decoded tool by exact tool name and
// recursive schema matcher. It never infers workspace roles from tool-name
// substrings and never inspects the workspace filesystem.
//
// It returns an immutable, fully-mapped binding, or nil with an error that
// explains why no complete alternative matched.
func compileWorkspaceBinding(alternatives []config.ExecutionWorkspaceToolAlternative, tools any) (*workspaceBinding, error) {
	if len(alternatives) == 0 {
		return nil, fmt.Errorf("no configured workspace tool alternatives")
	}
	schemasByName, err := normalizeToolSchemas(tools)
	if err != nil {
		return nil, err
	}
	var lastErr error
	for _, alt := range alternatives {
		binding, err := bindAlternative(alt, schemasByName)
		if err != nil {
			lastErr = err
			continue
		}
		return binding, nil
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("no workspace tool alternative matched the provided tools")
	}
	return nil, lastErr
}

// normalizeToolSchemas normalizes every decoded tool definition into a schema
// view keyed by its exact tool name. Tools without a name are ignored; the
// first definition wins on duplicate names. It handles OpenAI Chat nested
// function wrappers, flat OpenAI parameters, and Anthropic input_schema shapes.
func normalizeToolSchemas(tools any) (map[string]*workspaceToolSchema, error) {
	var entries []any
	switch typed := tools.(type) {
	case []any:
		entries = typed
	case []anthropicTool:
		entries = make([]any, len(typed))
		for i, tool := range typed {
			entries[i] = tool
		}
	default:
		return nil, fmt.Errorf("unsupported workspace tool slice type %T", tools)
	}

	out := make(map[string]*workspaceToolSchema, len(entries))
	for _, rawTool := range entries {
		schema := extractToolSchema(rawTool)
		if schema == nil {
			continue
		}
		if _, exists := out[schema.name]; exists {
			continue
		}
		out[schema.name] = schema
	}
	return out, nil
}

// extractToolSchema pulls the normalized schema from a single tool entry. It
// recognizes the actual OpenAI Chat function wrapper
// ({type:"function",function:{name,description,parameters}}), the flat OpenAI
// shape ({name,parameters}), and the Anthropic shape ({name,input_schema}).
func extractToolSchema(rawTool any) *workspaceToolSchema {
	switch tool := rawTool.(type) {
	case map[string]any:
		return extractMappedToolSchema(tool)
	case anthropicTool:
		return extractAnthropicToolSchema(tool)
	default:
		return nil
	}
}

func extractMappedToolSchema(m map[string]any) *workspaceToolSchema {
	name, _ := m["name"].(string)
	desc, _ := m["description"].(string)

	var schemaObj map[string]any

	// OpenAI Chat nested function wrapper.
	if fn, ok := m["function"].(map[string]any); ok {
		if name == "" {
			name, _ = fn["name"].(string)
		}
		if desc == "" {
			desc, _ = fn["description"].(string)
		}
		if params, ok := fn["parameters"].(map[string]any); ok {
			schemaObj = params
		}
	}
	// Anthropic input_schema.
	if schemaObj == nil {
		if s, ok := m["input_schema"].(map[string]any); ok {
			schemaObj = s
		}
	}
	// Flat OpenAI parameters.
	if schemaObj == nil {
		if s, ok := m["parameters"].(map[string]any); ok {
			schemaObj = s
		}
	}

	if strings.TrimSpace(name) == "" {
		return nil
	}
	return &workspaceToolSchema{
		name:        name,
		description: desc,
		schema:      schemaObj,
		properties:  schemaObjectProperties(schemaObj),
	}
}

// extractAnthropicToolSchema normalizes the concrete native Messages decoder
// value. InputSchema is deliberately decoded into a new map so a binding does
// not retain the request's RawMessage buffer or infer a role by reflection.
func extractAnthropicToolSchema(tool anthropicTool) *workspaceToolSchema {
	if strings.TrimSpace(tool.Name) == "" || len(tool.InputSchema) == 0 {
		return nil
	}
	decoder := json.NewDecoder(bytes.NewReader(tool.InputSchema))
	decoder.UseNumber()
	var schema map[string]any
	if err := decoder.Decode(&schema); err != nil || schema == nil {
		return nil
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil
	}
	return &workspaceToolSchema{
		name:        tool.Name,
		description: tool.Description,
		schema:      cloneAnyMap(schema),
		properties:  schemaObjectProperties(schema),
	}
}

// bindAlternative compiles a single configured alternative against the
// normalized actual tools. Every declared operation must bind, and the
// alternative must satisfy write-with-parents or separate-prepare completeness.
func bindAlternative(alt config.ExecutionWorkspaceToolAlternative, schemasByName map[string]*workspaceToolSchema) (*workspaceBinding, error) {
	name := strings.TrimSpace(alt.Name)
	ops := make(map[workspaceOperationKind]*workspaceOperationBinding, len(alt.Operations))
	for _, kind := range canonicalOperationOrder {
		cfgOp, ok := alt.Operations[string(kind)]
		if !ok {
			continue
		}
		opBinding, err := bindOperation(kind, cfgOp, schemasByName)
		if err != nil {
			return nil, fmt.Errorf("alternative %q operation %q: %w", name, kind, err)
		}
		ops[kind] = opBinding
	}
	if len(ops) == 0 {
		return nil, fmt.Errorf("alternative %q declares no recognized operations", name)
	}
	if err := validateAlternativeCompleteness(name, ops); err != nil {
		return nil, err
	}
	binding := &workspaceBinding{alternativeName: name, operations: ops}
	binding.fingerprint = computeBindingFingerprint(binding)
	return binding, nil
}

// validateAlternativeCompleteness enforces the write-with-parents or
// separate-prepare completeness invariant: a write operation that cannot create
// missing parents requires a prepare operation in the same alternative.
func validateAlternativeCompleteness(name string, ops map[workspaceOperationKind]*workspaceOperationBinding) error {
	write, hasWrite := ops[opKindWrite]
	if hasWrite && !write.createsParents {
		if _, hasPrepare := ops[opKindPrepare]; !hasPrepare {
			return fmt.Errorf("alternative %q: write cannot create parents and no prepare operation is declared", name)
		}
	}
	return nil
}

// bindOperation binds one configured operation to its actual tool by exact name
// and recursive schema matcher, resolves the argument map, validates mapped
// fields against the actual schema, and copies the immutable result matcher.
func bindOperation(kind workspaceOperationKind, cfgOp config.ExecutionWorkspaceOperation, schemasByName map[string]*workspaceToolSchema) (*workspaceOperationBinding, error) {
	toolName := strings.TrimSpace(cfgOp.ToolName)
	if toolName == "" {
		return nil, fmt.Errorf("tool_name must not be empty")
	}
	schema, ok := schemasByName[toolName]
	if !ok {
		return nil, fmt.Errorf("tool %q is not present in the request tools", toolName)
	}
	if len(cfgOp.SchemaMatcher) == 0 {
		return nil, fmt.Errorf("schema_matcher must not be empty")
	}
	if !schemaMatcherMatches(cfgOp.SchemaMatcher, schema.schema) {
		return nil, fmt.Errorf("tool %q schema does not satisfy the configured schema_matcher", toolName)
	}
	if len(cfgOp.ArgumentMap) == 0 {
		return nil, fmt.Errorf("argument_map must not be empty")
	}
	if len(cfgOp.ResultMatcher) == 0 {
		return nil, fmt.Errorf("result_matcher must not be empty")
	}
	ob := &workspaceOperationBinding{
		op:               kind,
		toolName:         toolName,
		schemaMatcher:    cloneAnyMap(cfgOp.SchemaMatcher),
		argumentMap:      cloneAnyMap(cfgOp.ArgumentMap),
		resultMatcher:    cloneAnyMap(cfgOp.ResultMatcher),
		createsParents:   cfgOp.CreatesParents,
		normalizedSchema: cloneWorkspaceToolSchema(schema),
	}
	if err := resolveArgumentMap(ob, kind); err != nil {
		return nil, err
	}
	if err := validateMappedFields(ob, schema); err != nil {
		return nil, err
	}
	return ob, nil
}

// cloneWorkspaceToolSchema detaches the compiled binding from the request's
// decoded tool map. A caller can reuse or mutate its decoded request after
// admission, but that must not alter the request-local binding contract.
func cloneWorkspaceToolSchema(schema *workspaceToolSchema) *workspaceToolSchema {
	if schema == nil {
		return nil
	}
	return &workspaceToolSchema{
		name:        schema.name,
		description: schema.description,
		schema:      cloneAnyMap(schema.schema),
		properties:  cloneAnyMap(schema.properties),
	}
}

// resolveArgumentMap interprets the configured argument_map into structured or
// command encoding fields. The presence of a "command" field name selects
// command mode. A "path" mapping is always required; write additionally
// requires a "content" mapping.
func resolveArgumentMap(ob *workspaceOperationBinding, kind workspaceOperationKind) error {
	am := ob.argumentMap
	pathField, ok := stringField(am, "path")
	if !ok {
		return fmt.Errorf("argument_map requires a non-empty %q field name", "path")
	}
	ob.pathField = pathField
	if content, ok := stringField(am, "content"); ok {
		ob.contentField = content
	}
	if modeField, ok := stringField(am, "mode"); ok {
		ob.modeField = modeField
	}

	if command, ok := stringField(am, "command"); ok {
		ob.mode = modeCommand
		ob.commandField = command
		argv, err := parseArgvTemplate(am["argv"])
		if err != nil {
			return err
		}
		placeholders, err := validateCommandArgvTemplate(argv)
		if err != nil {
			return err
		}
		if placeholders["{path}"] != 1 {
			return fmt.Errorf("command argv template must reference the {path} placeholder exactly once")
		}
		if kind == opKindWrite && placeholders["{content}"] != 1 {
			return fmt.Errorf("write command argv template must reference the {content} placeholder exactly once")
		}
		ob.argvTemplate = argv
	} else {
		ob.mode = modeStructured
	}

	if kind == opKindWrite && ob.contentField == "" {
		return fmt.Errorf("write argument_map requires a non-empty %q field name", "content")
	}
	return nil
}

// validateMappedFields ties the argument map to the actual tool schema. In
// structured mode every mapped field must be declared by the schema; in command
// mode the synthesized command field must be declared by the schema.
func validateMappedFields(ob *workspaceOperationBinding, schema *workspaceToolSchema) error {
	check := func(role, field string) error {
		if field == "" {
			return nil
		}
		root := strings.SplitN(field, ".", 2)[0]
		if _, ok := schema.properties[root]; !ok {
			return fmt.Errorf("mapped %s field %q is not declared by tool %q schema", role, field, schema.name)
		}
		return nil
	}
	switch ob.mode {
	case modeStructured:
		if err := check("path", ob.pathField); err != nil {
			return err
		}
		if err := check("content", ob.contentField); err != nil {
			return err
		}
		if err := check("mode", ob.modeField); err != nil {
			return err
		}
	case modeCommand:
		if err := check("command", ob.commandField); err != nil {
			return err
		}
	}
	return nil
}

// schemaMatcherMatches reports whether the actual tool schema satisfies the
// configured recursive schema matcher (a deep subset match).
func schemaMatcherMatches(matcher map[string]any, schema map[string]any) bool {
	if schema == nil {
		schema = map[string]any{}
	}
	return deepSubsetMatch(map[string]any(matcher), map[string]any(schema))
}

// deepSubsetMatch reports whether actual contains everything declared by
// matcher. Maps match as subsets, slices require each matcher element to be
// found in actual, and scalars compare by value. A small operator vocabulary
// is supported for string matcher leaves: "$any", "$string", "$number",
// "$bool".
func deepSubsetMatch(matcher, actual any) bool {
	switch m := matcher.(type) {
	case map[string]any:
		am, ok := actual.(map[string]any)
		if !ok {
			return false
		}
		for key, mv := range m {
			av, ok := am[key]
			if !ok {
				return false
			}
			if !deepSubsetMatch(mv, av) {
				return false
			}
		}
		return true
	case []any:
		as, ok := actual.([]any)
		if !ok {
			return false
		}
		for _, mv := range m {
			found := false
			for _, av := range as {
				if deepSubsetMatch(mv, av) {
					found = true
					break
				}
			}
			if !found {
				return false
			}
		}
		return true
	case string:
		switch m {
		case "$any":
			return actual != nil
		case "$string":
			_, ok := actual.(string)
			return ok
		case "$number":
			_, ok := toFloat(actual)
			return ok
		case "$bool":
			_, ok := actual.(bool)
			return ok
		}
		s, ok := actual.(string)
		return ok && s == m
	default:
		return valuesEqual(matcher, actual)
	}
}

// valuesEqual compares two scalar values, normalizing numeric types so that a
// config int and a decoded json.Number/float64 compare equal.
func valuesEqual(a, b any) bool {
	if af, ok := toFloat(a); ok {
		if bf, ok := toFloat(b); ok {
			return af == bf
		}
		return false
	}
	return reflect.DeepEqual(a, b)
}

// toFloat converts any supported numeric representation to a float64.
func toFloat(v any) (float64, bool) {
	switch n := v.(type) {
	case float64:
		return n, true
	case float32:
		return float64(n), true
	case int:
		return float64(n), true
	case int32:
		return float64(n), true
	case int64:
		return float64(n), true
	case json.Number:
		if f, err := n.Float64(); err == nil {
			return f, true
		}
	}
	return 0, false
}

// computeBindingFingerprint produces a deterministic sha256 hex digest of the
// selected alternative's canonical configuration plus the normalized actual
// schemas. json.Marshal sorts object keys, so the digest is stable regardless
// of Go map iteration order or endpoint tool-definition shape.
func computeBindingFingerprint(b *workspaceBinding) string {
	opsDesc := make(map[string]any, len(b.operations))
	for kind, ob := range b.operations {
		var normalizedSchema any
		if ob.normalizedSchema != nil {
			normalizedSchema = ob.normalizedSchema.schema
		}
		opsDesc[string(kind)] = map[string]any{
			"tool_name":         ob.toolName,
			"mode":              string(ob.mode),
			"schema_matcher":    ob.schemaMatcher,
			"argument_map":      ob.argumentMap,
			"result_matcher":    ob.resultMatcher,
			"creates_parents":   ob.createsParents,
			"normalized_schema": normalizedSchema,
		}
	}
	desc := map[string]any{
		"alternative": b.alternativeName,
		"operations":  opsDesc,
	}
	raw, _ := json.Marshal(desc)
	sum := sha256.Sum256(raw)
	return hex.EncodeToString(sum[:])
}

// stringField returns a trimmed non-empty string value for key, or false.
func stringField(m map[string]any, key string) (string, bool) {
	v, ok := m[key]
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	if !ok {
		return "", false
	}
	s = strings.TrimSpace(s)
	if s == "" {
		return "", false
	}
	return s, true
}

// parseArgvTemplate validates and copies the command argv template.
func parseArgvTemplate(v any) ([]string, error) {
	raw, ok := v.([]any)
	if !ok || len(raw) == 0 {
		return nil, fmt.Errorf("command argument_map requires a non-empty %q template array", "argv")
	}
	out := make([]string, 0, len(raw))
	for i, item := range raw {
		s, ok := item.(string)
		if !ok {
			return nil, fmt.Errorf("argv template token %d is not a string", i)
		}
		out = append(out, s)
	}
	return out, nil
}

// validateCommandArgvTemplate permits placeholders only as whole argv tokens.
// This makes command mapping unambiguous: Edge determines exactly which argv
// element receives each canonical value instead of accepting shell fragments or
// unsupported interpolation syntax.
func validateCommandArgvTemplate(argv []string) (map[string]int, error) {
	counts := make(map[string]int, 2)
	for _, token := range argv {
		switch token {
		case "{path}", "{content}":
			counts[token]++
		default:
			if strings.ContainsAny(token, "{}") {
				return nil, fmt.Errorf("command argv template has unsupported placeholder token %q", token)
			}
		}
	}
	return counts, nil
}

// cloneAnyMap deep-copies a decoded JSON map so the compiled binding is
// independent of later config mutation.
func cloneAnyMap(m map[string]any) map[string]any {
	if m == nil {
		return nil
	}
	out := make(map[string]any, len(m))
	for k, v := range m {
		out[k] = cloneAnyValue(v)
	}
	return out
}

func cloneAnyValue(v any) any {
	switch t := v.(type) {
	case map[string]any:
		out := make(map[string]any, len(t))
		for k, vv := range t {
			out[k] = cloneAnyValue(vv)
		}
		return out
	case []any:
		out := make([]any, len(t))
		for i, vv := range t {
			out[i] = cloneAnyValue(vv)
		}
		return out
	default:
		return t
	}
}

// bindingFingerprint returns the immutable binding fingerprint.
func (b *workspaceBinding) bindingFingerprint() string { return b.fingerprint }

// operation returns the compiled operation binding for kind, or nil.
func (b *workspaceBinding) operation(kind workspaceOperationKind) *workspaceOperationBinding {
	return b.operations[kind]
}

// createsParents reports whether the selected write operation creates missing
// parents. It returns false when the binding has no write operation.
func (b *workspaceBinding) createsParents() bool {
	if write, ok := b.operations[opKindWrite]; ok {
		return write.createsParents
	}
	return false
}
