package monitor

import (
	"context"
	"errors"
	"fmt"
	"sync"

	"github.com/tidbcloud/account/commons/database"

	"github.com/google/uuid"
	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/dao/role"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type RoleBindingMonitorSvcIface interface {
	MonitorRoleBinding(ctx context.Context, fixDirtyData bool)
}

var (
	roleBindingMonitorSvcInitOnce sync.Once
	roleBindingMonitorSvcInstance RoleBindingMonitorSvcIface
)

func GetRoleBindingMonitorSvc() (RoleBindingMonitorSvcIface, error) {
	roleBindingMonitorSvcInitOnce.Do(func() {
		roleBindingMonitorSvcInstance = NewRoleBindingMonitorSvc(database.DBConn.DB, dao.GetDaoHub())
	})
	return roleBindingMonitorSvcInstance, nil
}

type RoleBindingMonitorSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewRoleBindingMonitorSvc(db *gorm.DB, hub *dao.DaoHub) RoleBindingMonitorSvcIface {
	return &RoleBindingMonitorSvc{
		db:  db,
		hub: hub,
	}
}

func (s *RoleBindingMonitorSvc) MonitorRoleBinding(ctx context.Context, fixDirtyData bool) {
	monitorFs := []MonitorRoleBindingFunc{UserProjectRoleMonitorFunc}
	s.monitorRoleBindingTable(ctx, fixDirtyData, monitorFs)
}

func (s *RoleBindingMonitorSvc) monitorRoleBindingTable(ctx context.Context, fixDirtyData bool, monitorFs []MonitorRoleBindingFunc) {
	tilog.Info(ctx, "MonitorRoleBindingTable start")
	var start uint64 = 0
	var limit = 10
	for {
		tilog.Info(ctx, fmt.Sprintf("MonitorRoleBindingTable get startID %d", start))
		roleBindings, err := s.hub.Role.BatchSelectRoleBindings(ctx, s.db, start, limit)
		if err != nil {
			tilog.Error(ctx, "MonitorRoleBindingTable BatchSelectRoleBindings err", zap.Any("err", err))
			return
		}
		if len(roleBindings) == 0 {
			tilog.Info(ctx, "MonitorRoleBindingTable end")
			return
		}
		for _, binding := range roleBindings {
			// set the max id to start
			if binding.ID.Uint64() > start {
				start = binding.ID.Uint64()
			}

			traceID := uuid.New().String()
			tilog.Info(ctx, fmt.Sprintf("MonitorRoleBindingTable handle roleBinding %d", binding.ID.Uint64()), zap.Any("traceID", traceID), zap.Any("roleBinding", binding))

			p := MonitorRoleBindingParam{
				TraceID:      traceID,
				Db:           s.db,
				Hub:          s.hub,
				RoleBinding:  binding,
				FixDirtyData: fixDirtyData,
			}

			for _, f := range monitorFs {
				f(ctx, &p)
			}
		}
	}
}

type MonitorRoleBindingParam struct {
	TraceID      string
	Db           *gorm.DB
	Hub          *dao.DaoHub
	RoleBinding  entity.RoleBinding
	FixDirtyData bool
}

type MonitorRoleBindingFunc func(ctx context.Context, p *MonitorRoleBindingParam)

