package notion

import "encoding/json"

// Nullable decodes one input field that can be absent, explicitly null, or
// carry a real value. The ported upstream executors build request bodies with
// compactObject, which drops only absent (undefined) keys, so absent and
// explicit null are distinct input states even when a guard maps both to
// "do not send".
//
// Upstream decides per field which state reaches the wire:
//
//   - `in_trash: typeof input.in_trash === "boolean" ? input.in_trash :
//     undefined` (update_page in_trash, is_locked, erase_content) forwards
//     real booleans — including false — and treats null exactly like absent.
//   - `typeof input.markdown === "string" ? input.markdown : undefined`
//     (create_page markdown, update_page title) forwards real strings —
//     including "" — while null is dropped.
//
// Compact encodes both guards: a present value is returned as-is (false and ""
// survive) and absent or null collapse to nil, which the body builder omits.
// A later batch that ports a field forwarding null verbatim (update_block
// spreads its passthrough rest into compactObject, which keeps nulls) can
// branch on Set and Null directly.
type Nullable[T any] struct {
	// Set reports whether the key was present in the input JSON.
	Set bool
	// Null reports whether a present key held the JSON null literal.
	Null bool
	// Value holds the decoded value of a present, non-null key.
	Value T
}

// UnmarshalJSON records the absent/null/value state of one input key. Go
// never calls it for absent keys, so absence leaves the zero Nullable.
func (nullable *Nullable[T]) UnmarshalJSON(data []byte) error {
	if string(data) == "null" {
		nullable.Set = true
		nullable.Null = true
		var zero T
		nullable.Value = zero
		return nil
	}
	nullable.Set = true
	nullable.Null = false
	return json.Unmarshal(data, &nullable.Value)
}

// Present reports a present, non-null value, mirroring the upstream typeof
// guards that forward typed values.
func (nullable Nullable[T]) Present() bool {
	return nullable.Set && !nullable.Null
}

// Compact returns the value to place in a request body: the decoded value
// when one is present (false and "" are real values), and nil — which the
// body builder omits — for absent and null keys.
func (nullable Nullable[T]) Compact() any {
	if !nullable.Present() {
		return nil
	}
	return nullable.Value
}
