package approvalir

import (
	"fmt"
	"regexp"
	"strings"
	"time"
)

var decimalCoefficientPattern = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)

// FieldUpdateValueKind selects the sole non-null arm carried by a
// FieldUpdateValue.
type FieldUpdateValueKind string

const (
	// FieldUpdateValueKindSourceText is for an authoring grammar that supplies
	// text without a schema type. The product decoder removes product wire
	// markers before setting SourceText; the L0 field-update materializer then
	// schema-coerces that text exactly once or rejects it.
	FieldUpdateValueKindSourceText FieldUpdateValueKind = "source_text"
	FieldUpdateValueKindString     FieldUpdateValueKind = "string"
	FieldUpdateValueKindBoolean    FieldUpdateValueKind = "boolean"
	FieldUpdateValueKindInteger    FieldUpdateValueKind = "integer"
	FieldUpdateValueKindDecimal    FieldUpdateValueKind = "decimal"
	FieldUpdateValueKindDate       FieldUpdateValueKind = "date"
	FieldUpdateValueKindDateTime   FieldUpdateValueKind = "datetime"
	FieldUpdateValueKindReference  FieldUpdateValueKind = "reference"
)

// Valid reports whether k is a defined FieldUpdateValueKind.
func (k FieldUpdateValueKind) Valid() bool {
	switch k {
	case FieldUpdateValueKindSourceText, FieldUpdateValueKindString,
		FieldUpdateValueKindBoolean, FieldUpdateValueKindInteger,
		FieldUpdateValueKindDecimal, FieldUpdateValueKindDate,
		FieldUpdateValueKindDateTime, FieldUpdateValueKindReference:
		return true
	default:
		return false
	}
}

// Decimal represents an exact base-10 value as coefficient * 10^-scale.
type Decimal struct {
	Coefficient string
	Scale       uint32
}

// FieldUpdateValue is a closed tagged union. Exactly one pointer arm is set
// unless Null is true. Pointer arms preserve false, zero, and the empty string.
// Null is distinct from an absent action and clears the target field.
type FieldUpdateValue struct {
	Kind       FieldUpdateValueKind
	Null       bool
	SourceText *string
	String     *string
	Boolean    *bool
	Integer    *int64
	Decimal    *Decimal
	Date       *string
	DateTime   *string
	Reference  *string
}

// Validate rejects unknown kinds, ambiguous union arms, malformed exact
// decimals, and invalid date/datetime encodings.
func (v FieldUpdateValue) Validate() error {
	if !v.Kind.Valid() {
		return fmt.Errorf("invalid kind %q", v.Kind)
	}
	armCount := populatedArmCount(
		v.SourceText != nil,
		v.String != nil,
		v.Boolean != nil,
		v.Integer != nil,
		v.Decimal != nil,
		v.Date != nil,
		v.DateTime != nil,
		v.Reference != nil,
	)
	if v.Null {
		if armCount != 0 {
			return fmt.Errorf("null value cannot carry a non-null arm")
		}
		return nil
	}
	if armCount != 1 {
		return fmt.Errorf("kind %q requires exactly one non-null arm, got %d", v.Kind, armCount)
	}
	armMatches := false
	switch v.Kind {
	case FieldUpdateValueKindSourceText:
		armMatches = v.SourceText != nil
	case FieldUpdateValueKindString:
		armMatches = v.String != nil
	case FieldUpdateValueKindBoolean:
		armMatches = v.Boolean != nil
	case FieldUpdateValueKindInteger:
		armMatches = v.Integer != nil
	case FieldUpdateValueKindDecimal:
		armMatches = v.Decimal != nil
		if armMatches && !decimalCoefficientPattern.MatchString(v.Decimal.Coefficient) {
			return fmt.Errorf("decimal coefficient %q is not canonical", v.Decimal.Coefficient)
		}
	case FieldUpdateValueKindDate:
		armMatches = v.Date != nil
		if armMatches {
			parsed, err := time.Parse("2006-01-02", *v.Date)
			if err != nil || parsed.Format("2006-01-02") != *v.Date {
				return fmt.Errorf("date %q must use YYYY-MM-DD", *v.Date)
			}
		}
	case FieldUpdateValueKindDateTime:
		armMatches = v.DateTime != nil
		if armMatches {
			if _, err := time.Parse(time.RFC3339Nano, *v.DateTime); err != nil {
				return fmt.Errorf("datetime %q must use RFC3339", *v.DateTime)
			}
		}
	case FieldUpdateValueKindReference:
		armMatches = v.Reference != nil
		if armMatches && strings.TrimSpace(*v.Reference) == "" {
			return fmt.Errorf("reference is empty; use Null to clear a reference field")
		}
	}
	if !armMatches {
		return fmt.Errorf("kind %q does not match its populated arm", v.Kind)
	}
	return nil
}

func populatedArmCount(values ...bool) int {
	count := 0
	for _, value := range values {
		if value {
			count++
		}
	}
	return count
}
