package standardaction

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"sort"
	"strings"

	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	seedrows "github.com/pingcap-inc/nexuscrm-contract/pkg/seedrows"
)

// CatalogInvocation is the immutable, app-blind execution projection of one
// action authored inside a Catalog component. It deliberately carries no
// repository handle: Catalog remains the only metadata authority for this
// path, while Service continues to own the generic input/default/guard rules.
type CatalogInvocation struct {
	action         Action
	schema         []seedrows.ActionSchemaRow
	fieldDefaults  []seedrows.ActionFieldDefaultRow
	guards         []seedrows.ActionInputGuardRow
	internalScopes []constants.StandardActionInternalAccessScope
}

// PreparedCatalogInvocation is an immutable, request-scoped execution plan.
// It contains only Catalog-authored defaults and internal scopes already
// authorized and input-validated for one invocation. Bind rebases those facts
// onto the transaction context; mutable permission state is still fenced by
// the Runtime transaction owner before commit.
type PreparedCatalogInvocation struct {
	actionID, flowDeveloperName, actionRef string
	canonicalInputs                        []byte
	fieldDefaults                          []seedrows.ActionFieldDefaultRow
	internalScopes                         []constants.StandardActionInternalAccessScope
}

// Inputs returns a detached copy of the fully defaulted and validated input
// document prepared before the transaction. The transaction must not decode
// the original caller payload again or input-scoped Catalog defaults disappear.
func (prepared *PreparedCatalogInvocation) Inputs() (map[string]interface{}, error) {
	if prepared == nil || len(prepared.canonicalInputs) == 0 {
		return nil, fmt.Errorf("prepared Catalog action inputs are unavailable")
	}
	inputs := map[string]interface{}{}
	if err := json.Unmarshal(prepared.canonicalInputs, &inputs); err != nil {
		return nil, fmt.Errorf("decode prepared Catalog action inputs: %w", err)
	}
	return inputs, nil
}

func (prepared *PreparedCatalogInvocation) Bind(ctx context.Context) context.Context {
	if prepared == nil || ctx == nil {
		return ctx
	}
	execCtx := ctx
	if len(prepared.fieldDefaults) > 0 {
		execCtx = automation.WithActionFieldDefaults(
			execCtx, append([]seedrows.ActionFieldDefaultRow(nil), prepared.fieldDefaults...),
		)
	}
	execCtx = constants.WithStandardActionExecutionRootFlow(execCtx, prepared.actionID, prepared.flowDeveloperName)
	execCtx = constants.WithStandardActionExecutionRef(execCtx, prepared.actionRef)
	if len(prepared.internalScopes) > 0 {
		execCtx = constants.WithStandardActionInternalAccessScopes(
			execCtx, prepared.actionID, append([]constants.StandardActionInternalAccessScope(nil), prepared.internalScopes...),
		)
	}
	return execCtx
}

// CatalogActionNotFoundError distinguishes an absent caller-selected action
// from malformed immutable component authority. Serving adapters map absence
// to their neutral missing classification without exposing component content.
type CatalogActionNotFoundError struct {
	ActionRef string
}

func (err *CatalogActionNotFoundError) Error() string {
	if err == nil {
		return "Catalog action is not present"
	}
	return fmt.Sprintf("Catalog action %q is not present", err.ActionRef)
}

// ResolveCatalogInvocation selects exactly one action from one already
// authenticated immutable Catalog component. componentIdentity must be the
// component's immutable identity, not a caller-made business key.
func ResolveCatalogInvocation(
	componentIdentity string,
	canonicalBehavior []byte,
	actionRef string,
) (*CatalogInvocation, error) {
	if componentIdentity == "" || componentIdentity != strings.TrimSpace(componentIdentity) ||
		actionRef == "" || actionRef != strings.TrimSpace(actionRef) ||
		len(canonicalBehavior) == 0 {
		return nil, fmt.Errorf("Catalog action component identity, action reference, and behavior are required")
	}

	var definitions []seedrows.StandardActionDef
	decoder := json.NewDecoder(bytes.NewReader(canonicalBehavior))
	if err := decoder.Decode(&definitions); err != nil {
		return nil, fmt.Errorf("decode Catalog action component: %w", err)
	}
	if definitions == nil {
		return nil, fmt.Errorf("Catalog action component must be an array")
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil, fmt.Errorf("Catalog action component contains trailing JSON")
	}

	seen := make(map[string]struct{}, len(definitions))
	selected := -1
	for index := range definitions {
		definition := &definitions[index]
		if err := validateCatalogActionDefinition(*definition); err != nil {
			return nil, fmt.Errorf("invalid Catalog action component: %w", err)
		}
		key := strings.ToLower(definition.Name)
		if _, duplicate := seen[key]; duplicate {
			return nil, fmt.Errorf("Catalog action component contains duplicate action %q", definition.Name)
		}
		seen[key] = struct{}{}
		if definition.Name == actionRef {
			if selected >= 0 {
				return nil, fmt.Errorf("Catalog action component contains duplicate action reference %q", actionRef)
			}
			selected = index
		}
	}
	if selected < 0 {
		return nil, &CatalogActionNotFoundError{ActionRef: actionRef}
	}
	return catalogInvocationFromDefinition(componentIdentity, definitions[selected]), nil
}

