package catalogsnapshot

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"github.com/nexuscrm/kernel/pkg/formula"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/approvalir"
)

const maxApprovalIdentityBytes = 255

// ApprovalNodeStorageID is the single owner for deterministic Catalog-native
// ProcessNode identity shared by Approval execution and query projections.
func ApprovalNodeStorageID(definitionID, stepName string) string {
	sum := sha256.Sum256([]byte(definitionID + "\x00step\x00" + strings.ToLower(strings.TrimSpace(stepName))))
	return "catalog-approval-node-" + hex.EncodeToString(sum[:16])
}

// ApprovalDefinitionInput is one neutral approval definition already lowered
// by the owning L1 compiler. Envelope carries no product source grammar;
// Catalog strictly opens it, binds it to one effective object, and stamps the
// request revision before Runtime can consume it.
type ApprovalDefinitionInput struct {
	CatalogRevision CatalogRevision
	DefinitionID    string
	ObjectAPIName   string
	DeveloperName   string
	Envelope        []byte
}

// ApprovalDefinition is the immutable neutral definition returned from a
// request-bound RuntimeMetadataView. Runtime state remains in TiDB; this value
// contains no approval instance, work item, credential, or product wire data.
type ApprovalDefinition struct {
	DefinitionID  string
	ObjectAPIName string
	DeveloperName string
	SourceDigest  string
	ContentDigest Digest
	Definition    approvalir.Definition
}

type approvalDefinitionRecord struct {
	DefinitionID  string                `json:"definitionId"`
	ObjectAPIName string                `json:"objectApiName"`
	DeveloperName string                `json:"developerName"`
	SourceDigest  string                `json:"sourceDigest"`
	ContentDigest Digest                `json:"contentDigest"`
	Definition    approvalir.Definition `json:"definition"`
}

func inputHasApprovalRuntimeProjection(values []RuntimeProjectionInput) bool {
	for _, value := range values {
		if profileUsesApprovalAuthority(value.Profile) {
			return true
		}
	}
	return false
}

func profileUsesApprovalAuthority(profile ProjectionProfile) bool {
	return profile == ProjectionApprovalExecutionV1 || profile == ProjectionApprovalReadV1
}

func buildApprovalDefinitions(
	revision CatalogRevision,
	inputs []ApprovalDefinitionInput,
	objects map[string]*models.ObjectMetadata,
) ([]approvalDefinitionRecord, error) {
	result := make([]approvalDefinitionRecord, 0, len(inputs))
	ids := make(map[string]struct{}, len(inputs))
	names := make(map[string]struct{}, len(inputs))
	validator := approvalir.FormulaValidatorFunc(func(source string) error {
		return formula.NewEngine().ValidateFilterFormulaExpression(source, nil)
	})
	for index, input := range inputs {
		path := fmt.Sprintf("approvalDefinitions[%d]", index)
		if err := requireSourceRevision(path, revision, input.CatalogRevision); err != nil {
			return nil, err
		}
		if err := requireApprovalIdentity(path+".definitionId", input.DefinitionID); err != nil {
			return nil, err
		}
		if err := requireApprovalIdentity(path+".objectApiName", input.ObjectAPIName); err != nil {
			return nil, err
		}
		if err := requireApprovalIdentity(path+".developerName", input.DeveloperName); err != nil {
			return nil, err
		}
		object, exists := objects[foldIdentity(input.ObjectAPIName)]
		if !exists || object.APIName != input.ObjectAPIName {
			return nil, conflict(
				ConflictInvalidDefinition, path+".objectApiName",
				"approval definition object %q is absent or not canonical", input.ObjectAPIName,
			)
		}
		envelope, err := approvalir.DecodeDefinitionEnvelope(input.Envelope, validator)
		if err != nil {
			return nil, conflict(
				ConflictInvalidDefinition, path+".envelope",
				"open neutral approval definition: %v", err,
			)
		}
		if err := validateApprovalExecutionDefinition(path+".envelope.definition", &envelope.Definition); err != nil {
			return nil, err
		}
		canonicalEnvelope, err := marshalCanonical(approvalir.DefinitionEnvelope{
			Schema:       approvalir.DefinitionEnvelopeSchemaV1,
			SourceDigest: envelope.SourceDigest,
			Definition:   envelope.Definition,
		})
		if err != nil {
			return nil, conflict(
				ConflictEncoding, path+".envelope",
				"encode canonical approval definition: %v", err,
			)
		}
		idKey := foldIdentity(input.DefinitionID)
		if _, duplicate := ids[idKey]; duplicate {
			return nil, conflict(
				ConflictInvalidDefinition, path+".definitionId",
				"approval definition %q is duplicated case-insensitively", input.DefinitionID,
			)
		}
		ids[idKey] = struct{}{}
		nameKey := foldIdentity(input.ObjectAPIName) + "\x00" + foldIdentity(input.DeveloperName)
		if _, duplicate := names[nameKey]; duplicate {
			return nil, conflict(
				ConflictInvalidDefinition, path+".developerName",
				"approval definition name %q is duplicated for object %q", input.DeveloperName, input.ObjectAPIName,
			)
		}
		names[nameKey] = struct{}{}
		result = append(result, approvalDefinitionRecord{
			DefinitionID: input.DefinitionID, ObjectAPIName: input.ObjectAPIName,
			DeveloperName: input.DeveloperName, SourceDigest: envelope.SourceDigest,
			ContentDigest: ComputeDigest(canonicalEnvelope), Definition: envelope.Definition,
		})
	}
	sort.Slice(result, func(i, j int) bool {
		left := foldIdentity(result[i].ObjectAPIName) + "\x00" + foldIdentity(result[i].DeveloperName) + "\x00" + foldIdentity(result[i].DefinitionID)
		right := foldIdentity(result[j].ObjectAPIName) + "\x00" + foldIdentity(result[j].DeveloperName) + "\x00" + foldIdentity(result[j].DefinitionID)
		return left < right
	})
	return result, nil
}

