package allowlist

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"strconv"
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/spf13/viper"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	ov "github.com/tidbcloud/config-service/oneconfig/viper"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

var defaultAllowListConfigName = "oneconfig-allow-lock-list/list.yaml"

type AllowLockList struct {
	AllowList      *UnionAllowList `json:"allowList"`
	LockList       *LockList       `json:"lockList"`
	lock           *sync.RWMutex
	configName     string
	reloadInterval time.Duration
}
type UnionAllowList struct {
	Dedicated *AllowList `json:"dedicated"`
}

func (l *AllowLockList) DedicatedAllowList() *AllowList {
	l.lock.Lock()
	defer l.lock.Unlock()

	return l.AllowList.Dedicated
}

func (l *AllowLockList) GetLockList() *LockList {
	l.lock.Lock()
	defer l.lock.Unlock()

	return l.LockList
}

func NewAllowList(
	ctx context.Context,
	enable bool,
	configName string,
	accountCli account.AccountAPIServiceClient,
	reloadInterval time.Duration,
) (*AllowLockList, error) {
	list := newAllowLockList()

	// disable allow and lock list
	if !enable {
		return list, nil
	}

	if configName == "" {
		configName = defaultAllowListConfigName
	}
	list.configName = configName

	if reloadInterval == 0 {
		list.reloadInterval = 5 * time.Second
	} else {
		list.reloadInterval = reloadInterval
	}

	if accountCli == nil {
		return nil, errors.New("account client is nil")
	}
	list.AllowList.Dedicated.accountCli = accountCli

	err := list.loadRemote(ctx)
	if err != nil {
		return nil, err
	}
	list.loadAccountCache(ctx)

	return list, nil
}

func (l *AllowLockList) loadAccountCache(ctx context.Context) {
	// 12 hour to refresh account cache
	ticker := time.NewTicker(12 * time.Hour)
	go func() {
		_ = l.AllowList.Dedicated.PrefetchOrgsInfo(ctx)
		defer ticker.Stop()
		for range ticker.C {
			err := l.AllowList.Dedicated.PrefetchOrgsInfo(ctx)
			if err != nil {
				tilog.Error(ctx, "prefetch: failed to get org info", zap.Error(err))
			}
		}
	}()
}

func (l *AllowLockList) loadRemote(ctx context.Context) error {
	viper.SetConfigType("yaml")
	err := viper.AddRemoteProvider(ov.Provider, ov.Endpoint, l.configName)
	if err != nil {
		return fmt.Errorf("failed to set remote allow lsit: %w", err)
	}

	err = viper.ReadRemoteConfig()
	if err != nil {
		return fmt.Errorf("failed to read remote allow list: %w", err)
	}

	err = l.load(ctx)
	if err != nil {
		return fmt.Errorf("failed to load remote allow list: %w", err)
	}

	ticker := time.NewTicker(l.reloadInterval)
	// open a goroutine to watch remote changes forever
	go func() {
		defer ticker.Stop()
		for range ticker.C {
			// reload org allowlist
			err := viper.WatchRemoteConfig()
			if err != nil {
				tilog.Error(ctx, "unable to watch remote allow list", zap.Error(err))
				AllowListWatchRemoteFailedCounter.WithLabelValues().Inc()
				continue
			}

			err = l.load(ctx)
			if err != nil {
				tilog.Error(ctx, "unable to reload allow list", zap.Error(err))
				AllowListReloadRemoteFailedCounter.WithLabelValues().Inc()
				continue
			}
			tilog.Info(ctx, "reload allow and lock list success")
		}
	}()

	return nil
}

func (l *AllowLockList) load(ctx context.Context) error {
	l.lock.Lock()
	defer l.lock.Unlock()

	newList := newAllowLockList()

	if err := viper.Unmarshal(newList); err != nil {
		return fmt.Errorf("failed to unmarshal allow and lock list one config: %w", err)
	}

	tilog.Info(
		ctx,
		"load allow and lock list",
		zap.Any("allowList", newList.AllowList),
		zap.Any("lockList", newList.LockList),
	)
	newList.AllowList.sortRules()
	newList.LockList.sort()

	recordLockListMetrics(l.LockList.OrgIds, newList.LockList.OrgIds)

	oldAllowList := l.AllowList
	oldLockList := l.LockList

	// refresh
	l.AllowList = newList.AllowList
	l.LockList = newList.LockList

	// keep account client
	l.AllowList.Dedicated.accountCli = oldAllowList.Dedicated.accountCli

	// keep allow list cache
	if reflect.DeepEqual(l.AllowList.Dedicated.Rules, oldAllowList.Dedicated.Rules) &&
		l.AllowList.Dedicated.affectOrgCreatedTime.Equal(oldAllowList.Dedicated.affectOrgCreatedTime) {
		l.AllowList.Dedicated.allowCache = oldAllowList.Dedicated.allowCache
		tilog.Info(ctx, "allow list not changed, keep cache", zap.Any("allowCache", l.AllowList.Dedicated.allowCache))
	} else {
		tilog.Info(ctx, "allow list changed, refresh cache")
	}

	if reflect.DeepEqual(l.LockList.OrgIds, oldLockList.OrgIds) {
		l.LockList.lockCache = oldLockList.lockCache
		tilog.Info(ctx, "lock list not changed, keep cache", zap.Any("lockCache", l.LockList.lockCache))
	} else {
		tilog.Info(ctx, "lock list changed, refresh cache")
	}

	// set affect org created after
	if err := l.AllowList.setAffectOrgCreatedTime(); err != nil {
		return fmt.Errorf("failed to set affected org created after time: %w", err)
	}

	return nil
}

// For testing only
func (l *AllowLockList) Assign(newList *UnionAllowList) {
	l.lock.Lock()
	defer l.lock.Unlock()
	l.AllowList = newList
}

func (l *UnionAllowList) setAffectOrgCreatedTime() error {
	if err := l.Dedicated.setAffectOrgCreatedTime(); err != nil {
		return fmt.Errorf("set dedicated affected org : %w", err)
	}

	return nil
}

func (l *UnionAllowList) sortRules() {
	l.Dedicated.sortRules()
}

func recordLockListMetrics(oldList, newList []uint64) {
	// record lock list metrics
	for _, orgID := range oldList {
		OrgLockListGauge.DeletePartialMatch(prometheus.Labels{"org_id": strconv.FormatUint(orgID, 10)})
	}

	for _, orgID := range newList {
		OrgLockListGauge.WithLabelValues(strconv.FormatUint(orgID, 10)).Set(1)
	}
}

func newAllowLockList() *AllowLockList {
	// set dedicated default allow all to false
	return NewAllowLockList(false)
}

func NewAllowLockList(allowAllDedicated bool) *AllowLockList {
	return &AllowLockList{
		LockList: &LockList{
			OrgIds:         []uint64{},
			SuperUsers:     []string{},
			LockAll:        false,
			lockCache:      make(map[uint64]bool),
			superUserCache: make(map[string]bool),
			lock:           &sync.Mutex{},
		},
		AllowList: &UnionAllowList{
			Dedicated: &AllowList{
				AffectOrgCreatedAfter: "",
				Rules:                 []orgRule{},
				AllowAll:              allowAllDedicated,
				affectOrgCreatedTime:  time.Time{},
				allowCache:            make(map[uint64]map[uint64]bool),
				lock:                  &sync.Mutex{},
				accountCli:            nil,
			},
		},
		lock: &sync.RWMutex{},
	}
}
