package packs

import (
	"context"
	"fmt"

	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

// newUIActionManagedComponentFromRows builds the managed component from an
// already-expanded, sorted standardActionRow slice plus the RAW payload bytes
// and the engine ManifestComponent. It is shared by the internal build path
// (newSourceBackedUIActionComponent) and the engine implementation of
// cp.PresentationComponentBuildContext.AddUIActionComponent.
//
// The payload field is the raw bytes, never re-marshalled, so ExpectedPayload()
// (and thus the oracle PayloadHash) is identical across both paths. The
// expectedProjection closure returns the parsed action rows — INCLUDING Label —
// so a deep-compare over ExpectedProjection() bites on parsed-content drift that
// the raw-byte PayloadHash cannot see.
func newUIActionManagedComponentFromRows(packName string, component ManifestComponent, payload []byte, rows []standardActionRow) PackComponent {
	projection := append([]standardActionRow(nil), rows...)
	return &sourceBackedManagedComponent{
		packName:          packName,
		manifestComponent: component,
		payload:           payload,
		expectedProjection: func() (any, error) {
			return projection, nil
		},
		apply: func(ctx context.Context, deps ApplyDeps) error {
			return applyStandardActions(ctx, deps.Metadata, deps.DB, packName, payload, componentPayloadLabel(component))
		},
		verify: func(_ context.Context, deps VerifyDeps) error {
			return verifyStandardActions(deps.DB, packName, payload, componentPayloadLabel(component))
		},
	}
}

// standardActionRowsFromDeclarations re-derives the canonical, sorted
// standardActionRow projection from the POST-fan-out cp.UIActionDeclaration
// slice the external (contract-only) handler produces. It reproduces
// expandStandardActionRows' canonicalization exactly: deterministic ID,
// owner-pack stamping, and canonical Config JSON. Building the projection FROM
// the declarations (rather than re-parsing the raw payload) is deliberate — it
// makes a Label (or any field) corruption in a declaration flow into
// ExpectedProjection(), which is exactly what the mutation test asserts the
// deep-compare catches.
func standardActionRowsFromDeclarations(packName string, decls []cp.UIActionDeclaration) ([]standardActionRow, error) {
	rows := make(map[string]standardActionRow, len(decls))
	for _, d := range decls {
		config, err := canonicalActionConfigString(string(d.Config))
		if err != nil {
			return nil, fmt.Errorf("action %s config: %w", d.Name, err)
		}
		key := standardActionBusinessKey(d.ObjectAPIName, d.Name)
		if _, ok := rows[key]; ok {
			return nil, fmt.Errorf("duplicate action business key %s.%s", d.ObjectAPIName, d.Name)
		}
		rows[key] = standardActionRow{
			ID:            StandardActionID(d.ObjectAPIName, d.Name),
			ObjectAPIName: d.ObjectAPIName,
			Name:          d.Name,
			Label:         d.Label,
			Type:          d.Type,
			Icon:          d.Icon,
			TargetObject:  d.TargetObject,
			Config:        config,
			OwnerPack:     packName,
		}
	}
	return sortedStandardActionRows(rows), nil
}
