package mysql

import (
	"context"
	"errors"

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

	"wucher/internal/domain/auth"
	"wucher/internal/domain/user"
	"wucher/internal/shared/pkg/uuidv7"
)

type UserRepository struct {
	db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
	return &UserRepository{db: db}
}

func (r *UserRepository) Create(ctx context.Context, u *user.User) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
			u.CreatedBy = actor
			u.UpdatedBy = actor
		}
		if err := tx.Create(u).Error; err != nil {
			return err
		}
		return replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs)
	})
}

func (r *UserRepository) Update(ctx context.Context, u *user.User) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if actor := actorUserIDFromContext(ctx); len(actor) > 0 {
			u.UpdatedBy = actor
		}
		// Persist only user columns and avoid relation side-effects from preloaded structs.
		if err := tx.Omit(clause.Associations).Save(u).Error; err != nil {
			return err
		}
		// Keep legacy and current image columns in sync when image patch is requested.
		if u.ProfileAttachmentPatchSet {
			updates := map[string]any{
				"profile_attachment_id": nil,
				"image_attachment_id":   nil,
			}
			if len(u.ProfileAttachmentID) == 16 {
				updates["profile_attachment_id"] = append([]byte(nil), u.ProfileAttachmentID...)
				updates["image_attachment_id"] = append([]byte(nil), u.ProfileAttachmentID...)
			}
			if err := tx.Model(&user.User{}).Where("id = ?", u.ID).Updates(updates).Error; err != nil {
				return err
			}
		}
		if u.RoleIDs == nil {
			return nil
		}
		return replaceUserRolesTx(ctx, tx, u.ID, u.RoleID, u.RoleIDs)
	})
}

func (r *UserRepository) Delete(ctx context.Context, id []byte) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Model(&user.User{}).
			Where("id = ?", id).
			Updates(map[string]any{
				"image_attachment_id":   nil,
				"profile_attachment_id": nil,
			}).Error; err != nil {
			return err
		}
		if idStr, err := uuidv7.BytesToString(id); err == nil && idStr != "" {
			if err := tx.Exec("DELETE FROM attachments WHERE ref_type = ? AND ref_id = ?", "user_profile", idStr).Error; err != nil {
				return err
			}
		}
		if err := tx.Where("user_id = ?", id).Delete(&auth.UserRole{}).Error; err != nil {
			return err
		}
		return tx.Delete(&user.User{}, "id = ?", id).Error
	})
}

func (r *UserRepository) GetByID(ctx context.Context, id []byte) (*user.User, error) {
	var u user.User
	err := r.db.WithContext(ctx).
		Model(&user.User{}).
		Select("users.*, roles.name AS role_name, roles.description AS role_description").
		Joins("LEFT JOIN roles ON roles.id = users.role_id").
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		Where("users.id = ?", id).
		First(&u).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachUserDomainRoles(ctx, r.db, &u); err != nil {
		return nil, err
	}
	return &u, nil
}

func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*user.User, error) {
	var u user.User
	err := r.db.WithContext(ctx).
		Model(&user.User{}).
		Select("users.*, roles.name AS role_name, roles.description AS role_description").
		Joins("LEFT JOIN roles ON roles.id = users.role_id").
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		Where("users.email = ?", email).
		First(&u).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachUserDomainRoles(ctx, r.db, &u); err != nil {
		return nil, err
	}
	return &u, nil
}

func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*user.User, error) {
	var u user.User
	err := r.db.WithContext(ctx).
		Model(&user.User{}).
		Select("users.*, roles.name AS role_name, roles.description AS role_description").
		Joins("LEFT JOIN roles ON roles.id = users.role_id").
		Preload("ProfileAttachment").
		Preload("ProfileAttachment.File").
		Where("users.username = ?", username).
		First(&u).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if err := attachUserDomainRoles(ctx, r.db, &u); err != nil {
		return nil, err
	}
	return &u, nil
}

func (r *UserRepository) GetLastNameByID(ctx context.Context, id []byte) (string, error) {
	var u user.User
	err := r.db.WithContext(ctx).
		Table("users").
		Select("last_name").
		Where("id = ?", id).
		First(&u).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return "", nil
	}
	if err != nil {
		return "", err
	}
	return u.LastName, nil
}

// GetShortNameByID returns the user's profile short name (pilot or technician
// 3-letter code, e.g. "GAN"), or an empty string when the user has no profile
// short name set. Callers apply their own fallback (e.g. initials).
func (r *UserRepository) GetShortNameByID(ctx context.Context, id []byte) (string, error) {
	var out struct {
		ShortName string `gorm:"column:short_name"`
	}
	err := r.db.WithContext(ctx).
		Table("users u").
		Select("COALESCE(NULLIF(pp.short_name, ''), NULLIF(tp.short_name, ''), '') AS short_name").
		Joins("LEFT JOIN pilot_profiles pp ON pp.user_id = u.id").
		Joins("LEFT JOIN technician_profiles tp ON tp.user_id = u.id").
		Where("u.id = ?", id).
		Take(&out).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return "", nil
	}
	if err != nil {
		return "", err
	}
	return out.ShortName, nil
}

func (r *UserRepository) List(ctx context.Context, filter string, sort string, limit, offset int) ([]user.User, int64, error) {
	var users []user.User
	var total int64

	base := r.db.WithContext(ctx).
		Model(&user.User{}).
		Joins("LEFT JOIN roles ON roles.id = users.role_id")
	if filter != "" {
		like := "%" + filter + "%"
		base = base.Where("users.email LIKE ? OR users.username LIKE ? OR users.first_name LIKE ? OR users.last_name LIKE ?", like, like, like, like)
	}
	query := base.Select("users.*, roles.name AS role_name, roles.description AS role_description")
	query = query.Preload("ProfileAttachment").Preload("ProfileAttachment.File")
	if sort != "" {
		query = query.Order(sort)
	}
	if err := base.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	if limit > 0 {
		query = query.Limit(limit).Offset(offset)
	}
	if err := query.Find(&users).Error; err != nil {
		return nil, 0, err
	}
	ptrs := make([]*user.User, 0, len(users))
	for i := range users {
		ptrs = append(ptrs, &users[i])
	}
	if err := attachUserDomainRoles(ctx, r.db, ptrs...); err != nil {
		return nil, 0, err
	}
	return users, total, nil
}
