package main

import (
	"fmt"
	"strings"

	"github.com/tidbcloud/account/internel/dao/entity"
)

const (
	CheckResultDualWriteOK = "dual-write-ok"
	CheckResultMissing     = "missing"
	CheckResultPolyOrgOK   = "poly-org-ok"
	CheckResultConflict    = "conflict"
)

type Target struct {
	Type entity.TargetType
	ID   entity.ID
}

type ScopeRole struct {
	Type entity.ScopeType
	ID   entity.ID
	Role string
}

type TargetRoles struct {
	Target Target
	Roles  map[ScopeRole]int
}

func NewTargetRoles(target Target) *TargetRoles {
	return &TargetRoles{
		Target: target,
		Roles:  make(map[ScopeRole]int),
	}
}

func (g *TargetRoles) addRole(role ScopeRole, count int) int {
	g.Roles[role] += count
	return g.Roles[role]
}

func (g *TargetRoles) AddRole(role ScopeRole) int {
	return g.addRole(role, 1)
}

func (g *TargetRoles) RemoveRole(role ScopeRole) int {
	return g.addRole(role, -1)
}

type CheckError struct {
	TargetType entity.TargetType
	TargetID   entity.ID
	ScopeType  entity.ScopeType
	ScopeID    entity.ID
	Role       string
	Count      int
}

func (e *CheckError) String() string {
	pattern := ""
	if e.Count > 0 {
		pattern = "[FAIL] role '%s' on %s:%d for target %s:%d in RB(%d)"

	} else if e.Count < 0 {
		pattern = "[FAIL] role '%s' on %s:%d for target %s:%d in RB(%d)"

	} else {
		pattern = "[ OK ] role '%s' on %s:%d for target %s:%d in RB(%d)"
	}

	return fmt.Sprintf(pattern, e.Role, e.ScopeType, e.ScopeID, e.TargetType, e.TargetID, e.Count)
}

func (g *TargetRoles) makeStatus(all bool) (string, []CheckError) {
	errList := make([]CheckError, 0)
	zero, positive, negative := 0, 0, 0
	for scope, count := range g.Roles {
		if count == 0 {
			zero++
			if !all {
				continue
			}
		}

		err := CheckError{
			TargetType: g.Target.Type,
			TargetID:   g.Target.ID,
			ScopeType:  scope.Type,
			ScopeID:    scope.ID,
			Role:       scope.Role,
			Count:      count,
		}
		errList = append(errList, err)
		if count > 0 {
			positive++

		} else if count < 0 {
			negative++
		}
	}

	result := ""
	if positive == 0 && negative == 0 {
		result = CheckResultDualWriteOK

	} else if positive == 0 && negative > 0 {
		result = CheckResultMissing

	} else if positive > 0 && negative == 0 {
		result = CheckResultPolyOrgOK

	} else {
		result = CheckResultConflict
	}

	return result, errList
}

func (g *TargetRoles) Status() (string, []CheckError) {
	return g.makeStatus(false)
}

func (g *TargetRoles) FullStatus() (string, []CheckError) {
	return g.makeStatus(true)
}

type RoleChecker struct {
	Roles map[Target]*TargetRoles
}

func NewRoleChecker() *RoleChecker {
	return &RoleChecker{
		Roles: make(map[Target]*TargetRoles),
	}
}

func (c *RoleChecker) Add(target Target, scope ScopeRole) {
	if _, ok := c.Roles[target]; !ok {
		c.Roles[target] = NewTargetRoles(target)
	}

	c.Roles[target].AddRole(scope)
}

func (c *RoleChecker) Remove(target Target, scope ScopeRole) {
	if _, ok := c.Roles[target]; !ok {
		c.Roles[target] = NewTargetRoles(target)
	}

	c.Roles[target].RemoveRole(scope)
}

func MakeOnRoleBinding(role *entity.RoleBinding) (Target, ScopeRole) {
	target := Target{
		Type: role.TargetType,
		ID:   role.TargetID,
	}

	scope := ScopeRole{
		Type: role.ScopeType,
		ID:   role.ScopeID,
		Role: role.Role,
	}

	return target, scope
}

func (c *RoleChecker) AddRole(role *entity.RoleBinding) {
	target, scope := MakeOnRoleBinding(role)
	c.Add(target, scope)
}

func (c *RoleChecker) AddRoles(roles []entity.RoleBinding) {
	for _, role := range roles {
		c.AddRole(&role)
	}
}

func MakeOrgRoletOnUser(user *entity.User) (Target, ScopeRole) {
	target := Target{
		Type: entity.RoleBindingTargetUser,
		ID:   user.ID,
	}

	scope := ScopeRole{
		Type: entity.RoleBindingScopeOrg,
		ID:   user.TenantID,
		Role: user.Role.Encode(),
	}

	return target, scope
}

func (c *RoleChecker) AddUser(user *entity.User) {
	target, scope := MakeOrgRoletOnUser(user)
	c.Remove(target, scope)
}

func (c *RoleChecker) AddUsers(users []entity.User) {
	for _, user := range users {
		c.AddUser(&user)
	}
}

func MakeProjectRoleOnUserProjectBinding(binding *UserProjectBinding) (Target, ScopeRole) {
	target := Target{
		Type: entity.RoleBindingTargetUser,
		ID:   binding.UserID,
	}

	scope := ScopeRole{
		Type: entity.RoleBindingScopeProject,
		ID:   binding.ProjectID,
		Role: entity.RoleProjectOwner,
	}

	return target, scope
}

func (c *RoleChecker) AddProject(project UserProjectBinding) {
	target, scope := MakeProjectRoleOnUserProjectBinding(&project)
	c.Remove(target, scope)
}

func (c *RoleChecker) AddProjects(projects []UserProjectBinding) {
	for _, project := range projects {
		c.AddProject(project)
	}
}

func isExpected(s string, array []string) bool {
	for _, x := range array {
		if x == s {
			return true
		}
	}

	return false
}

type ReportCallback func(target Target, roles *TargetRoles, result string, errList []CheckError)

func (c *RoleChecker) ReportOn(out *Output, target Target, expected []string, cb ReportCallback) bool {
	roles, ok := c.Roles[target]
	if !ok {
		return true
	}

	result, errList := roles.FullStatus()
	if isExpected(result, expected) {
		return true
	}

	cb(target, roles, result, errList)
	return false
}

func (c *RoleChecker) Report(out *Output, expecteds []string, cb ReportCallback) (int, int) {
	total, success := 0, 0
	for target := range c.Roles {
		ok := c.ReportOn(out, target, expecteds, cb)
		total += 1
		if ok {
			success += 1
		}
	}

	out.Printf(strings.Repeat("-", 100)+"\n", []string{})
	out.Printf("total targets: %d, success: %d\n", total, success)
	return total, success
}

func OutputWithInfo(out *Output, target Target, roles *TargetRoles, result string, errList []CheckError, msgList []string) {
	out.Printf("[%s] %s:%d\n", result, target.Type, target.ID)
	for _, err := range errList {
		out.Printf("  - %s\n", err.String())
	}

	for _, msg := range msgList {
		out.Printf("  + NOTE: %s\n", msg)
	}
}

func isSystemUser(user *entity.User) bool {
	if user != nil {
		return strings.Contains(user.Email, "-system@pingcap.com")
	}

	return false
}
