package main

import (
	"fmt"

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

type Doctor struct {
	DoFix   bool
	Output  *Output
	User    map[uint64]*entity.User
	Project map[uint64]*UserProjectBinding
}

type DoctorHandler func(Target, *TargetRoles, string, []CheckError) (bool, []string)

func (d *Doctor) AddUser(user *entity.User) {
	id := user.ID.Uint64()
	d.User[id] = user
}

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

func (d *Doctor) GetUser(id uint64) *entity.User {
	return d.User[id]
}

func (d *Doctor) isUserOrgChangedManually(target Target, roles *TargetRoles, result string, errList []CheckError) (bool, []string) {
	if target.Type != entity.RoleBindingTargetUser || result != CheckResultConflict {
		return false, nil
	}

	var messages []string
	orgFrom := make([]string, 0)
	orgTo := make([]string, 0)

	for _, err := range errList {
		if err.ScopeType == entity.RoleBindingScopeOrg {
			if err.Count == 0 {
				continue
			}

			m := fmt.Sprintf("%s:%d (%s)", err.ScopeType, err.ScopeID, err.Role)
			if err.Count > 0 {
				orgFrom = append(orgFrom, m)

			} else if err.Count < 0 {
				orgTo = append(orgTo, m)
			}
		}
	}

	if len(orgFrom) > 0 && len(orgTo) > 0 {
		m := fmt.Sprintf("user %d changed org from %v to %v, probably by manually database operation",
			target.ID, orgFrom, orgTo)
		messages = append(messages, m)
	}

	return true, messages
}

func (d *Doctor) isUserDeletedManually(target Target, roles *TargetRoles, result string, errList []CheckError) (bool, []string) {
	if target.Type != entity.RoleBindingTargetUser {
		return false, nil
	}

	messages := make([]string, 0)
	user := d.GetUser(target.ID.Uint64())
	if user == nil {
		m := fmt.Sprintf("user %d is not existed, MAY BE deleted manually",
			target.ID.Uint64())
		messages = append(messages, m)
	}

	return true, messages
}

func (d *Doctor) isUserWithMultipleOrgRoles(target Target, roles *TargetRoles, result string, errList []CheckError) (bool, []string) {
	if target.Type != entity.RoleBindingTargetUser {
		return false, nil
	}

	idMap := map[uint64]int{}
	oldRelation := make([]string, 0)
	newRelation := make([]string, 0)

	for _, err := range errList {
		if err.ScopeType != entity.RoleBindingScopeOrg {
			continue
		}

		org := fmt.Sprintf("%s:%d (%s)", err.ScopeType, err.ScopeID, err.Role)
		idMap[err.ScopeID.Uint64()] += 1

		if err.Count > 0 {
			newRelation = append(newRelation, org)

		} else if err.Count == 0 {
			oldRelation = append(oldRelation, org)

		} else {
			return false, nil
		}
	}

	if len(newRelation) == 0 || (len(oldRelation) == 0 && len(newRelation) == 1) {
		return false, nil
	}

	var messages []string
	if len(oldRelation) == 0 && len(idMap) > 1 {
		m := fmt.Sprintf("old relation is empty, and user in multiple orgs, poly-org MAY BE affected, %v", newRelation)
		messages = append(messages, m)
	}

	conflict := false
	for _, c := range idMap {
		if c > 1 {
			conflict = true
			break
		}
	}

	if conflict {
		m := fmt.Sprintf("user %d have multiple orgs roles in conflict old=%v new=%v",
			target.ID, oldRelation, newRelation)
		messages = append(messages, m)

	} else {
		m := fmt.Sprintf("user %d is in multiple orgs, user-orgs: %v, role-bindings: %v",
			target.ID, oldRelation, newRelation)
		messages = append(messages, m)
	}

	return true, messages
}

func (d *Doctor) isSystemUserCreatedByOthers(target Target, roles *TargetRoles, result string, errList []CheckError) (bool, []string) {
	if target.Type != entity.RoleBindingTargetUser {
		return false, nil
	}

	userID := target.ID.Uint64()
	user := d.GetUser(userID)
	if user == nil {
		return false, nil
	}

	if !isSystemUser(user) {
		return false, nil
	}

	orgOwnerFound := false
	orgOtherFound := false
	otherRoles := make([]string, 0)

	for _, err := range errList {
		if err.ScopeType != entity.RoleBindingScopeOrg {
			continue
		}

		if err.Count >= 0 {
			continue
		}

		if err.ScopeID == user.TenantID {
			if err.Role == entity.RoleOrgOwner {
				orgOwnerFound = true

			} else {
				orgOtherFound = true
				otherRoles = append(otherRoles, err.Role)
			}
		}
	}

	messages := make([]string, 0)
	if !orgOwnerFound && !orgOtherFound {
		return false, nil

	} else if orgOwnerFound && !orgOtherFound {
		m := fmt.Sprintf("system user %d is created by others, without writing role binding",
			userID)
		messages = append(messages, m)

	} else if !orgOwnerFound && orgOtherFound {
		m := fmt.Sprintf("system user %d is created by others, and changed to role %v",
			userID, otherRoles)
		messages = append(messages, m)
	} else if orgOwnerFound && orgOtherFound {
		m := fmt.Sprintf("system user %d is created by others, with both owner and other roles [INVALID]",
			userID)
		messages = append(messages, m)
	}

	return true, messages
}

func (d *Doctor) CallDoctorList(target Target, roles *TargetRoles, result string, errList []CheckError, doctors []DoctorHandler) {
	var detected bool
	var messages []string
	for _, doctor := range doctors {
		var m []string
		detected, m = doctor(target, roles, result, errList)
		if detected {
			messages = append(messages, m...)
		}
	}

	OutputWithInfo(d.Output, target, roles, result, errList, messages)
}

func (d *Doctor) FixCallback(target Target, roles *TargetRoles, result string, errList []CheckError) {
	doctors := []DoctorHandler{
		d.isUserOrgChangedManually,
		d.isUserDeletedManually,
		d.isUserWithMultipleOrgRoles,
		d.isSystemUserCreatedByOthers,
	}

	d.CallDoctorList(target, roles, result, errList, doctors)
}

func NewDoctor(out *Output) *Doctor {
	doctor := &Doctor{
		User:   make(map[uint64]*entity.User),
		Output: out,
	}

	return doctor
}
