package config

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

// Config holds all non-secret sentry runtime configuration.
// Secret values (DB creds, API keys) are fetched separately via Secrets Manager
// using the SecretARN* fields as pointers.
type Config struct {
	Env                 string
	Region              string
	S3Bucket            string
	CRMInternalURL      string
	CycleInterval       time.Duration
	LRUCap              int
	DBWorkerPool        int
	QueryTimeout        time.Duration
	ResetSchedule       []ResetTime
	ResetWindow         time.Duration
	ResetEpochReference time.Time

	// SecretARNTargets points at a JSON-array secret describing every
	// TiDB target to monitor. Format:
	//   [
	//     {
	//       "name": "prod",
	//       "tidb": {"host":..., "port":4000, "user":..., "password":..., "database":...},
	//       "crm":  {"url": "...", "token": "..."}   // optional
	//     },
	//     {
	//       "name": "staging",
	//       "tidb": {...},
	//       "crm":  null
	//     }
	//   ]
	// Each target gets its own cycle, its own DB connection, its own
	// LRU, its own S3 prefix, and its own suppressor. Cycles run
	// sequentially per tick.
	SecretARNTargets   string
	SecretARNDashScope string
	SecretARNLark      string
}

// ResetTime is a UTC wall-clock time at which the staging TiDB branch is reset.
type ResetTime struct {
	Hour   int
	Minute int
}

// Load reads sentry configuration from environment variables.
// Required: SENTRY_ENV. All other fields have defaults or may be empty.
func Load() (*Config, error) {
	env := os.Getenv("SENTRY_ENV")
	if env == "" {
		return nil, errors.New("SENTRY_ENV is required")
	}

	cycleInterval, err := time.ParseDuration(getenvDefault("SENTRY_CYCLE_INTERVAL", "10m"))
	if err != nil {
		return nil, fmt.Errorf("SENTRY_CYCLE_INTERVAL: %w", err)
	}

	windowMin, err := strconv.Atoi(getenvDefault("SENTRY_RESET_WINDOW_MIN", "20"))
	if err != nil {
		return nil, fmt.Errorf("SENTRY_RESET_WINDOW_MIN: %w", err)
	}
	if windowMin < 0 {
		return nil, fmt.Errorf("SENTRY_RESET_WINDOW_MIN must be non-negative, got %d", windowMin)
	}

	ref, err := time.Parse(time.RFC3339, getenvDefault("SENTRY_RESET_EPOCH_REFERENCE", "2026-01-01T00:00:00Z"))
	if err != nil {
		return nil, fmt.Errorf("SENTRY_RESET_EPOCH_REFERENCE: %w", err)
	}

	schedule, err := parseResetSchedule(os.Getenv("SENTRY_RESET_SCHEDULE"))
	if err != nil {
		return nil, err
	}

	return &Config{
		Env:                 env,
		Region:              os.Getenv("SENTRY_REGION"),
		S3Bucket:            os.Getenv("SENTRY_S3_BUCKET"),
		CRMInternalURL:      os.Getenv("SENTRY_CRM_INTERNAL_URL"),
		CycleInterval:       cycleInterval,
		LRUCap:              getIntDefault("SENTRY_LRU_CAP", 300),
		DBWorkerPool:        getIntDefault("SENTRY_DB_WORKER_POOL", 8),
		QueryTimeout:        getDurDefault("SENTRY_QUERY_TIMEOUT", 5*time.Second),
		ResetSchedule:       schedule,
		ResetWindow:         time.Duration(windowMin) * time.Minute,
		ResetEpochReference: ref,
		SecretARNTargets:    os.Getenv("SENTRY_SECRET_TARGETS"),
		SecretARNDashScope:  os.Getenv("SENTRY_SECRET_DASHSCOPE"),
		SecretARNLark:       os.Getenv("SENTRY_SECRET_LARK"),
	}, nil
}

func parseResetSchedule(s string) ([]ResetTime, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil, nil
	}
	var out []ResetTime
	for _, part := range strings.Split(s, ",") {
		part = strings.TrimSpace(part)
		hm := strings.SplitN(part, ":", 2)
		if len(hm) != 2 || hm[0] == "" || hm[1] == "" {
			return nil, fmt.Errorf("SENTRY_RESET_SCHEDULE: invalid entry %q", part)
		}
		h, err := strconv.Atoi(hm[0])
		if err != nil || h < 0 || h > 23 {
			return nil, fmt.Errorf("SENTRY_RESET_SCHEDULE: bad hour in %q", part)
		}
		m, err := strconv.Atoi(hm[1])
		if err != nil || m < 0 || m > 59 {
			return nil, fmt.Errorf("SENTRY_RESET_SCHEDULE: bad minute in %q", part)
		}
		out = append(out, ResetTime{Hour: h, Minute: m})
	}
	return out, nil
}

func getenvDefault(k, d string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return d
}

func getIntDefault(k string, d int) int {
	if v := os.Getenv(k); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			return n
		}
	}
	return d
}

func getDurDefault(k string, d time.Duration) time.Duration {
	if v := os.Getenv(k); v != "" {
		if n, err := time.ParseDuration(v); err == nil {
			return n
		}
	}
	return d
}
