package mysql

import (
	"context"
	"errors"
	"strings"
	"time"

	"gorm.io/gorm"

	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/shared/pkg/uuidv7"
)

type FileManagerFolderRepository struct {
	db *gorm.DB
}

func NewFileManagerFolderRepository(db *gorm.DB) *FileManagerFolderRepository {
	return &FileManagerFolderRepository{db: db}
}

func (r *FileManagerFolderRepository) Create(ctx context.Context, row *filemanager.Folder) error {
	return r.db.WithContext(ctx).Create(row).Error
}

func (r *FileManagerFolderRepository) Update(ctx context.Context, row *filemanager.Folder) error {
	return r.db.WithContext(ctx).Save(row).Error
}

func (r *FileManagerFolderRepository) MoveSubtree(ctx context.Context, id []byte, targetParentID []byte, targetName string, targetNameNormalized string, updatedBy []byte) (*filemanager.Folder, error) {
	var moved filemanager.Folder
	err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var current filemanager.Folder
		if err := tx.Model(&filemanager.Folder{}).
			Where("id = ? AND deleted_at IS NULL", id).
			First(&current).Error; err != nil {
			return err
		}

		targetDepth := 0
		targetParentPath := ""
		if len(targetParentID) != 0 {
			if folderRepoBytesEqual16(targetParentID, current.ID) {
				return filemanager.ErrInvalidMove
			}

			var parent filemanager.Folder
			if err := tx.Model(&filemanager.Folder{}).
				Where("id = ? AND deleted_at IS NULL", targetParentID).
				First(&parent).Error; err != nil {
				return err
			}

			isDescendant, err := folderRepoIsDescendantFolder(tx, targetParentID, current.ID)
			if err != nil {
				return err
			}
			if isDescendant {
				return filemanager.ErrInvalidMove
			}

			targetDepth = parent.Depth + 1
			targetParentPath = parent.PathCache
		}

		var dupTotal int64
		if err := folderParentScope(
			tx.Model(&filemanager.Folder{}),
			targetParentID,
		).Where("name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL AND id <> ?", targetNameNormalized, id).Count(&dupTotal).Error; err != nil {
			return err
		}
		if dupTotal > 0 {
			return gorm.ErrDuplicatedKey
		}

		oldPath := current.PathCache
		oldDepth := current.Depth
		newPath := folderRepoBuildPathCache(targetParentPath, targetName)

		res := tx.Model(&filemanager.Folder{}).
			Where("id = ? AND deleted_at IS NULL", id).
			Updates(map[string]any{
				"parent_id":       targetParentID,
				"name":            targetName,
				"name_normalized": targetNameNormalized,
				"depth":           targetDepth,
				"path_cache":      newPath,
				"updated_by":      updatedBy,
				"updated_at":      time.Now().UTC(),
			})
		if res.Error != nil {
			return res.Error
		}
		if res.RowsAffected == 0 {
			return gorm.ErrRecordNotFound
		}

		depthDelta := targetDepth - oldDepth
		if oldPath != newPath || depthDelta != 0 {
			if err := folderRepoRewriteDescendantTree(tx, current.ID, oldPath, newPath, depthDelta, updatedBy); err != nil {
				return err
			}
		}

		current.ParentID = targetParentID
		current.Name = targetName
		current.NameNormalized = targetNameNormalized
		current.Depth = targetDepth
		current.PathCache = newPath
		current.UpdatedBy = updatedBy
		moved = current
		return nil
	})
	if err != nil {
		return nil, err
	}
	return &moved, nil
}

