package binding

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

	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

// ValidationError is a stable, path-aware Binding configuration error.
type ValidationError struct {
	Code    string
	Path    string
	Message string
}

func (err ValidationError) Error() string {
	if err.Path == "" {
		return err.Message
	}
	return err.Path + ": " + err.Message
}

// DecodeRevisionSpec strictly decodes one complete semantic revision document.
// Unknown fields, duplicate keys, trailing values, and unsupported source
// union members are rejected before normalization.
func DecodeRevisionSpec(raw []byte) (RevisionSpec, error) {
	var spec RevisionSpec
	if err := decodeStrictJSON(raw, &spec); err != nil {
		return RevisionSpec{}, err
	}
	return spec, nil
}

// DecodeSource strictly decodes the source half accepted by Binding management
// transports. Semantic union checks run in NormalizeRevision.
func DecodeSource(raw []byte) (Source, error) {
	var source Source
	if err := decodeStrictJSON(raw, &source); err != nil {
		return Source{}, err
	}
	return source, nil
}

// DecodeInputPolicy strictly decodes the input-policy half accepted by Binding
// management transports. An omitted/empty policy expands to two empty objects.
func DecodeInputPolicy(raw []byte) (InputPolicy, error) {
	if len(bytes.TrimSpace(raw)) == 0 {
		return InputPolicy{Defaults: json.RawMessage(`{}`), Constants: json.RawMessage(`{}`)}, nil
	}
	var policy InputPolicy
	if err := decodeStrictJSON(raw, &policy); err != nil {
		return InputPolicy{}, err
	}
	if len(bytes.TrimSpace(policy.Defaults)) == 0 {
		policy.Defaults = json.RawMessage(`{}`)
	}
	if len(bytes.TrimSpace(policy.Constants)) == 0 {
		policy.Constants = json.RawMessage(`{}`)
	}
	return policy, nil
}

// DecodeRevisionParts assembles strict transport fragments without asking a
// management layer to use permissive json.Unmarshal.
func DecodeRevisionParts(planID string, planVersionID string, sourceRaw []byte, policyRaw []byte) (RevisionSpec, error) {
	source, err := DecodeSource(sourceRaw)
	if err != nil {
		return RevisionSpec{}, err
	}
	policy, err := DecodeInputPolicy(policyRaw)
	if err != nil {
		return RevisionSpec{}, err
	}
	return RevisionSpec{
		PlanID: strings.TrimSpace(planID), PlanVersionID: strings.TrimSpace(planVersionID),
		InputPolicy: policy, Source: source,
	}, nil
}

// DecodeConfig strictly decodes the persisted config_json and attaches its
// authoritative Plan columns for validation and digest verification.
func DecodeConfig(planID string, planVersionID string, raw []byte) (RevisionSpec, error) {
	var config Config
	if err := decodeStrictJSON(raw, &config); err != nil {
		return RevisionSpec{}, err
	}
	return RevisionSpec{
		PlanID: planID, PlanVersionID: planVersionID,
		InputPolicy: config.InputPolicy, Source: config.Source,
	}, nil
}

// VerifyDigest checks a normalized revision spec against a persisted digest.
// Callers loading config_json should DecodeConfig and NormalizeRevision against
// the pinned Plan artifact before calling this function.
func VerifyDigest(spec RevisionSpec, digest string) bool {
	expected, err := revisionDigest(
		strings.TrimSpace(spec.PlanID),
		strings.TrimSpace(spec.PlanVersionID),
		Config{InputPolicy: spec.InputPolicy, Source: spec.Source},
	)
	return err == nil && strings.TrimSpace(digest) == expected
}

// NormalizeRevision validates a Binding revision against the exact compiled
// Plan input contract and returns deterministic persistence/digest material.
func NormalizeRevision(spec RevisionSpec, contract plan.ArtifactContract) (NormalizedRevision, error) {
	spec.PlanID = strings.TrimSpace(spec.PlanID)
	spec.PlanVersionID = strings.TrimSpace(spec.PlanVersionID)
	if spec.PlanID == "" {
		return NormalizedRevision{}, invalid("binding.plan_id", "$.planId", "planId is required")
	}
	if spec.PlanVersionID == "" {
		return NormalizedRevision{}, invalid("binding.plan_version_id", "$.planVersionId", "planVersionId is required")
	}

	defaults, defaultsJSON, err := normalizePolicyObject(contract, spec.InputPolicy.Defaults, "$.inputPolicy.defaults")
	if err != nil {
		return NormalizedRevision{}, err
	}
	constants, constantsJSON, err := normalizePolicyObject(contract, spec.InputPolicy.Constants, "$.inputPolicy.constants")
	if err != nil {
		return NormalizedRevision{}, err
	}
	for _, name := range defaults.Keys() {
		if constants.Has(name) {
			return NormalizedRevision{}, invalid(
				"binding.input_policy_collision",
				"$.inputPolicy.constants."+name,
				fmt.Sprintf("input %q cannot be declared in both defaults and constants", name),
			)
		}
	}
	spec.InputPolicy = InputPolicy{Defaults: defaultsJSON, Constants: constantsJSON}

	switch spec.Source.Type = SourceType(strings.TrimSpace(string(spec.Source.Type))); spec.Source.Type {
	case SourceTypeSchedule:
		if spec.Source.Schedule == nil {
			return NormalizedRevision{}, invalid("binding.source_union", "$.source", "schedule source requires source.schedule")
		}
		schedule, inputs, err := normalizeScheduleSource(*spec.Source.Schedule, contract)
		if err != nil {
			return NormalizedRevision{}, err
		}
		if _, err := plan.FinalizeInput(contract, defaults, inputs, constants); err != nil {
			return NormalizedRevision{}, wrapPlanInputError(err, "$.source.schedule.inputs")
		}
		spec.Source.Schedule = &schedule
	default:
		return NormalizedRevision{}, invalid("binding.source_type", "$.source.type", "source type must be schedule")
	}

	config := Config{InputPolicy: spec.InputPolicy, Source: spec.Source}
	configJSON, err := json.Marshal(config)
	if err != nil {
		return NormalizedRevision{}, fmt.Errorf("encode canonical Binding config: %w", err)
	}
	if len(configJSON) > MaxConfigBytes {
		return NormalizedRevision{}, invalid("binding.config_too_large", "$", fmt.Sprintf("Binding config must not exceed %d bytes", MaxConfigBytes))
	}
	digest, err := revisionDigest(spec.PlanID, spec.PlanVersionID, config)
	if err != nil {
		return NormalizedRevision{}, err
	}
	return NormalizedRevision{Spec: spec, ConfigJSON: configJSON, Digest: digest}, nil
}

