package externaladmin

import (
	"context"
	"database/sql"
	"errors"
	"sort"
	"strings"
	"time"

	"hyapp-admin-server/internal/model"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const (
	maxHierarchyAccounts = 5000
	maxParentCandidates  = 500
	// user-service bounds the trusted owner IN-list at 1000. Enforce the same
	// contract before issuing any team request instead of letting an oversized
	// hierarchy fail differently across list, invitation and mutation routes.
	maxExternalScopeOwners = 1000
)

type portalScope struct {
	RegionID                   int64
	OwnerUserIDs               []int64
	ExternalSuperAdminAccounts []model.ExternalAdminAccount
}

func normalizeIdentity(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", model.ExternalAdminIdentityCountryManager:
		// Empty is accepted only for rolling-compatible create callers and historical
		// rows; the persisted/current wire value is always the explicit default.
		return model.ExternalAdminIdentityCountryManager
	case model.ExternalAdminIdentityLocal:
		return model.ExternalAdminIdentityLocal
	case model.ExternalAdminIdentityExternalSuperAdmin:
		return model.ExternalAdminIdentityExternalSuperAdmin
	default:
		return ""
	}
}

func requiredParentIdentity(identity string) (string, bool) {
	switch normalizeIdentity(identity) {
	case model.ExternalAdminIdentityExternalSuperAdmin:
		return model.ExternalAdminIdentityCountryManager, true
	case model.ExternalAdminIdentityCountryManager:
		return model.ExternalAdminIdentityLocal, false
	default:
		return "", false
	}
}

// activeUserRegion resolves the current canonical region instead of copying it into
// the portal account. Joining active regions makes deleted/disabled mappings fail
// closed while retaining an indexed users(app_code,user_id) point lookup.
func (s *Service) activeUserRegion(ctx context.Context, appCode string, userID int64) (int64, error) {
	if s.userDB == nil || userID <= 0 {
		return 0, ErrInvalidInput
	}
	var regionID int64
	err := s.userDB.QueryRowContext(ctx, `
		SELECT u.region_id
		FROM users u
		JOIN regions rg
		  ON rg.app_code = u.app_code
		 AND rg.region_id = u.region_id
		 AND rg.status = 'active'
		WHERE u.app_code = ?
		  AND u.user_id = ?
		  AND u.status = 'active'
		  AND COALESCE(u.region_id, 0) > 0
		LIMIT 1`, normalizeAppCode(appCode), userID).Scan(&regionID)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, nil
	}
	return regionID, err
}

func (s *Service) activeUserRegions(ctx context.Context, appCode string, userIDs []int64) (map[int64]int64, error) {
	result := make(map[int64]int64, len(userIDs))
	uniqueIDs := uniquePositiveInt64s(userIDs)
	if len(uniqueIDs) == 0 {
		return result, nil
	}
	if s.userDB == nil {
		return nil, errors.New("user mysql is not configured")
	}
	placeholders := strings.TrimRight(strings.Repeat("?,", len(uniqueIDs)), ",")
	args := make([]any, 0, len(uniqueIDs)+1)
	args = append(args, normalizeAppCode(appCode))
	for _, userID := range uniqueIDs {
		args = append(args, userID)
	}
	rows, err := s.userDB.QueryContext(ctx, `
		SELECT u.user_id, u.region_id
		FROM users u
		JOIN regions rg
		  ON rg.app_code = u.app_code
		 AND rg.region_id = u.region_id
		 AND rg.status = 'active'
		WHERE u.app_code = ?
		  AND u.status = 'active'
		  AND COALESCE(u.region_id, 0) > 0
		  AND u.user_id IN (`+placeholders+`)`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var userID int64
		var regionID int64
		if err := rows.Scan(&userID, &regionID); err != nil {
			return nil, err
		}
		result[userID] = regionID
	}
	return result, rows.Err()
}

