package automation

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

const flowSourceStandard = "standard"

func (e *FlowExecutor) getActiveFlowVersion(ctx context.Context, apiName string, user *models.UserSession) (*models.FlowVersion, error) {
	// A prepared Runtime invocation has already selected the complete immutable
	// Flow closure for one Catalog revision.  Every metadata read performed by
	// preview discovery must stay inside that closure just like execution does;
	// never let an injected or repository-backed active-version resolver reopen
	// live authority under a prepared context.
	if version, found, prepared := preparedFlowVersion(ctx, apiName); prepared {
		if !found {
			return nil, fmt.Errorf("%w: %q", ErrPreparedFlowClosureMissing, apiName)
		}
		return version, nil
	}
	if e.flowVersionFn != nil {
		return e.flowVersionFn(ctx, apiName, user)
	}
	apiName = strings.TrimSpace(apiName)
	frc := flowRuntimeContextFromCtx(ctx)
	if cached, ok := frc.CachedActiveFlowVersion(apiName); ok {
		return cached, nil
	}
	lookupCtx := ctx
	if constants.StandardActionExecutionOwnsRootFlow(ctx, apiName) {
		lookupCtx = constants.WithSecurityContext(ctx, constants.SystemContext, "FlowExecutor: standard-action root Flow metadata resolution")
	}
	result, err := e.query.QueryWithSpec(lookupCtx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableFlowDefinition,
		Filter:        queryspec.FieldPredicate("DeveloperName", "=", apiName),
		Limit:         1,
	}, user)
	if err != nil {
		return nil, err
	}
	if result.TotalSize == 0 || len(result.Records) == 0 {
		return nil, nil
	}

	activeVersionId, _ := result.Records[0]["ActiveVersionId"].(string)
	if activeVersionId == "" {
		return nil, nil
	}
	definition := result.Records[0]
	definitionIsCustomized := flowRecordBool(definition["IsCustomized"])
	definitionOwnerPack := flowRecordString(definition["OwnerPack"])

	version, err := e.getFlowVersionById(lookupCtx, activeVersionId, user)
	if version != nil {
		version.DefinitionIsCustomized = definitionIsCustomized
		version.DefinitionOwnerPack = definitionOwnerPack
		version.DefinitionRuntimeLoaded = true
		if version.ProcessType == "" {
			version.ProcessType = flowRecordString(definition["ProcessType"])
		}
	}
	if err == nil && version != nil {
		frc.SetCachedActiveFlowVersion(apiName, version)
	}
	return version, err
}

func (e *FlowExecutor) getFlowVersionById(ctx context.Context, versionId string, user *models.UserSession) (*models.FlowVersion, error) {
	stageStart := time.Now()
	result, err := e.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableFlow,
		Filter:        queryspec.FieldPredicate(constants.FieldID, "=", versionId),
		Limit:         1,
	}, user)
	if err != nil {
		logFlowVersionResolveTiming(ctx, "", versionId, "", "query_flow", "error", stageStart, err)
		return nil, err
	}
	if result.TotalSize == 0 || len(result.Records) == 0 {
		logFlowVersionResolveTiming(ctx, "", versionId, "", "query_flow", "not_found", stageStart, nil)
		return nil, nil
	}
	logFlowVersionResolveTiming(ctx, "", versionId, "", "query_flow", "success", stageStart, nil)

	record := result.Records[0]
	version := &models.FlowVersion{
		Id: record[constants.FieldID].(string),
	}
	if status, ok := record["Status"].(string); ok {
		version.Status = status
	}
	if defId, ok := record["DefinitionId"].(string); ok {
		version.DefinitionId = defId
	}
	if source, ok := record["Source"].(string); ok {
		version.Source = source
	}
	if metaJSON, ok := record["Metadata"].(string); ok {
		version.MetadataJSON = metaJSON
	}
	stageStart = time.Now()
	version.HydrateMetadata()
	logFlowVersionResolveTiming(ctx, "", version.Id, version.DefinitionId, "hydrate_metadata", "success", stageStart, nil)
	return version, nil
}

