package schema

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"git.cloudinit.dev/coreci/orca/internal/jobspec"
)

// UpdateValidator validates the rolling/canary/blue-green update stanza
// (P03). The schema validator (ServiceValidator) already enforces that
// the strategy is one of rolling/canary/blue-green and that the update
// block is present for a Service. UpdateValidator adds the
// field-level validation:
//
//   - max_parallel: integer in [1, count] (defaults to 1 when unset)
//   - min_healthy_time: a valid time.Duration when set (time.ParseDuration)
//   - healthy_deadline: a valid time.Duration when set (time.ParseDuration)
//   - canary: an integer count in [0, count] OR a percentage string of
//     the form "<n>%" where n is in [0, 100] (the parser already accepts
//     both shapes; the validator accepts them too). Only meaningful
//     for the canary strategy; ignored (but still validated for shape)
//     for rolling/blue-green.
//   - auto_promote: boolean (no validation beyond the parser's
//     true/false parse; the field is always populated)
//
// The validator is pure (no I/O). Violations return a clear error
// listing every problem found, mirroring the per-field style of
// ServiceValidator.
type UpdateValidator struct{}

// Validate validates the UpdateBlock on the given spec. The spec must
// be non-nil and carry a Count (services have count ≥ 1 per
// ServiceValidator). When spec.Update is nil the validator returns an
// error (the update block is required for Service; this validator
// assumes the caller has already established the spec is a Service).
func (UpdateValidator) Validate(spec *jobspec.WorkloadSpec) error {
	if spec == nil {
		return fmt.Errorf("schema/Update: spec is nil")
	}
	if spec.Update == nil {
		return fmt.Errorf("schema/Update: update block is nil")
	}
	var errs []string
	u := spec.Update

	switch u.Strategy {
	case "rolling", "canary", "blue-green":
	case "":
		errs = append(errs, "update strategy required (one of rolling, canary, blue-green)")
	default:
		errs = append(errs, fmt.Sprintf("update strategy %q invalid (want one of rolling, canary, blue-green)", u.Strategy))
	}

	// max_parallel defaults to 1 when unset (0); validate the range
	// only when the user has set it explicitly.
	if u.MaxParallel != 0 {
		if u.MaxParallel < 1 {
			errs = append(errs, fmt.Sprintf("update.max_parallel must be ≥ 1, got %d", u.MaxParallel))
		}
		if spec.Count > 0 && u.MaxParallel > spec.Count {
			errs = append(errs, fmt.Sprintf("update.max_parallel %d exceeds count %d (must be 1..count)", u.MaxParallel, spec.Count))
		}
	}

	if u.MinHealthyTime != "" {
		if _, err := time.ParseDuration(u.MinHealthyTime); err != nil {
			errs = append(errs, fmt.Sprintf("update.min_healthy_time %q is not a valid duration: %v", u.MinHealthyTime, err))
		}
	}
	if u.HealthyDeadline != "" {
		if _, err := time.ParseDuration(u.HealthyDeadline); err != nil {
			errs = append(errs, fmt.Sprintf("update.healthy_deadline %q is not a valid duration: %v", u.HealthyDeadline, err))
		}
	}

	// canary accepts an integer count (0..count) or a percentage
	// ("<n>%" with n in 0..100). The field is only meaningful for the
	// canary strategy but we validate the shape regardless so a typo
	// in a rolling/blue-green stanza still surfaces.
	if u.Canary != "" {
		if err := validateCanary(u.Canary, spec.Count); err != nil {
			errs = append(errs, err.Error())
		}
	}

	// auto_promote is a bool; no extra validation beyond the parser.

	return composeErrors("schema/Update", errs)
}

// validateCanary validates the canary field shape: either an integer
// count (0..count) or a percentage string "<n>%" (n in 0..100). count
// is the spec.Count; when count is 0 (e.g. a DaemonSet or unset), the
// integer-count upper bound is not enforced (only the percentage
// bound is enforced, since percentage does not depend on count).
func validateCanary(canary string, count int) error {
	c := strings.TrimSpace(canary)
	if c == "" {
		return nil
	}
	if strings.HasSuffix(c, "%") {
		nStr := strings.TrimSuffix(c, "%")
		n, err := strconv.Atoi(strings.TrimSpace(nStr))
		if err != nil {
			return fmt.Errorf("update.canary %q is not a valid percentage (want \"<n>%%\")", canary)
		}
		if n < 0 || n > 100 {
			return fmt.Errorf("update.canary percentage %d out of range (want 0..100)", n)
		}
		return nil
	}
	n, err := strconv.Atoi(c)
	if err != nil {
		return fmt.Errorf("update.canary %q is not a valid count or percentage (want integer or \"<n>%%\")", canary)
	}
	if n < 0 {
		return fmt.Errorf("update.canary count %d must be ≥ 0", n)
	}
	if count > 0 && n > count {
		return fmt.Errorf("update.canary count %d exceeds count %d (must be 0..count)", n, count)
	}
	return nil
}
