package openai

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

// workspaceEncodedPayload is the deterministic, self-contained payload the Edge
// produces for caller execution. The Edge never executes it and never inspects
// the workspace; it only produces it from the compiled binding and the issued
// tool call.
type workspaceEncodedPayload struct {
	fingerprint string
	alternative string
	operation   workspaceOperationKind
	mode        workspaceBindingMode
	toolName    string
	// publicCallID is the IOP-issued tool call id; providerCallID is the
	// provider-native id. Both are carried into receipt correlation.
	publicCallID   string
	providerCallID string
	// safePath is the lexically normalized, containment-checked relative path.
	safePath string
	// structuredArgs is the outgoing argument map keyed by actual tool field
	// names. In structured mode it carries typed values unchanged; in command
	// mode it carries only the synthesized command field.
	structuredArgs map[string]any
	// Command-mode encoding. commandArgv holds the resolved, unquoted argv in
	// deterministic template order; commandString is its shell-safe joining.
	commandField  string
	commandArgv   []string
	commandString string
	// containmentGuard is the caller-executed guard expression. The Edge never
	// evaluates it; it is returned verbatim to the caller.
	containmentGuard string
	// correlationDigest seals the complete issued payload. Receipt matching
	// recomputes it before trusting any mutable in-memory fields.
	correlationDigest string
}

// workspaceResult is a caller-reported workspace operation result the codec
// correlates against an issued payload.
type workspaceResult struct {
	// callID is the tool call id the caller reports the result for. It must
	// equal the issued public or provider id.
	callID string
	// status is the caller-reported outcome (e.g. "success", "error").
	status string
	// body is the caller-reported result body, if any.
	body json.RawMessage
}

// workspaceResultReceipt records the correlation between a caller-reported
// result and the binding/payload that produced the call.
type workspaceResultReceipt struct {
	fingerprint    string
	alternative    string
	operation      workspaceOperationKind
	toolName       string
	publicCallID   string
	providerCallID string
	path           string
	status         string
	// resultHash is a sha256 of the compacted result body, empty when opaque.
	resultHash string
	// matched is true only when identity, operation, path, guard, and the
	// configured result matcher all correlate.
	matched bool
	// mismatchReason explains why matched is false.
	mismatchReason string
}

// encodeWorkspaceCall produces a deterministic, safe payload for one operation
// of the compiled binding from an issued tool call. It preserves typed
// structured values, synthesizes deterministic shell-safe commands in command
// mode, carries the public/provider identities, and emits a caller-executable
// containment guard. It returns an error when the call does not match the bound
// tool, the mapped path is missing, or the path fails lexical containment.
func encodeWorkspaceCall(b *workspaceBinding, op workspaceOperationKind, call normalizedToolCall) (*workspaceEncodedPayload, error) {
	if b == nil {
		return nil, fmt.Errorf("nil binding")
	}
	ob := b.operation(op)
	if ob == nil {
		return nil, fmt.Errorf("binding has no %q operation", op)
	}
	if call.Arguments == nil {
		return nil, fmt.Errorf("nil call arguments")
	}
	if strings.TrimSpace(call.ID) == "" {
		return nil, fmt.Errorf("call is missing a public tool call id")
	}
	if strings.TrimSpace(call.Name) != ob.toolName {
		return nil, fmt.Errorf("call tool %q does not match bound tool %q for operation %q", call.Name, ob.toolName, op)
	}

	rawPath, ok := lookupMappedArgument(call.Arguments, ob.pathField)
	if !ok {
		return nil, fmt.Errorf("call is missing mapped path field %q", ob.pathField)
	}
	pathStr, ok := rawPath.(string)
	if !ok || strings.TrimSpace(pathStr) == "" {
		return nil, fmt.Errorf("mapped path field %q is not a non-empty string", ob.pathField)
	}
	safePath := lexicalNormalizePath(pathStr)
	if err := validateContainment(safePath); err != nil {
		return nil, err
	}

	payload := &workspaceEncodedPayload{
		fingerprint:    b.fingerprint,
		alternative:    b.alternativeName,
		operation:      op,
		mode:           ob.mode,
		toolName:       ob.toolName,
		publicCallID:   strings.TrimSpace(call.ID),
		providerCallID: strings.TrimSpace(call.ProviderCallID),
		safePath:       safePath,
	}
	payload.containmentGuard = synthesizeContainmentGuard(safePath, ob.createsParents)

	switch ob.mode {
	case modeStructured:
		if err := encodeStructured(payload, ob, call, safePath); err != nil {
			return nil, err
		}
	case modeCommand:
		if err := encodeCommand(payload, ob, call, safePath); err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("unknown binding mode %q", ob.mode)
	}
	if ob.mode == modeCommand {
		payload.commandString = payload.containmentGuard + " && " + payload.commandString
		payload.structuredArgs[ob.commandField] = payload.commandString
	}
	payload.correlationDigest = computePayloadCorrelationDigest(payload)
	if payload.correlationDigest == "" {
		return nil, fmt.Errorf("issued payload cannot be canonically correlated")
	}
	return payload, nil
}

