package extensionflow

import (
	"encoding/json"
	"fmt"
	"sort"

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

// Agent Extension Catalog state (Extension Platform stage 6, product proposal).
//
// The server persists the deterministic JSON projection of an Agent's effective
// Extension command set — the Agent-visible routing identity and the precise
// authorized action set — as the "what was published at this revision"
// reference for no-op change detection. Credentials, token expiry/refresh,
// connection health, endpoint/binding, and source metadata are deliberately
// absent — they do not restart an Agent.

// catalogAction is one authorized action paired with its FULL effective safety
// contract (Class) resolved from the pinned Definition.
type catalogAction struct {
	Name   string
	Safety spec.Safety
}

// catalogEntry is one Agent authorization projected to its secret-free,
// Agent-visible identity: the Extension name, the pinned Definition domain (a
// server-internal join key used to resolve the registered Definition for
// descriptor projection; it is not part of the persisted state), and the
// precise allowed action set with effective safety.
type catalogEntry struct {
	ExtensionName     string
	DefinitionDomain  string
	DefinitionVersion int
	DefinitionDigest  string
	Actions           []catalogAction
}

// catalogEntryFromAuthorization projects a durable Agent authorization and its
// resolved Definition into a secret-free catalog entry. It is fail-closed: an
// allowed action that is blank, duplicated, or NOT declared by the Definition
// returns an error rather than silently mapping to an empty safety contract.
// Actions are returned in canonical sorted order.
func catalogEntryFromAuthorization(extensionName string, definition registry.Definition, allowedActions []string) (catalogEntry, error) {
	if extensionName == "" {
		return catalogEntry{}, fmt.Errorf("catalog entry Extension name is required")
	}
	safetyByName := make(map[string]spec.Safety, len(definition.Actions))
	for _, action := range definition.Actions {
		safetyByName[action.Name] = action.Safety
	}
	seen := make(map[string]struct{}, len(allowedActions))
	actions := make([]catalogAction, 0, len(allowedActions))
	for _, name := range allowedActions {
		if name == "" {
			return catalogEntry{}, fmt.Errorf("catalog entry contains a blank action")
		}
		if _, dup := seen[name]; dup {
			return catalogEntry{}, fmt.Errorf("catalog entry contains duplicate action %q", name)
		}
		safety, ok := safetyByName[name]
		if !ok {
			return catalogEntry{}, fmt.Errorf("catalog entry action %q is not declared by definition %q", name, definition.Domain)
		}
		seen[name] = struct{}{}
		actions = append(actions, catalogAction{Name: name, Safety: safety})
	}
	sort.Slice(actions, func(i, j int) bool { return actions[i].Name < actions[j].Name })
	return catalogEntry{
		ExtensionName:     extensionName,
		DefinitionDomain:  definition.Domain,
		DefinitionVersion: definition.Version,
		DefinitionDigest:  definition.Digest,
		Actions:           actions,
	}, nil
}

// catalogStateEntryJSON is the canonical persisted projection of one entry.
type catalogStateEntryJSON struct {
	ExtensionName     string                   `json:"extension"`
	DefinitionDomain  string                   `json:"domain"`
	DefinitionVersion int                      `json:"definitionVersion"`
	DefinitionDigest  string                   `json:"definitionDigest"`
	Actions           []catalogStateActionJSON `json:"actions"`
}

// catalogStateActionJSON is the canonical persisted projection of one action's
// effective safety contract.
type catalogStateActionJSON struct {
	Name  string `json:"name"`
	Class string `json:"class"`
}

// marshalCatalogState derives the canonical persisted state for one Agent's
// effective Extension command set. It is a pure, deterministic function:
// entries are sorted by Extension name (actions are already canonical-sorted
// by catalogEntryFromAuthorization), so equal command sets always produce
// byte-equal state and the stored value is directly comparable and readable.
// An empty command set marshals to the exact string "[]".
func marshalCatalogState(entries []catalogEntry) (string, error) {
	sorted := make([]catalogEntry, len(entries))
	for i := range entries {
		sorted[i] = entries[i]
		sorted[i].Actions = append([]catalogAction(nil), entries[i].Actions...)
		sort.Slice(sorted[i].Actions, func(a, b int) bool {
			return sorted[i].Actions[a].Name < sorted[i].Actions[b].Name
		})
	}
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].ExtensionName < sorted[j].ExtensionName })

	seenExtensionName := make(map[string]struct{}, len(sorted))
	entryJSON := make([]catalogStateEntryJSON, 0, len(sorted))
	for i := range sorted {
		e := &sorted[i]
		if e.ExtensionName == "" {
			return "", fmt.Errorf("catalog entry Extension name is required")
		}
		if e.DefinitionDomain == "" ||
			e.DefinitionVersion <= 0 ||
			e.DefinitionDigest == "" {
			return "", fmt.Errorf(
				"catalog entry %q Definition identity is invalid",
				e.ExtensionName,
			)
		}
		if _, dup := seenExtensionName[e.ExtensionName]; dup {
			return "", fmt.Errorf("duplicate catalog Extension name %q", e.ExtensionName)
		}
		seenExtensionName[e.ExtensionName] = struct{}{}

		seenAction := make(map[string]struct{}, len(e.Actions))
		actionsJSON := make([]catalogStateActionJSON, 0, len(e.Actions))
		for _, a := range e.Actions {
			if a.Name == "" {
				return "", fmt.Errorf("catalog entry %q contains a blank action", e.ExtensionName)
			}
			if _, dup := seenAction[a.Name]; dup {
				return "", fmt.Errorf("catalog entry %q contains duplicate action %q", e.ExtensionName, a.Name)
			}
			seenAction[a.Name] = struct{}{}
			actionsJSON = append(actionsJSON, catalogStateActionJSON{
				Name:  a.Name,
				Class: a.Safety.Class,
			})
		}
		entryJSON = append(entryJSON, catalogStateEntryJSON{
			ExtensionName:     e.ExtensionName,
			DefinitionDomain:  e.DefinitionDomain,
			DefinitionVersion: e.DefinitionVersion,
			DefinitionDigest:  e.DefinitionDigest,
			Actions:           actionsJSON,
		})
	}
	encoded, err := json.Marshal(entryJSON)
	if err != nil {
		return "", fmt.Errorf("encode extension catalog state: %w", err)
	}
	return string(encoded), nil
}
