package devops

import (
	"encoding/json"
	"fmt"
	"math"
	"slices"
	"sort"
	"strconv"
	"strings"
)

// DefaultStartPolicy is the start policy used when a caller does not pick one.
//
// It is deliberately `immediate` rather than the operation's preferred policy:
// this CLI exists for unattended handling, and `manual` would leave an approved
// instance waiting for a second, human-only gate in Ops Portal. Approval itself
// is still enforced by the server before anything runs.
const DefaultStartPolicy = StartPolicyImmediate

// BuildParams validates caller-supplied params against an operation definition
// and converts them into the string map the API expects.
//
// Const params are injected from the definition and rejected as caller input;
// unknown params, missing required params and enum violations are errors, so a
// malformed call fails here rather than halfway through an approved operation.
func BuildParams(op *Operation, in map[string]any) (map[string]string, error) {
	out := make(map[string]string, len(op.Params))

	accepted := acceptedParamNames(op)
	for name, raw := range in {
		if raw == nil {
			continue
		}
		def, ok := op.Params[name]
		if !ok {
			return nil, fmt.Errorf("unknown param %q for operation %s, accepted params: %s",
				name, op.Name, strings.Join(accepted, ", "))
		}
		if def.Const != nil {
			return nil, fmt.Errorf("param %q of operation %s is fixed by the server and must not be set",
				name, op.Name)
		}

		value, err := paramValueToString(name, def, raw)
		if err != nil {
			return nil, err
		}
		if err := validateParamValue(name, def, value); err != nil {
			return nil, err
		}
		out[name] = value
	}

	missing := make([]string, 0)
	for name, def := range op.Params {
		if def.Const != nil {
			out[name] = *def.Const
			continue
		}
		if _, ok := out[name]; ok {
			continue
		}
		if def.Default != nil {
			out[name] = *def.Default
			continue
		}
		if !def.Optional {
			missing = append(missing, name)
		}
	}
	if len(missing) > 0 {
		sort.Strings(missing)
		return nil, fmt.Errorf("missing required params for operation %s: %s",
			op.Name, strings.Join(missing, ", "))
	}

	return out, nil
}

