package allowlist

import (
	"cmp"
	"context"
	"fmt"
	"slices"
	"sync"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.buf.build/protocolbuffers/go/linuxgit/tidbcloud/base"
	"go.uber.org/zap"
)

/*
allow list rule priority
1. org_id in AllowList.Rules
	1.1.  AllowList.Rules[org_id].BlockAll
	1.2.  project_id in AllowList.Rules[org_id].BlockProjectIds, return false
	1.3.  AllowList.Rules[org_id].AllowAll, return true
	1.4.  project_id in AllowList.Rules[org_id].AllowProjectIds, return true
2. AllowList.AllowAll return true
3. AllowList.AffectOrgCreatedAfter has value
	3.1. if org created after AllowList.AffectOrgCreatedAfter, return true
	3.2. if org created before AllowList.AffectOrgCreatedAfter, return false
4. default return false
*/

// TODO: move it into AllowLockList
var orgInfoCache = make(map[uint64]*account.Org)

type AllowList struct {
	AffectOrgCreatedAfter string    `yaml:"affectOrgCreatedAfter" default:"2099-01-01T00:00:00Z"`
	Rules                 []orgRule `yaml:"rules"`
	AllowAll              bool      `yaml:"allowAll"`

	// internal
	affectOrgCreatedTime time.Time
	allowCache           map[uint64]map[uint64]bool
	lock                 *sync.Mutex
	accountCli           account.AccountAPIServiceClient
}

type orgRule struct {
	OrgId           uint64   `yaml:"orgId"`
	AllowAll        bool     `yaml:"allowAll"`
	BlockAll        bool     `yaml:"blockAll"`
	AllowProjectIds []uint64 `yaml:"allowProjectIds"`
	BlockProjectIds []uint64 `yaml:"blockProjectIds"`
}

// For test only
func (l *AllowList) AddAllow(orgId, projectId uint64) {
	l.lock.Lock()
	defer l.lock.Unlock()

	cacheRule, ok := l.allowCache[orgId]
	if !ok {
		cacheRule = make(map[uint64]bool)
	}
	cacheRule[projectId] = true
	l.allowCache[orgId] = cacheRule

	for _, rule := range l.Rules {
		if rule.OrgId == orgId {
			if !lo.Contains(rule.AllowProjectIds, projectId) {
				rule.AllowProjectIds = append(rule.AllowProjectIds, projectId)
			}
			return
		}
	}
	l.Rules = append(l.Rules, orgRule{
		AllowProjectIds: []uint64{projectId},
	})
}

// TODO: return a reason string
func (l *AllowList) IsAllow(ctx context.Context, orgId, projectId uint64) (bool, error) {
	l.lock.Lock()
	defer l.lock.Unlock()

	// init cache
	if l.allowCache[orgId] == nil {
		l.allowCache[orgId] = make(map[uint64]bool)
	}

	// check cache
	if projectAllow, exist := l.allowCache[orgId]; exist {
		if allow, exist := projectAllow[projectId]; exist {
			tilog.Info(
				ctx,
				"allow list cache hit",
				zap.Uint64("org_id", orgId),
				zap.Uint64("project_id", projectId),
				zap.Bool("allow", allow),
			)
			return allow, nil
		}
	}

	/*
		1. org_id in AllowList.Rules
	*/
	for _, rule := range l.Rules {
		if rule.OrgId != orgId {
			continue
		}
		/*
			1.1.  AllowList.Rules[org_id].BlockAll, return false
		*/
		if rule.BlockAll {
			l.allowCache[orgId][projectId] = false
			return false, nil
		}

		/*
			1.2.  project_id in AllowList.Rules[org_id].BlockProjectIds, return false
		*/
		if slices.Contains(rule.BlockProjectIds, projectId) {
			l.allowCache[orgId][projectId] = false
			return false, nil
		}

		/*
			1.3.  AllowList.Rules[org_id].AllowAll, return true
		*/
		if rule.AllowAll {
			l.allowCache[orgId][projectId] = true
			return true, nil
		}

		/*
			1.4.  project_id in AllowList.Rules[org_id].AllowProjectIds, return true
		*/
		if slices.Contains(rule.AllowProjectIds, projectId) {
			l.allowCache[orgId][projectId] = true
			return true, nil
		}
	}

	/*
		2. AllowList.AllowAll, return true
	*/
	if l.AllowAll {
		l.allowCache[orgId][projectId] = true
		return true, nil
	}

	/*
		3. AllowList.AffectOrgCreatedAfter has value
	*/
	tilog.Info(ctx, "AllowList.AffectOrgCreatedAfter", zap.String("time", l.AffectOrgCreatedAfter))
	if l.AffectOrgCreatedAfter != "" && time.Now().After(l.affectOrgCreatedTime) {
		if l.accountCli != nil { // for test
			orgInfo, err := l.getOrgInfo(ctx, orgId)

			if err != nil {
				tilog.Error(ctx, "failed to get org info to check allow list", zap.Error(err))
				return false, err
			}

			/*
				3.1. if org created after AllowList.AffectOrgCreatedAfter, return true
			*/
			if orgInfo != nil && orgInfo.CreatedAt != nil &&
				orgInfo.CreatedAt.AsTime().After(l.affectOrgCreatedTime) {
				tilog.Info(ctx, "org created after AllowList.AffectOrgCreatedAfter", zap.Uint64("org_id", orgId))
				l.allowCache[orgId][projectId] = true
				return true, nil
			}

			/*
				3.2. if org created before AllowList.AffectOrgCreatedAfter, return false
			*/
			tilog.Info(ctx, "org created before AllowList.AffectOrgCreatedAfter", zap.Uint64("org_id", orgId))
			l.allowCache[orgId][projectId] = false
			return false, nil
		}
	}

	/*
		4. default return false
	*/
	tilog.Info(ctx, "allowlist default return false", zap.Uint64("org_id", orgId))
	l.allowCache[orgId][projectId] = false
	return false, nil
}

