package registry

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

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

// builtinDigestDomain prefixes the hashed bytes for a Builtin runtime digest.
// It is intentionally distinct from the External Package digest domain so a
// Builtin digest and an External package digest can never be confused for the
// same identity, and changing the projection requires a new domain.
const builtinDigestDomain = "agentteam.extension.builtin/v1\x00"

// builtinDigestDomainV2 prefixes the policy-bearing Builtin digest projection.
// ConnectionRequired intentionally remains on v1 to preserve every existing
// Builtin digest; only non-default policies use v2.
const builtinDigestDomainV2 = "agentteam.extension.builtin/v2\x00"

// buildDefinitionFromPackage derives the COMMON Definition (Agent-visible
// catalog, no HTTP binding) from a Spec-conforming Package. The input is
// already canonical (RegisterExternal runs NormalizeDefinition first); this
// builder deep-copies schemas and canonicalizes examples into the deterministic JSON
// tree (map[string]any / []any / scalars) so a later clone-on-read never
// aliases a caller-supplied typed slice such as []string, and defensively
// re-sorts actions by name so the stored Catalog is always in canonical order
// even if a future caller bypasses normalization. The digest is the package's
// canonical digest, which covers the full Definition including the HTTP binding.
func buildDefinitionFromPackage(pkg spec.Package) Definition {
	sorted := sortedPackageActions(pkg.Actions)
	actions := make([]DefinitionAction, 0, len(sorted))
	for _, a := range sorted {
		actions = append(actions, DefinitionAction{
			Name:         a.Name,
			Summary:      a.Catalog.Summary,
			Description:  a.Catalog.Description,
			Safety:       a.Catalog.Safety,
			InputSchema:  cloneSchema(a.Catalog.InputSchema),
			OutputSchema: cloneSchema(a.Catalog.OutputSchema),
			Example:      detachedJSONValue(a.Catalog.Example),
		})
	}
	return Definition{
		Domain:      pkg.Metadata.Domain,
		Version:     pkg.Metadata.Version,
		Digest:      pkg.Digest,
		Name:        pkg.Metadata.Name,
		Description: pkg.Metadata.Description,
		Actions:     actions,
	}
}

// buildStoredDefinition returns a detached, normalized copy of def suitable for
// caching: schemas are deep-copied, examples are normalized to the canonical
// JSON tree (map[string]any / []any / scalars) so a later clone-on-read never
// aliases a caller-supplied typed slice such as []string, and actions are
// defensively re-sorted by name so the stored Catalog is always in canonical
// order. It is the Builtin-side mirror of buildDefinitionFromPackage.
func buildStoredDefinition(def Definition) Definition {
	cp := def
	sorted := sortedDefinitionActions(def.Actions)
	cp.Actions = make([]DefinitionAction, len(sorted))
	for i, a := range sorted {
		cp.Actions[i] = DefinitionAction{
			Name:           a.Name,
			Summary:        a.Summary,
			Description:    a.Description,
			Safety:         a.Safety,
			RequiredScopes: sortedUniqueExactStrings(a.RequiredScopes),
			InputSchema:    cloneSchema(a.InputSchema),
			OutputSchema:   cloneSchema(a.OutputSchema),
			Example:        detachedJSONValue(a.Example),
		}
	}
	return cp
}

// buildExternalBindingFromPackage derives the kind-specific HTTP binding for an
// External Extension from a Spec-conforming Package. The input is already
// canonical (RegisterExternal runs NormalizeDefinition first, which sorts auth
// profiles, scopes, success statuses, and content types); this builder detaches
// nested slices and maps from the caller's input and defensively re-sorts
// actions by name so the binding order matches the canonical Catalog order.
func buildExternalBindingFromPackage(pkg spec.Package) ExternalBinding {
	sorted := sortedPackageActions(pkg.Actions)
	actions := make([]ExternalBindingAction, 0, len(sorted))
	for _, a := range sorted {
		actions = append(actions, ExternalBindingAction{
			Name:    a.Name,
			Auth:    cloneActionAuth(a.Auth),
			Binding: cloneBinding(a.Binding),
		})
	}
	auth := pkg.Metadata.Auth
	if len(auth.Profiles) > 0 {
		profiles := make([]spec.AuthProfile, len(auth.Profiles))
		copy(profiles, auth.Profiles)
		auth.Profiles = profiles
	}
	return ExternalBinding{
		Endpoint: pkg.Metadata.Endpoint,
		Auth:     auth,
		Actions:  actions,
	}
}

