package approvalir

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"slices"
	"strings"
	"unicode/utf8"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

const (
	PublicationBundleSchemaV1   = "nexuscrm.approval-publication-bundle/v1"
	MaxPublicationBundleBytes   = 16 * 1024 * 1024
	maxPublicationIdentityBytes = 255
)

// PublicationBundle is the immutable deployment-time handoff from an
// app-owned Approval compiler to Catalog. SourceDigest binds the definitions
// to the exact selected source tree; the bundle is neutral compiler input and
// cannot serve or activate itself.
type PublicationBundle struct {
	Schema       string                `json:"schema"`
	SourceDigest string                `json:"sourceDigest"`
	Definitions  []PublishedDefinition `json:"definitions"`
}

type PublishedDefinition struct {
	DefinitionID   string             `json:"definitionId"`
	ObjectID       string             `json:"objectId"`
	DefinitionName string             `json:"definitionName"`
	Envelope       DefinitionEnvelope `json:"definitionEnvelope"`
}

// EncodePublicationBundle validates, canonically orders, and encodes one
// neutral publication bundle. The returned digest covers the canonical bytes.
func EncodePublicationBundle(bundle PublicationBundle, validator FormulaValidator) ([]byte, string, error) {
	canonical, err := validateAndCanonicalizePublicationBundle(bundle, validator)
	if err != nil {
		return nil, "", err
	}
	raw, err := json.Marshal(canonical)
	if err != nil {
		return nil, "", fmt.Errorf("encode approval publication bundle: %w", err)
	}
	if len(raw) > MaxPublicationBundleBytes {
		return nil, "", fmt.Errorf("approval publication bundle size %d exceeds %d", len(raw), MaxPublicationBundleBytes)
	}
	sum := sha256.Sum256(raw)
	return raw, "sha256:" + hex.EncodeToString(sum[:]), nil
}

// DecodePublicationBundle rejects unknown fields, non-canonical bytes,
// duplicate identities, malformed envelopes, and unbound metadata sources.
func DecodePublicationBundle(data []byte, validator FormulaValidator) (*PublicationBundle, string, error) {
	if len(data) == 0 || len(data) > MaxPublicationBundleBytes {
		return nil, "", fmt.Errorf("approval publication bundle size %d is outside 1..%d", len(data), MaxPublicationBundleBytes)
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	var bundle PublicationBundle
	if err := decoder.Decode(&bundle); err != nil {
		return nil, "", fmt.Errorf("decode approval publication bundle: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return nil, "", fmt.Errorf("decode approval publication bundle: trailing JSON value")
		}
		return nil, "", fmt.Errorf("decode approval publication bundle: trailing content: %w", err)
	}
	canonical, err := validateAndCanonicalizePublicationBundle(bundle, validator)
	if err != nil {
		return nil, "", err
	}
	raw, digest, err := EncodePublicationBundle(canonical, validator)
	if err != nil {
		return nil, "", err
	}
	if !bytes.Equal(data, raw) {
		return nil, "", fmt.Errorf("approval publication bundle is not canonical")
	}
	return &canonical, digest, nil
}

func validateAndCanonicalizePublicationBundle(bundle PublicationBundle, validator FormulaValidator) (PublicationBundle, error) {
	if bundle.Schema != PublicationBundleSchemaV1 {
		return PublicationBundle{}, fmt.Errorf("unsupported approval publication bundle schema %q", bundle.Schema)
	}
	if !validSourceDigest(bundle.SourceDigest) {
		return PublicationBundle{}, fmt.Errorf("invalid approval publication source digest %q", bundle.SourceDigest)
	}
	if len(bundle.Definitions) == 0 {
		return PublicationBundle{}, fmt.Errorf("approval publication bundle has no definitions")
	}
	result := bundle
	result.Definitions = append([]PublishedDefinition(nil), bundle.Definitions...)
	slices.SortFunc(result.Definitions, func(left, right PublishedDefinition) int {
		return strings.Compare(publicationDefinitionKey(left), publicationDefinitionKey(right))
	})
	ids := make(map[string]struct{}, len(result.Definitions))
	names := make(map[string]struct{}, len(result.Definitions))
	for index := range result.Definitions {
		definition := &result.Definitions[index]
		if err := validPublicationIdentity("definitionId", definition.DefinitionID); err != nil {
			return PublicationBundle{}, fmt.Errorf("definitions[%d]: %w", index, err)
		}
		if !common.ValidObjectIdentifier(definition.ObjectID) {
			return PublicationBundle{}, fmt.Errorf("definitions[%d]: approval publication objectId %q is not canonical", index, definition.ObjectID)
		}
		if err := validPublicationIdentity("definitionName", definition.DefinitionName); err != nil {
			return PublicationBundle{}, fmt.Errorf("definitions[%d]: %w", index, err)
		}
		id := strings.ToLower(definition.DefinitionID)
		if _, duplicate := ids[id]; duplicate {
			return PublicationBundle{}, fmt.Errorf("approval publication definition id %q is duplicated", definition.DefinitionID)
		}
		ids[id] = struct{}{}
		name := strings.ToLower(definition.ObjectID) + "\x00" + strings.ToLower(definition.DefinitionName)
		if _, duplicate := names[name]; duplicate {
			return PublicationBundle{}, fmt.Errorf("approval publication definition %s/%s is duplicated", definition.ObjectID, definition.DefinitionName)
		}
		names[name] = struct{}{}
		if err := validateDefinitionUTF8(&definition.Envelope.Definition); err != nil {
			return PublicationBundle{}, fmt.Errorf("definitions[%d]: %w", index, err)
		}
		envelopeRaw, err := json.Marshal(definition.Envelope)
		if err != nil {
			return PublicationBundle{}, fmt.Errorf("definitions[%d] encode definition envelope: %w", index, err)
		}
		envelope, err := DecodeDefinitionEnvelope(envelopeRaw, validator)
		if err != nil {
			return PublicationBundle{}, fmt.Errorf("definitions[%d]: %w", index, err)
		}
		definition.Envelope = *envelope
	}
	return result, nil
}

func publicationDefinitionKey(value PublishedDefinition) string {
	return strings.ToLower(value.ObjectID) + "\x00" + strings.ToLower(value.DefinitionName) + "\x00" + strings.ToLower(value.DefinitionID)
}

func validPublicationIdentity(field, value string) error {
	if value == "" || value != strings.TrimSpace(value) || len(value) > maxPublicationIdentityBytes || !utf8.ValidString(value) {
		return fmt.Errorf("approval publication %s %q is not canonical", field, value)
	}
	for _, char := range value {
		if char < 0x21 || char > 0x7e {
			return fmt.Errorf("approval publication %s %q is not canonical", field, value)
		}
	}
	return nil
}