func (l *AllowList) getOrgInfo(ctx context.Context, tenantID uint64) (*account.Org, error) {
	orgInfo, ok := orgInfoCache[tenantID]
	if ok {
		AllowListGetOrgInfoCacheHitCounter.WithLabelValues().Inc()
		return orgInfo, nil
	}
	AllowListGetOrgInfoCacheMissCounter.WithLabelValues().Inc()

	resp, err := l.accountCli.GetOrgByID(ctx, &account.GetOrgByIDRequest{Id: tenantID})
	if err != nil {
		return nil, err
	}

	if resp.BaseResp.ErrCode != 0 {
		return nil, fmt.Errorf("failed to get org info: %s", resp.BaseResp.ErrMsg)
	}

	orgInfoCache[tenantID] = resp.Org
	return resp.Org, nil
}

func (l *AllowList) PrefetchOrgsInfo(ctx context.Context) error {
	page := int64(1)
	for {
		resp, err := l.accountCli.ListOrgsAdvance(ctx,
			&account.ListOrgsParam{
				Pager: &base.Pager{
					Page:     page,
					PageSize: 100,
				},
			})
		if err != nil {
			return err
		}

		if resp.BaseResp != nil && resp.BaseResp.ErrCode != 0 {
			return fmt.Errorf("failed to list orgs: %s", resp.BaseResp.ErrMsg)
		}

		// list all orgs
		if len(resp.Orgs) == 0 {
			break
		}

		l.lock.Lock()
		for _, org := range resp.Orgs {
			orgInfoCache[org.Id] = org
		}
		l.lock.Unlock()

		page++

		// sleep 5s to avoid too many requests
		time.Sleep(5 * time.Second)
	}
	return nil
}

// No need to expose
func (l *AllowList) setAffectOrgCreatedTime() error {
	l.lock.Lock()
	defer l.lock.Unlock()

	if l.AffectOrgCreatedAfter == "" {
		return nil
	}

	t, err := time.Parse(time.RFC3339, l.AffectOrgCreatedAfter)
	if err != nil {
		return err
	}
	l.affectOrgCreatedTime = t
	return nil
}

// sort rules by org_id
func (l *AllowList) sortRules() {
	l.lock.Lock()
	defer l.lock.Unlock()

	// sort rules by org_id
	slices.SortFunc(l.Rules, func(a, b orgRule) int {
		return cmp.Compare(a.OrgId, b.OrgId)
	})

	for _, rule := range l.Rules {
		slices.Sort(rule.AllowProjectIds)
		slices.Sort(rule.BlockProjectIds)
	}
}
