// Package binding implements the pure, deterministic JSON-to-HTTP binding for
// digest-pinned External Extension ActionPlans.
package binding

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strings"

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

	"github.com/tidbcloud/AgentTeam/internal/extension/registry"
	"github.com/tidbcloud/AgentTeam/internal/extension/spec"
)

const MaxInputBytes = 1 << 20

var ErrInputTooLarge = errors.New("extension input exceeds 1 MiB")

const actionInputHashDomain = "agentteam.extension.action-input/v1\x00"

// ValidatedInput is the deterministic, secret-free result of validating one Action
// input. Builtin execution uses it directly; External execution additionally
// binds the input to an HTTP request.
type ValidatedInput struct {
	ValidatedJSON  json.RawMessage
	SecretFreeHash string
}

// ValidateActionInput validates exactly one JSON document against the pinned
// Action schema and returns validated deterministic JSON plus a secret-free
// digest suitable for deployment-keyed audit hashing.
func ValidateActionInput(
	schema map[string]any,
	raw json.RawMessage,
) (ValidatedInput, error) {
	if len(raw) > MaxInputBytes {
		return ValidatedInput{}, ErrInputTooLarge
	}
	if schema == nil {
		return ValidatedInput{}, errors.New("extension action input schema is required")
	}
	input, err := decodeOneJSON(raw)
	if err != nil {
		return ValidatedInput{}, fmt.Errorf("decode extension action input: %w", err)
	}
	if err := validateInput(schema, input); err != nil {
		return ValidatedInput{}, fmt.Errorf("validate extension action input: %w", err)
	}
	validatedJSON, err := json.Marshal(input)
	if err != nil {
		return ValidatedInput{}, fmt.Errorf("encode validated extension action input: %w", err)
	}
	sum := sha256.Sum256(append([]byte(actionInputHashDomain), validatedJSON...))
	return ValidatedInput{
		ValidatedJSON:  append(json.RawMessage(nil), validatedJSON...),
		SecretFreeHash: "sha256:" + hex.EncodeToString(sum[:]),
	}, nil
}

// ResponsePolicy is the immutable response policy copied from the ActionPlan.
type ResponsePolicy struct {
	Mode            string
	SuccessStatuses []int
	ContentTypes    []string
}

// BoundRequest is a secret-free, deterministic outbound request description.
// Provider credentials are deliberately absent and must be injected by the
// guarded egress layer after binding.
type BoundRequest struct {
	Method         string
	URL            *url.URL
	Headers        http.Header
	Body           []byte
	ResponsePolicy ResponsePolicy
	SecretFreeHash string
}

// Bind validates input against the digest-pinned AgentTeam schema and produces the
// exact secret-free HTTP request. It has no clock, RNG, environment, filesystem,
// DNS, network, or mutable configuration dependency.
func Bind(plan registry.ActionPlan, raw json.RawMessage) (BoundRequest, error) {
	if len(raw) > MaxInputBytes {
		return BoundRequest{}, ErrInputTooLarge
	}
	if plan.DefinitionDigest == "" || plan.Domain == "" || plan.ActionName == "" {
		return BoundRequest{}, errors.New("bind extension action: incomplete ActionPlan identity")
	}
	if plan.InputSchema == nil {
		return BoundRequest{}, errors.New("bind extension action: input schema is required")
	}
	input, err := decodeOneJSON(raw)
	if err != nil {
		return BoundRequest{}, fmt.Errorf("bind extension action: decode input: %w", err)
	}
	if err := validateInput(plan.InputSchema, input); err != nil {
		return BoundRequest{}, fmt.Errorf("bind extension action: validate input: %w", err)
	}
	base, err := validateEndpoint(plan.Endpoint)
	if err != nil {
		return BoundRequest{}, err
	}
	method := strings.ToUpper(plan.Binding.Method)
	switch method {
	case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete:
	default:
		return BoundRequest{}, fmt.Errorf("bind extension action: unsupported method %q", plan.Binding.Method)
	}

	headers := make(http.Header)
	headerTargets := make(map[string]struct{})
	staticNames := make([]string, 0, len(plan.Binding.StaticHeaders))
	for name := range plan.Binding.StaticHeaders {
		staticNames = append(staticNames, name)
	}
	sort.Strings(staticNames)
	for _, name := range staticNames {
		if err := setBoundHeader(headers, headerTargets, name, plan.Binding.StaticHeaders[name]); err != nil {
			return BoundRequest{}, err
		}
	}

	pathValues, query, err := bindParameters(
		input,
		plan.Binding.Parameters,
		headers,
		headerTargets,
	)
	if err != nil {
		return BoundRequest{}, err
	}

	targetURL, err := bindURL(base, plan.Binding.Path, pathValues, query)
	if err != nil {
		return BoundRequest{}, err
	}
	body, err := bindBody(input, plan.Binding.Body)
	if err != nil {
		return BoundRequest{}, err
	}
	if plan.Binding.Body != nil {
		if err := setBoundHeader(
			headers, headerTargets, "Content-Type", plan.Binding.Body.ContentType,
		); err != nil {
			return BoundRequest{}, err
		}
	}
	policy := ResponsePolicy{
		Mode:            plan.Binding.Response.Mode,
		SuccessStatuses: append([]int(nil), plan.Binding.Response.SuccessStatuses...),
		ContentTypes:    append([]string(nil), plan.Binding.Response.ContentTypes...),
	}
	bound := BoundRequest{
		Method: method, URL: targetURL, Headers: headers, Body: body,
		ResponsePolicy: policy,
	}
	bound.SecretFreeHash, err = hashBoundRequest(bound)
	if err != nil {
		return BoundRequest{}, fmt.Errorf("bind extension action: hash request: %w", err)
	}
	return bound, nil
}