// validateBuiltinCatalog validates the COMMON catalog of a Builtin Definition
// via spec.ValidateCatalog — the SAME shared catalog validator that an
// External Definition passes through, scoped to the fields a Builtin carries
// (metadata identity, per-action name/safety/schema/example). A Builtin has no
// HTTP binding, so endpoint/auth-profile/binding validation is not applied, but
// the catalog contract is identical: an invalid domain, a non-kebab action name,
// an uncompilable schema, an example that violates its schema, or a
// contradictory safety contract (destructive flag disagreeing with the class)
// is rejected.
func validateBuiltinCatalog(def Definition) error {
	for _, action := range def.Actions {
		seen := make(map[string]struct{}, len(action.RequiredScopes))
		for _, scope := range action.RequiredScopes {
			if strings.TrimSpace(scope) != scope || scope == "" {
				return fmt.Errorf(
					"registry: validate builtin catalog %q: action %q has an invalid required scope",
					def.Domain,
					action.Name,
				)
			}
			if _, duplicate := seen[scope]; duplicate {
				return fmt.Errorf(
					"registry: validate builtin catalog %q: action %q has duplicate required scope %q",
					def.Domain,
					action.Name,
					scope,
				)
			}
			seen[scope] = struct{}{}
		}
	}
	meta, actions := builtinCatalogInput(def)
	if err := spec.ValidateCatalog(meta, actions); err != nil {
		return fmt.Errorf("registry: validate builtin catalog %q: %w", def.Domain, err)
	}
	return nil
}

// builtinCatalogInput converts a registry Definition (the common, kind-independent
// catalog) into the spec.Metadata + []spec.Action shape that
// spec.ValidateCatalog consumes. Only the common catalog fields are mapped;
// the HTTP binding and auth profiles are zero-valued (a Builtin has none) and
// ValidateCatalog does not validate them. Examples are canonicalized to the
// standard JSON tree (map[string]any / []any / scalars) so the schema validator
// can accept a caller-supplied typed slice such as []string.
func builtinCatalogInput(def Definition) (spec.Metadata, []spec.Action) {
	meta := spec.Metadata{
		SchemaVersion: 1,
		Domain:        def.Domain,
		Version:       def.Version,
		Name:          def.Name,
		Description:   def.Description,
	}
	actions := make([]spec.Action, len(def.Actions))
	for i, a := range def.Actions {
		actions[i] = spec.Action{
			Name: a.Name,
			Catalog: spec.Catalog{
				Summary:      a.Summary,
				Description:  a.Description,
				Safety:       a.Safety,
				InputSchema:  a.InputSchema,
				OutputSchema: a.OutputSchema,
				Example:      detachedJSONValue(a.Example),
			},
		}
	}
	return meta, actions
}

// sortedPackageActions returns a detached copy of actions sorted by name. It is
// used by the storage builders so the STORED Catalog is always in canonical
// (name-sorted) order regardless of the caller's input order.
func sortedPackageActions(actions []spec.Action) []spec.Action {
	sorted := append([]spec.Action(nil), actions...)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name })
	return sorted
}

// sortedDefinitionActions returns a detached copy of actions sorted by name. It
// is the Builtin-side mirror of sortedPackageActions.
func sortedDefinitionActions(actions []DefinitionAction) []DefinitionAction {
	sorted := append([]DefinitionAction(nil), actions...)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name })
	return sorted
}

// builtinDigestProjection is the canonical projection of a Builtin Definition
// over which the RUNTIME digest is computed. The revision IS included in the
// runtime digest: a revision change is an execution-behavior change, and the
// digest is what authorization, lookup-by-digest, fingerprint, and
// stale-detection anchor on. Excluding the revision would let a Builtin swap
// its executor across restart without any digest-visible signal. Actions are
// sorted by name and examples are canonicalized so the digest depends only on
// content, not on history.
type builtinDigestProjection struct {
	SchemaVersion int                   `json:"schemaVersion"`
	Kind          string                `json:"kind"`
	Domain        string                `json:"domain"`
	Version       int                   `json:"version"`
	Name          string                `json:"name"`
	Description   string                `json:"description,omitempty"`
	Revision      string                `json:"revision"`
	Actions       []builtinDigestAction `json:"actions"`
}

