package appauthority

import (
	"fmt"
	"sort"
	"strconv"
	"unicode/utf16"
	"unicode/utf8"
)

func CanonicalAppManifest(manifest AppManifest) ([]byte, error) {
	if err := manifest.Validate(); err != nil {
		return nil, err
	}
	return canonicalJSON(appManifestValue(manifest))
}

func CanonicalMetadataBundle(bundle MetadataBundle, manifest AppManifest) ([]byte, error) {
	// Validate the structural bundle without recursively requiring the manifest's
	// digest to already equal the bytes being computed.
	if err := validateBundleStructure(bundle, manifest); err != nil {
		return nil, err
	}
	return canonicalJSON(metadataBundleValue(bundle))
}

// CanonicalComponentDescriptor returns RFC 8785 JCS for one already-validated
// closed MetadataBundle component descriptor. Callers that need authority
// validation must first verify the containing package.
func CanonicalComponentDescriptor(component Component) ([]byte, error) {
	return canonicalJSON(componentValue(component))
}

func validateBundleStructure(bundle MetadataBundle, manifest AppManifest) error {
	manifest.MetadataBundleDigest = zeroDigest
	if manifest.SchemaVersion == "" {
		return fmt.Errorf("app manifest is required")
	}
	if err := manifest.Validate(); err != nil {
		return err
	}
	if bundle.SchemaVersion != MetadataBundleSchemaVersion || bundle.Components == nil {
		return fmt.Errorf("metadata bundle has invalid schema or null components")
	}
	owned := stringSet(manifest.OwnedNamespaces)
	aliases := map[string]struct{}{}
	for _, dependency := range manifest.Dependencies {
		aliases[dependency.Alias] = struct{}{}
	}
	last := ""
	ids := map[string]struct{}{}
	for i, component := range bundle.Components {
		if err := component.validate(owned, aliases); err != nil {
			return fmt.Errorf("metadata bundle component[%d]: %w", i, err)
		}
		if i > 0 && component.ComponentID <= last {
			return fmt.Errorf("metadata bundle components must be strictly sorted by component_id")
		}
		last = component.ComponentID
		ids[component.ComponentID] = struct{}{}
	}
	for i, component := range bundle.Components {
		for _, dependency := range component.LocalDependencies {
			if _, exists := ids[dependency]; !exists || dependency == component.ComponentID {
				return fmt.Errorf("metadata bundle component[%d] has invalid local dependency %q", i, dependency)
			}
		}
	}
	return rejectComponentCycles(bundle.Components)
}

func appManifestValue(m AppManifest) map[string]any {
	dependencies := make([]any, len(m.Dependencies))
	for i, dependency := range m.Dependencies {
		dependencies[i] = map[string]any{
			"alias": dependency.Alias, "app_id": dependency.AppID,
			"maximum_version_exclusive": dependency.MaximumVersionExclusive,
			"minimum_version_inclusive": dependency.MinimumVersionInclusive,
			"required_capabilities":     stringsValue(dependency.RequiredCapabilities),
		}
	}
	protocols := make([]any, len(m.RequiredProtocols))
	for i, requirement := range m.RequiredProtocols {
		protocols[i] = protocolValue(requirement)
	}
	capabilities := make([]any, len(m.ProvidedCapabilities))
	for i, capability := range m.ProvidedCapabilities {
		value := map[string]any{
			"capability_id": capability.CapabilityID, "kind": capability.Kind,
			"required_runtime_capabilities": stringsValue(capability.RequiredRuntimeCapabilities),
		}
		if capability.HandlerRef != "" {
			value["handler_ref"] = capability.HandlerRef
		}
		capabilities[i] = value
	}
	return map[string]any{
		"app_id": m.AppID, "dependencies": dependencies,
		"frontend_compatibility": frontendValue(m.FrontendCompatibility),
		"metadata_bundle_digest": m.MetadataBundleDigest,
		"owned_namespaces":       stringsValue(m.OwnedNamespaces),
		"provided_capabilities":  capabilities, "publisher_id": m.PublisherID,
		"required_engine_capabilities": stringsValue(m.RequiredEngineCapabilities),
		"required_protocols":           protocols, "schema_version": m.SchemaVersion,
		"semantic_version": m.SemanticVersion,
	}
}

func metadataBundleValue(bundle MetadataBundle) map[string]any {
	components := make([]any, len(bundle.Components))
	for i, component := range bundle.Components {
		components[i] = componentValue(component)
	}
	return map[string]any{"components": components, "schema_version": bundle.SchemaVersion}
}