// resolvePortalScope rebuilds the hierarchy on every authenticated request. Status,
// parent changes and region changes therefore take effect without trusting stale
// browser claims or persisting duplicated owner-service state in the admin database.
func (s *Service) resolvePortalScope(ctx context.Context, account model.ExternalAdminAccount) (portalScope, error) {
	scope := portalScope{OwnerUserIDs: []int64{}, ExternalSuperAdminAccounts: []model.ExternalAdminAccount{}}
	account.IdentityType = normalizeIdentity(account.IdentityType)
	if account.IdentityType == "" {
		return scope, ErrInvalidHierarchy
	}
	regionID, err := s.activeUserRegion(ctx, account.AppCode, account.LinkedAppUserID)
	if err != nil {
		return scope, err
	}
	scope.RegionID = regionID
	if regionID == 0 {
		return scope, nil
	}

	valid, err := s.accountParentChainIsActive(ctx, account, regionID)
	if err != nil {
		return scope, err
	}
	if !valid {
		return scope, nil
	}
	scope.OwnerUserIDs = append(scope.OwnerUserIDs, account.LinkedAppUserID)

	descendants, superAdmins, err := s.loadPortalDescendants(ctx, account)
	if err != nil {
		return scope, err
	}
	if len(descendants) == 0 {
		return scope, nil
	}
	userIDs := make([]int64, 0, len(descendants))
	for _, descendant := range descendants {
		userIDs = append(userIDs, descendant.LinkedAppUserID)
	}
	regions, err := s.activeUserRegions(ctx, account.AppCode, userIDs)
	if err != nil {
		return scope, err
	}
	validAccounts := make(map[uint64]struct{}, len(descendants))
	switch account.IdentityType {
	case model.ExternalAdminIdentityCountryManager:
		for _, superAdmin := range superAdmins {
			if superAdmin.ParentAccountID == nil || *superAdmin.ParentAccountID != account.ID || regions[superAdmin.LinkedAppUserID] != regionID {
				continue
			}
			validAccounts[superAdmin.ID] = struct{}{}
		}
	case model.ExternalAdminIdentityLocal:
		validManagers := make(map[uint64]struct{}, len(descendants))
		for _, descendant := range descendants {
			if normalizeIdentity(descendant.IdentityType) != model.ExternalAdminIdentityCountryManager ||
				descendant.ParentAccountID == nil || *descendant.ParentAccountID != account.ID ||
				regions[descendant.LinkedAppUserID] != regionID {
				continue
			}
			validManagers[descendant.ID] = struct{}{}
			validAccounts[descendant.ID] = struct{}{}
		}
		// A Local must not inherit a SuperAdmin through a Manager that moved to
		// another region. Filter by the already validated parent-account set before
		// adding either business scope roots or the SuperAdmin menu projection.
		for _, superAdmin := range superAdmins {
			if superAdmin.ParentAccountID == nil || regions[superAdmin.LinkedAppUserID] != regionID {
				continue
			}
			if _, parentValid := validManagers[*superAdmin.ParentAccountID]; !parentValid {
				continue
			}
			validAccounts[superAdmin.ID] = struct{}{}
		}
	}
	for _, descendant := range descendants {
		if _, valid := validAccounts[descendant.ID]; valid {
			scope.OwnerUserIDs = append(scope.OwnerUserIDs, descendant.LinkedAppUserID)
		}
	}
	for _, superAdmin := range superAdmins {
		if _, valid := validAccounts[superAdmin.ID]; valid {
			scope.ExternalSuperAdminAccounts = append(scope.ExternalSuperAdminAccounts, superAdmin)
		}
	}
	scope.OwnerUserIDs = uniquePositiveInt64s(scope.OwnerUserIDs)
	if len(scope.OwnerUserIDs) > maxExternalScopeOwners {
		return portalScope{OwnerUserIDs: []int64{}, ExternalSuperAdminAccounts: []model.ExternalAdminAccount{}}, ErrInvalidHierarchy
	}
	return scope, nil
}