// encodeStructured drives the outgoing argument map only from the compiled
// argument map. The path is replaced with the containment-checked safe path;
// content and mode values are carried through byte-for-byte with their original
// types. No arbitrary extra fields are copied and no shell encoding is applied.
func encodeStructured(payload *workspaceEncodedPayload, ob *workspaceOperationBinding, call normalizedToolCall, safePath string) error {
	args := make(map[string]any)
	setMappedArgument(args, ob.pathField, safePath)

	if ob.contentField != "" {
		if value, ok := lookupMappedArgument(call.Arguments, ob.contentField); ok {
			setMappedArgument(args, ob.contentField, cloneAnyValue(value))
		} else if ob.op == opKindWrite {
			return fmt.Errorf("write call is missing mapped content field %q", ob.contentField)
		}
	}
	if ob.modeField != "" {
		if value, ok := lookupMappedArgument(call.Arguments, ob.modeField); ok {
			setMappedArgument(args, ob.modeField, cloneAnyValue(value))
		}
	}

	payload.structuredArgs = args
	return nil
}

// encodeCommand synthesizes a deterministic command from the fixed argv
// template. Placeholders {path} and {content} are substituted with the safe
// path and the mapped content; every other token is a literal. Each argv
// element is shell-safe single-quoted, so command output is stable regardless
// of Go map iteration order and content bytes are preserved exactly.
func encodeCommand(payload *workspaceEncodedPayload, ob *workspaceOperationBinding, call normalizedToolCall, safePath string) error {
	var content string
	if ob.contentField != "" {
		if value, ok := lookupMappedArgument(call.Arguments, ob.contentField); ok {
			content = commandArgumentString(value)
		} else if ob.op == opKindWrite {
			return fmt.Errorf("write call is missing mapped content field %q", ob.contentField)
		}
	}

	argv := make([]string, 0, len(ob.argvTemplate))
	for _, token := range ob.argvTemplate {
		switch token {
		case "{path}":
			argv = append(argv, safePath)
		case "{content}":
			argv = append(argv, content)
		default:
			argv = append(argv, token)
		}
	}

	quoted := make([]string, len(argv))
	for i, arg := range argv {
		quoted[i] = singleQuoteShell(arg)
	}

	payload.commandField = ob.commandField
	payload.commandArgv = argv
	payload.commandString = strings.Join(quoted, " ")
	payload.structuredArgs = map[string]any{ob.commandField: payload.commandString}
	return nil
}

// setMappedArgument assigns value at the dot-path key within args, creating
// intermediate maps as needed.
func setMappedArgument(args map[string]any, dotPath string, value any) {
	parts := strings.Split(dotPath, ".")
	current := args
	for i := 0; i < len(parts)-1; i++ {
		next, ok := current[parts[i]].(map[string]any)
		if !ok {
			next = make(map[string]any)
			current[parts[i]] = next
		}
		current = next
	}
	current[parts[len(parts)-1]] = value
}

// commandArgumentString renders a mapped value for command substitution.
// Strings are used as-is; other JSON values are marshaled deterministically.
func commandArgumentString(value any) string {
	if s, ok := value.(string); ok {
		return s
	}
	raw, err := json.Marshal(value)
	if err != nil {
		return ""
	}
	return string(raw)
}