func componentValue(component Component) map[string]any {
	refs := make([]any, len(component.ExternalComponentRefs))
	for index, ref := range component.ExternalComponentRefs {
		refs[index] = map[string]any{
			"business_key": ref.BusinessKey, "component_kind": ref.ComponentKind,
			"dependency_alias": ref.DependencyAlias, "namespace_id": ref.NamespaceID,
		}
	}
	return map[string]any{
		"business_key": component.BusinessKey, "component_format_version": uint64(component.ComponentFormatVersion),
		"component_id": component.ComponentID, "component_kind": component.ComponentKind,
		"content_digest": component.ContentDigest, "definition_version": component.DefinitionVersion,
		"external_component_refs": refs, "local_dependencies": stringsValue(component.LocalDependencies),
		"namespace_id": component.NamespaceID, "override_policy": component.OverridePolicy,
		"removal_policy": component.RemovalPolicy,
	}
}

func frontendValue(frontend FrontendCompatibility) map[string]any {
	value := map[string]any{"mode": frontend.Mode}
	if frontend.HostSDK != nil {
		value["host_sdk"] = protocolValue(*frontend.HostSDK)
	}
	if frontend.ContributionSchema != nil {
		value["contribution_schema"] = protocolValue(*frontend.ContributionSchema)
	}
	return value
}

func protocolValue(protocol ProtocolRequirement) map[string]any {
	return map[string]any{"major": uint64(protocol.Major), "minimum_minor": uint64(protocol.MinimumMinor), "protocol_id": protocol.ProtocolID}
}

func stringsValue(values []string) []any {
	result := make([]any, len(values))
	for i, value := range values {
		result[i] = value
	}
	return result
}

func canonicalJSON(value any) ([]byte, error) {
	return appendCanonical(nil, value)
}

func appendCanonical(dst []byte, value any) ([]byte, error) {
	switch value := value.(type) {
	case nil:
		return append(dst, "null"...), nil
	case bool:
		return strconv.AppendBool(dst, value), nil
	case string:
		return appendJSONString(dst, value)
	case uint64:
		return strconv.AppendUint(dst, value, 10), nil
	case []any:
		dst = append(dst, '[')
		for i, item := range value {
			if i > 0 {
				dst = append(dst, ',')
			}
			var err error
			dst, err = appendCanonical(dst, item)
			if err != nil {
				return nil, err
			}
		}
		return append(dst, ']'), nil
	case map[string]any:
		keys := make([]string, 0, len(value))
		for key := range value {
			keys = append(keys, key)
		}
		sort.Slice(keys, func(i, j int) bool { return compareUTF16(keys[i], keys[j]) < 0 })
		dst = append(dst, '{')
		for i, key := range keys {
			if i > 0 {
				dst = append(dst, ',')
			}
			var err error
			dst, err = appendJSONString(dst, key)
			if err != nil {
				return nil, err
			}
			dst = append(dst, ':')
			dst, err = appendCanonical(dst, value[key])
			if err != nil {
				return nil, err
			}
		}
		return append(dst, '}'), nil
	default:
		return nil, fmt.Errorf("unsupported canonical JSON value %T", value)
	}
}

func appendJSONString(dst []byte, value string) ([]byte, error) {
	if !utf8.ValidString(value) {
		return nil, fmt.Errorf("invalid UTF-8 JSON string")
	}
	dst = append(dst, '"')
	for _, r := range value {
		switch r {
		case '"', '\\':
			dst = append(dst, '\\', byte(r))
		case '\b':
			dst = append(dst, `\b`...)
		case '\t':
			dst = append(dst, `\t`...)
		case '\n':
			dst = append(dst, `\n`...)
		case '\f':
			dst = append(dst, `\f`...)
		case '\r':
			dst = append(dst, `\r`...)
		default:
			if r < 0x20 {
				dst = append(dst, `\u00`...)
				hex := "0123456789abcdef"
				dst = append(dst, hex[byte(r)>>4], hex[byte(r)&0xf])
			} else {
				dst = utf8.AppendRune(dst, r)
			}
		}
	}
	return append(dst, '"'), nil
}

func compareUTF16(left, right string) int {
	l := utf16.Encode([]rune(left))
	r := utf16.Encode([]rune(right))
	limit := len(l)
	if len(r) < limit {
		limit = len(r)
	}
	for i := 0; i < limit; i++ {
		if l[i] < r[i] {
			return -1
		}
		if l[i] > r[i] {
			return 1
		}
	}
	switch {
	case len(l) < len(r):
		return -1
	case len(l) > len(r):
		return 1
	default:
		return 0
	}
}