func (e *FlowExecutor) ensureFlowVersionDefinitionRuntimeFields(ctx context.Context, version *models.FlowVersion, flowAPIName string, user *models.UserSession) (string, error) {
	flowAPIName = strings.TrimSpace(flowAPIName)
	if version == nil || version.DefinitionRuntimeLoaded {
		return flowAPIName, nil
	}
	if e.flowVersionFn != nil {
		// In-memory test seams own their FlowVersion provenance; do not issue an
		// unrelated FlowDefinition query against test doubles.
		return flowAPIName, nil
	}
	if e.query == nil || strings.TrimSpace(version.DefinitionId) == "" {
		// Direct unit tests and in-memory test seams may construct FlowVersion
		// values without a backing FlowDefinition row. Production paths either
		// preload the fields (ExecuteFlow/trigger/scheduler) or have e.query.
		return flowAPIName, nil
	}
	loadedAPIName, err := e.hydrateFlowVersionDefinitionRuntimeFields(ctx, version, user)
	if err != nil {
		return "", err
	}
	if flowAPIName == "" {
		flowAPIName = loadedAPIName
	}
	return flowAPIName, nil
}

func (e *FlowExecutor) hydrateFlowVersionDefinitionRuntimeFields(ctx context.Context, version *models.FlowVersion, user *models.UserSession) (string, error) {
	if version == nil {
		return "", fmt.Errorf("flow version runtime identity: missing FlowVersion")
	}
	if strings.TrimSpace(version.DefinitionId) == "" {
		return "", fmt.Errorf("flow version %q runtime identity: missing DefinitionId", version.Id)
	}
	if e.query == nil {
		return "", fmt.Errorf("flow version %q runtime identity: query service unavailable", version.Id)
	}
	stageStart := time.Now()
	result, err := e.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableFlowDefinition,
		Filter:        queryspec.FieldPredicate(constants.FieldID, "=", version.DefinitionId),
		Limit:         1,
		// ResumeInterview must preserve paused interviews whose FlowDefinition
		// was soft-deleted after the Wait. FlowVersion still carries executable
		// metadata; the parent row is needed only for stable runtime identity.
		ScanAll: true,
	}, user)
	if err != nil {
		logFlowVersionResolveTiming(ctx, flowAPINameFromVersion(version), version.Id, version.DefinitionId, "query_flow_definition", "error", stageStart, err)
		return "", err
	}
	if result == nil || result.TotalSize == 0 || len(result.Records) == 0 {
		err := fmt.Errorf("flow version %q runtime identity: FlowDefinition %q not found", version.Id, version.DefinitionId)
		logFlowVersionResolveTiming(ctx, flowAPINameFromVersion(version), version.Id, version.DefinitionId, "query_flow_definition", "not_found", stageStart, err)
		return "", err
	}
	logFlowVersionResolveTiming(ctx, flowAPINameFromVersion(version), version.Id, version.DefinitionId, "query_flow_definition", "success", stageStart, nil)
	definition := result.Records[0]
	stageStart = time.Now()
	version.DefinitionIsCustomized = flowRecordBool(definition["IsCustomized"])
	version.DefinitionOwnerPack = flowRecordString(definition["OwnerPack"])
	version.DefinitionRuntimeLoaded = true
	if version.ProcessType == "" {
		version.ProcessType = flowRecordString(definition["ProcessType"])
	}
	loadedName := flowRecordString(definition["DeveloperName"])
	logFlowVersionResolveTiming(ctx, loadedName, version.Id, version.DefinitionId, "apply_definition_fields", "success", stageStart, nil)
	return loadedName, nil
}

func flowAPINameFromVersion(version *models.FlowVersion) string {
	if version == nil {
		return ""
	}
	return strings.TrimSpace(version.Metadata.Label)
}

