package mysql

import (
	"context"

	"gorm.io/gorm"

	"wucher/internal/domain/auth"
	"wucher/internal/domain/user"
)

type userRoleJoinRow struct {
	UserID          []byte `gorm:"column:user_id"`
	RoleID          []byte `gorm:"column:role_id"`
	RoleName        string `gorm:"column:role_name"`
	RoleDescription string `gorm:"column:role_description"`
}

func normalizeAssignedRoleIDs(primaryRoleID []byte, roleIDs [][]byte) [][]byte {
	out := make([][]byte, 0, len(roleIDs)+1)
	seen := make(map[string]struct{}, len(roleIDs)+1)

	appendRole := func(roleID []byte) {
		if len(roleID) != 16 {
			return
		}
		key := string(roleID)
		if _, exists := seen[key]; exists {
			return
		}
		seen[key] = struct{}{}
		out = append(out, append([]byte(nil), roleID...))
	}

	appendRole(primaryRoleID)
	for i := range roleIDs {
		appendRole(roleIDs[i])
	}

	return out
}

func replaceUserRolesTx(ctx context.Context, tx *gorm.DB, userID, primaryRoleID []byte, roleIDs [][]byte) error {
	normalized := normalizeAssignedRoleIDs(primaryRoleID, roleIDs)
	if len(userID) != 16 {
		return nil
	}

	if err := tx.Where("user_id = ?", userID).Delete(&auth.UserRole{}).Error; err != nil {
		return err
	}
	if len(normalized) == 0 {
		return nil
	}

	actor := actorUserIDFromContext(ctx)
	items := make([]auth.UserRole, 0, len(normalized))
	for i := range normalized {
		item := auth.UserRole{
			UserID: userID,
			RoleID: normalized[i],
		}
		if len(actor) > 0 {
			item.CreatedBy = actor
			item.UpdatedBy = actor
		}
		items = append(items, item)
	}

	return tx.Create(&items).Error
}

func listUserRoleIDsTx(tx *gorm.DB, userID []byte) ([][]byte, error) {
	var rows []struct {
		RoleID []byte `gorm:"column:role_id"`
	}
	if err := tx.Table("user_roles").
		Select("role_id").
		Where("user_id = ?", userID).
		Find(&rows).Error; err != nil {
		return nil, err
	}

	roleIDs := make([][]byte, 0, len(rows))
	for i := range rows {
		if len(rows[i].RoleID) != 16 {
			continue
		}
		roleIDs = append(roleIDs, append([]byte(nil), rows[i].RoleID...))
	}
	return roleIDs, nil
}

func replacePrimaryUserRoleTx(ctx context.Context, tx *gorm.DB, userID, currentPrimaryRoleID, newPrimaryRoleID []byte) error {
	roleIDs, err := listUserRoleIDsTx(tx, userID)
	if err != nil {
		return err
	}

	filtered := make([][]byte, 0, len(roleIDs))
	currentPrimaryKey := string(currentPrimaryRoleID)
	newPrimaryKey := string(newPrimaryRoleID)
	for i := range roleIDs {
		key := string(roleIDs[i])
		if key == currentPrimaryKey || key == newPrimaryKey {
			continue
		}
		filtered = append(filtered, roleIDs[i])
	}

	return replaceUserRolesTx(ctx, tx, userID, newPrimaryRoleID, filtered)
}

func loadUserRoleRows(ctx context.Context, db *gorm.DB, userIDs [][]byte) (map[string][]userRoleJoinRow, error) {
	rowsByUserID := make(map[string][]userRoleJoinRow, len(userIDs))
	if len(userIDs) == 0 {
		return rowsByUserID, nil
	}

	var rows []userRoleJoinRow
	if err := db.WithContext(ctx).
		Table("user_roles ur").
		Select("ur.user_id, roles.id AS role_id, roles.name AS role_name, roles.description AS role_description").
		Joins("JOIN roles ON roles.id = ur.role_id").
		Where("ur.user_id IN ?", userIDs).
		Order("roles.name ASC").
		Find(&rows).Error; err != nil {
		return nil, err
	}

	for i := range rows {
		key := string(rows[i].UserID)
		rowsByUserID[key] = append(rowsByUserID[key], userRoleJoinRow{
			UserID:          append([]byte(nil), rows[i].UserID...),
			RoleID:          append([]byte(nil), rows[i].RoleID...),
			RoleName:        rows[i].RoleName,
			RoleDescription: rows[i].RoleDescription,
		})
	}

	return rowsByUserID, nil
}