func bindParameters(
	input any,
	parameters []spec.Parameter,
	headers http.Header,
	headerTargets map[string]struct{},
) (map[string]string, []queryPair, error) {
	pathValues := make(map[string]string)
	query := make([]queryPair, 0)
	for _, parameter := range parameters {
		value, found, err := lookupJSONPointer(input, parameter.From)
		if err != nil {
			return nil, nil, fmt.Errorf(
				"bind extension action: parameter %q: %w", parameter.Name, err,
			)
		}
		if !found {
			if parameter.In == "path" {
				return nil, nil, fmt.Errorf(
					"bind extension action: required path parameter %q is absent", parameter.Name,
				)
			}
			continue
		}
		if value == nil {
			return nil, nil, fmt.Errorf(
				"bind extension action: non-body parameter %q cannot be null", parameter.Name,
			)
		}
		switch parameter.In {
		case "path":
			serialized, err := serializeSimple(value, parameter.Explode)
			if err != nil {
				return nil, nil, fmt.Errorf(
					"bind extension action: path parameter %q: %w", parameter.Name, err,
				)
			}
			if _, duplicate := pathValues[parameter.Name]; duplicate {
				return nil, nil, fmt.Errorf(
					"bind extension action: duplicate path target %q", parameter.Name,
				)
			}
			pathValues[parameter.Name] = url.PathEscape(serialized)
		case "query":
			pairs, err := serializeForm(parameter.Name, value, parameter.Explode, parameter.AllowReserved)
			if err != nil {
				return nil, nil, fmt.Errorf(
					"bind extension action: query parameter %q: %w", parameter.Name, err,
				)
			}
			query = append(query, pairs...)
		case "header":
			serialized, err := serializeSimple(value, parameter.Explode)
			if err != nil {
				return nil, nil, fmt.Errorf(
					"bind extension action: header parameter %q: %w", parameter.Name, err,
				)
			}
			if err := setBoundHeader(headers, headerTargets, parameter.Name, serialized); err != nil {
				return nil, nil, err
			}
		default:
			return nil, nil, fmt.Errorf(
				"bind extension action: unsupported parameter target %q", parameter.In,
			)
		}
	}
	return pathValues, query, nil
}

func decodeOneJSON(raw []byte) (any, error) {
	if len(raw) == 0 {
		return nil, errors.New("input is required")
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return nil, err
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != nil {
		if errors.Is(err, io.EOF) {
			return value, nil
		}
		return nil, errors.New("input must be exactly one JSON document")
	}
	return nil, errors.New("input must be exactly one JSON document")
}

func validateInput(schema map[string]any, input any) error {
	compiler := jsonschema.NewCompiler()
	compiler.DefaultDraft(jsonschema.Draft2020)
	if err := compiler.AddResource("extension-input.json", cloneJSON(schema)); err != nil {
		return err
	}
	compiled, err := compiler.Compile("extension-input.json")
	if err != nil {
		return err
	}
	return compiled.Validate(input)
}

func validateEndpoint(endpoint spec.Endpoint) (*url.URL, error) {
	base, err := url.Parse(endpoint.BaseURL)
	if err != nil {
		return nil, fmt.Errorf("bind extension action: invalid endpoint: %w", err)
	}
	if base.Scheme != "https" || base.Host == "" || base.User != nil ||
		base.RawQuery != "" || base.Fragment != "" {
		return nil, errors.New("bind extension action: endpoint must be a fixed credential-free HTTPS URL")
	}
	return base, nil
}

func setBoundHeader(
	headers http.Header,
	targets map[string]struct{},
	name string,
	value string,
) error {
	if isReservedHeader(name) {
		return fmt.Errorf("bind extension action: reserved header %q", name)
	}
	if !validHeaderName(name) || strings.ContainsAny(value, "\r\n") {
		return fmt.Errorf("bind extension action: invalid header %q", name)
	}
	key := strings.ToLower(name)
	if _, duplicate := targets[key]; duplicate {
		return fmt.Errorf("bind extension action: duplicate header target %q", name)
	}
	targets[key] = struct{}{}
	headers.Set(name, value)
	return nil
}

func cloneJSON(value any) any {
	switch typed := value.(type) {
	case map[string]any:
		out := make(map[string]any, len(typed))
		for key, item := range typed {
			out[key] = cloneJSON(item)
		}
		return out
	case []any:
		out := make([]any, len(typed))
		for i, item := range typed {
			out[i] = cloneJSON(item)
		}
		return out
	default:
		return value
	}
}
