package cli

import (
	"fmt"
	"math"
	"strings"
	"time"

	"github.com/spf13/pflag"
)

// Per-component upper bounds. A cache-safety grace window is measured in days
// to a few years; anything past these is a typo or an attempt to overflow the
// cutoff arithmetic. They are chosen so cutoff = now.AddDate(-y,-m,-d) can never
// overflow the int year addition or the int64 range of time.Time (representable
// to ~year 292 billion), so the destructive cutoff can never silently wrap.
const (
	maxDurationYears  = 100000
	maxDurationMonths = 100000 * 12
	maxDurationDays   = 100000 * 366
)

var _ pflag.Value = (*duration)(nil)

// duration is a calendar-aware grace window . It holds a tuple
// (years, months, days) rather than a time.Duration.
type duration struct {
	years  int
	months int
	days   int
}

func registerDurationFlag(flags *pflag.FlagSet, v *duration, name string, def string, usage string) {
	if err := v.Set(def); err != nil {
		panic(fmt.Sprintf("invalid default for --%s: %v", name, err))
	}
	flags.Var(v, name, usage)
}

// Type implements the `pflags.Value` interface.
func (d *duration) Type() string {
	return "duration"
}

// String implements the `pflags.Value` interface.
func (d *duration) String() string {
	sb := &strings.Builder{}
	if d.years != 0 {
		fmt.Fprintf(sb, "%dy", d.years)
	}
	if d.months != 0 {
		fmt.Fprintf(sb, "%dm", d.months)
	}
	if d.days != 0 {
		fmt.Fprintf(sb, "%dd", d.days)
	}

	if sb.Len() == 0 {
		return "0d"
	}
	return sb.String()
}

// Set parses a combinable value like "1y2m10d", "7d" or "30d". Each component is
// a non-negative integer followed by one of the units y, m (month), d, and each
// unit may appear at most once. An empty value or a value with no components is
// rejected.
//
// Set implements the `pflags.Value` interface.
func (d *duration) Set(raw string) error {
	parsed := duration{}
	seen := map[byte]bool{}
	components := 0

	for i := 0; i < len(raw); {
		c := raw[i]
		if c < '0' || c > '9' {
			return usageError{fmt.Sprintf("invalid duration %q", raw)}
		}

		n := 0
		for i < len(raw) && raw[i] >= '0' && raw[i] <= '9' {
			digit := int(raw[i] - '0')
			if n > (math.MaxInt-digit)/10 {
				return usageError{fmt.Sprintf("invalid duration %q (out of range)", raw)}
			}
			n = n*10 + digit
			i++
		}
		if i >= len(raw) {
			return usageError{fmt.Sprintf("invalid duration %q (number %d has no unit)", raw, n)}
		}

		unit := raw[i]
		if seen[unit] {
			return usageError{fmt.Sprintf("invalid duration %q (unit %q repeated)", raw, string(unit))}
		}
		switch unit {
		case 'y':
			if n > maxDurationYears {
				return usageError{fmt.Sprintf("invalid duration %q (years exceeds %d)", raw, maxDurationYears)}
			}
			parsed.years = n
		case 'm':
			if n > maxDurationMonths {
				return usageError{fmt.Sprintf("invalid duration %q (months exceeds %d)", raw, maxDurationMonths)}
			}
			parsed.months = n
		case 'd':
			if n > maxDurationDays {
				return usageError{fmt.Sprintf("invalid duration %q (days exceeds %d)", raw, maxDurationDays)}
			}
			parsed.days = n
		default:
			return usageError{fmt.Sprintf("invalid duration %q (unknown unit %q)", raw, string(unit))}
		}
		seen[unit] = true
		components++
		i++
	}

	if components == 0 {
		return usageError{fmt.Sprintf("invalid duration %q", raw)}
	}

	*d = parsed
	return nil
}

func (d duration) cutoff(tm time.Time) time.Time {
	return tm.AddDate(-d.years, -d.months, -d.days)
}

func (d duration) isZero() bool {
	return d.years == 0 && d.months == 0 && d.days == 0
}
