package timeexpr

import (
	"fmt"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

const durationExamples = `"30m", "3h30m", or "1d"`

// ParseDuration parses durations accepted by time.ParseDuration and also supports "d" for 24-hour days.
// Empty, zero, and negative durations are rejected.
func ParseDuration(value string) (time.Duration, error) {
	return parseDuration(value, false)
}

func parseDuration(value string, allowZero bool) (time.Duration, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return 0, fmt.Errorf("duration must not be empty")
	}

	duration, err := time.ParseDuration(trimmed)
	if err != nil && strings.Contains(trimmed, "d") {
		duration, err = parseDurationWithDays(trimmed)
	}
	if err != nil {
		return 0, fmt.Errorf("invalid duration %q (must be like %s)", value, durationExamples)
	}
	if duration < 0 || (!allowZero && duration == 0) {
		return 0, fmt.Errorf("duration must be positive")
	}

	return duration, nil
}

// ParseNowExpressionToUnixNano parses "now" and "now-<duration>" expressions.
// The duration syntax matches time.ParseDuration, with an added "d" unit for 24-hour days.
//
// When the input does not start with "now", matched is false and err is nil.
// For malformed now expressions, matched is true with a non-nil error.
func ParseNowExpressionToUnixNano(value string, now time.Time) (unixNano int64, matched bool, err error) {
	if value == "now" {
		return now.UnixNano(), true, nil
	}

	if !strings.HasPrefix(value, "now") {
		return 0, false, nil
	}

	if !strings.HasPrefix(value, "now-") {
		return 0, true, fmt.Errorf(
			"invalid now expression %q (only \"now\" and \"now-<duration>\" are supported)",
			value,
		)
	}

	durationText := strings.TrimSpace(strings.TrimPrefix(value, "now-"))
	if durationText == "" {
		return 0, true, fmt.Errorf("invalid now expression %q (missing duration after \"now-\")", value)
	}

	duration, parseErr := parseDuration(durationText, true)
	if parseErr != nil {
		return 0, true, fmt.Errorf(
			"invalid now expression %q (duration must be like %s)",
			value,
			durationExamples,
		)
	}

	return now.Add(-duration).UnixNano(), true, nil
}

func parseDurationWithDays(value string) (time.Duration, error) {
	expanded, err := expandDayUnits(value)
	if err != nil {
		return 0, err
	}
	return time.ParseDuration(expanded)
}

func expandDayUnits(value string) (string, error) {
	if value == "" {
		return "", fmt.Errorf("duration must not be empty")
	}

	body := value
	sign := ""
	if body[0] == '+' || body[0] == '-' {
		sign = body[:1]
		body = body[1:]
	}

	var expanded strings.Builder
	expanded.WriteString(sign)
	for i := 0; i < len(body); {
		numberStart := i
		sawDigit := false
		for i < len(body) && isASCIIDigit(body[i]) {
			sawDigit = true
			i++
		}
		if i < len(body) && body[i] == '.' {
			i++
			for i < len(body) && isASCIIDigit(body[i]) {
				sawDigit = true
				i++
			}
		}
		if !sawDigit {
			return "", fmt.Errorf("invalid duration")
		}

		unitStart := i
		for i < len(body) && isDurationUnitByte(body[i]) {
			i++
		}
		if unitStart == i {
			return "", fmt.Errorf("missing duration unit")
		}

		numberText := body[numberStart:unitStart]
		unitText := body[unitStart:i]
		if unitText != "d" {
			expanded.WriteString(numberText)
			expanded.WriteString(unitText)
			continue
		}

		days, err := strconv.ParseFloat(numberText, 64)
		if err != nil {
			return "", err
		}
		hoursText := strconv.FormatFloat(days*24, 'f', -1, 64)
		expanded.WriteString(hoursText)
		expanded.WriteByte('h')
	}

	return expanded.String(), nil
}

func isASCIIDigit(value byte) bool {
	return value >= '0' && value <= '9'
}

func isDurationUnitByte(value byte) bool {
	return (value >= 'a' && value <= 'z') || value >= utf8.RuneSelf
}