func normalizePolicyObject(contract plan.ArtifactContract, raw json.RawMessage, path string) (plan.NormalizedInput, json.RawMessage, error) {
	if len(bytes.TrimSpace(raw)) == 0 {
		raw = json.RawMessage(`{}`)
	}
	value, err := plan.NormalizePartialInputJSON(contract, raw)
	if err != nil {
		return plan.NormalizedInput{}, nil, wrapPlanInputError(err, path)
	}
	deterministic, err := value.DeterministicJSON()
	if err != nil {
		return plan.NormalizedInput{}, nil, err
	}
	return value, deterministic, nil
}

func revisionDigest(planID string, planVersionID string, config Config) (string, error) {
	payload, err := json.Marshal(struct {
		PlanID        string `json:"planId"`
		PlanVersionID string `json:"planVersionId"`
		Config        Config `json:"config"`
	}{PlanID: planID, PlanVersionID: planVersionID, Config: config})
	if err != nil {
		return "", fmt.Errorf("encode Binding digest projection: %w", err)
	}
	hash := sha256.New()
	_, _ = hash.Write([]byte(DigestDomain))
	_, _ = hash.Write(payload)
	return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
}

func wrapPlanInputError(err error, prefix string) error {
	var inputError plan.InputError
	if errors.As(err, &inputError) {
		path := prefix
		if inputError.Path != "" && inputError.Path != "$" {
			path += strings.TrimPrefix(inputError.Path, "$")
		}
		return invalid("binding."+inputError.Code, path, inputError.Message)
	}
	return err
}

func invalid(code string, path string, message string) error {
	return ValidationError{Code: code, Path: path, Message: message}
}

func decodeStrictJSON(raw []byte, destination any) error {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 {
		return invalid("binding.invalid_json", "$", "Binding config must be one JSON object")
	}
	if trimmed[0] != '{' {
		return invalid("binding.invalid_json", "$", "Binding config must be one JSON object")
	}
	if len(raw) > MaxConfigBytes {
		return invalid("binding.config_too_large", "$", fmt.Sprintf("Binding config must not exceed %d bytes", MaxConfigBytes))
	}
	if err := rejectDuplicateJSONFields(raw); err != nil {
		return err
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(destination); err != nil {
		return invalid("binding.invalid_json", "$", err.Error())
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return invalid("binding.invalid_json", "$", "Binding config must contain exactly one JSON value")
		}
		return invalid("binding.invalid_json", "$", err.Error())
	}
	return nil
}

func rejectDuplicateJSONFields(raw []byte) error {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.UseNumber()
	if err := consumeJSONValue(decoder, "$", 0); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
		if err == nil {
			return invalid("binding.invalid_json", "$", "Binding config must contain exactly one JSON value")
		}
		return invalid("binding.invalid_json", "$", err.Error())
	}
	return nil
}

func consumeJSONValue(decoder *json.Decoder, path string, depth int) error {
	if depth > 64 {
		return invalid("binding.json_too_deep", path, "Binding config JSON nesting must not exceed 64")
	}
	token, err := decoder.Token()
	if err != nil {
		return invalid("binding.invalid_json", path, err.Error())
	}
	delimiter, ok := token.(json.Delim)
	if !ok {
		return nil
	}
	switch delimiter {
	case '{':
		seen := map[string]bool{}
		for decoder.More() {
			keyToken, err := decoder.Token()
			if err != nil {
				return invalid("binding.invalid_json", path, err.Error())
			}
			key, ok := keyToken.(string)
			if !ok {
				return invalid("binding.invalid_json", path, "object key must be a string")
			}
			child := path + "." + key
			if seen[key] {
				return invalid("binding.duplicate_field", child, fmt.Sprintf("field %q appears more than once", key))
			}
			seen[key] = true
			if err := consumeJSONValue(decoder, child, depth+1); err != nil {
				return err
			}
		}
		_, err = decoder.Token()
	case '[':
		index := 0
		for decoder.More() {
			if err := consumeJSONValue(decoder, fmt.Sprintf("%s[%d]", path, index), depth+1); err != nil {
				return err
			}
			index++
		}
		_, err = decoder.Token()
	default:
		return invalid("binding.invalid_json", path, "unexpected JSON delimiter")
	}
	if err != nil {
		return invalid("binding.invalid_json", path, err.Error())
	}
	return nil
}