// lexicalNormalizePath applies deterministic path normalization without
// touching the filesystem: it trims, converts backslashes, collapses repeated
// slashes, and resolves "." segments while preserving a leading slash so
// validateContainment can reject absolute paths. ".." segments are preserved
// so validateContainment can reject traversal.
func lexicalNormalizePath(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return ""
	}
	isAbsolute := strings.HasPrefix(raw, "/")
	raw = strings.ReplaceAll(raw, "\\", "/")
	for strings.Contains(raw, "//") {
		raw = strings.ReplaceAll(raw, "//", "/")
	}
	parts := strings.Split(raw, "/")
	resolved := make([]string, 0, len(parts))
	for i, part := range parts {
		if part == "." {
			continue
		}
		if i == 0 && part == "" && isAbsolute {
			resolved = append(resolved, "")
			continue
		}
		if part == "" {
			continue
		}
		resolved = append(resolved, part)
	}
	return strings.Join(resolved, "/")
}

// validateContainment lexically rejects paths that cannot be safely contained
// in the workspace before any encoding: empty, over-long, absolute, traversal,
// null-byte, and shell-metacharacter paths.
func validateContainment(path string) error {
	if path == "" {
		return fmt.Errorf("empty path")
	}
	if len(path) > 4096 {
		return fmt.Errorf("path exceeds maximum length of 4096 characters")
	}
	if strings.HasPrefix(path, "/") {
		return fmt.Errorf("absolute path is not allowed: %q", path)
	}
	for _, segment := range strings.Split(path, "/") {
		if segment == ".." {
			return fmt.Errorf("path traversal is not allowed: %q", path)
		}
	}
	if strings.ContainsRune(path, 0) {
		return fmt.Errorf("path contains null byte")
	}
	for _, r := range path {
		switch {
		case r >= 'a' && r <= 'z':
		case r >= 'A' && r <= 'Z':
		case r >= '0' && r <= '9':
		case r == '.' || r == '-' || r == '_' || r == '/' || r == ' ':
		default:
			return fmt.Errorf("path contains unsafe character %q", string(r))
		}
	}
	return nil
}

// synthesizeContainmentGuard returns a concrete caller-executed shell guard.
// It resolves the canonical workspace cwd and either the existing target or a
// canonical existing ancestor before the operation. Resolving the target itself
// when it already exists is essential: checking only the parent would allow a
// final-component symlink to escape the workspace. Parent-capable operations
// may retain a validated nonexistent suffix after fencing their nearest existing
// ancestor; operations without that capability still require the immediate
// parent to exist. The Edge never evaluates this guard or accesses a workspace.
func synthesizeContainmentGuard(relPath string, createsParents bool) string {
	quoted := singleQuoteShell(relPath)
	var b strings.Builder
	b.WriteString("{ ")
	b.WriteString(`IOP_WS_ROOT=$(realpath "${IOP_WORKSPACE_CWD:-.}") || exit 1; `)
	b.WriteString(`if [ "$IOP_WS_ROOT" = "/" ]; then IOP_WS_PREFIX=""; else IOP_WS_PREFIX="$IOP_WS_ROOT"; fi; `)
	b.WriteString(`IOP_WS_CANDIDATE="$IOP_WS_ROOT/`)
	b.WriteString(relPath)
	b.WriteString(`"; `)
	b.WriteString(`if [ -e "$IOP_WS_CANDIDATE" ] || [ -L "$IOP_WS_CANDIDATE" ]; then IOP_WS_TARGET=$(realpath "$IOP_WS_CANDIDATE") || exit 1; `)
	b.WriteString(`else `)
	if createsParents {
		b.WriteString(`IOP_WS_ANCESTOR="$IOP_WS_CANDIDATE"; IOP_WS_SUFFIX=""; `)
		b.WriteString(`while [ ! -e "$IOP_WS_ANCESTOR" ] && [ ! -L "$IOP_WS_ANCESTOR" ]; do IOP_WS_NAME=$(basename -- "$IOP_WS_ANCESTOR") || exit 1; `)
		b.WriteString(`if [ -n "$IOP_WS_SUFFIX" ]; then IOP_WS_SUFFIX="$IOP_WS_NAME/$IOP_WS_SUFFIX"; else IOP_WS_SUFFIX="$IOP_WS_NAME"; fi; `)
		b.WriteString(`IOP_WS_ANCESTOR=$(dirname -- "$IOP_WS_ANCESTOR") || exit 1; done; `)
		b.WriteString(`IOP_WS_ANCESTOR=$(realpath "$IOP_WS_ANCESTOR") || exit 1; `)
		b.WriteString(`IOP_WS_TARGET="$IOP_WS_ANCESTOR/$IOP_WS_SUFFIX"; `)
	} else {
		b.WriteString(`IOP_WS_PARENT=$(realpath "$(dirname -- "$IOP_WS_CANDIDATE")") || exit 1; `)
		b.WriteString(`IOP_WS_TARGET="$IOP_WS_PARENT/$(basename -- `)
		b.WriteString(quoted)
		b.WriteString(`)"; `)
	}
	b.WriteString(`fi; `)
	b.WriteString(`case "$IOP_WS_TARGET/" in "$IOP_WS_PREFIX"/*) : ;; *) echo 'iop: path escapes workspace root' >&2; exit 1 ;; esac; }`)
	return b.String()
}