func (e *FlowExecutor) standardActionScopedChildContext(ctx context.Context, apiName string, user *models.UserSession) (context.Context, error) {
	hasInternalScopes := constants.HasStandardActionInternalAccessScopes(ctx)
	hasProtectedFlowMarker := constants.HasStandardActionProtectedFlowExecution(ctx)
	if !hasInternalScopes && !hasProtectedFlowMarker {
		return ctx, nil
	}
	packOwned, err := e.activeFlowIsPackOwned(ctx, apiName, user)
	if err != nil {
		return nil, err
	}
	if packOwned {
		return ctx, nil
	}
	if hasInternalScopes {
		ctx = constants.WithoutStandardActionInternalAccessScopes(ctx)
	}
	if hasProtectedFlowMarker {
		ctx = constants.WithoutStandardActionProtectedFlowExecution(ctx)
	}
	// Standard-action preview no-op semantics are only valid while executing
	// pack-owned standard flow metadata. Custom/admin child flows keep the
	// generic preview contract: recordUpdate and protected finalizers fail closed.
	ctx = withoutFlowPreviewOptions(ctx)
	return ctx, nil
}

func (e *FlowExecutor) activeFlowIsPackOwned(ctx context.Context, apiName string, user *models.UserSession) (bool, error) {
	if version, found, prepared := preparedFlowVersion(ctx, apiName); prepared {
		if !found {
			return false, fmt.Errorf("%w: %q", ErrPreparedFlowClosureMissing, apiName)
		}
		return flowVersionIsPackOwnedStandard(version), nil
	}
	if e.flowPackOwnedFn != nil {
		return e.flowPackOwnedFn(ctx, apiName, user)
	}
	if e.flowVersionFn != nil {
		version, err := e.flowVersionFn(ctx, apiName, user)
		if err != nil {
			return false, err
		}
		return flowVersionIsPackOwnedStandard(version), nil
	}
	if e.query == nil || strings.TrimSpace(apiName) == "" {
		return false, nil
	}

	defResult, err := e.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableFlowDefinition,
		Filter:        queryspec.FieldPredicate("DeveloperName", "=", apiName),
		Limit:         1,
	}, user)
	if err != nil {
		return false, err
	}
	if defResult == nil || defResult.TotalSize == 0 || len(defResult.Records) == 0 {
		return false, nil
	}
	definition := defResult.Records[0]
	if flowRecordBool(definition["IsCustomized"]) {
		return false, nil
	}
	activeVersionID, _ := definition["ActiveVersionId"].(string)
	if strings.TrimSpace(activeVersionID) == "" {
		return false, nil
	}

	flowResult, err := e.query.QueryWithSpec(ctx, queryspec.QuerySpec{
		ObjectAPIName: constants.TableFlow,
		Filter:        queryspec.FieldPredicate(constants.FieldID, "=", activeVersionID),
		Limit:         1,
	}, user)
	if err != nil {
		return false, err
	}
	if flowResult == nil || flowResult.TotalSize == 0 || len(flowResult.Records) == 0 {
		return false, nil
	}
	source, _ := flowResult.Records[0]["Source"].(string)
	return source == flowSourceStandard, nil
}

func flowVersionIsPackOwnedStandard(version *models.FlowVersion) bool {
	return version != nil &&
		!version.DefinitionIsCustomized &&
		strings.TrimSpace(version.Source) == flowSourceStandard
}

func flowIdentityIsPackOwnedStandard(identity FlowExecutionIdentity) bool {
	return !identity.DefinitionIsCustomized &&
		strings.TrimSpace(identity.VersionSource) == flowSourceStandard
}

func flowRecordBool(value interface{}) bool {
	switch v := value.(type) {
	case bool:
		return v
	case int:
		return v != 0
	case int64:
		return v != 0
	case float64:
		return v != 0
	case string:
		return strings.EqualFold(strings.TrimSpace(v), "true") || strings.TrimSpace(v) == "1"
	default:
		return false
	}
}

func flowRecordString(value interface{}) string {
	if s, ok := value.(string); ok {
		return strings.TrimSpace(s)
	}
	return ""
}
