package registry

import (
	"fmt"
	"strings"

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

	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

const DefaultManifestObjectSchema = `{"type":"object"}`
const AppEventMessageMention = "agentteam.message.mention"
const AppEventNotificationAction = "agentteam.app.notification.action"

const (
	AppNotificationTargetHumanDM    = "human_dm"
	AppNotificationTargetBoundScope = "bound_scope"

	AppNotificationContentMessage         = "message"
	AppNotificationContentInteractiveCard = "interactive_card"
)

var manifestAppSupportedEvents = map[string]struct{}{
	AppEventMessageMention:     {},
	AppEventNotificationAction: {},
}

var manifestAppNotificationTargets = map[string]struct{}{
	AppNotificationTargetHumanDM:    {},
	AppNotificationTargetBoundScope: {},
}

var manifestAppNotificationContentKinds = map[string]struct{}{
	AppNotificationContentMessage:         {},
	AppNotificationContentInteractiveCard: {},
}

// StoredAppNotificationCapability is the normalized notification surface
// declared by a App manifest.
type StoredAppNotificationCapability struct {
	Supported bool
	Targets   []string
	Content   []string
}

// StoredAppRuntimeCapability is the normalized App capability view stored in an app manifest.
type StoredAppRuntimeCapability struct {
	Supported        bool
	Transports       []string
	DefaultTransport string
	Events           []string
	Scopes           []string
	Notifications    StoredAppNotificationCapability
	ConfigSchema     string
}

func JSONObjectOrEmpty(value map[string]any) map[string]any {
	if value == nil {
		return map[string]any{}
	}
	return value
}

func DecodeStoredJSONObject(payload []byte, field string) (map[string]any, error) {
	trimmed := strings.TrimSpace(string(payload))
	if trimmed == "" || trimmed == "null" {
		return map[string]any{}, nil
	}

	var decoded map[string]any
	if err := model.UnmarshalJSONObject("app "+field, []byte(trimmed), &decoded); err != nil {
		return nil, fmt.Errorf("decode app %s: %w", field, err)
	}
	if decoded == nil {
		return map[string]any{}, nil
	}
	return decoded, nil
}

func CompileManifestSchema(field string, schemaJSON string) (*jsonschema.Schema, error) {
	trimmed := strings.TrimSpace(schemaJSON)
	if trimmed == "" || trimmed == "null" {
		trimmed = DefaultManifestObjectSchema
	}

	var document any
	if err := model.UnmarshalJSONObject("app "+field+" schema", []byte(trimmed), &document); err != nil {
		return nil, fmt.Errorf("decode app %s schema: %w", field, err)
	}

	resource := "app://manifest/" + strings.TrimSpace(field) + ".json"
	compiler := jsonschema.NewCompiler()
	if err := compiler.AddResource(resource, document); err != nil {
		return nil, fmt.Errorf("add app %s schema resource: %w", field, err)
	}
	schema, err := compiler.Compile(resource)
	if err != nil {
		return nil, fmt.Errorf("compile app %s schema: %w", field, err)
	}
	return schema, nil
}

func ValidateJSONObjectAgainstManifestSchema(field string, schemaJSON string, value map[string]any) error {
	schema, err := CompileManifestSchema(field, schemaJSON)
	if err != nil {
		return badRequest(fmt.Sprintf("App manifest %s schema is invalid.", field))
	}
	if err := schema.Validate(JSONObjectOrEmpty(value)); err != nil {
		return badRequest(fmt.Sprintf("App %s does not match the manifest schema: %s", field, err))
	}
	return nil
}

func ParseStoredManifest(payload []byte) (model.StoredAppManifest, error) {
	manifest := model.StoredAppManifest{
		Scopes:            []string{},
		ConfigSchema:      DefaultManifestObjectSchema,
		CredentialsSchema: DefaultManifestObjectSchema,
	}

	trimmed := strings.TrimSpace(string(payload))
	if trimmed == "" || trimmed == "null" {
		return manifest, nil
	}

	var document map[string]any
	if err := model.UnmarshalJSONObject("stored app manifest", []byte(trimmed), &document); err != nil {
		return model.StoredAppManifest{}, fmt.Errorf("decode stored app manifest: %w", err)
	}
	if document == nil {
		return manifest, nil
	}

	runtimeCapability, err := storedAppRuntimeCapabilityFromDocument(document)
	if err != nil {
		return model.StoredAppManifest{}, err
	}
	if runtimeCapability.Supported {
		manifest.AppCallbackURL, err = manifestCallbackURL(document)
		if err != nil {
			return model.StoredAppManifest{}, fmt.Errorf("decode stored app manifest callback: %w", err)
		}
	}

	scopes, err := OptionalStringSliceField(document, "scopes")
	if err != nil {
		return model.StoredAppManifest{}, fmt.Errorf("decode stored app manifest scopes: %w", err)
	}
	// App-specific scopes are authoritative only when they are declared by the
	// App capability itself. Older or hand-written manifests may carry them at
	// the top level; silently ignore those entries so they cannot manufacture a
	// runtime grant without the matching App event/capability declaration.
	filteredScopes := make([]string, 0, len(scopes))
	for _, scope := range scopes {
		if isRuntimeCapabilityScope(scope) && (!runtimeCapability.Supported || !containsString(runtimeCapability.Scopes, scope)) {
			continue
		}
		filteredScopes = append(filteredScopes, scope)
	}
	scopes = filteredScopes
	if runtimeCapability.Supported {
		scopes = append(scopes, runtimeCapability.Scopes...)
	}
	manifest.Scopes = SortedUniqueScopes(scopes)
	manifest.ConfigSchema = StoredManifestSchemaJSON(document["config_schema"])
	if runtimeCapability.Supported {
		manifest.ConfigSchema = runtimeCapability.ConfigSchema
	}
	manifest.CredentialsSchema = StoredManifestSchemaJSON(document["credentials_schema"])
	return manifest, nil
}

// ParseStoredAppRuntimeCapability returns the normalized App capability stored in a manifest.
func ParseStoredAppRuntimeCapability(payload []byte) (StoredAppRuntimeCapability, error) {
	trimmed := strings.TrimSpace(string(payload))
	if trimmed == "" || trimmed == "null" {
		return StoredAppRuntimeCapability{ConfigSchema: DefaultManifestObjectSchema}, nil
	}

	var document map[string]any
	if err := model.UnmarshalJSONObject("stored app app capability manifest", []byte(trimmed), &document); err != nil {
		return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app app capability manifest: %w", err)
	}
	if document == nil {
		return StoredAppRuntimeCapability{ConfigSchema: DefaultManifestObjectSchema}, nil
	}
	return storedAppRuntimeCapabilityFromDocument(document)
}

func storedAppRuntimeCapabilityFromDocument(document map[string]any) (StoredAppRuntimeCapability, error) {
	empty := StoredAppRuntimeCapability{ConfigSchema: DefaultManifestObjectSchema}
	rawCapabilities, ok := document["capabilities"]
	if !ok || rawCapabilities == nil {
		return empty, nil
	}
	capabilities, ok := rawCapabilities.(map[string]any)
	if !ok {
		return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest capabilities: capabilities must be an object")
	}
	rawApp, ok := capabilities["runtime"]
	if !ok || rawApp == nil {
		return empty, nil
	}
	app, ok := rawApp.(map[string]any)
	if !ok {
		return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest capabilities.runtime: app capability must be an object")
	}
	transports := []string{}
	defaultTransport := ""
	rawRuntimeValue, hasRuntime := app["runtime"]
	if hasRuntime && rawRuntimeValue != nil {
		rawRuntime, ok := rawRuntimeValue.(map[string]any)
		if !ok {
			return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest capabilities.runtime.runtime: runtime must be an object")
		}
		var err error
		transports, err = OptionalStringSliceField(rawRuntime, "transports")
		if err != nil {
			return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest app runtime transports: %w", err)
		}
		transports = UniqueTrimmedStrings(transports)
		defaultTransport, _ = rawRuntime["default_transport"].(string)
		defaultTransport = strings.TrimSpace(defaultTransport)
		if defaultTransport == "" && len(transports) > 0 {
			defaultTransport = transports[0]
		}
	} else {
		hasRuntime = false
	}
	events, err := OptionalStringSliceField(app, "events")
	if err != nil {
		return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest app events: %w", err)
	}
	events, err = NormalizeAppEvents(events)
	if err != nil {
		return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest app events: %w", err)
	}
	scopes, err := OptionalStringSliceField(app, "scopes")
	if err != nil {
		return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest app scopes: %w", err)
	}
	notifications, err := storedAppNotificationsFromDocument(app)
	if err != nil {
		return StoredAppRuntimeCapability{}, err
	}
	// Built-in Apps such as Approval are dispatched by an internal handler
	// and therefore intentionally have no callback/runner transport. External
	// mention Apps still require a pinned runtime: otherwise a hand-written
	// manifest could become channel-addable without any deliverable transport.
	if !hasRuntime {
		if containsString(events, AppEventMessageMention) {
			return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest capabilities.runtime.runtime: runtime is required for agentteam.message.mention")
		}
		if !notifications.Supported {
			return StoredAppRuntimeCapability{}, fmt.Errorf("decode stored app manifest capabilities.runtime.runtime: runtime is required for external Apps")
		}
	}
	return StoredAppRuntimeCapability{
		Supported:        true,
		Transports:       transports,
		DefaultTransport: defaultTransport,
		Events:           events,
		Scopes:           SortedUniqueScopes(scopes),
		Notifications:    notifications,
		ConfigSchema:     StoredManifestSchemaJSON(app["config_schema"]),
	}, nil
}

func storedAppNotificationsFromDocument(app map[string]any) (StoredAppNotificationCapability, error) {
	rawNotifications, ok := app["notifications"]
	if !ok || rawNotifications == nil {
		return StoredAppNotificationCapability{}, nil
	}
	notifications, ok := rawNotifications.(map[string]any)
	if !ok {
		return StoredAppNotificationCapability{}, fmt.Errorf("decode stored app manifest capabilities.runtime.notifications: notifications must be an object")
	}
	targets, err := OptionalStringSliceField(notifications, "targets")
	if err != nil {
		return StoredAppNotificationCapability{}, fmt.Errorf("decode stored app manifest app notification targets: %w", err)
	}
	targets, err = normalizeAppNotificationValues("target", targets, manifestAppNotificationTargets)
	if err != nil {
		return StoredAppNotificationCapability{}, fmt.Errorf("decode stored app manifest app notifications: %w", err)
	}
	content, err := OptionalStringSliceField(notifications, "content")
	if err != nil {
		return StoredAppNotificationCapability{}, fmt.Errorf("decode stored app manifest app notification content: %w", err)
	}
	content, err = normalizeAppNotificationValues("content kind", content, manifestAppNotificationContentKinds)
	if err != nil {
		return StoredAppNotificationCapability{}, fmt.Errorf("decode stored app manifest app notifications: %w", err)
	}
	return StoredAppNotificationCapability{Supported: true, Targets: targets, Content: content}, nil
}

func normalizeAppNotificationValues(field string, values []string, supported map[string]struct{}) ([]string, error) {
	values = UniqueTrimmedStrings(values)
	for _, value := range values {
		if _, ok := supported[value]; !ok {
			return nil, fmt.Errorf("%s %q is not supported", field, value)
		}
	}
	return values, nil
}

// NormalizeAppEvents validates and normalizes App manifest event keys.
func NormalizeAppEvents(events []string) ([]string, error) {
	events = UniqueTrimmedStrings(events)
	for _, event := range events {
		if _, ok := manifestAppSupportedEvents[event]; !ok {
			return nil, fmt.Errorf("event %q is not supported", event)
		}
	}
	return events, nil
}

func isRuntimeCapabilityScope(scope string) bool {
	return strings.HasPrefix(strings.TrimSpace(scope), "app.")
}

func StoredManifestSchemaJSON(value any) string {
	switch value := value.(type) {
	case nil:
		return DefaultManifestObjectSchema
	case map[string]any, bool:
		payload, err := model.MarshalJSONObject("stored manifest schema", value)
		if err != nil {
			return DefaultManifestObjectSchema
		}
		trimmed := strings.TrimSpace(string(payload))
		if trimmed == "" || trimmed == "null" {
			return DefaultManifestObjectSchema
		}
		return trimmed
	default:
		return DefaultManifestObjectSchema
	}
}
