package repository

import (
	"context"

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

type BaseRepository[T any] interface {
	GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
	GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
	GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
	First(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) (*T, error)

	CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
	CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error

	UpdateOne(ctx context.Context, id uint, entity *T, modifier func(*gorm.DB) *gorm.DB) error
	UpdateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
	PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error

	DeleteOne(ctx context.Context, id uint) error
	DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error

	Upsert(ctx context.Context, entity *T, conflictColumns []clause.Column, modifier func(*gorm.DB) *gorm.DB) error

	WithTx(tx *gorm.DB) BaseRepository[T]
	DB() *gorm.DB
}

type BaseRepositoryImpl[T any] struct {
	db *gorm.DB
}

func NewBaseRepository[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
	return &BaseRepositoryImpl[T]{db: db}
}

func (r *BaseRepositoryImpl[T]) GetAll(
	ctx context.Context,
	offset, limit int,
	modifier func(*gorm.DB) *gorm.DB,
) ([]T, int64, error) {
	var entities []T
	var total int64

	q := r.db.WithContext(ctx).Model(new(T))
	if modifier != nil {
		q = modifier(q)
	}

	if err := q.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	if err := q.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
		return nil, 0, err
	}

	return entities, total, nil
}

func (r *BaseRepositoryImpl[T]) GetByID(
	ctx context.Context,
	id uint,
	modifier func(*gorm.DB) *gorm.DB,
) (*T, error) {
	entity := new(T)
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	if err := q.First(entity, id).Error; err != nil {
		return nil, err
	}
	return entity, nil
}

func (r *BaseRepositoryImpl[T]) GetByIDs(
	ctx context.Context,
	ids []uint,
	modifier func(*gorm.DB) *gorm.DB,
) ([]T, error) {
	var entities []T
	q := r.db.WithContext(ctx).Model(new(T))
	if modifier != nil {
		q = modifier(q)
	}
	if err := q.Where("id IN ?", ids).Find(&entities).Error; err != nil {
		return nil, err
	}

	if len(entities) == 0 {
		return nil, gorm.ErrRecordNotFound
	}

	return entities, nil
}

func (r *BaseRepositoryImpl[T]) First(
	ctx context.Context,
	modifier func(*gorm.DB) *gorm.DB,
) (*T, error) {
	entity := new(T)
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	if err := q.First(entity).Error; err != nil {
		return nil, err
	}
	return entity, nil
}

// ---- CREATE ----
func (r *BaseRepositoryImpl[T]) CreateOne(
	ctx context.Context,
	entity *T,
	modifier func(*gorm.DB) *gorm.DB,
) error {
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	return q.Create(entity).Error
}

func (r *BaseRepositoryImpl[T]) CreateMany(
	ctx context.Context,
	entities []*T,
	modifier func(*gorm.DB) *gorm.DB,
) error {
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	return q.Create(&entities).Error
}

// ---- UPDATE ----
func (r *BaseRepositoryImpl[T]) UpdateOne(
	ctx context.Context,
	id uint,
	entity *T,
	modifier func(*gorm.DB) *gorm.DB,
) error {
	q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
	if modifier != nil {
		q = modifier(q)
	}

	result := q.Updates(entity)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}

	return nil
}

func (r *BaseRepositoryImpl[T]) UpdateMany(
	ctx context.Context,
	entities []*T,
	modifier func(*gorm.DB) *gorm.DB,
) error {
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}

	result := q.Save(&entities)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}

	return nil
}

func (r *BaseRepositoryImpl[T]) PatchOne(
	ctx context.Context,
	id uint,
	updates map[string]any,
	modifier func(*gorm.DB) *gorm.DB,
) error {
	q := r.db.WithContext(ctx)
	if modifier != nil {
		q = modifier(q)
	}
	q = q.Model(new(T)).Where("id = ?", id)

	result := q.Updates(updates)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}

	return nil
}

// ---- DELETE ----
func (r *BaseRepositoryImpl[T]) DeleteOne(ctx context.Context, id uint) error {
	result := r.db.WithContext(ctx).Delete(new(T), id)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (r *BaseRepositoryImpl[T]) DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error {
	q := r.db.WithContext(ctx).Model(new(T))
	if modifier != nil {
		q = modifier(q)
	}

	result := q.Delete(new(T))
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}

	return nil
}

// ---- UPSERT ----
func (r *BaseRepositoryImpl[T]) Upsert(
	ctx context.Context,
	entity *T,
	conflictColumns []clause.Column,
	modifier func(*gorm.DB) *gorm.DB,
) error {
	q := r.db.WithContext(ctx).Clauses(clause.OnConflict{
		Columns:   conflictColumns,
		UpdateAll: true,
	})
	if modifier != nil {
		q = modifier(q)
	}
	return q.Create(entity).Error
}

func (r *BaseRepositoryImpl[T]) WithTx(tx *gorm.DB) BaseRepository[T] {
	return &BaseRepositoryImpl[T]{db: tx}
}

func (r *BaseRepositoryImpl[T]) DB() *gorm.DB {
	return r.db
}