func orderUserRoleRows(primaryRoleID []byte, rows []userRoleJoinRow) []userRoleJoinRow {
	if len(rows) < 2 || len(primaryRoleID) != 16 {
		return rows
	}

	primaryKey := string(primaryRoleID)
	ordered := make([]userRoleJoinRow, 0, len(rows))
	for i := range rows {
		if string(rows[i].RoleID) == primaryKey {
			ordered = append(ordered, rows[i])
			break
		}
	}
	for i := range rows {
		if string(rows[i].RoleID) == primaryKey {
			continue
		}
		ordered = append(ordered, rows[i])
	}
	return ordered
}

func loadRoleRowsByID(ctx context.Context, db *gorm.DB, roleIDs [][]byte) (map[string]userRoleJoinRow, error) {
	out := make(map[string]userRoleJoinRow, len(roleIDs))
	if len(roleIDs) == 0 {
		return out, nil
	}

	var rows []struct {
		ID          []byte `gorm:"column:id"`
		Name        string `gorm:"column:name"`
		Description string `gorm:"column:description"`
	}
	if err := db.WithContext(ctx).
		Table("roles").
		Select("id, name, description").
		Where("id IN ?", roleIDs).
		Find(&rows).Error; err != nil {
		return nil, err
	}

	for i := range rows {
		out[string(rows[i].ID)] = userRoleJoinRow{
			RoleID:          append([]byte(nil), rows[i].ID...),
			RoleName:        rows[i].Name,
			RoleDescription: rows[i].Description,
		}
	}

	return out, nil
}

func attachAuthUserRoles(ctx context.Context, db *gorm.DB, users ...*auth.User) error {
	userIDs := make([][]byte, 0, len(users))
	missingPrimaryRoleIDs := make([][]byte, 0, len(users))
	for i := range users {
		if users[i] == nil || len(users[i].ID) != 16 {
			continue
		}
		userIDs = append(userIDs, users[i].ID)
		if len(users[i].RoleID) == 16 {
			missingPrimaryRoleIDs = append(missingPrimaryRoleIDs, users[i].RoleID)
		}
	}

	rowsByUserID, err := loadUserRoleRows(ctx, db, userIDs)
	if err != nil {
		return err
	}
	roleRowsByID, err := loadRoleRowsByID(ctx, db, missingPrimaryRoleIDs)
	if err != nil {
		return err
	}

	for i := range users {
		if users[i] == nil {
			continue
		}

		rows := orderUserRoleRows(users[i].RoleID, rowsByUserID[string(users[i].ID)])
		if len(rows) == 0 && len(users[i].RoleID) == 16 {
			if roleRow, ok := roleRowsByID[string(users[i].RoleID)]; ok {
				rows = []userRoleJoinRow{roleRow}
			}
		}

		users[i].RoleIDs = make([][]byte, 0, len(rows))
		users[i].Roles = make([]auth.Role, 0, len(rows))
		for j := range rows {
			users[i].RoleIDs = append(users[i].RoleIDs, append([]byte(nil), rows[j].RoleID...))
			users[i].Roles = append(users[i].Roles, auth.Role{
				ID:          append([]byte(nil), rows[j].RoleID...),
				Name:        rows[j].RoleName,
				Description: rows[j].RoleDescription,
			})
		}
	}

	return nil
}

func attachUserDomainRoles(ctx context.Context, db *gorm.DB, users ...*user.User) error {
	userIDs := make([][]byte, 0, len(users))
	for i := range users {
		if users[i] == nil || len(users[i].ID) != 16 {
			continue
		}
		userIDs = append(userIDs, users[i].ID)
	}

	rowsByUserID, err := loadUserRoleRows(ctx, db, userIDs)
	if err != nil {
		return err
	}

	for i := range users {
		if users[i] == nil {
			continue
		}

		rows := orderUserRoleRows(users[i].RoleID, rowsByUserID[string(users[i].ID)])
		if len(rows) == 0 && len(users[i].RoleID) == 16 {
			rows = []userRoleJoinRow{{
				RoleID:          append([]byte(nil), users[i].RoleID...),
				RoleName:        users[i].RoleName,
				RoleDescription: users[i].RoleDescription,
			}}
		}

		users[i].RoleIDs = make([][]byte, 0, len(rows))
		users[i].Roles = make([]user.RoleSummary, 0, len(rows))
		for j := range rows {
			users[i].RoleIDs = append(users[i].RoleIDs, append([]byte(nil), rows[j].RoleID...))
			users[i].Roles = append(users[i].Roles, user.RoleSummary{
				ID:          append([]byte(nil), rows[j].RoleID...),
				Name:        rows[j].RoleName,
				Description: rows[j].RoleDescription,
			})
		}
	}

	return nil
}