func validateApprovalExecutionDefinition(path string, definition *approvalir.Definition) error {
	if definition == nil {
		return conflict(ConflictInvalidDefinition, path, "approval definition is nil")
	}
	groups := [][]approvalir.Action{
		definition.InitialSubmissionActions,
		definition.StepApprovalActions,
		definition.FinalApprovalActions,
		definition.FinalRejectionActions,
		definition.RecallActions,
		definition.ReassignActions,
	}
	for _, step := range definition.Steps {
		groups = append(groups, step.ApprovalActions, step.RejectionActions)
	}
	for groupIndex, actions := range groups {
		for actionIndex, action := range actions {
			if action.Reference != nil {
				return conflict(
					ConflictInvalidDefinition,
					fmt.Sprintf("%s.actionGroups[%d][%d]", path, groupIndex, actionIndex),
					"unresolved approval action reference %q cannot enter a serving view",
					action.Reference.APIName,
				)
			}
		}
	}
	return nil
}

func approvalFlowNames(values []approvalDefinitionRecord) []string {
	seen := map[string]string{}
	appendActions := func(actions []approvalir.Action) {
		for _, action := range actions {
			if action.Kind != approvalir.ActionKindFlow || action.Flow == nil {
				continue
			}
			name := strings.TrimSpace(action.Flow.APIName)
			if name != "" {
				seen[foldIdentity(name)] = name
			}
		}
	}
	for _, value := range values {
		definition := value.Definition
		appendActions(definition.InitialSubmissionActions)
		appendActions(definition.StepApprovalActions)
		appendActions(definition.FinalApprovalActions)
		appendActions(definition.FinalRejectionActions)
		appendActions(definition.RecallActions)
		appendActions(definition.ReassignActions)
		for _, step := range definition.Steps {
			appendActions(step.ApprovalActions)
			appendActions(step.RejectionActions)
		}
	}
	result := make([]string, 0, len(seen))
	for _, value := range seen {
		result = append(result, value)
	}
	sort.Slice(result, func(i, j int) bool { return identityLess(result[i], result[j]) })
	return result
}

func selectedApprovalDefinitions(values []approvalDefinitionRecord, selected map[string]struct{}) []approvalDefinitionRecord {
	result := make([]approvalDefinitionRecord, 0, len(selected))
	for _, value := range values {
		if _, exists := selected[foldIdentity(value.DefinitionID)]; exists {
			result = append(result, value)
		}
	}
	return result
}

func validateApprovalDefinitionRecord(
	path string,
	record approvalDefinitionRecord,
	objects map[string]string,
) error {
	if err := requireApprovalIdentity(path+".definitionId", record.DefinitionID); err != nil {
		return err
	}
	if err := requireApprovalIdentity(path+".objectApiName", record.ObjectAPIName); err != nil {
		return err
	}
	if err := requireApprovalIdentity(path+".developerName", record.DeveloperName); err != nil {
		return err
	}
	canonicalObjectAPIName, exists := objects[foldIdentity(record.ObjectAPIName)]
	if !exists || canonicalObjectAPIName != record.ObjectAPIName {
		return fmt.Errorf("%s references absent or noncanonical object %q", path, record.ObjectAPIName)
	}
	if !validDigest(Digest(record.SourceDigest)) || !validDigest(record.ContentDigest) {
		return fmt.Errorf("%s has an invalid source or content digest", path)
	}
	envelope, err := marshalCanonical(approvalir.DefinitionEnvelope{
		Schema:       approvalir.DefinitionEnvelopeSchemaV1,
		SourceDigest: record.SourceDigest,
		Definition:   record.Definition,
	})
	if err != nil {
		return fmt.Errorf("%s cannot reconstruct its definition envelope: %w", path, err)
	}
	if ComputeDigest(envelope) != record.ContentDigest {
		return fmt.Errorf("%s content digest does not match its definition envelope", path)
	}
	if err := record.Definition.ValidateWithFormulaValidator(
		approvalir.FormulaValidatorFunc(func(source string) error {
			return formula.NewEngine().ValidateFilterFormulaExpression(source, nil)
		}),
	); err != nil {
		return fmt.Errorf("%s definition is invalid: %w", path, err)
	}
	if err := validateApprovalExecutionDefinition(path+".definition", &record.Definition); err != nil {
		return err
	}
	return nil
}

