package repository

import (
	"context"
	"errors"
	"fmt"
	"time"

	"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
	"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gorm.io/gorm"
)

type ExpenseRepository interface {
	repository.BaseRepository[entity.Expense]
	IdExists(ctx context.Context, id uint) (bool, error)
	GetNextSequence(ctx context.Context) (int, error)
	GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
	WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
	CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
	DeleteOne(ctx context.Context, id uint) error
	GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error)
}

type ExpenseRepositoryImpl struct {
	*repository.BaseRepositoryImpl[entity.Expense]
}

func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
	return &ExpenseRepositoryImpl{
		BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db),
	}
}

func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
	return repository.Exists[entity.Expense](ctx, r.DB(), id)
}

func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
	var sequence int
	err := r.DB().Raw("SELECT nextval('expenses_ref_seq')").Scan(&sequence).Error
	if err != nil {
		return 0, err
	}
	return sequence, nil
}

func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) {
	var expense entity.Expense
	err := r.DB().WithContext(ctx).
		Where("id = ?", id).
		Preload("Supplier").
		First(&expense).Error
	if err != nil {
		return nil, err
	}
	return &expense, nil
}

func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		if pfkID == 0 && kandangID == 0 {
			return db
		}
		q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id")
		if pfkID > 0 && kandangID > 0 {
			return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID)
		}
		if pfkID > 0 {
			return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
		}
		return q.Where("expense_nonstocks.kandang_id = ?", kandangID)
	}
}

func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) {
	if pfkID == 0 && kandangID == 0 {
		return 0, nil
	}

	var ids []uint64
	if err := r.DB().WithContext(ctx).
		Table("expenses").
		Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
		Group("expenses.id").Where("expenses.deleted_at IS NULL").
		Pluck("expenses.id", &ids).Error; err != nil {
		return 0, err
	}
	if len(ids) == 0 {
		return 0, nil
	}

	var unfinished int64
	for _, id := range ids {
		var latest entity.Approval
		err := r.DB().WithContext(ctx).
			Table("approvals").
			Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
			Order("action_at DESC").
			Limit(1).
			First(&latest).Error
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, err
		}
		if isFinished != nil {
			if !isFinished(&latest) {
				unfinished++
			}
		}
	}
	return unfinished, nil
}

func (r *ExpenseRepositoryImpl) DeleteOne(ctx context.Context, id uint) error {
	// Cast to uint64 to match entity.Id type
	id64 := uint64(id)
	deletedAt := time.Now()

	// Use raw SQL with interpolated integer to avoid type issues
	// Interpolate id directly as integer literal (safe because it's uint64)
	result := r.DB().WithContext(ctx).
		Exec(`UPDATE "expenses" SET "deleted_at" = $1 WHERE "id" = `+fmt.Sprintf("%d", id64)+` AND "deleted_at" IS NULL`,
			deletedAt)

	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (r *ExpenseRepositoryImpl) GetProgressRows(ctx context.Context, startDate, endDate time.Time, allowedLocationIDs []uint, restrict bool) ([]exportprogress.Row, error) {
	const unassignedSQL = "'" + exportprogress.UnassignedKandangName + "'"
	query := r.DB().WithContext(ctx).
		Table("expenses AS e").
		Select(`
			'Expenses' AS module,
			COALESCE(pf.flock_name, loc.name, fallback_loc.name, 'Unknown Farm') AS farm_name,
			COALESCE(k.name, `+unassignedSQL+`, 'Unknown Kandang') AS kandang_name,
			CAST(DATE(e.transaction_date) AS TEXT) AS activity_date,
			COUNT(*) AS count
		`).
		Joins("LEFT JOIN (SELECT DISTINCT expense_id, project_flock_kandang_id, kandang_id FROM expense_nonstocks) en ON en.expense_id = e.id").
		Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = en.project_flock_kandang_id").
		Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
		Joins("LEFT JOIN kandangs k ON k.id = COALESCE(en.kandang_id, pfk.kandang_id)").
		Joins("LEFT JOIN locations loc ON loc.id = k.location_id").
		Joins("LEFT JOIN locations fallback_loc ON fallback_loc.id = e.location_id").
		Where("e.deleted_at IS NULL").
		Where("DATE(e.transaction_date) >= DATE(?)", startDate).
		Where("DATE(e.transaction_date) <= DATE(?)", endDate)

	if restrict {
		if len(allowedLocationIDs) == 0 {
			return []exportprogress.Row{}, nil
		}
		query = query.Where("e.location_id IN ?", allowedLocationIDs)
	}

	type progressRowResult struct {
		Module       string
		FarmName     string
		KandangName  string
		ActivityDate string
		Count        int
	}
	scanned := make([]progressRowResult, 0)
	err := query.
		Group("DATE(e.transaction_date), COALESCE(pf.flock_name, loc.name, fallback_loc.name, 'Unknown Farm'), COALESCE(k.name, " + unassignedSQL + ", 'Unknown Kandang')").
		Order("activity_date ASC, farm_name ASC, kandang_name ASC").
		Scan(&scanned).Error
	if err != nil {
		return nil, err
	}

	rows := make([]exportprogress.Row, 0, len(scanned))
	for _, item := range scanned {
		activityDate, err := exportprogress.ParseActivityDate(item.ActivityDate)
		if err != nil {
			return nil, err
		}
		rows = append(rows, exportprogress.Row{
			Module:       item.Module,
			FarmName:     item.FarmName,
			KandangName:  item.KandangName,
			ActivityDate: activityDate,
			Count:        item.Count,
		})
	}
	return rows, nil
}