type builtinDigestProjectionV2 struct {
	SchemaVersion    int                   `json:"schemaVersion"`
	Kind             string                `json:"kind"`
	ConnectionPolicy string                `json:"connectionPolicy"`
	Domain           string                `json:"domain"`
	Version          int                   `json:"version"`
	Name             string                `json:"name"`
	Description      string                `json:"description,omitempty"`
	Revision         string                `json:"revision"`
	Actions          []builtinDigestAction `json:"actions"`
}

type builtinDigestAction struct {
	Name           string         `json:"name"`
	Summary        string         `json:"summary,omitempty"`
	Description    string         `json:"description,omitempty"`
	Safety         spec.Safety    `json:"safety"`
	RequiredScopes []string       `json:"requiredScopes,omitempty"`
	InputSchema    map[string]any `json:"inputSchema,omitempty"`
	// OutputSchema drops omitempty so nil (no output contract) and {} (explicit
	// unconstrained output schema) have distinct digest contributions; both are
	// Agent-visible and semantically different.
	OutputSchema map[string]any `json:"outputSchema"`
	Example      any            `json:"example,omitempty"`
}

// computeBuiltinDigest returns "sha256:<64 lowercase hex>" computed over the
// canonical runtime projection of def INCLUDING the revision. The same catalog
// with a DIFFERENT revision produces a DIFFERENT digest, so a revision change
// propagates to authorization, fingerprint, and stale-detection as a new
// identity rather than being invisible.
func computeBuiltinDigest(def Definition, revision string) (string, error) {
	return computeBuiltinDigestWithPolicy(def, revision, ConnectionRequired)
}

func computeBuiltinDigestWithPolicy(
	def Definition,
	revision string,
	policy ConnectionPolicy,
) (string, error) {
	actions := make([]builtinDigestAction, 0, len(def.Actions))
	for _, a := range def.Actions {
		actions = append(actions, builtinDigestAction{
			Name:        a.Name,
			Summary:     a.Summary,
			Description: a.Description,
			Safety:      a.Safety,
			RequiredScopes: sortedUniqueExactStrings(
				a.RequiredScopes,
			),
			InputSchema:  cloneSchema(a.InputSchema),
			OutputSchema: cloneSchema(a.OutputSchema),
			Example:      detachedJSONValue(a.Example),
		})
	}
	// Sort by name for a stable, order-independent digest.
	for i := 1; i < len(actions); i++ {
		for j := i; j > 0 && actions[j-1].Name > actions[j].Name; j-- {
			actions[j-1], actions[j] = actions[j], actions[j-1]
		}
	}
	var (
		projection any
		domain     string
	)
	switch policy {
	case ConnectionRequired:
		projection = builtinDigestProjection{
			SchemaVersion: 1,
			Kind:          "builtin",
			Domain:        def.Domain,
			Version:       def.Version,
			Name:          def.Name,
			Description:   def.Description,
			Revision:      revision,
			Actions:       actions,
		}
		domain = builtinDigestDomain
	case ConnectionNone:
		projection = builtinDigestProjectionV2{
			SchemaVersion:    2,
			Kind:             "builtin",
			ConnectionPolicy: "none",
			Domain:           def.Domain,
			Version:          def.Version,
			Name:             def.Name,
			Description:      def.Description,
			Revision:         revision,
			Actions:          actions,
		}
		domain = builtinDigestDomainV2
	default:
		return "", ErrInvalidConnectionPolicy
	}
	encoded, err := encodeDeterministicJSON(projection)
	if err != nil {
		return "", fmt.Errorf("encode builtin projection: %w", err)
	}
	h := sha256.New()
	h.Write([]byte(domain))
	h.Write(encoded)
	return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil
}

func sortedUniqueExactStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	result := make([]string, 0, len(values))
	for _, value := range values {
		if value == "" {
			continue
		}
		if _, duplicate := seen[value]; duplicate {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	sort.Strings(result)
	return result
}

// encodeDeterministicJSON marshals with lexicographically-sorted map keys and HTML
// escaping disabled so the byte output depends only on the value, mirroring the
// stage-1 canonical encoder used for External digests.
func encodeDeterministicJSON(v any) ([]byte, error) {
	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	if err := enc.Encode(v); err != nil {
		return nil, err
	}
	// json.Encoder appends a trailing newline; drop it for a stable digest.
	return bytes.TrimRight(buf.Bytes(), "\n"), nil
}