func (r *FileManagerFolderRepository) Delete(ctx context.Context, id []byte, deletedBy []byte, purgeAt *time.Time) error {
	now := time.Now().UTC()
	nameSlot := deletedNameSlot(id)
	updates := map[string]any{
		"deleted_at": now,
		"deleted_by": deletedBy,
		"updated_by": deletedBy,
		"name_slot":  nameSlot,
	}
	if purgeAt != nil {
		v := purgeAt.UTC()
		updates["purge_at"] = v
	}

	res := r.db.WithContext(ctx).
		Model(&filemanager.Folder{}).
		Where("id = ? AND deleted_at IS NULL", id).
		Updates(updates)
	if res.Error != nil {
		return res.Error
	}
	if res.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (r *FileManagerFolderRepository) Restore(ctx context.Context, id []byte, restoredBy []byte) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var row filemanager.Folder
		if err := tx.Model(&filemanager.Folder{}).Where("id = ?", id).First(&row).Error; err != nil {
			return err
		}
		if row.DeletedAt == nil {
			return gorm.ErrRecordNotFound
		}

		var total int64
		if err := folderParentScope(
			tx.Model(&filemanager.Folder{}),
			row.ParentID,
		).Where("name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL", row.NameNormalized).Count(&total).Error; err != nil {
			return err
		}
		if total > 0 {
			return gorm.ErrDuplicatedKey
		}

		updates := map[string]any{
			"deleted_at": nil,
			"deleted_by": nil,
			"purge_at":   nil,
			"updated_by": restoredBy,
			"name_slot":  "live",
		}
		res := tx.Model(&filemanager.Folder{}).
			Where("id = ? AND deleted_at IS NOT NULL", id).
			Updates(updates)
		if res.Error != nil {
			return res.Error
		}
		if res.RowsAffected == 0 {
			return gorm.ErrRecordNotFound
		}
		return nil
	})
}