// singleQuoteShell returns a POSIX single-quoted encoding of s. Bytes inside
// single quotes are literal, so content is preserved exactly; embedded single
// quotes are closed, escaped, and reopened.
func singleQuoteShell(s string) string {
	return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
}

// matchResultReceipt correlates a caller-reported result against an issued
// payload. A matched receipt requires the reported call id to equal the issued
// public or provider id, the result body to parse, and the operation's
// configured result matcher to match the normalized {status, result} envelope.
// Opaque, error-shaped, wrong-id, and matcher-mismatched results do not match.
func matchResultReceipt(b *workspaceBinding, payload *workspaceEncodedPayload, result workspaceResult) *workspaceResultReceipt {
	receipt := &workspaceResultReceipt{
		operation: payload.operation,
		toolName:  payload.toolName,
		path:      payload.safePath,
		status:    result.status,
	}
	if b != nil {
		receipt.fingerprint = b.fingerprint
		receipt.alternative = b.alternativeName
	}
	receipt.publicCallID = payload.publicCallID
	receipt.providerCallID = payload.providerCallID
	if len(result.body) > 0 {
		receipt.resultHash = sha256ResultHash(result.body)
	}

	if reason := matchResultCorrelation(b, payload, result); reason != "" {
		receipt.mismatchReason = reason
		return receipt
	}
	ob := b.operation(payload.operation)

	normalized, err := normalizeResultEnvelope(result)
	if err != nil {
		receipt.mismatchReason = "result body is not valid JSON"
		return receipt
	}
	if hasExplicitErrorSignal(normalized) {
		receipt.mismatchReason = "result contains an explicit error signal"
		return receipt
	}
	if !deepSubsetMatch(map[string]any(ob.resultMatcher), normalized) {
		receipt.mismatchReason = "result does not satisfy the configured result matcher"
		return receipt
	}

	receipt.matched = true
	return receipt
}

// matchResultCorrelation validates only immutable issue identity. Callers use
// it to distinguish an exact caller-reported operation failure from malformed,
// unknown, or untrusted continuation input before considering cleanup.
func matchResultCorrelation(b *workspaceBinding, payload *workspaceEncodedPayload, result workspaceResult) string {
	if b == nil || payload == nil || b.fingerprint != payload.fingerprint {
		return "payload does not belong to binding"
	}
	if payload.correlationDigest == "" || payload.correlationDigest != computePayloadCorrelationDigest(payload) {
		return "issued payload correlation digest does not match"
	}
	if b.operation(payload.operation) == nil {
		return "binding has no such operation"
	}
	reportedID := strings.TrimSpace(result.callID)
	if reportedID == "" {
		return "result is missing a tool call id"
	}
	if reportedID != payload.publicCallID && reportedID != payload.providerCallID {
		return "result call id does not correlate with the issued call"
	}
	return ""
}