func UserProjectRoleMonitorFunc(ctx context.Context, p *MonitorRoleBindingParam) {
	roleBinding := p.RoleBinding
	hub := p.Hub
	db := p.Db

	if roleBinding.Status == entity.RoleBindingStatusActive {
		if roleBinding.TargetType == entity.RoleBindingTargetUser {
			user, err := hub.User.GetUserByID(ctx, db, roleBinding.TargetID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					user = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetUserByID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if user == nil || user.Status != entity.UserStatusActive {
				tilog.Error(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, role_binding status is active, but user not exist or status not active, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
				return
			}
		} else if roleBinding.TargetType == entity.RoleBindingTargetAPIKey {
			apiKey, err := hub.ApiKey.GetApiKeyByID(ctx, db, roleBinding.TargetID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					apiKey = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetApiKeyByID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if apiKey == nil || apiKey.Status != entity.Active {
				tilog.Error(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, role_binding status is active, but api_key not exist or is deleted, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
				return
			}
		}

		if roleBinding.ScopeType == entity.RoleBindingScopeOrg {
			org, err := hub.Org.GetOrgByOrgID(ctx, db, roleBinding.ScopeID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					org = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetOrgByOrgID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if org == nil || org.Status != entity.TenantStatusActive {
				tilog.Error(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, identity have org role, but org not exist or status not active, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
			}

		} else if roleBinding.ScopeType == entity.RoleBindingScopeProject {
			project, err := hub.Project.GetProjectByID(ctx, db, roleBinding.ScopeID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					project = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetProjectByID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if project == nil || project.Status != entity.ProjectStatusActive {
				tilog.Error(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, identity have project role, but project not exist or status not active, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
				return
			}

			if roleBinding.TargetType == entity.RoleBindingTargetUser {
				filter := role.RoleBindingFilter{
					ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
					ScopeIDs:   []entity.ID{project.TenantID},
					TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
					TargetIDs:  []entity.ID{roleBinding.TargetID},
				}
				orgRoleBindings, err := hub.Role.ListRoleBindings(ctx, db, filter)
				if err != nil {
					tilog.Error(ctx, "MonitorRoleBindingFunc list org roleBinding error", zap.Any("traceID", p.TraceID), zap.Any("filter", filter), zap.Any("err", err))
					return
				}
				// check user have org role
				if len(orgRoleBindings) == 0 {
					tilog.Error(ctx, fmt.Sprintf(
						"MonitorRoleBindingFunc found dirty data, identity have project role, but do not have org role, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
						roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
					if p.FixDirtyData {
						fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
					}
				} else {
					orgRoleBinding := orgRoleBindings[0]
					// user ref project role is active and user must have relative org active role
					if orgRoleBinding.Status != entity.RoleBindingStatusActive {
						tilog.Error(ctx, fmt.Sprintf(
							"MonitorRoleBindingFunc found dirty data, identity have project active role, but relate org role is not active, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
							roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
						if p.FixDirtyData {
							fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
						}
					}
				}
			}

		}

	} else {
		if roleBinding.TargetType == entity.RoleBindingTargetUser {
			user, err := hub.User.GetUserByID(ctx, db, roleBinding.TargetID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					user = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetUserByID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if user == nil || (user.Status != entity.UserStatusActive && user.Status != entity.UserStatusPending) {
				tilog.Warn(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, inactive role_binding relate user, but user not exist or status not active/pending, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
				return
			}
		} else if roleBinding.TargetType == entity.RoleBindingTargetAPIKey {
			apiKey, err := hub.ApiKey.GetApiKeyByID(ctx, db, roleBinding.TargetID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					apiKey = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetApiKeyByID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if apiKey == nil || apiKey.Status != entity.Active {
				tilog.Warn(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, inactive role_binding relate api_key, but api_key not exist or is deleted, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
			}
		}

		if roleBinding.ScopeType == entity.RoleBindingScopeOrg {
			org, err := hub.Org.GetOrgByOrgID(ctx, db, roleBinding.ScopeID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					org = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetOrgByOrgID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if org == nil || (org.Status != entity.TenantStatusActive && org.Status != entity.TenantStatusPending) {
				tilog.Warn(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, identity have inactive org role, but org not exist or status not active, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
			}

		} else if roleBinding.ScopeType == entity.RoleBindingScopeProject {
			project, err := hub.Project.GetProjectByID(ctx, db, roleBinding.ScopeID.Uint64())
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					project = nil
				} else {
					tilog.Error(ctx, "MonitorRoleBindingFunc GetProjectByID error", zap.Any("traceID", p.TraceID), zap.Any("err", err))
					return
				}
			}
			if project == nil || project.Status != entity.ProjectStatusActive {
				tilog.Warn(ctx, fmt.Sprintf(
					"MonitorRoleBindingFunc found dirty data, identity have inactive project role, but project not exist or status not active, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
					roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
				if p.FixDirtyData {
					fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
				}
				return
			}

			if roleBinding.TargetType == entity.RoleBindingTargetUser {
				filter := role.RoleBindingFilter{
					ScopeTypes: []entity.ScopeType{entity.RoleBindingScopeOrg},
					ScopeIDs:   []entity.ID{project.TenantID},
					TargetType: []entity.TargetType{entity.RoleBindingTargetUser},
					TargetIDs:  []entity.ID{roleBinding.TargetID},
				}
				orgRoleBindings, err := hub.Role.ListRoleBindings(ctx, db, filter)
				if err != nil {
					tilog.Error(ctx, "MonitorRoleBindingFunc list org roleBinding error", zap.Any("traceID", p.TraceID), zap.Any("filter", filter), zap.Any("err", err))
					return
				}
				// check user have org role
				if len(orgRoleBindings) == 0 {
					tilog.Error(ctx, fmt.Sprintf(
						"MonitorRoleBindingFunc found dirty data, identity have inactive project role, but do not have org role, role_binding_id: %d, target_type: %s, target_id: %d, scope_type: %s, scope_id: %d",
						roleBinding.ID.Uint64(), roleBinding.TargetType.String(), roleBinding.TargetID.Uint64(), roleBinding.ScopeType.String(), roleBinding.ScopeID.Uint64()), zap.Any("traceID", p.TraceID))
					if p.FixDirtyData {
						fixDirtyData(ctx, db, hub, roleBinding, p.TraceID)
					}
				}
			}

		}
	}
}

func fixDirtyData(ctx context.Context, db *gorm.DB, hub *dao.DaoHub, roleBinding entity.RoleBinding, traceID string) {
	err := hub.Role.DeleteRoleByID(ctx, db, roleBinding.ID.Uint64())
	if err != nil {
		tilog.Error(ctx, "MonitorRoleBindingFunc delete dirty data error", zap.Any("traceID", traceID), zap.Any("role_binding", roleBinding), zap.Any("err", err))
	} else {
		tilog.Info(ctx, "MonitorRoleBindingFunc delete dirty data succ", zap.Any("traceID", traceID), zap.Any("role_binding", roleBinding))
	}
}