func (invocation *CatalogInvocation) ActionRef() string {
	if invocation == nil {
		return ""
	}
	return invocation.action.Name
}

func (invocation *CatalogInvocation) DefinitionRef() string {
	if invocation == nil {
		return ""
	}
	return invocation.action.FlowDeveloperName
}

func (invocation *CatalogInvocation) PreviewDefinitionRef() string {
	if invocation == nil {
		return ""
	}
	return invocation.action.PreviewFlowDeveloperName
}

func (invocation *CatalogInvocation) Atomic() bool {
	return invocation != nil && invocation.action.IsAtomic
}

// Prepare authorizes and validates one invocation using the existing generic
// standard-action engine, then stamps only the selected Catalog action's
// defaults and internal scopes onto the supplied execution context. Callers
// should pass the transaction context that will execute the Flow.
func (invocation *CatalogInvocation) Prepare(
	ctx context.Context,
	permissions PermissionChecker,
	metadata MetadataProvider,
	inputs map[string]interface{},
	user *models.UserSession,
) (context.Context, *RequestOutcome, *InputReject, error) {
	prepared, outcome, reject, err := invocation.PreparePlan(ctx, permissions, metadata, inputs, user)
	if err != nil || outcome != nil || reject != nil {
		return ctx, outcome, reject, err
	}
	return prepared.Bind(ctx), nil, nil, nil
}

// PreparePlan performs the same authority and input checks as Prepare but
// returns a transaction-rebasable immutable plan rather than a context tied to
// the preflight call.
func (invocation *CatalogInvocation) PreparePlan(
	ctx context.Context,
	permissions PermissionChecker,
	metadata MetadataProvider,
	inputs map[string]interface{},
	user *models.UserSession,
) (*PreparedCatalogInvocation, *RequestOutcome, *InputReject, error) {
	if invocation == nil || ctx == nil || permissions == nil || user == nil || inputs == nil {
		return nil, nil, nil, fmt.Errorf("Catalog action invocation, context, permissions, user, and inputs are required")
	}
	action := invocation.action
	service := NewService(nil, permissions).WithRuntime(RuntimeDeps{Metadata: metadata})
	if outcome := service.AuthorizeAction(ctx, &action, user); outcome != nil {
		return nil, outcome, nil, nil
	}
	// A Catalog component is closed authority. Unlike a repository-backed
	// legacy action, an explicitly empty input schema means that the action
	// accepts no caller-controlled inputs; it must not become an open schema.
	if len(invocation.schema) == 0 && len(inputs) != 0 {
		return nil, nil, &InputReject{
			Code:           RejectInvalidField,
			Message:        "Catalog action does not declare caller inputs",
			diagnosticKind: diagnosticCatalogInputSchemaEmpty,
		}, nil
	}

	execCtx := ctx
	if defaults := recordScopedActionDefaults(invocation.fieldDefaults); len(defaults) > 0 {
		execCtx = automation.WithActionFieldDefaults(execCtx, defaults)
	}
	execCtx = constants.WithStandardActionExecutionRootFlow(
		execCtx, action.ID, action.FlowDeveloperName,
	)
	execCtx = constants.WithStandardActionExecutionRef(execCtx, action.Name)
	if len(invocation.internalScopes) > 0 {
		execCtx = constants.WithStandardActionInternalAccessScopes(
			execCtx, action.ID, invocation.internalScopes,
		)
	}
	prepared := PreparedInvocation{
		Action: &action, Context: execCtx,
		Schema: append([]seedrows.ActionSchemaRow(nil), invocation.schema...),
		FieldDefaults: append(
			[]seedrows.ActionFieldDefaultRow(nil), invocation.fieldDefaults...,
		),
		Guards: append([]seedrows.ActionInputGuardRow(nil), invocation.guards...),
	}
	if reject := service.ValidateFullInput(
		execCtx, prepared, inputs, user, metadata, service.formulaEngine(),
	); reject != nil {
		return nil, nil, reject, nil
	}
	canonicalInputs, err := json.Marshal(inputs)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("encode prepared Catalog action inputs: %w", err)
	}
	return &PreparedCatalogInvocation{
		actionID: action.ID, flowDeveloperName: action.FlowDeveloperName, actionRef: action.Name,
		canonicalInputs: append([]byte(nil), canonicalInputs...),
		fieldDefaults:   append([]seedrows.ActionFieldDefaultRow(nil), recordScopedActionDefaults(invocation.fieldDefaults)...),
		internalScopes:  append([]constants.StandardActionInternalAccessScope(nil), invocation.internalScopes...),
	}, nil, nil, nil
}