func (s *Service) accountParentChainIsActive(ctx context.Context, account model.ExternalAdminAccount, regionID int64) (bool, error) {
	return s.accountParentChainIsActiveWithDB(ctx, s.db, account, regionID, false)
}

// accountParentChainIsActiveWithDB validates the complete bounded portal chain
// using the caller's admin transaction when hierarchy rows are being created or
// rebound. The identity order has a fixed maximum depth
// SuperAdmin -> country manager -> Local, so this never becomes an unbounded
// recursive query even if a manually corrupted row points back to a child.
func (s *Service) accountParentChainIsActiveWithDB(
	ctx context.Context,
	db *gorm.DB,
	account model.ExternalAdminAccount,
	regionID int64,
	lockAncestors bool,
) (bool, error) {
	identity := normalizeIdentity(account.IdentityType)
	if identity == model.ExternalAdminIdentityLocal {
		return account.ParentAccountID == nil, nil
	}
	if account.ParentAccountID == nil {
		return identity == model.ExternalAdminIdentityCountryManager, nil
	}
	expectedIdentity, _ := requiredParentIdentity(identity)
	if expectedIdentity == "" {
		return false, nil
	}
	var parent model.ExternalAdminAccount
	query := db.WithContext(ctx)
	if lockAncestors {
		// Parent mutation and account creation already run in a transaction. Share
		// locking every ancestor prevents a concurrent disable/rebind from making a
		// child valid only for the instant between validation and commit.
		query = query.Clauses(clause.Locking{Strength: "SHARE"})
	}
	err := query.Where("id = ? AND app_code = ? AND identity_type = ? AND status = ?",
		*account.ParentAccountID, account.AppCode, expectedIdentity, model.ExternalAdminStatusActive).
		Take(&parent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	parentRegionID, err := s.activeUserRegion(ctx, account.AppCode, parent.LinkedAppUserID)
	if err != nil {
		return false, err
	}
	if parentRegionID != regionID {
		return false, nil
	}
	// A SuperAdmin must not stay authorized through a Manager whose optional
	// Local binding has become invalid. Identity transitions strictly decrease
	// from SuperAdmin -> Manager -> Local, so this checks the complete bounded
	// chain without introducing an unbounded recursive authorization query.
	return s.accountParentChainIsActiveWithDB(ctx, db, parent, regionID, lockAncestors)
}

func (s *Service) loadPortalDescendants(ctx context.Context, account model.ExternalAdminAccount) ([]model.ExternalAdminAccount, []model.ExternalAdminAccount, error) {
	switch normalizeIdentity(account.IdentityType) {
	case model.ExternalAdminIdentityExternalSuperAdmin:
		return []model.ExternalAdminAccount{}, []model.ExternalAdminAccount{}, nil
	case model.ExternalAdminIdentityCountryManager:
		superAdmins, err := s.queryChildren(ctx, account.AppCode, []uint64{account.ID}, model.ExternalAdminIdentityExternalSuperAdmin)
		return superAdmins, superAdmins, err
	case model.ExternalAdminIdentityLocal:
		managers, err := s.queryChildren(ctx, account.AppCode, []uint64{account.ID}, model.ExternalAdminIdentityCountryManager)
		if err != nil {
			return nil, nil, err
		}
		managerIDs := make([]uint64, 0, len(managers))
		for _, manager := range managers {
			managerIDs = append(managerIDs, manager.ID)
		}
		superAdmins, err := s.queryChildren(ctx, account.AppCode, managerIDs, model.ExternalAdminIdentityExternalSuperAdmin)
		if err != nil {
			return nil, nil, err
		}
		descendants := make([]model.ExternalAdminAccount, 0, len(managers)+len(superAdmins))
		descendants = append(descendants, managers...)
		descendants = append(descendants, superAdmins...)
		return descendants, superAdmins, nil
	default:
		return nil, nil, ErrInvalidHierarchy
	}
}

func (s *Service) queryChildren(ctx context.Context, appCode string, parentIDs []uint64, identity string) ([]model.ExternalAdminAccount, error) {
	if len(parentIDs) == 0 {
		return []model.ExternalAdminAccount{}, nil
	}
	items := []model.ExternalAdminAccount{}
	err := s.db.WithContext(ctx).
		Where("app_code = ? AND parent_account_id IN ? AND identity_type = ? AND status = ?",
			normalizeAppCode(appCode), parentIDs, identity, model.ExternalAdminStatusActive).
		Order("id ASC").
		Limit(maxHierarchyAccounts + 1).
		Find(&items).Error
	if err != nil {
		return nil, err
	}
	// Silent truncation would broaden/lose team authorization unpredictably. Fail the
	// request instead, so operators must split an oversized hierarchy deliberately.
	if len(items) > maxHierarchyAccounts {
		return nil, ErrInvalidHierarchy
	}
	return items, nil
}

func (s *Service) validateParentForChild(ctx context.Context, tx *gorm.DB, appCode string, identity string, parentID *uint64, childRegionID int64) (*model.ExternalAdminAccount, error) {
	identity = normalizeIdentity(identity)
	expectedIdentity, parentRequired := requiredParentIdentity(identity)
	if identity == "" || childRegionID <= 0 {
		return nil, ErrInvalidHierarchy
	}
	if parentID == nil {
		if parentRequired {
			return nil, ErrInvalidHierarchy
		}
		return nil, nil
	}
	if *parentID == 0 || expectedIdentity == "" {
		return nil, ErrInvalidHierarchy
	}
	var parent model.ExternalAdminAccount
	err := tx.Clauses(clause.Locking{Strength: "SHARE"}).
		Where("id = ? AND app_code = ? AND identity_type = ? AND status = ?",
			*parentID, normalizeAppCode(appCode), expectedIdentity, model.ExternalAdminStatusActive).
		Take(&parent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, ErrInvalidHierarchy
	}
	if err != nil {
		return nil, err
	}
	parentRegionID, err := s.activeUserRegion(ctx, appCode, parent.LinkedAppUserID)
	if err != nil {
		return nil, err
	}
	if parentRegionID == 0 || parentRegionID != childRegionID {
		return nil, ErrInvalidHierarchy
	}
	// A country manager with a stale/disabled/cross-region Local parent is not a
	// valid SuperAdmin parent. Likewise, a manually corrupted Local row with its
	// own parent cannot authorize a manager. Validate the full chain inside the
	// same transaction instead of creating a child that immediately fails closed.
	valid, err := s.accountParentChainIsActiveWithDB(ctx, tx, parent, childRegionID, true)
	if err != nil {
		return nil, err
	}
	if !valid {
		return nil, ErrInvalidHierarchy
	}
	return &parent, nil
}

func (s *Service) ListParentCandidates(ctx context.Context, input ParentCandidateInput) (ParentCandidateResult, error) {
	if s.db == nil {
		return ParentCandidateResult{}, errors.New("admin mysql is not configured")
	}
	input.AppCode = normalizeAppCode(input.AppCode)
	identity := normalizeIdentity(input.IdentityType)
	if input.AppCode == "" || (strings.TrimSpace(input.IdentityType) != "" && identity == "") {
		return ParentCandidateResult{}, ErrInvalidInput
	}
	var child model.ExternalAdminAccount
	var regionID int64
	if input.ChildAccountID > 0 {
		if err := s.db.WithContext(ctx).Where("id = ? AND app_code = ?", input.ChildAccountID, input.AppCode).Take(&child).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return ParentCandidateResult{}, ErrAccountNotFound
			}
			return ParentCandidateResult{}, err
		}
		identity = normalizeIdentity(child.IdentityType)
		if identity == "" {
			return ParentCandidateResult{}, ErrInvalidHierarchy
		}
		var err error
		regionID, err = s.activeUserRegion(ctx, input.AppCode, child.LinkedAppUserID)
		if err != nil {
			return ParentCandidateResult{}, err
		}
	} else {
		target, err := s.ResolveTarget(ctx, input.AppCode, input.TargetUserID)
		if err != nil {
			return ParentCandidateResult{}, err
		}
		regionID, err = s.activeUserRegion(ctx, input.AppCode, target.UserID)
		if err != nil {
			return ParentCandidateResult{}, err
		}
	}
	expectedIdentity, _ := requiredParentIdentity(identity)
	if expectedIdentity == "" || regionID <= 0 {
		return ParentCandidateResult{Items: []AccountReference{}}, nil
	}

	query := s.db.WithContext(ctx).
		Where("app_code = ? AND identity_type = ? AND status = ?", input.AppCode, expectedIdentity, model.ExternalAdminStatusActive)
	if keyword := strings.TrimSpace(input.Keyword); keyword != "" {
		query = query.Where("username LIKE ?", "%"+keyword+"%")
	}
	candidates := []model.ExternalAdminAccount{}
	if err := query.Order("username ASC, id ASC").Limit(maxParentCandidates + 1).Find(&candidates).Error; err != nil {
		return ParentCandidateResult{}, err
	}
	truncated := len(candidates) > maxParentCandidates
	if truncated {
		candidates = candidates[:maxParentCandidates]
	}
	userIDs := make([]int64, 0, len(candidates))
	for _, candidate := range candidates {
		userIDs = append(userIDs, candidate.LinkedAppUserID)
	}
	regions, err := s.activeUserRegions(ctx, input.AppCode, userIDs)
	if err != nil {
		return ParentCandidateResult{}, err
	}
	users, err := s.loadLinkedUsers(ctx, input.AppCode, userIDs)
	if err != nil {
		return ParentCandidateResult{}, err
	}
	items := make([]AccountReference, 0, len(candidates))
	for _, candidate := range candidates {
		if regions[candidate.LinkedAppUserID] != regionID {
			continue
		}
		// Candidate visibility is authorization-sensitive: a country manager whose
		// optional Local chain has drifted must not be selectable for a new
		// SuperAdmin merely because the manager row itself is still active.
		valid, err := s.accountParentChainIsActive(ctx, candidate, regionID)
		if err != nil {
			return ParentCandidateResult{}, err
		}
		if !valid {
			continue
		}
		items = append(items, accountReference(candidate, users[candidate.LinkedAppUserID]))
	}
	return ParentCandidateResult{Items: items, Truncated: truncated}, nil
}

