package config

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"strconv"
	"sync/atomic"
	"time"
)

type Config struct {
	Port    int
	Timeout time.Duration
	Debug   bool
}
type rawConfig struct {
	Port    int    `json:"port"`
	Timeout string `json:"timeout"`
	Debug   bool   `json:"debug"`
}
type Lookup func(string) (string, bool)

func Load(file io.Reader, lookup Lookup, args []string) (Config, error) {
	raw := rawConfig{Port: 8080, Timeout: "2s"}
	if file != nil {
		dec := json.NewDecoder(file)
		dec.DisallowUnknownFields()
		if err := dec.Decode(&raw); err != nil {
			return Config{}, fmt.Errorf("config file: %w", err)
		}
		var extra any
		if err := dec.Decode(&extra); err != io.EOF {
			return Config{}, errors.New("config file: expected exactly one JSON value")
		}
	}
	if v, ok := lookup("APP_PORT"); ok {
		n, err := strconv.Atoi(v)
		if err != nil {
			return Config{}, errors.New("APP_PORT: must be an integer")
		}
		raw.Port = n
	}
	if v, ok := lookup("APP_TIMEOUT"); ok {
		if _, err := time.ParseDuration(v); err != nil {
			return Config{}, errors.New("APP_TIMEOUT: must be a duration")
		}
		raw.Timeout = v
	}
	if v, ok := lookup("APP_DEBUG"); ok {
		b, err := strconv.ParseBool(v)
		if err != nil {
			return Config{}, errors.New("APP_DEBUG: must be a boolean")
		}
		raw.Debug = b
	}
	fs := flag.NewFlagSet("app", flag.ContinueOnError)
	fs.SetOutput(io.Discard)
	fs.IntVar(&raw.Port, "port", raw.Port, "listen port")
	fs.StringVar(&raw.Timeout, "timeout", raw.Timeout, "request timeout")
	fs.BoolVar(&raw.Debug, "debug", raw.Debug, "debug logging")
	if err := fs.Parse(args); err != nil {
		return Config{}, err
	}
	if fs.NArg() != 0 {
		return Config{}, errors.New("unexpected positional arguments")
	}
	d, err := time.ParseDuration(raw.Timeout)
	if err != nil || d <= 0 {
		return Config{}, errors.New("timeout: must be a positive duration")
	}
	if raw.Port < 1 || raw.Port > 65535 {
		return Config{}, errors.New("port: must be between 1 and 65535")
	}
	return Config{raw.Port, d, raw.Debug}, nil
}

type Manager struct{ current atomic.Pointer[Config] }

func NewManager(c Config) *Manager { m := new(Manager); m.current.Store(&c); return m }
func (m *Manager) Current() Config { return *m.current.Load() }
func (m *Manager) Reload(file io.Reader, lookup Lookup, args []string) error {
	c, err := Load(file, lookup, args)
	if err != nil {
		return err
	}
	m.current.Store(&c)
	return nil
}