func (r *FileManagerFolderRepository) Purge(ctx context.Context, id []byte) error {
	res := r.db.WithContext(ctx).
		Where("id = ? AND deleted_at IS NOT NULL", id).
		Delete(&filemanager.Folder{})
	if res.Error != nil {
		return res.Error
	}
	if res.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (r *FileManagerFolderRepository) GetByID(ctx context.Context, id []byte) (*filemanager.Folder, error) {
	var row filemanager.Folder
	err := r.db.WithContext(ctx).
		Where("id = ? AND deleted_at IS NULL", id).
		First(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	return &row, err
}

func (r *FileManagerFolderRepository) GetByIDAny(ctx context.Context, id []byte) (*filemanager.Folder, error) {
	var row filemanager.Folder
	err := r.db.WithContext(ctx).
		Where("id = ?", id).
		First(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	return &row, err
}

func (r *FileManagerFolderRepository) GetByParentAndName(ctx context.Context, parentID []byte, nameNormalized string) (*filemanager.Folder, error) {
	var row filemanager.Folder
	q := folderParentScope(r.db.WithContext(ctx).Model(&filemanager.Folder{}), parentID).
		Where("name_normalized = ? AND name_slot = 'live' AND deleted_at IS NULL", nameNormalized)

	err := q.First(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	return &row, err
}

func (r *FileManagerFolderRepository) ListByParent(ctx context.Context, parentID []byte, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
	var rows []filemanager.Folder
	var total int64

	base := folderParentScope(
		r.db.WithContext(ctx).
			Model(&filemanager.Folder{}).
			Where("deleted_at IS NULL AND name_slot = 'live'"),
		parentID,
	)

	query := base
	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(&rows).Error; err != nil {
		return nil, 0, err
	}

	return rows, total, nil
}

func (r *FileManagerFolderRepository) SearchByName(ctx context.Context, name string, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
	var rows []filemanager.Folder
	var total int64

	base := r.db.WithContext(ctx).
		Model(&filemanager.Folder{}).
		Where("deleted_at IS NULL AND name_slot = 'live' AND name LIKE ?", "%"+name+"%")

	query := base
	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(&rows).Error; err != nil {
		return nil, 0, err
	}

	return rows, total, nil
}

func (r *FileManagerFolderRepository) ListTrash(ctx context.Context, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
	var rows []filemanager.Folder
	var total int64

	base := r.db.WithContext(ctx).
		Model(&filemanager.Folder{}).
		Where("deleted_at IS NOT NULL")

	query := base
	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(&rows).Error; err != nil {
		return nil, 0, err
	}

	return rows, total, nil
}

func (r *FileManagerFolderRepository) ListTrashByParent(ctx context.Context, parentID []byte, sort string, limit, offset int) ([]filemanager.Folder, int64, error) {
	var rows []filemanager.Folder
	var total int64

	base := folderParentScope(
		r.db.WithContext(ctx).
			Model(&filemanager.Folder{}).
			Where("deleted_at IS NOT NULL"),
		parentID,
	)

	query := base
	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(&rows).Error; err != nil {
		return nil, 0, err
	}

	return rows, total, nil
}

func folderParentScope(tx *gorm.DB, parentID []byte) *gorm.DB {
	if len(parentID) == 0 {
		return tx.Where("parent_id IS NULL")
	}
	return tx.Where("parent_id = ?", parentID)
}

func deletedNameSlot(id []byte) string {
	if len(id) == 16 {
		if v, err := uuidv7.BytesToString(id); err == nil {
			return v
		}
	}
	if v, err := uuidv7.BytesToString(uuidv7.MustBytes()); err == nil {
		return v
	}
	return "deleted"
}

func folderRepoIsDescendantFolder(tx *gorm.DB, candidateID []byte, ancestorID []byte) (bool, error) {
	currentID := append([]byte(nil), candidateID...)
	for len(currentID) != 0 {
		if folderRepoBytesEqual16(currentID, ancestorID) {
			return true, nil
		}

		var current filemanager.Folder
		err := tx.Model(&filemanager.Folder{}).
			Where("id = ? AND deleted_at IS NULL", currentID).
			First(&current).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil
		}
		if err != nil {
			return false, err
		}
		if len(current.ParentID) == 0 {
			return false, nil
		}
		currentID = append([]byte(nil), current.ParentID...)
	}
	return false, nil
}

func folderRepoRewriteDescendantTree(tx *gorm.DB, rootID []byte, oldPrefix string, newPrefix string, depthDelta int, updatedBy []byte) error {
	queue := [][]byte{append([]byte(nil), rootID...)}
	for len(queue) > 0 {
		parentID := queue[0]
		queue = queue[1:]

		var children []filemanager.Folder
		if err := tx.Model(&filemanager.Folder{}).
			Where("parent_id = ?", parentID).
			Find(&children).Error; err != nil {
			return err
		}

		for i := range children {
			child := children[i]
			pathCache := child.PathCache
			if pathCache != "" {
				pathCache = folderRepoReplacePathPrefix(pathCache, oldPrefix, newPrefix)
			}
			depth := child.Depth + depthDelta
			if depth < 0 {
				depth = 0
			}

			if err := tx.Model(&filemanager.Folder{}).
				Where("id = ?", child.ID).
				Updates(map[string]any{
					"path_cache": pathCache,
					"depth":      depth,
					"updated_by": updatedBy,
					"updated_at": time.Now().UTC(),
				}).Error; err != nil {
				return err
			}
			queue = append(queue, append([]byte(nil), child.ID...))
		}
	}
	return nil
}

func folderRepoReplacePathPrefix(pathCache string, oldPrefix string, newPrefix string) string {
	if oldPrefix == "" {
		return pathCache
	}
	if pathCache == oldPrefix {
		return newPrefix
	}
	prefixWithSlash := oldPrefix + "/"
	if strings.HasPrefix(pathCache, prefixWithSlash) {
		return newPrefix + strings.TrimPrefix(pathCache, oldPrefix)
	}
	return pathCache
}

func folderRepoBuildPathCache(parentPath, name string) string {
	parentPath = strings.TrimSpace(parentPath)
	if parentPath == "" {
		return "/" + name
	}
	parentPath = strings.TrimRight(parentPath, "/")
	return parentPath + "/" + name
}

func folderRepoBytesEqual16(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