// acceptedParamNames lists the params a caller may set, sorted.
func acceptedParamNames(op *Operation) []string {
	names := make([]string, 0, len(op.Params))
	for name, def := range op.Params {
		if def.Const != nil {
			continue
		}
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

func paramValueToString(name string, def ParamDefine, raw any) (string, error) {
	switch def.Type {
	case ParamTypeInt:
		switch v := raw.(type) {
		case string:
			if _, err := strconv.ParseInt(v, 10, 64); err != nil {
				return "", fmt.Errorf("param %q must be an integer, got %q", name, v)
			}
			return v, nil
		case json.Number:
			return v.String(), nil
		case float64:
			if v != math.Trunc(v) {
				return "", fmt.Errorf("param %q must be an integer, got %v", name, v)
			}
			return strconv.FormatInt(int64(v), 10), nil
		case int:
			return strconv.Itoa(v), nil
		case int64:
			return strconv.FormatInt(v, 10), nil
		default:
			return "", fmt.Errorf("param %q must be an integer, got %T", name, raw)
		}

	case ParamTypeMap:
		switch v := raw.(type) {
		case string:
			var probe map[string]string
			if err := json.Unmarshal([]byte(v), &probe); err != nil {
				return "", fmt.Errorf("param %q must be a JSON object of string values: %w", name, err)
			}
			return v, nil
		case map[string]any:
			flat := make(map[string]string, len(v))
			for k, item := range v {
				s, ok := item.(string)
				if !ok {
					return "", fmt.Errorf("param %q must be a JSON object of string values, key %q is %T", name, k, item)
				}
				flat[k] = s
			}
			encoded, err := json.Marshal(flat)
			if err != nil {
				return "", fmt.Errorf("param %q: %w", name, err)
			}
			return string(encoded), nil
		default:
			return "", fmt.Errorf("param %q must be a JSON object of string values, got %T", name, raw)
		}

	default:
		switch v := raw.(type) {
		case string:
			return v, nil
		case bool:
			return strconv.FormatBool(v), nil
		case json.Number:
			return v.String(), nil
		case float64:
			if v == math.Trunc(v) {
				return strconv.FormatInt(int64(v), 10), nil
			}
			return strconv.FormatFloat(v, 'f', -1, 64), nil
		default:
			return "", fmt.Errorf("param %q must be a string, got %T", name, raw)
		}
	}
}

func validateParamValue(name string, def ParamDefine, value string) error {
	if len(def.Enum) > 0 && !slices.Contains(def.Enum, value) {
		return fmt.Errorf("param %q must be one of [%s], got %q", name, strings.Join(def.Enum, ", "), value)
	}
	if len(def.MultiEnum) > 0 && value != "" {
		for _, item := range strings.Split(value, ",") {
			item = strings.TrimSpace(item)
			if !slices.Contains(def.MultiEnum, item) {
				return fmt.Errorf("param %q must be a comma-separated subset of [%s], got %q",
					name, strings.Join(def.MultiEnum, ", "), item)
			}
		}
	}
	return nil
}

// BuildActions builds the action list for a create/preview request from the
// operation definition, applying the same start policy to every action.
func BuildActions(op *Operation, policy ActionStartPolicy, scheduleAt *int64) ([]CreateInstanceAction, error) {
	if policy == "" {
		policy = DefaultStartPolicy
	}
	if err := validateStartPolicy(policy, scheduleAt); err != nil {
		return nil, err
	}

	required := op.RequiredActions()
	if len(required) == 0 {
		return nil, fmt.Errorf("operation %s defines no actions", op.Name)
	}

	actions := make([]CreateInstanceAction, 0, len(required))
	for _, a := range required {
		actions = append(actions, CreateInstanceAction{
			Name:            a.Name,
			StartPolicy:     policy,
			StartScheduleAt: scheduleAt,
		})
	}
	return actions, nil
}

// ResolveActions validates caller-supplied action overrides against the
// operation definition, keeping the definition's action order.
func ResolveActions(op *Operation, overrides []CreateInstanceAction) ([]CreateInstanceAction, error) {
	known := make(map[string]bool, len(op.Actions))
	for _, a := range op.Actions {
		known[a.Name] = true
	}

	byName := make(map[string]CreateInstanceAction, len(overrides))
	for _, override := range overrides {
		if !known[override.Name] {
			return nil, fmt.Errorf("unknown action %q for operation %s", override.Name, op.Name)
		}
		policy := override.StartPolicy
		if policy == "" {
			policy = DefaultStartPolicy
		}
		if err := validateStartPolicy(policy, override.StartScheduleAt); err != nil {
			return nil, err
		}
		override.StartPolicy = policy
		byName[override.Name] = override
	}

	actions := make([]CreateInstanceAction, 0, len(overrides))
	for _, a := range op.Actions {
		if resolved, ok := byName[a.Name]; ok {
			actions = append(actions, resolved)
		}
	}
	if len(actions) == 0 {
		return nil, fmt.Errorf("no actions selected for operation %s", op.Name)
	}
	return actions, nil
}

func validateStartPolicy(policy ActionStartPolicy, scheduleAt *int64) error {
	switch policy {
	case StartPolicyManual, StartPolicyImmediate:
		return nil
	case StartPolicySchedule:
		if scheduleAt == nil {
			return fmt.Errorf("start_schedule_at is required when start_policy is %q", StartPolicySchedule)
		}
		return nil
	default:
		return fmt.Errorf("unknown start_policy %q, valid values: %s, %s, %s",
			policy, StartPolicyManual, StartPolicyImmediate, StartPolicySchedule)
	}
}
