package services

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

	"github.com/nexuscrm/automation/triggerprojection"
	"github.com/nexuscrm/persistence"
	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
)

func (sm *ServiceManager) FlowInterviewsContract() contract.FlowInterviews {
	if sm == nil {
		return nil
	}
	if sm.NeutralFlowInterviews != nil {
		return sm.NeutralFlowInterviews
	}
	if sm.FlowInterviewSvc == nil {
		return nil
	}
	return sm
}

func (sm *ServiceManager) FlowAuthoringContract() contract.FlowAuthoring {
	if sm == nil {
		return nil
	}
	if sm.NeutralFlowAuthoring != nil {
		return sm.NeutralFlowAuthoring
	}
	if sm.Metadata == nil {
		return nil
	}
	return sm
}

func (sm *ServiceManager) ListFlowInterviews(ctx context.Context, user *models.UserSession) ([]contract.FlowInterviewRecord, error) {
	if sm == nil || sm.FlowInterviewSvc == nil {
		return nil, appErrors.NewInternalError("flow interview service unavailable", nil)
	}
	interviews, err := sm.FlowInterviewSvc.ListInterviews(ctx, user)
	if err != nil {
		return nil, err
	}
	out := make([]contract.FlowInterviewRecord, 0, len(interviews))
	for _, interview := range interviews {
		out = append(out, flowInterviewRecordFromModel(&interview))
	}
	return out, nil
}

func (sm *ServiceManager) GetFlowInterview(ctx context.Context, interviewID string, user *models.UserSession) (*contract.FlowInterviewRecord, error) {
	if sm == nil || sm.FlowInterviewSvc == nil {
		return nil, appErrors.NewInternalError("flow interview service unavailable", nil)
	}
	interview, err := sm.FlowInterviewSvc.GetInterview(ctx, interviewID, user)
	if err != nil {
		return nil, err
	}
	if interview == nil {
		return nil, nil
	}
	out := flowInterviewRecordFromModel(interview)
	return &out, nil
}

func (sm *ServiceManager) DeleteFlowInterview(ctx context.Context, interviewID string, user *models.UserSession) error {
	if sm == nil || sm.FlowInterviewSvc == nil {
		return appErrors.NewInternalError("flow interview service unavailable", nil)
	}
	return sm.FlowInterviewSvc.DeleteInterview(ctx, interviewID, user)
}