func requireApprovalIdentity(path, value string) error {
	if err := requireCanonicalIdentity(path, value); err != nil {
		return err
	}
	if len(value) > maxApprovalIdentityBytes || !utf8.ValidString(value) || strings.IndexFunc(value, unicode.IsControl) >= 0 {
		return conflict(ConflictInvalidIdentity, path, "identity is invalid or exceeds %d bytes", maxApprovalIdentityBytes)
	}
	return nil
}

func approvalRecordToPublic(record approvalDefinitionRecord) (ApprovalDefinition, bool) {
	definition, err := cloneJSON(record.Definition)
	if err != nil {
		return ApprovalDefinition{}, false
	}
	return ApprovalDefinition{
		DefinitionID: record.DefinitionID, ObjectAPIName: record.ObjectAPIName,
		DeveloperName: record.DeveloperName, SourceDigest: record.SourceDigest,
		ContentDigest: record.ContentDigest, Definition: definition,
	}, true
}

// ApprovalDefinition resolves one exact definition from the request-bound
// immutable Runtime view. Missing or wrong-profile views fail closed.
func (v *RuntimeMetadataView) ApprovalDefinition(definitionID string) (ApprovalDefinition, bool) {
	if v == nil || !profileUsesApprovalAuthority(v.doc.Profile) {
		return ApprovalDefinition{}, false
	}
	index, exists := v.approvalDefinitionsByID[foldIdentity(definitionID)]
	if !exists {
		return ApprovalDefinition{}, false
	}
	return approvalRecordToPublic(v.doc.ApprovalDefinitions[index])
}

// ApprovalDefinitionForObject resolves the one immutable definition bound to
// an object/developer-name pair. Runtime uses this to verify legacy storage
// identity anchors without granting those rows semantic authority.
func (v *RuntimeMetadataView) ApprovalDefinitionForObject(
	objectAPIName string,
	developerName string,
) (ApprovalDefinition, bool) {
	if v == nil || !profileUsesApprovalAuthority(v.doc.Profile) {
		return ApprovalDefinition{}, false
	}
	indices := v.approvalDefinitionsByObject[foldIdentity(objectAPIName)]
	var selected ApprovalDefinition
	found := 0
	for _, index := range indices {
		record := v.doc.ApprovalDefinitions[index]
		if !strings.EqualFold(record.DeveloperName, developerName) {
			continue
		}
		value, ok := approvalRecordToPublic(record)
		if !ok {
			return ApprovalDefinition{}, false
		}
		selected = value
		found++
	}
	return selected, found == 1
}

// ActiveApprovalDefinitionsForObject returns the effective active definitions
// for one exact object, ordered by ProcessOrder and then stable identity.
func (v *RuntimeMetadataView) ActiveApprovalDefinitionsForObject(objectAPIName string) []ApprovalDefinition {
	if v == nil || !profileUsesApprovalAuthority(v.doc.Profile) {
		return nil
	}
	indices := v.approvalDefinitionsByObject[foldIdentity(objectAPIName)]
	result := make([]ApprovalDefinition, 0, len(indices))
	for _, index := range indices {
		record := v.doc.ApprovalDefinitions[index]
		if !record.Definition.Active {
			continue
		}
		value, ok := approvalRecordToPublic(record)
		if !ok {
			return nil
		}
		result = append(result, value)
	}
	sort.Slice(result, func(i, j int) bool {
		leftOrder, rightOrder := int(^uint(0)>>1), int(^uint(0)>>1)
		if result[i].Definition.ProcessOrder != nil {
			leftOrder = *result[i].Definition.ProcessOrder
		}
		if result[j].Definition.ProcessOrder != nil {
			rightOrder = *result[j].Definition.ProcessOrder
		}
		if leftOrder != rightOrder {
			return leftOrder < rightOrder
		}
		return identityLess(result[i].DefinitionID, result[j].DefinitionID)
	})
	return result
}

// ActiveApprovalObjectAPINames returns the canonical object closure owned by
// the active Approval definitions in this immutable request view. Runtime uses
// it to resolve an objectless submit without consulting repository-global
// metadata or allowing Catalog order to choose between colliding record IDs.
func (v *RuntimeMetadataView) ActiveApprovalObjectAPINames() []string {
	if v == nil || !profileUsesApprovalAuthority(v.doc.Profile) {
		return nil
	}
	seen := make(map[string]struct{}, len(v.doc.ApprovalDefinitions))
	result := make([]string, 0, len(v.doc.ApprovalDefinitions))
	for _, record := range v.doc.ApprovalDefinitions {
		if !record.Definition.Active {
			continue
		}
		key := foldIdentity(record.ObjectAPIName)
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		result = append(result, record.ObjectAPIName)
	}
	sort.Slice(result, func(i, j int) bool {
		left, right := foldIdentity(result[i]), foldIdentity(result[j])
		if left != right {
			return left < right
		}
		return result[i] < result[j]
	})
	return result
}