func catalogInvocationFromDefinition(
	componentIdentity string,
	definition seedrows.StandardActionDef,
) *CatalogInvocation {
	sum := sha256.Sum256([]byte(componentIdentity + "\x00" + definition.Name))
	actionID := "catalog-action-sha256-v1:" + hex.EncodeToString(sum[:])
	invocation := &CatalogInvocation{action: Action{
		ID: actionID, Name: definition.Name,
		FlowDeveloperName: definition.FlowDeveloperName,
		RequiredPermissions: append(
			[]string(nil), definition.RequiredPermissions...,
		),
		IsAtomic: definition.IsAtomic, IsActive: true,
	}}
	if definition.PreviewFlowDeveloperName != nil {
		invocation.action.PreviewFlowDeveloperName = *definition.PreviewFlowDeveloperName
	}
	for _, row := range definition.Inputs {
		invocation.schema = append(invocation.schema, catalogActionSchemaRow("input", row))
	}
	for _, row := range definition.FieldDefaults {
		invocation.fieldDefaults = append(invocation.fieldDefaults, seedrows.ActionFieldDefaultRow{
			TargetObject: row.TargetObject, FieldAPIName: row.FieldAPIName,
			ValueFormula: row.ValueFormula, Condition: row.Condition, Priority: row.Priority,
		})
	}
	for _, row := range definition.InputGuards {
		invocation.guards = append(invocation.guards, seedrows.ActionInputGuardRow{
			Condition: row.Condition, RequiredPermission: row.RequiredPermission,
			RequiredObject: row.RequiredObject, RequiredOperation: row.RequiredOperation,
			ErrorMessage: row.ErrorMessage, GatedInput: row.GatedInput, SortOrder: row.SortOrder,
		})
	}
	for _, row := range definition.InternalAccessScopes {
		invocation.internalScopes = append(
			invocation.internalScopes,
			constants.StandardActionInternalAccessScope{
				StandardActionID: actionID, ObjectAPIName: row.ObjectAPIName,
				Operation: row.Operation,
			},
		)
	}
	sort.SliceStable(invocation.schema, func(i, j int) bool {
		if invocation.schema[i].Direction != invocation.schema[j].Direction {
			return invocation.schema[i].Direction < invocation.schema[j].Direction
		}
		if invocation.schema[i].SortOrder != invocation.schema[j].SortOrder {
			return invocation.schema[i].SortOrder < invocation.schema[j].SortOrder
		}
		return invocation.schema[i].APIName < invocation.schema[j].APIName
	})
	sort.SliceStable(invocation.fieldDefaults, func(i, j int) bool {
		left, right := invocation.fieldDefaults[i], invocation.fieldDefaults[j]
		if left.TargetObject != right.TargetObject {
			return left.TargetObject < right.TargetObject
		}
		if left.FieldAPIName != right.FieldAPIName {
			return left.FieldAPIName < right.FieldAPIName
		}
		return left.Priority > right.Priority
	})
	sort.SliceStable(invocation.guards, func(i, j int) bool {
		return invocation.guards[i].SortOrder < invocation.guards[j].SortOrder
	})
	return invocation
}

func catalogActionSchemaRow(
	direction string,
	row seedrows.StandardActionSchemaDef,
) seedrows.ActionSchemaRow {
	return seedrows.ActionSchemaRow{
		Direction: direction, APIName: row.APIName, DataType: row.DataType,
		ReferenceTo: row.ReferenceTo, Label: row.Label, UISection: row.UISection,
		UIMode: row.UIMode, TargetObject: row.TargetObject,
		TargetFieldAPIName:        row.TargetFieldAPIName,
		PrefillSourceFieldAPIName: row.PrefillSourceFieldAPIName,
		RequiredPicklistTag:       row.RequiredPicklistTag,
		MappingSourceObject:       row.MappingSourceObject,
		MappingActionType:         row.MappingActionType,
		IsCollection:              row.IsCollection,
		IsRequired:                row.IsRequired, SortOrder: row.SortOrder,
	}
}