func (sm *ServiceManager) ListFlowDefinitions(ctx context.Context) ([]contract.FlowDefinitionRecord, error) {
	if sm == nil || sm.Metadata == nil {
		return nil, appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	defs := sm.Metadata.GetFlowDefinitions(ctx)
	versionIDs := make([]string, 0, len(defs))
	for _, def := range defs {
		if def != nil && strings.TrimSpace(def.ActiveVersionId) != "" {
			versionIDs = append(versionIDs, def.ActiveVersionId)
		}
	}
	versions, err := sm.Metadata.GetFlowVersionsByIDs(ctx, versionIDs)
	if err != nil {
		slog.WarnContext(ctx, "failed to load FlowVersions for TriggerOrder projection", "defIDs", versionIDs, "error", err)
	}
	out := make([]contract.FlowDefinitionRecord, 0, len(defs))
	for _, def := range defs {
		out = append(out, flowDefinitionRecordFromModel(def, versions))
	}
	return out, nil
}

func (sm *ServiceManager) GetFlowDefinition(ctx context.Context, id string) (*contract.FlowDefinitionRecord, error) {
	if sm == nil || sm.Metadata == nil {
		return nil, appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	def := sm.Metadata.GetFlowDefinitionById(ctx, id)
	if def == nil {
		return nil, nil
	}
	var versions map[string]*models.FlowVersion
	if strings.TrimSpace(def.ActiveVersionId) != "" {
		var err error
		versions, err = sm.Metadata.GetFlowVersionsByIDs(ctx, []string{def.ActiveVersionId})
		if err != nil {
			slog.WarnContext(ctx, "failed to load FlowVersion for TriggerOrder projection", "defID", id, "error", err)
		}
	}
	out := flowDefinitionRecordFromModel(def, versions)
	return &out, nil
}

func (sm *ServiceManager) CreateFlowDefinition(ctx context.Context, def contract.FlowDefinitionWrite) (*contract.FlowDefinitionCreateResult, error) {
	if sm == nil || sm.Metadata == nil {
		return nil, appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	model := flowDefinitionModelFromWrite(def)
	if err := sm.Metadata.CreateFlowDefinition(ctx, &model); err != nil {
		return nil, flowAuthoringError(err)
	}
	return &contract.FlowDefinitionCreateResult{ID: model.Id}, nil
}

func (sm *ServiceManager) PatchFlowDefinition(ctx context.Context, id string, patch contract.FlowAdminDefinitionPatchIntent) error {
	if sm == nil || sm.Metadata == nil {
		return appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	return flowAuthoringError(sm.Metadata.ApplyFlowDefinitionPatch(ctx, id, patch))
}

func (sm *ServiceManager) DeleteFlowDefinition(ctx context.Context, id string) error {
	if sm == nil || sm.Metadata == nil {
		return appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	return flowAuthoringError(sm.Metadata.DeleteFlowDefinition(ctx, id))
}

func (sm *ServiceManager) ListFlowVersions(ctx context.Context, definitionID string) ([]contract.FlowVersionRecord, error) {
	if sm == nil || sm.Metadata == nil {
		return nil, appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	versions, err := sm.Metadata.GetFlowVersions(ctx, definitionID)
	if err != nil {
		return nil, flowAuthoringError(err)
	}
	out := make([]contract.FlowVersionRecord, 0, len(versions))
	for _, version := range versions {
		out = append(out, flowVersionRecordFromModel(version))
	}
	return out, nil
}

func (sm *ServiceManager) GetFlowVersion(ctx context.Context, id string) (*contract.FlowVersionRecord, error) {
	if sm == nil || sm.Metadata == nil {
		return nil, appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	version := sm.Metadata.GetFlowVersionById(ctx, id)
	if version == nil {
		return nil, nil
	}
	out := flowVersionRecordFromModel(version)
	return &out, nil
}

func (sm *ServiceManager) CreateFlowVersion(ctx context.Context, version contract.FlowVersionWrite) (*contract.FlowVersionCreateResult, error) {
	if sm == nil || sm.Metadata == nil {
		return nil, appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	req := flowVersionModelFromWrite(version)
	if err := hydrateFlowVersionMetadataRaw(&req); err != nil {
		return nil, appErrors.NewInvalidFieldError(err.Error())
	}
	if err := sm.Metadata.ValidateFlowVersionAuthoring(ctx, &req); err != nil {
		return nil, flowAuthoringError(err)
	}

	var createdDevName string
	var createdDupName bool
	err := sm.Metadata.WithToolingWriteTx(ctx, func(txCtx context.Context) error {
		if strings.TrimSpace(req.DefinitionId) == "" {
			label := "New Flow"
			developerName := "New_Flow"
			processType := req.ProcessType
			if strings.TrimSpace(processType) == "" {
				return appErrors.NewInvalidFieldError("FlowVersion ProcessType is required when creating a FlowDefinition")
			}
			if req.Metadata.Label != "" {
				label = req.Metadata.Label
				developerName = sanitizeFlowDeveloperName(label)
			}
			developerName = fmt.Sprintf("%s_%d", developerName, time.Now().UnixMilli())
			createdDevName = developerName

			flowDef := &models.FlowDefinition{
				MasterLabel:   label,
				DeveloperName: developerName,
				ProcessType:   processType,
				Description:   req.Metadata.Description,
			}
			projection, err := triggerprojection.Resolve(
				flowDef,
				&models.FlowVersion{ProcessType: processType, Metadata: req.Metadata, MetadataRaw: req.MetadataRaw},
			)
			if err != nil {
				return fmt.Errorf("resolve trigger projection: %w", err)
			}
			if projection.Kind != triggerprojection.KindUnknown {
				flowDef.TriggerObjectOrEvent = projection.ObjectOrEvent
				flowDef.TriggerType = projection.TriggerType
			}
			if err := sm.Metadata.CreateFlowDefinitionForLifecycle(txCtx, flowDef); err != nil {
				if persistence.IsDuplicateKeyError(err) {
					createdDupName = true
				}
				return err
			}
			req.DefinitionId = flowDef.Id
		}
		return sm.Metadata.CreateFlowVersion(txCtx, &req)
	})
	if err != nil {
		if errors.Is(err, triggerprojection.ErrMalformed) {
			return nil, appErrors.NewInvalidFieldError(err.Error())
		}
		if createdDupName {
			return nil, &contract.FlowAuthoringError{
				Kind:          contract.FlowAuthoringErrorDuplicateAutoCreatedDeveloper,
				DeveloperName: createdDevName,
				Cause:         err,
			}
		}
		return nil, flowAuthoringError(err)
	}

	return &contract.FlowVersionCreateResult{ID: req.Id, VersionNumber: req.VersionNumber}, nil
}

func (sm *ServiceManager) PatchFlowVersion(ctx context.Context, id string, patch contract.FlowAdminVersionPatchIntent) error {
	if sm == nil || sm.Metadata == nil {
		return appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	return flowAuthoringError(sm.Metadata.ApplyFlowVersionPatch(ctx, id, patch))
}

func (sm *ServiceManager) DeleteFlowVersion(ctx context.Context, id string) error {
	if sm == nil || sm.Metadata == nil {
		return appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	return flowAuthoringError(sm.Metadata.DeleteFlowVersion(ctx, id))
}

func (sm *ServiceManager) ActivateFlowVersion(ctx context.Context, id string) error {
	if sm == nil || sm.Metadata == nil {
		return appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	return flowAuthoringError(sm.Metadata.ActivateFlowVersion(ctx, id))
}

func (sm *ServiceManager) DeactivateFlowVersion(ctx context.Context, id string) error {
	if sm == nil || sm.Metadata == nil {
		return appErrors.NewInternalError("flow authoring service unavailable", nil)
	}
	return flowAuthoringError(sm.Metadata.DeactivateFlowVersion(ctx, id))
}

func flowAuthoringError(err error) error {
	if err == nil {
		return nil
	}
	if appErrors.IsInternal(err) {
		return err
	}
	if appErrors.IsNotFound(err) {
		return appErrors.NewNotFoundWireError(err.Error()).WithCause(err)
	}
	if flsErr, ok := appErrors.AsFLSDenied(err); ok {
		return &contract.FlowAuthoringError{
			Kind:    contract.FlowAuthoringErrorFieldDenied,
			Message: flsErr.Message,
			Fields:  append([]string(nil), flsErr.Fields...),
			Cause:   err,
		}
	}
	if persistence.IsDuplicateKeyError(err) {
		switch persistence.ExtractDuplicateKeyName(err.Error()) {
		case "idx_Flow_DefinitionId_VersionNumber":
			return &contract.FlowAuthoringError{Kind: contract.FlowAuthoringErrorDuplicateVersion, Cause: err}
		case "DeveloperName":
			return &contract.FlowAuthoringError{Kind: contract.FlowAuthoringErrorDuplicateDefinitionDeveloper, Cause: err}
		}
	}
	var lockErr *appErrors.LockContentionError
	if errors.As(err, &lockErr) || persistence.IsLockContention(err) {
		return &contract.FlowAuthoringError{Kind: contract.FlowAuthoringErrorLockContention, Cause: err}
	}
	return err
}

func flowInterviewRecordFromModel(interview *models.FlowInterview) contract.FlowInterviewRecord {
	if interview == nil {
		return contract.FlowInterviewRecord{}
	}
	return contract.FlowInterviewRecord{
		ID:              interview.Id,
		FlowRevisionID:  interview.FlowVersionId,
		InterviewLabel:  interview.InterviewLabel,
		InterviewStatus: interview.InterviewStatus,
		CurrentElement:  interview.CurrentElement,
		OwnerID:         interview.OwnerId,
		CreatedDate:     interview.CreatedDate,
		PausedDate:      interview.PausedDate,
		PausedUntil:     interview.PausedUntil,
	}
}

func flowDefinitionRecordFromModel(def *models.FlowDefinition, versions map[string]*models.FlowVersion) contract.FlowDefinitionRecord {
	if def == nil {
		return contract.FlowDefinitionRecord{}
	}
	var triggerOrder *int
	if def.ActiveVersionId != "" && len(versions) > 0 {
		if ver := versions[def.ActiveVersionId]; ver != nil {
			triggerOrder = triggerprojection.ResolveTriggerOrder(ver)
		}
	}
	return contract.FlowDefinitionRecord{
		ID:                   def.Id,
		DeveloperName:        def.DeveloperName,
		MasterLabel:          def.MasterLabel,
		Description:          def.Description,
		ProcessType:          def.ProcessType,
		TriggerType:          def.TriggerType,
		TriggerObjectOrEvent: def.TriggerObjectOrEvent,
		ActiveVersionID:      def.ActiveVersionId,
		LatestVersionID:      def.LatestVersionId,
		CreatedDate:          def.CreatedDate,
		LastModifiedDate:     def.LastModifiedDate,
		TriggerOrder:         triggerOrder,
	}
}

func flowDefinitionModelFromWrite(def contract.FlowDefinitionWrite) models.FlowDefinition {
	return models.FlowDefinition{
		Id:                   def.ID,
		DeveloperName:        def.DeveloperName,
		MasterLabel:          def.MasterLabel,
		Description:          def.Description,
		ProcessType:          def.ProcessType,
		TriggerType:          def.TriggerType,
		TriggerObjectOrEvent: def.TriggerObjectOrEvent,
		ActiveVersionId:      def.ActiveVersionID,
		LatestVersionId:      def.LatestVersionID,
	}
}

func flowVersionRecordFromModel(version *models.FlowVersion) contract.FlowVersionRecord {
	if version == nil {
		return contract.FlowVersionRecord{}
	}
	return contract.FlowVersionRecord{
		ID:               version.Id,
		DefinitionID:     version.DefinitionId,
		MasterLabel:      version.MasterLabel,
		VersionNumber:    version.VersionNumber,
		Status:           version.Status,
		Description:      version.Description,
		ProcessType:      version.ProcessType,
		MetadataRaw:      append([]byte(nil), version.MetadataRaw...),
		CreatedDate:      version.CreatedDate,
		LastModifiedDate: version.LastModifiedDate,
	}
}

func flowVersionModelFromWrite(version contract.FlowVersionWrite) models.FlowVersion {
	return models.FlowVersion{
		Id:           version.ID,
		DefinitionId: version.DefinitionID,
		MasterLabel:  version.MasterLabel,
		Status:       version.Status,
		Description:  version.Description,
		ProcessType:  version.ProcessType,
		MetadataRaw:  append([]byte(nil), version.MetadataRaw...),
	}
}

func hydrateFlowVersionMetadataRaw(version *models.FlowVersion) error {
	if version == nil || len(version.MetadataRaw) == 0 {
		return nil
	}
	if err := json.Unmarshal(version.MetadataRaw, &version.Metadata); err != nil {
		return fmt.Errorf("%w: parse flow metadata: %v", triggerprojection.ErrMalformed, err)
	}
	return nil
}

func sanitizeFlowDeveloperName(label string) string {
	result := ""
	for _, r := range label {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
			result += string(r)
		} else if r == ' ' || r == '-' {
			result += "_"
		}
	}
	if result == "" {
		result = "Flow"
	}
	return result
}