func (s *Service) UpdateAccountParent(ctx context.Context, appCode string, id uint64, input UpdateParentInput) (ParentUpdateResult, error) {
	appCode = normalizeAppCode(appCode)
	if s.db == nil {
		return ParentUpdateResult{}, errors.New("admin mysql is not configured")
	}
	if appCode == "" || id == 0 || input.ExpectedRevision == 0 {
		return ParentUpdateResult{}, ErrInvalidInput
	}
	result := ParentUpdateResult{}
	var account model.ExternalAdminAccount
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND app_code = ?", id, appCode).Take(&account).Error; err != nil {
			return err
		}
		if account.PermissionRevision != input.ExpectedRevision {
			return ErrParentConflict
		}
		regionID, err := s.activeUserRegion(ctx, appCode, account.LinkedAppUserID)
		if err != nil {
			return err
		}
		if _, err := s.validateParentForChild(ctx, tx, appCode, account.IdentityType, input.ParentAccountID, regionID); err != nil {
			return err
		}
		if equalUint64Pointers(account.ParentAccountID, input.ParentAccountID) {
			return nil
		}
		if err := tx.Model(&account).Updates(map[string]any{
			"parent_account_id":   input.ParentAccountID,
			"permission_revision": account.PermissionRevision + 1,
		}).Error; err != nil {
			return err
		}
		if err := revokeAllSessions(tx, account.ID, "hierarchy_changed", time.Now().UTC().UnixMilli()); err != nil {
			return err
		}
		result.Changed = true
		result.SessionsRevoked = true
		return nil
	})
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return ParentUpdateResult{}, ErrAccountNotFound
	}
	if err != nil {
		return ParentUpdateResult{}, err
	}
	account = model.ExternalAdminAccount{}
	if err := s.db.WithContext(ctx).Where("id = ? AND app_code = ?", id, appCode).Take(&account).Error; err != nil {
		return ParentUpdateResult{}, err
	}
	dto, err := s.accountDTOWithRelations(ctx, account)
	if err != nil {
		return ParentUpdateResult{}, err
	}
	result.Account = dto
	return result, nil
}

