package plugin

import (
	"fmt"
	"strings"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
)

const (
	dedicatedAccountPluginName   = "DedicatedAccount"
	dedicatedAcctAnnoForProjects = "account.cloud.pingcap.com/dedicated-for-projects"
	dedicatedAcctAnnoForTenants  = "account.cloud.pingcap.com/dedicated-for-tenants"
)

var (
	_ FilterPlugin = &dedicatedAccount{}
)

type dedicatedAccountState struct {
	acctForProjects map[string][]string // key: account name, value: project ids
	acctForTenants  map[string][]string // key: account name, value: tenant ids
}

func (c *dedicatedAccountState) hasAcct(acct string) (has bool, projects []string, tenants []string) {
	if _, exist := c.acctForProjects[acct]; exist {
		has = true
		projects = c.acctForProjects[acct]
	}
	if _, exist := c.acctForTenants[acct]; exist {
		has = true
		tenants = c.acctForTenants[acct]
	}
	return
}

func (c *dedicatedAccountState) hasNetwork(network *cloudv1.Network) (bool, string) {
	projectID := network.Labels[cloudv1.ProjectTagKey]
	tenantID := network.Labels[cloudv1.TenantTagKey]
	if projectID != "" {
		if has, acct := c.hasProject(projectID); has {
			return true, acct
		}
	}
	if tenantID != "" {
		if has, acct := c.hasTenant(tenantID); has {
			return true, acct
		}
	}
	return false, ""
}

func (c *dedicatedAccountState) hasProject(pid string) (bool, string) {
	for acct, pids := range c.acctForProjects {
		for _, p := range pids {
			if p == pid {
				return true, acct
			}
		}
	}
	return false, ""
}

func (c *dedicatedAccountState) hasTenant(tid string) (bool, string) {
	for acct, tids := range c.acctForTenants {
		for _, t := range tids {
			if t == tid {
				return true, acct
			}
		}
	}
	return false, ""
}

// dedicatedAccount binds networks of specific business to a dedicated account, and other network
// can not use these accounts.
//
// TODO: let platform to set the `accountRef` when creating a Network to skip schedule
type dedicatedAccount struct {
}

func newDedicatedAccount(cfg config.AcctSchedPluginConfig) (Plugin, error) {
	return &dedicatedAccount{}, nil
}

func (p *dedicatedAccount) Name() string {
	return dedicatedAccountPluginName
}

func (p *dedicatedAccount) PreFilter(ctx *SchedContext, network *cloudv1.Network, accts []*cloudv1.Account) *ActionResult {
	state := &dedicatedAccountState{
		acctForProjects: map[string][]string{},
		acctForTenants:  map[string][]string{},
	}
	for _, acct := range accts {
		if dedicated, projects, tenants := p.isDedicatedAcct(acct); dedicated {
			state.acctForProjects[acct.Name] = projects
			state.acctForTenants[acct.Name] = tenants
		}
	}
	ctx.State[p.Name()] = state

	return NewActionResult(Success)
}

func (p *dedicatedAccount) Filter(ctx *SchedContext, network *cloudv1.Network, acct *cloudv1.Account) *ActionResult {
	state, ok := ctx.State[p.Name()].(*dedicatedAccountState)
	if !ok {
		return NewActionResult(Error, fmt.Sprintf("context of plugin %s not found in context", p.Name()))
	}
	isDcAcct, _, _ := state.hasAcct(acct.Name)
	isSpNetwork, acctName := state.hasNetwork(network)

	if isSpNetwork {
		if acctName == acct.Name {
			ctx.Logger.WithValues("plugin", p.Name(), "account", acct.Name).Info("use a dedicated account for this network")
			return NewActionResult(Success)
		}

		// network must use another account
		return NewActionResult(Unschedulable, fmt.Sprintf("must use account %s", acctName))
	}

	if isDcAcct {
		// account is dedicated, but not for this network
		return NewActionResult(Unschedulable, fmt.Sprintf("can not use the dedicated account %s", acct.Name))
	}

	// not a dedicated account, and not a specific network, so this account can be used by this network
	return NewActionResult(Success)
}

func (p *dedicatedAccount) isDedicatedAcct(acct *cloudv1.Account) (is bool, projects []string, tenants []string) {
	if projectStr, exist := acct.Annotations[dedicatedAcctAnnoForProjects]; exist {
		is = true
		if projectStr != "" {
			projects = strings.Split(projectStr, ",")
		}
	}
	if tenantStr, exist := acct.Annotations[dedicatedAcctAnnoForTenants]; exist {
		is = true
		if tenantStr != "" {
			tenants = strings.Split(tenantStr, ",")
		}
	}
	return
}
