package exec

import (
	"bytes"
	"encoding/json"
	"fmt"

	invopopjsonschema "github.com/invopop/jsonschema"
)

// agentOptionalString preserves update's three wire states: omitted means no
// change, null clears the value, and a string sets it. Its JSON language is
// deliberately closed to null|string.
type agentOptionalString struct {
	Set   bool
	Value *string
}

// IsZero lets encoding/json omit an absent update field while preserving an
// explicitly present null.
func (value agentOptionalString) IsZero() bool { return !value.Set }

func (value *agentOptionalString) UnmarshalJSON(raw []byte) error {
	raw = bytes.TrimSpace(raw)
	value.Set = true
	if bytes.Equal(raw, []byte("null")) {
		value.Value = nil
		return nil
	}
	var decoded string
	if err := json.Unmarshal(raw, &decoded); err != nil {
		return fmt.Errorf("agent update value must be a string or null: %w", err)
	}
	value.Value = &decoded
	return nil
}

func (value agentOptionalString) MarshalJSON() ([]byte, error) {
	if value.Value == nil {
		return []byte("null"), nil
	}
	return json.Marshal(*value.Value)
}

func (agentOptionalString) JSONSchema() *invopopjsonschema.Schema {
	return &invopopjsonschema.Schema{
		OneOf: []*invopopjsonschema.Schema{
			{Type: "string"},
			{Type: "null"},
		},
	}
}