// workspaceResultIsExact reports whether a caller result carries a
// self-describing operation report. An explicit failure status is exact on its
// own; otherwise the non-empty body must decode into the normalized
// {status, result} envelope. Opaque or malformed success bodies are untrusted
// and stay fail-closed.
func workspaceResultIsExact(result workspaceResult) bool {
	if hasExplicitErrorSignal(map[string]any{"status": result.status}) {
		return true
	}
	if len(bytes.TrimSpace(result.body)) == 0 {
		return false
	}
	_, err := normalizeResultEnvelope(result)
	return err == nil
}

// normalizeResultEnvelope builds the {status, result} envelope the configured
// result matcher is evaluated against. An empty body yields a nil result, so an
// opaque result cannot satisfy a matcher that requires result fields.
func normalizeResultEnvelope(result workspaceResult) (map[string]any, error) {
	envelope := map[string]any{"status": result.status}
	if len(bytes.TrimSpace(result.body)) == 0 {
		envelope["result"] = nil
		return envelope, nil
	}
	var decoded any
	decoder := json.NewDecoder(bytes.NewReader(result.body))
	decoder.UseNumber()
	if err := decoder.Decode(&decoded); err != nil {
		return nil, err
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return nil, fmt.Errorf("multiple JSON values are not allowed")
		}
		return nil, err
	}
	envelope["result"] = decoded
	return envelope, nil
}

// computePayloadCorrelationDigest binds every issued value that affects caller
// execution or receipt admission. json.Marshal gives map keys a canonical
// ordering, preserving typed values while avoiding Go map iteration variance.
func computePayloadCorrelationDigest(payload *workspaceEncodedPayload) string {
	if payload == nil {
		return ""
	}
	description := map[string]any{
		"fingerprint":       payload.fingerprint,
		"alternative":       payload.alternative,
		"operation":         string(payload.operation),
		"mode":              string(payload.mode),
		"tool_name":         payload.toolName,
		"public_call_id":    payload.publicCallID,
		"provider_call_id":  payload.providerCallID,
		"safe_path":         payload.safePath,
		"structured_args":   payload.structuredArgs,
		"command_field":     payload.commandField,
		"command_argv":      payload.commandArgv,
		"command_string":    payload.commandString,
		"containment_guard": payload.containmentGuard,
	}
	raw, err := json.Marshal(description)
	if err != nil {
		return ""
	}
	sum := sha256.Sum256(raw)
	return hex.EncodeToString(sum[:])
}

// hasExplicitErrorSignal rejects success-shaped bodies that also declare an
// endpoint error. It intentionally treats only semantically non-empty error
// values as signals so optional null/false fields remain representable.
func hasExplicitErrorSignal(value any) bool {
	switch v := value.(type) {
	case map[string]any:
		for key, child := range v {
			normalizedKey := strings.ToLower(strings.TrimSpace(key))
			if (normalizedKey == "error" || normalizedKey == "errors") && errorValuePresent(child) {
				return true
			}
			if normalizedKey == "status" || normalizedKey == "type" {
				if text, ok := child.(string); ok {
					switch strings.ToLower(strings.TrimSpace(text)) {
					case "error", "failed", "failure":
						return true
					}
				}
			}
			if hasExplicitErrorSignal(child) {
				return true
			}
		}
	case []any:
		for _, child := range v {
			if hasExplicitErrorSignal(child) {
				return true
			}
		}
	}
	return false
}

func errorValuePresent(value any) bool {
	switch v := value.(type) {
	case nil:
		return false
	case bool:
		return v
	case string:
		return strings.TrimSpace(v) != ""
	case []any:
		return len(v) > 0
	case map[string]any:
		return len(v) > 0
	default:
		return true
	}
}

// sha256ResultHash computes a sha256 hex digest of the compacted result body
// for stable, order-independent receipt hashing.
func sha256ResultHash(body json.RawMessage) string {
	var buf bytes.Buffer
	if err := json.Compact(&buf, body); err != nil {
		buf.Reset()
		buf.Write(body)
	}
	sum := sha256.Sum256(buf.Bytes())
	return hex.EncodeToString(sum[:])
}