func (s *Service) ListExternalSuperAdmins(ctx context.Context, principal SessionPrincipal) (ListResult, error) {
	if normalizeIdentity(principal.IdentityType) == model.ExternalAdminIdentityExternalSuperAdmin {
		return ListResult{}, ErrInvalidHierarchy
	}
	var account model.ExternalAdminAccount
	if err := s.db.WithContext(ctx).Where("id = ? AND app_code = ?", principal.AccountID, principal.AppCode).Take(&account).Error; err != nil {
		return ListResult{}, err
	}
	scope, err := s.resolvePortalScope(ctx, account)
	if err != nil {
		return ListResult{}, err
	}
	users, err := s.loadLinkedUsers(ctx, principal.AppCode, linkedUserIDs(scope.ExternalSuperAdminAccounts))
	if err != nil {
		return ListResult{}, err
	}
	items := make([]AccountDTO, 0, len(scope.ExternalSuperAdminAccounts))
	for _, item := range scope.ExternalSuperAdminAccounts {
		items = append(items, accountDTO(item, users[item.LinkedAppUserID]))
	}
	return ListResult{Items: items, Page: 1, PageSize: len(items), Total: int64(len(items))}, nil
}

func (s *Service) accountDTOWithRelations(ctx context.Context, account model.ExternalAdminAccount) (AccountDTO, error) {
	accountUsers, err := s.loadLinkedUsers(ctx, account.AppCode, []int64{account.LinkedAppUserID})
	if err != nil {
		return AccountDTO{}, err
	}
	var parent *model.ExternalAdminAccount
	parentUsers := map[int64]LinkedUser{}
	if account.ParentAccountID != nil {
		var loaded model.ExternalAdminAccount
		if err := s.db.WithContext(ctx).Where("id = ? AND app_code = ?", *account.ParentAccountID, account.AppCode).Take(&loaded).Error; err == nil {
			parent = &loaded
			parentUsers, err = s.loadLinkedUsers(ctx, account.AppCode, []int64{loaded.LinkedAppUserID})
			if err != nil {
				return AccountDTO{}, err
			}
		} else if !errors.Is(err, gorm.ErrRecordNotFound) {
			return AccountDTO{}, err
		}
	}
	return accountDTOWithParent(account, accountUsers[account.LinkedAppUserID], parent, parentUsers), nil
}

func accountReference(account model.ExternalAdminAccount, user LinkedUser) AccountReference {
	if user.UserID == 0 {
		user.UserID = account.LinkedAppUserID
	}
	return AccountReference{
		ID: account.ID, Username: account.Username, IdentityType: normalizeIdentity(account.IdentityType),
		Status: account.Status, LinkedUser: user,
	}
}

func accountDTOWithParent(account model.ExternalAdminAccount, linkedUser LinkedUser, parent *model.ExternalAdminAccount, users map[int64]LinkedUser) AccountDTO {
	dto := accountDTO(account, linkedUser)
	if parent != nil {
		reference := accountReference(*parent, users[parent.LinkedAppUserID])
		dto.Parent = &reference
	}
	return dto
}

func linkedUserIDs(accounts []model.ExternalAdminAccount) []int64 {
	ids := make([]int64, 0, len(accounts))
	for _, account := range accounts {
		ids = append(ids, account.LinkedAppUserID)
	}
	return ids
}

func uniquePositiveInt64s(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	result := make([]int64, 0, len(values))
	for _, value := range values {
		if value <= 0 {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
	return result
}

func equalUint64Pointers(left *uint64, right *uint64) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return *left == *right
}
