// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package config

import (
	"context"
	"reflect"
	"sync"

	"code.gitea.io/gitea/modules/json"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/util"
)

type CfgSecKey struct {
	Sec, Key string
}

// OptionInterface is used to overcome Golang's generic interface limitation
type OptionInterface interface {
	GetDefaultValue() any
}

type Option[T any] struct {
	mu sync.RWMutex

	cfgSecKey CfgSecKey
	dynKey    string

	value      T
	defSimple  T
	defFunc    func() T
	emptyAsDef bool
	has        bool
	revision   int
}

func (opt *Option[T]) GetDefaultValue() any {
	return opt.DefaultValue()
}

func (opt *Option[T]) parse(key, valStr string) (v T) {
	v = opt.DefaultValue()
	if valStr != "" {
		if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
			log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
		}
	}
	return v
}

func (opt *Option[T]) HasValue(ctx context.Context) bool {
	_, _, has := opt.ValueRevision(ctx)
	return has
}

func (opt *Option[T]) Value(ctx context.Context) (v T) {
	v, _, _ = opt.ValueRevision(ctx)
	return v
}

func isZeroOrEmpty(v any) bool {
	if v == nil {
		return true // interface itself is nil
	}
	r := reflect.ValueOf(v)
	if r.IsZero() {
		return true
	}

	if r.Kind() == reflect.Slice || r.Kind() == reflect.Map {
		if r.IsNil() {
			return true
		}
		return r.Len() == 0
	}
	return false
}

func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) {
	dg := GetDynGetter()
	if dg == nil {
		// this is an edge case: the database is not initialized but the system setting is going to be used
		// it should panic to avoid inconsistent config values (from config / system setting) and fix the code
		panic("no config dyn value getter")
	}

	rev = dg.GetRevision(ctx)

	// if the revision in the database doesn't change, use the last value
	opt.mu.RLock()
	if rev == opt.revision {
		v = opt.value
		has = opt.has
		opt.mu.RUnlock()
		return v, rev, has
	}
	opt.mu.RUnlock()

	// try to parse the config and cache it
	var valStr *string
	if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue {
		valStr = &dynVal
	} else if cfgVal, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue {
		valStr = &cfgVal
	}
	if valStr == nil {
		v = opt.DefaultValue()
		has = false
	} else {
		v = opt.parse(opt.dynKey, *valStr)
		if opt.emptyAsDef && isZeroOrEmpty(v) {
			v = opt.DefaultValue()
		} else {
			has = true
		}
	}

	opt.mu.Lock()
	opt.value = v
	opt.revision = rev
	opt.has = has
	opt.mu.Unlock()
	return v, rev, has
}

func (opt *Option[T]) DynKey() string {
	return opt.dynKey
}

// WithDefaultFunc sets the default value with a function
// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice
func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] {
	opt.defFunc = f
	return opt
}

func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] {
	v := any(def)
	switch v.(type) {
	case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
	default:
		// TODO: use reflect to support convertible basic types like `type State string`
		r := reflect.ValueOf(v)
		if r.Kind() != reflect.Struct {
			panic("invalid type for default value, use WithDefaultFunc instead")
		}
	}
	opt.defSimple = def
	return opt
}

func (opt *Option[T]) WithEmptyAsDefault() *Option[T] {
	opt.emptyAsDef = true
	return opt
}

func (opt *Option[T]) DefaultValue() T {
	if opt.defFunc != nil {
		return opt.defFunc()
	}
	return opt.defSimple
}

func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] {
	opt.cfgSecKey = cfgSecKey
	return opt
}

var allConfigOptions = map[string]OptionInterface{}

func NewOption[T any](dynKey string) *Option[T] {
	v := &Option[T]{dynKey: dynKey}
	allConfigOptions[dynKey] = v
	return v
}

func GetConfigOption(dynKey string) OptionInterface {
	return allConfigOptions[dynKey]
}
