package config

import (
	"fmt"
	"math"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

type Source string

const (
	SourceDefault Source = "default"
	SourceEnv     Source = "environment"
	SourceDB      Source = "db override"
)

type AdminEnabledMode string

const (
	AdminEnabledAuto  AdminEnabledMode = "auto"
	AdminEnabledTrue  AdminEnabledMode = "true"
	AdminEnabledFalse AdminEnabledMode = "false"
)

const (
	SettingGuestUploadsEnabled      = "guest_uploads_enabled"
	SettingAPIEnabled               = "api_enabled"
	SettingZipDownloadsEnabled      = "zip_downloads_enabled"
	SettingOneTimeDownloadsEnabled  = "one_time_downloads_enabled"
	SettingRenewOnAccessEnabled     = "renew_on_access_enabled"
	SettingRenewOnDownloadEnabled   = "renew_on_download_enabled"
	SettingDefaultGuestExpirySecs   = "default_guest_expiry_seconds"
	SettingMaxGuestExpirySecs       = "max_guest_expiry_seconds"
	SettingGlobalMaxFileSizeBytes   = "global_max_file_size_bytes"
	SettingGlobalMaxBoxSizeBytes    = "global_max_box_size_bytes"
	SettingDefaultUserMaxFileBytes  = "default_user_max_file_size_bytes"
	SettingDefaultUserMaxBoxBytes   = "default_user_max_box_size_bytes"
	SettingSessionTTLSeconds        = "session_ttl_seconds"
	SettingBoxPollIntervalMS        = "box_poll_interval_ms"
	SettingThumbnailBatchSize       = "thumbnail_batch_size"
	SettingThumbnailIntervalSeconds = "thumbnail_interval_seconds"
	SettingDataDir                  = "data_dir"
)

type SettingType string

const (
	SettingTypeBool  SettingType = "bool"
	SettingTypeInt64 SettingType = "int64"
	SettingTypeInt   SettingType = "int"
	SettingTypeText  SettingType = "text"
)

type SettingDefinition struct {
	Key       string
	EnvName   string
	Label     string
	Type      SettingType
	Editable  bool
	HardLimit bool
	Minimum   int64
}

type SettingRow struct {
	Definition SettingDefinition
	Value      string
	Source     Source
}

type Config struct {
	DataDir    string
	UploadsDir string
	DBDir      string

	AdminPassword              string
	AdminUsername              string
	AdminEmail                 string
	AdminEnabled               AdminEnabledMode
	AdminCookieSecure          bool
	AllowAdminSettingsOverride bool

	GuestUploadsEnabled     bool
	APIEnabled              bool
	ZipDownloadsEnabled     bool
	OneTimeDownloadsEnabled bool
	RenewOnAccessEnabled    bool
	RenewOnDownloadEnabled  bool

	DefaultGuestExpirySeconds   int64
	MaxGuestExpirySeconds       int64
	GlobalMaxFileSizeBytes      int64
	GlobalMaxBoxSizeBytes       int64
	DefaultUserMaxFileSizeBytes int64
	DefaultUserMaxBoxSizeBytes  int64
	SessionTTLSeconds           int64
	BoxPollIntervalMS           int
	ThumbnailBatchSize          int
	ThumbnailIntervalSeconds    int

	sources map[string]Source
	values  map[string]string
}

var Definitions = []SettingDefinition{
	{Key: SettingDataDir, EnvName: "WARPBOX_DATA_DIR", Label: "Data directory", Type: SettingTypeText, Editable: false, HardLimit: true},
	{Key: SettingGuestUploadsEnabled, EnvName: "WARPBOX_GUEST_UPLOADS_ENABLED", Label: "Guest uploads enabled", Type: SettingTypeBool, Editable: true},
	{Key: SettingAPIEnabled, EnvName: "WARPBOX_API_ENABLED", Label: "API enabled", Type: SettingTypeBool, Editable: true},
	{Key: SettingZipDownloadsEnabled, EnvName: "WARPBOX_ZIP_DOWNLOADS_ENABLED", Label: "ZIP downloads enabled", Type: SettingTypeBool, Editable: true},
	{Key: SettingOneTimeDownloadsEnabled, EnvName: "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", Label: "One-time downloads enabled", Type: SettingTypeBool, Editable: true},
	{Key: SettingRenewOnAccessEnabled, EnvName: "WARPBOX_RENEW_ON_ACCESS_ENABLED", Label: "Renew on access enabled", Type: SettingTypeBool, Editable: true},
	{Key: SettingRenewOnDownloadEnabled, EnvName: "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", Label: "Renew on download enabled", Type: SettingTypeBool, Editable: true},
	{Key: SettingDefaultGuestExpirySecs, EnvName: "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", Label: "Default guest expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
	{Key: SettingMaxGuestExpirySecs, EnvName: "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", Label: "Max guest expiry seconds", Type: SettingTypeInt64, Editable: true, Minimum: 0},
	{Key: SettingGlobalMaxFileSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", Label: "Global max file size bytes", Type: SettingTypeInt64, Editable: false, HardLimit: true, Minimum: 0},
	{Key: SettingGlobalMaxBoxSizeBytes, EnvName: "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", Label: "Global max box size bytes", Type: SettingTypeInt64, Editable: false, HardLimit: true, Minimum: 0},
	{Key: SettingDefaultUserMaxFileBytes, EnvName: "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", Label: "Default user max file size bytes", Type: SettingTypeInt64, Editable: true, Minimum: 0},
	{Key: SettingDefaultUserMaxBoxBytes, EnvName: "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", Label: "Default user max box size bytes", Type: SettingTypeInt64, Editable: true, Minimum: 0},
	{Key: SettingSessionTTLSeconds, EnvName: "WARPBOX_SESSION_TTL_SECONDS", Label: "Session TTL seconds", Type: SettingTypeInt64, Editable: true, Minimum: 60},
	{Key: SettingBoxPollIntervalMS, EnvName: "WARPBOX_BOX_POLL_INTERVAL_MS", Label: "Box poll interval milliseconds", Type: SettingTypeInt, Editable: true, Minimum: 1000},
	{Key: SettingThumbnailBatchSize, EnvName: "WARPBOX_THUMBNAIL_BATCH_SIZE", Label: "Thumbnail batch size", Type: SettingTypeInt, Editable: true, Minimum: 1},
	{Key: SettingThumbnailIntervalSeconds, EnvName: "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", Label: "Thumbnail interval seconds", Type: SettingTypeInt, Editable: true, Minimum: 1},
}

func Load() (*Config, error) {
	cfg := &Config{
		DataDir:                    "./data",
		AdminUsername:              "admin",
		AdminEnabled:               AdminEnabledAuto,
		AllowAdminSettingsOverride: true,
		GuestUploadsEnabled:        true,
		APIEnabled:                 true,
		ZipDownloadsEnabled:        true,
		OneTimeDownloadsEnabled:    true,
		DefaultGuestExpirySeconds:  10,
		MaxGuestExpirySeconds:      48 * 60 * 60,
		SessionTTLSeconds:          24 * 60 * 60,
		BoxPollIntervalMS:          5000,
		ThumbnailBatchSize:         10,
		ThumbnailIntervalSeconds:   30,
		sources:                    make(map[string]Source),
		values:                     make(map[string]string),
	}

	cfg.captureDefaults()

	if err := cfg.applyStringEnv(SettingDataDir, "WARPBOX_DATA_DIR", &cfg.DataDir); err != nil {
		return nil, err
	}
	if err := cfg.applyStringEnv("", "WARPBOX_ADMIN_PASSWORD", &cfg.AdminPassword); err != nil {
		return nil, err
	}
	if err := cfg.applyStringEnv("", "WARPBOX_ADMIN_USERNAME", &cfg.AdminUsername); err != nil {
		return nil, err
	}
	if err := cfg.applyStringEnv("", "WARPBOX_ADMIN_EMAIL", &cfg.AdminEmail); err != nil {
		return nil, err
	}
	if raw := strings.TrimSpace(os.Getenv("WARPBOX_ADMIN_ENABLED")); raw != "" {
		mode := AdminEnabledMode(strings.ToLower(raw))
		if mode != AdminEnabledAuto && mode != AdminEnabledTrue && mode != AdminEnabledFalse {
			return nil, fmt.Errorf("WARPBOX_ADMIN_ENABLED must be auto, true, or false")
		}
		cfg.AdminEnabled = mode
	}
	if err := cfg.applyBoolEnv("", "WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE", &cfg.AllowAdminSettingsOverride); err != nil {
		return nil, err
	}
	if err := cfg.applyBoolEnv("", "WARPBOX_ADMIN_COOKIE_SECURE", &cfg.AdminCookieSecure); err != nil {
		return nil, err
	}

	envBools := []struct {
		key    string
		name   string
		target *bool
	}{
		{SettingGuestUploadsEnabled, "WARPBOX_GUEST_UPLOADS_ENABLED", &cfg.GuestUploadsEnabled},
		{SettingAPIEnabled, "WARPBOX_API_ENABLED", &cfg.APIEnabled},
		{SettingZipDownloadsEnabled, "WARPBOX_ZIP_DOWNLOADS_ENABLED", &cfg.ZipDownloadsEnabled},
		{SettingOneTimeDownloadsEnabled, "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", &cfg.OneTimeDownloadsEnabled},
		{SettingRenewOnAccessEnabled, "WARPBOX_RENEW_ON_ACCESS_ENABLED", &cfg.RenewOnAccessEnabled},
		{SettingRenewOnDownloadEnabled, "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", &cfg.RenewOnDownloadEnabled},
	}
	for _, item := range envBools {
		if err := cfg.applyBoolEnv(item.key, item.name, item.target); err != nil {
			return nil, err
		}
	}

	envInt64s := []struct {
		key    string
		name   string
		min    int64
		target *int64
	}{
		{SettingDefaultGuestExpirySecs, "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", 0, &cfg.DefaultGuestExpirySeconds},
		{SettingMaxGuestExpirySecs, "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", 0, &cfg.MaxGuestExpirySeconds},
		{SettingSessionTTLSeconds, "WARPBOX_SESSION_TTL_SECONDS", 60, &cfg.SessionTTLSeconds},
	}
	for _, item := range envInt64s {
		if err := cfg.applyInt64Env(item.key, item.name, item.min, item.target); err != nil {
			return nil, err
		}
	}
	sizeEnvVars := []struct {
		key       string
		mbName    string
		bytesName string
		target    *int64
	}{
		{SettingGlobalMaxFileSizeBytes, "WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", &cfg.GlobalMaxFileSizeBytes},
		{SettingGlobalMaxBoxSizeBytes, "WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", &cfg.GlobalMaxBoxSizeBytes},
		{SettingDefaultUserMaxFileBytes, "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", &cfg.DefaultUserMaxFileSizeBytes},
		{SettingDefaultUserMaxBoxBytes, "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", &cfg.DefaultUserMaxBoxSizeBytes},
	}
	for _, item := range sizeEnvVars {
		if err := cfg.applyMegabytesOrBytesEnv(item.key, item.mbName, item.bytesName, 0, item.target); err != nil {
			return nil, err
		}
	}

	envInts := []struct {
		key    string
		name   string
		min    int
		target *int
	}{
		{SettingBoxPollIntervalMS, "WARPBOX_BOX_POLL_INTERVAL_MS", 1000, &cfg.BoxPollIntervalMS},
		{SettingThumbnailBatchSize, "WARPBOX_THUMBNAIL_BATCH_SIZE", 1, &cfg.ThumbnailBatchSize},
		{SettingThumbnailIntervalSeconds, "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", 1, &cfg.ThumbnailIntervalSeconds},
	}
	for _, item := range envInts {
		if err := cfg.applyIntEnv(item.key, item.name, item.min, item.target); err != nil {
			return nil, err
		}
	}

	cfg.DataDir = filepath.Clean(cfg.DataDir)
	if strings.TrimSpace(cfg.DataDir) == "" || cfg.DataDir == "." && strings.TrimSpace(os.Getenv("WARPBOX_DATA_DIR")) == "" {
		cfg.DataDir = "data"
	}
	if cfg.AdminUsername = strings.TrimSpace(cfg.AdminUsername); cfg.AdminUsername == "" {
		return nil, fmt.Errorf("WARPBOX_ADMIN_USERNAME cannot be empty")
	}
	cfg.AdminEmail = strings.TrimSpace(cfg.AdminEmail)
	cfg.UploadsDir = filepath.Join(cfg.DataDir, "uploads")
	cfg.DBDir = filepath.Join(cfg.DataDir, "db")
	cfg.setValue(SettingDataDir, cfg.DataDir, cfg.sourceFor(SettingDataDir))
	return cfg, nil
}

func (cfg *Config) EnsureDirectories() error {
	for _, path := range []string{cfg.DataDir, cfg.UploadsDir, cfg.DBDir} {
		if err := os.MkdirAll(path, 0755); err != nil {
			return fmt.Errorf("create %s: %w", path, err)
		}
	}
	return nil
}

func (cfg *Config) ApplyOverrides(overrides map[string]string) error {
	if !cfg.AllowAdminSettingsOverride {
		return nil
	}
	for key, value := range overrides {
		if err := cfg.ApplyOverride(key, value); err != nil {
			return err
		}
	}
	return nil
}

func (cfg *Config) ApplyOverride(key string, value string) error {
	def, ok := Definition(key)
	if !ok {
		return fmt.Errorf("unknown setting %q", key)
	}
	if !def.Editable || def.HardLimit {
		return fmt.Errorf("setting %q cannot be changed from the admin UI", key)
	}

	switch def.Type {
	case SettingTypeBool:
		parsed, err := parseBool(value)
		if err != nil {
			return fmt.Errorf("%s: %w", key, err)
		}
		cfg.assignBool(key, parsed, SourceDB)
	case SettingTypeInt64:
		parsed, err := parseInt64(value, def.Minimum)
		if err != nil {
			return fmt.Errorf("%s: %w", key, err)
		}
		cfg.assignInt64(key, parsed, SourceDB)
	case SettingTypeInt:
		parsed64, err := parseInt64(value, def.Minimum)
		if err != nil {
			return fmt.Errorf("%s: %w", key, err)
		}
		cfg.assignInt(key, int(parsed64), SourceDB)
	default:
		return fmt.Errorf("setting %q is not runtime editable", key)
	}
	return nil
}

func (cfg *Config) SettingRows() []SettingRow {
	rows := make([]SettingRow, 0, len(Definitions))
	for _, def := range Definitions {
		rows = append(rows, SettingRow{
			Definition: def,
			Value:      cfg.values[def.Key],
			Source:     cfg.sourceFor(def.Key),
		})
	}
	return rows
}

func (cfg *Config) Source(key string) Source {
	return cfg.sourceFor(key)
}

func (cfg *Config) AdminLoginEnabled(hasAdminUser bool) bool {
	switch cfg.AdminEnabled {
	case AdminEnabledFalse:
		return false
	case AdminEnabledTrue:
		return hasAdminUser
	default:
		return hasAdminUser
	}
}

func Definition(key string) (SettingDefinition, bool) {
	for _, def := range Definitions {
		if def.Key == key {
			return def, true
		}
	}
	return SettingDefinition{}, false
}

func EditableDefinitions() []SettingDefinition {
	defs := make([]SettingDefinition, 0, len(Definitions))
	for _, def := range Definitions {
		if def.Editable && !def.HardLimit {
			defs = append(defs, def)
		}
	}
	return defs
}

func (cfg *Config) captureDefaults() {
	cfg.setValue(SettingDataDir, cfg.DataDir, SourceDefault)
	cfg.setValue(SettingGuestUploadsEnabled, formatBool(cfg.GuestUploadsEnabled), SourceDefault)
	cfg.setValue(SettingAPIEnabled, formatBool(cfg.APIEnabled), SourceDefault)
	cfg.setValue(SettingZipDownloadsEnabled, formatBool(cfg.ZipDownloadsEnabled), SourceDefault)
	cfg.setValue(SettingOneTimeDownloadsEnabled, formatBool(cfg.OneTimeDownloadsEnabled), SourceDefault)
	cfg.setValue(SettingRenewOnAccessEnabled, formatBool(cfg.RenewOnAccessEnabled), SourceDefault)
	cfg.setValue(SettingRenewOnDownloadEnabled, formatBool(cfg.RenewOnDownloadEnabled), SourceDefault)
	cfg.setValue(SettingDefaultGuestExpirySecs, strconv.FormatInt(cfg.DefaultGuestExpirySeconds, 10), SourceDefault)
	cfg.setValue(SettingMaxGuestExpirySecs, strconv.FormatInt(cfg.MaxGuestExpirySeconds, 10), SourceDefault)
	cfg.setValue(SettingGlobalMaxFileSizeBytes, strconv.FormatInt(cfg.GlobalMaxFileSizeBytes, 10), SourceDefault)
	cfg.setValue(SettingGlobalMaxBoxSizeBytes, strconv.FormatInt(cfg.GlobalMaxBoxSizeBytes, 10), SourceDefault)
	cfg.setValue(SettingDefaultUserMaxFileBytes, strconv.FormatInt(cfg.DefaultUserMaxFileSizeBytes, 10), SourceDefault)
	cfg.setValue(SettingDefaultUserMaxBoxBytes, strconv.FormatInt(cfg.DefaultUserMaxBoxSizeBytes, 10), SourceDefault)
	cfg.setValue(SettingSessionTTLSeconds, strconv.FormatInt(cfg.SessionTTLSeconds, 10), SourceDefault)
	cfg.setValue(SettingBoxPollIntervalMS, strconv.Itoa(cfg.BoxPollIntervalMS), SourceDefault)
	cfg.setValue(SettingThumbnailBatchSize, strconv.Itoa(cfg.ThumbnailBatchSize), SourceDefault)
	cfg.setValue(SettingThumbnailIntervalSeconds, strconv.Itoa(cfg.ThumbnailIntervalSeconds), SourceDefault)
}

func (cfg *Config) applyStringEnv(key string, name string, target *string) error {
	raw := os.Getenv(name)
	if raw == "" {
		return nil
	}
	*target = raw
	if key != "" {
		cfg.setValue(key, raw, SourceEnv)
	}
	return nil
}

func (cfg *Config) applyBoolEnv(key string, name string, target *bool) error {
	raw := strings.TrimSpace(os.Getenv(name))
	if raw == "" {
		return nil
	}
	parsed, err := parseBool(raw)
	if err != nil {
		return fmt.Errorf("%s: %w", name, err)
	}
	*target = parsed
	if key != "" {
		cfg.setValue(key, formatBool(parsed), SourceEnv)
	}
	return nil
}

func (cfg *Config) applyInt64Env(key string, name string, min int64, target *int64) error {
	raw := strings.TrimSpace(os.Getenv(name))
	if raw == "" {
		return nil
	}
	parsed, err := parseInt64(raw, min)
	if err != nil {
		return fmt.Errorf("%s: %w", name, err)
	}
	*target = parsed
	if key != "" {
		cfg.setValue(key, strconv.FormatInt(parsed, 10), SourceEnv)
	}
	return nil
}

func (cfg *Config) applyMegabytesOrBytesEnv(key string, mbName string, bytesName string, min int64, target *int64) error {
	if rawBytes := strings.TrimSpace(os.Getenv(bytesName)); rawBytes != "" {
		parsed, err := parseInt64(rawBytes, min)
		if err != nil {
			return fmt.Errorf("%s: %w", bytesName, err)
		}
		*target = parsed
		cfg.setValue(key, strconv.FormatInt(parsed, 10), SourceEnv)
		return nil
	}

	rawMB := strings.TrimSpace(os.Getenv(mbName))
	if rawMB == "" {
		return nil
	}
	parsedMB, err := parseInt64(rawMB, min)
	if err != nil {
		return fmt.Errorf("%s: %w", mbName, err)
	}
	if parsedMB > math.MaxInt64/(1024*1024) {
		return fmt.Errorf("%s: is too large", mbName)
	}
	parsedBytes := parsedMB * 1024 * 1024
	*target = parsedBytes
	cfg.setValue(key, strconv.FormatInt(parsedBytes, 10), SourceEnv)
	return nil
}

func (cfg *Config) applyIntEnv(key string, name string, min int, target *int) error {
	raw := strings.TrimSpace(os.Getenv(name))
	if raw == "" {
		return nil
	}
	parsed, err := parseInt(raw, min)
	if err != nil {
		return fmt.Errorf("%s: %w", name, err)
	}
	*target = parsed
	if key != "" {
		cfg.setValue(key, strconv.Itoa(parsed), SourceEnv)
	}
	return nil
}

func (cfg *Config) assignBool(key string, value bool, source Source) {
	switch key {
	case SettingGuestUploadsEnabled:
		cfg.GuestUploadsEnabled = value
	case SettingAPIEnabled:
		cfg.APIEnabled = value
	case SettingZipDownloadsEnabled:
		cfg.ZipDownloadsEnabled = value
	case SettingOneTimeDownloadsEnabled:
		cfg.OneTimeDownloadsEnabled = value
	case SettingRenewOnAccessEnabled:
		cfg.RenewOnAccessEnabled = value
	case SettingRenewOnDownloadEnabled:
		cfg.RenewOnDownloadEnabled = value
	}
	cfg.setValue(key, formatBool(value), source)
}

func (cfg *Config) assignInt64(key string, value int64, source Source) {
	switch key {
	case SettingDefaultGuestExpirySecs:
		cfg.DefaultGuestExpirySeconds = value
	case SettingMaxGuestExpirySecs:
		cfg.MaxGuestExpirySeconds = value
	case SettingDefaultUserMaxFileBytes:
		cfg.DefaultUserMaxFileSizeBytes = value
	case SettingDefaultUserMaxBoxBytes:
		cfg.DefaultUserMaxBoxSizeBytes = value
	case SettingSessionTTLSeconds:
		cfg.SessionTTLSeconds = value
	}
	cfg.setValue(key, strconv.FormatInt(value, 10), source)
}

func (cfg *Config) assignInt(key string, value int, source Source) {
	switch key {
	case SettingBoxPollIntervalMS:
		cfg.BoxPollIntervalMS = value
	case SettingThumbnailBatchSize:
		cfg.ThumbnailBatchSize = value
	case SettingThumbnailIntervalSeconds:
		cfg.ThumbnailIntervalSeconds = value
	}
	cfg.setValue(key, strconv.Itoa(value), source)
}

func (cfg *Config) setValue(key string, value string, source Source) {
	if key == "" {
		return
	}
	cfg.values[key] = value
	cfg.sources[key] = source
}

func (cfg *Config) sourceFor(key string) Source {
	source, ok := cfg.sources[key]
	if !ok {
		return SourceDefault
	}
	return source
}

func parseBool(value string) (bool, error) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "1", "t", "true", "y", "yes", "on":
		return true, nil
	case "0", "f", "false", "n", "no", "off":
		return false, nil
	default:
		return false, fmt.Errorf("must be a boolean")
	}
}

func parseInt64(value string, min int64) (int64, error) {
	parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
	if err != nil {
		return 0, fmt.Errorf("must be an integer")
	}
	if parsed < min {
		return 0, fmt.Errorf("must be at least %d", min)
	}
	return parsed, nil
}

func parseInt(value string, min int) (int, error) {
	parsed64, err := parseInt64(value, int64(min))
	if err != nil {
		return 0, err
	}
	if parsed64 > int64(^uint(0)>>1) {
		return 0, fmt.Errorf("is too large")
	}
	return int(parsed64), nil
}

func formatBool(value bool) string {
	if value {
		return "true"
	}
	return "false"
}