func validateCatalogActionDefinition(definition seedrows.StandardActionDef) error {
	if definition.Name == "" || definition.Name != strings.TrimSpace(definition.Name) ||
		definition.FlowDeveloperName == "" ||
		definition.FlowDeveloperName != strings.TrimSpace(definition.FlowDeveloperName) ||
		len(definition.RequiredPermissions) == 0 {
		return fmt.Errorf("action identity, Flow binding, and required permissions must be canonical")
	}
	if definition.PreviewFlowDeveloperName != nil &&
		(*definition.PreviewFlowDeveloperName == "" ||
			*definition.PreviewFlowDeveloperName != strings.TrimSpace(*definition.PreviewFlowDeveloperName)) {
		return fmt.Errorf("action %q contains an invalid preview Flow binding", definition.Name)
	}
	permissions := make(map[string]struct{}, len(definition.RequiredPermissions))
	for _, permission := range definition.RequiredPermissions {
		if permission == "" || permission != strings.TrimSpace(permission) {
			return fmt.Errorf("action %q contains an invalid required permission", definition.Name)
		}
		key := strings.ToLower(permission)
		if _, duplicate := permissions[key]; duplicate {
			return fmt.Errorf("action %q contains duplicate required permission %q", definition.Name, permission)
		}
		permissions[key] = struct{}{}
	}
	fields := make(map[string]struct{}, len(definition.Inputs)+len(definition.Outputs))
	for _, group := range []struct {
		direction string
		rows      []seedrows.StandardActionSchemaDef
	}{
		{direction: "input", rows: definition.Inputs},
		{direction: "output", rows: definition.Outputs},
	} {
		direction, rows := group.direction, group.rows
		for _, row := range rows {
			if row.APIName == "" || row.APIName != strings.TrimSpace(row.APIName) ||
				row.DataType == "" || row.DataType != strings.TrimSpace(row.DataType) {
				return fmt.Errorf("action %q contains invalid %s schema", definition.Name, direction)
			}
			key := direction + "\x00" + strings.ToLower(row.APIName)
			if _, duplicate := fields[key]; duplicate {
				return fmt.Errorf("action %q contains duplicate %s field %q", definition.Name, direction, row.APIName)
			}
			fields[key] = struct{}{}
		}
	}
	for _, row := range definition.FieldDefaults {
		if row.TargetObject == "" || row.TargetObject != strings.TrimSpace(row.TargetObject) ||
			row.FieldAPIName == "" || row.FieldAPIName != strings.TrimSpace(row.FieldAPIName) ||
			row.ValueFormula == "" || row.ValueFormula != strings.TrimSpace(row.ValueFormula) {
			return fmt.Errorf("action %q contains an invalid field default", definition.Name)
		}
	}
	for _, guard := range definition.InputGuards {
		hasPermission := strings.TrimSpace(guard.RequiredPermission) != ""
		hasObject := strings.TrimSpace(guard.RequiredObject) != "" ||
			strings.TrimSpace(guard.RequiredOperation) != ""
		if guard.Condition == "" || guard.Condition != strings.TrimSpace(guard.Condition) ||
			hasPermission == hasObject {
			return fmt.Errorf("action %q contains an invalid input guard", definition.Name)
		}
		if hasObject && (guard.RequiredObject == "" ||
			guard.RequiredObject != strings.TrimSpace(guard.RequiredObject) ||
			!catalogActionOperationSupported(guard.RequiredOperation)) {
			return fmt.Errorf("action %q contains an invalid object guard", definition.Name)
		}
	}
	for _, scope := range definition.InternalAccessScopes {
		if scope.ObjectAPIName == "" ||
			scope.ObjectAPIName != strings.TrimSpace(scope.ObjectAPIName) ||
			!catalogActionOperationSupported(scope.Operation) {
			return fmt.Errorf("action %q contains an invalid internal access scope", definition.Name)
		}
	}
	return nil
}

func catalogActionOperationSupported(operation string) bool {
	if operation != strings.TrimSpace(operation) {
		return false
	}
	switch operation {
	case constants.PermRead, constants.PermCreate, constants.PermEdit, constants.PermDelete:
		return true
	default:
		return false
	}
}
