// This is not autogenerated.
package academy

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
)

// QuizTimeLimit is a quiz time limit in minutes.
//
// It exists because both JSON shapes below are real production data and no
// plain scalar can decode both:
//
//   - a JSON number, which is what academy-theme emits today
//     (`int (or $p.timeLimit $p.time_limit 0)` in partials/test/single.html);
//   - a JSON string, which is what payloads persisted by earlier revisions
//     hold - "25", "0", "30", and the non-numeric sentinel "infinite".
//
// Typing the field as an integer breaks the stored rows; typing it as a string
// breaks every freshly built quiz. The schema therefore models it as
// `oneOf: [integer, string]` and this type absorbs both.
//
// A string that does not parse to a positive number means "no time limit" and
// normalizes to 0, matching the documented meaning of 0 and the behaviour the
// UI already relies on (`parseInt`, then `NaN` or <= 0 means no limit). It is
// deliberately lenient rather than strict: this whole construct was brought
// down by one unreadable record aborting an entire response, so an
// unrecognized time limit degrades to "untimed" instead of failing the decode.
type QuizTimeLimit int

// Minutes returns the time limit in minutes; 0 means no time limit.
func (t QuizTimeLimit) Minutes() int { return int(t) }

// IsUnlimited reports whether the quiz is untimed.
func (t QuizTimeLimit) IsUnlimited() bool { return t <= 0 }

// UnmarshalJSON decodes a time limit from either a JSON number or a JSON
// string. Unparseable and negative values decode to 0 (no time limit).
func (t *QuizTimeLimit) UnmarshalJSON(data []byte) error {
	if len(data) == 0 || string(data) == "null" {
		*t = 0
		return nil
	}

	// json.Number covers both integer- and float-shaped numbers, so a payload
	// carrying 25.0 decodes the same as 25 rather than falling through to the
	// string branch and erroring.
	var number json.Number
	if err := json.Unmarshal(data, &number); err == nil {
		*t = quizTimeLimitFromString(number.String())
		return nil
	}

	var text string
	if err := json.Unmarshal(data, &text); err != nil {
		return fmt.Errorf("decode quiz time limit from %s: %w", string(data), err)
	}

	// "infinite", "" and any other unparseable value all mean "no time limit".
	*t = quizTimeLimitFromString(text)
	return nil
}

// quizTimeLimitFromString parses a time limit written as text, truncating any
// fractional part. Anything that does not yield a positive number - a sentinel
// like "infinite", an empty string, junk, or a negative value - means "no time
// limit" and becomes 0.
func quizTimeLimitFromString(raw string) QuizTimeLimit {
	parsed, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
	// ErrRange is not a parse failure: the value is numeric but outside float64
	// range, and ParseFloat still returns a usable magnitude - +Inf on overflow,
	// zero on underflow. Falling through lets both settle on the same rules as
	// any other number, so "1e309" clamps like any other oversized value rather
	// than silently meaning "no time limit".
	if err != nil && !errors.Is(err, strconv.ErrRange) {
		return 0
	}
	if math.IsNaN(parsed) || parsed <= 0 {
		return 0
	}
	if parsed > math.MaxInt32 {
		return math.MaxInt32
	}
	return QuizTimeLimit(int(parsed))
}

// MarshalJSON always emits a number, normalizing the legacy string form so
// that everything written from here on is canonical.
func (t QuizTimeLimit) MarshalJSON() ([]byte, error) {
	if t < 0 {
		return json.Marshal(0)
	}
	return json.Marshal(int(t))
}
