package planner

import (
	"fmt"
	"reflect"

	"github.com/tidbcloud/dataflow-service-ng/app/models/domain"
)

const executionPlanVersion uint16 = 1

// SelectExecutionPlan converts user-visible choices into fixed internal
// engines and encodings. The returned plan is persisted and must be reused on
// retries and edits.
func SelectExecutionPlan(
	syncMode domain.DataPipelineSyncMode,
	destinationType domain.DataPipelineDestinationType,
	stagingType domain.DataPipelineStagingType,
) (domain.DataPipelineExecutionPlan, error) {
	if destinationType != domain.DataPipelineDestinationTypeTiDBLake {
		return domain.DataPipelineExecutionPlan{}, fmt.Errorf(
			"%w: destination.type is %q, and only %q is supported",
			ErrUnsupportedDestination, destinationType,
			domain.DataPipelineDestinationTypeTiDBLake)
	}
	if stagingType != domain.DataPipelineStagingTypeCustomerManagedS3 {
		return domain.DataPipelineExecutionPlan{}, fmt.Errorf(
			"%w: %s", ErrUnsupportedStaging, unsupportedStagingDetail(stagingType))
	}

	plan := domain.DataPipelineExecutionPlan{
		Version:         executionPlanVersion,
		DestinationType: destinationType,
		Snapshot: domain.DataPipelineSnapshotPlan{
			Engine: domain.DataPipelineSnapshotEngineDumpling,
			Format: domain.DataPipelineSnapshotFormatCSV,
		},
	}
	switch syncMode {
	case domain.DataPipelineSyncModeFull:
		return plan, nil
	case domain.DataPipelineSyncModeFullAndIncremental:
		plan.Incremental = &domain.DataPipelineIncrementalPlan{
			Engine: domain.DataPipelineIncrementalEngineTiCDC,
			Format: domain.DataPipelineIncrementalFormatCanalJSON,
		}
		return plan, nil
	default:
		return domain.DataPipelineExecutionPlan{}, fmt.Errorf(
			"%w: sync_mode is %q, and only %q or %q are supported",
			ErrUnsupportedSyncMode, syncMode,
			domain.DataPipelineSyncModeFull,
			domain.DataPipelineSyncModeFullAndIncremental)
	}
}

// unsupportedStagingDetail explains a rejected staging type in terms the caller
// can act on: the request field, the value that reached us, and what to send
// instead.
//
// The value is reported as the domain name rather than the enum number the
// caller sent, and those do not look alike - a Console sending
// DATA_PIPELINE_STAGING_TYPE_TIDB_MANAGED sees only "2" in its own payload,
// which is how a rejection naming "tidb_managed" ended up reading as a value
// nobody had sent. Naming the field and the accepted value closes that gap
// without the caller having to map the enum back by hand.
func unsupportedStagingDetail(stagingType domain.DataPipelineStagingType) string {
	// A reserved value is not a malformed one, and saying so is the difference
	// between "you sent garbage" and "this is not built yet".
	if stagingType == domain.DataPipelineStagingTypeTiDBManaged {
		return fmt.Sprintf(
			"staging.type %q names a managed staging area that does not exist yet; "+
				"send %q together with a staging.s3 block",
			stagingType, domain.DataPipelineStagingTypeCustomerManagedS3)
	}
	return fmt.Sprintf(
		"staging.type is %q, and only %q is supported",
		stagingType, domain.DataPipelineStagingTypeCustomerManagedS3)
}

func ValidateExecutionPlanImmutable(
	persisted, candidate domain.DataPipelineExecutionPlan,
) error {
	if !reflect.DeepEqual(persisted, candidate) {
		return ErrExecutionPlanImmutable
	}
	return nil
}
