package repositories

import (
	"context"
	"time"

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

type FarmDepreciationCandidateRow struct {
	ProjectFlockID uint
	FarmName       string
}

type FarmDepreciationLatestTransferRow struct {
	ProjectFlockID        uint
	FarmName              string
	ProjectFlockKandangID uint
	KandangID             uint
	KandangName           string
	HouseType             *string
	SourceProjectFlockID  uint
	TransferDate          time.Time
	TransferQty           float64
	TransferID            uint
}

type FarmDepreciationManualInputRow struct {
	Id             uint
	ProjectFlockID uint
	FarmName       string
	TotalCost      float64
	CutoverDate    time.Time
	Note           *string
}

type houseDepreciationPercentRow struct {
	HouseType           string
	Day                 int
	DepreciationPercent float64
}

type ExpenseDepreciationRepository interface {
	GetCandidateFarms(ctx context.Context, period time.Time, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationCandidateRow, error)
	GetSnapshotsByPeriodAndFarmIDs(ctx context.Context, period time.Time, farmIDs []uint) ([]entity.FarmDepreciationSnapshot, error)
	UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error
	DeleteSnapshotsFromDate(ctx context.Context, fromDate time.Time, farmIDs []uint) error
	GetLatestTransferInputsByFarms(ctx context.Context, period time.Time, farmIDs []uint) ([]FarmDepreciationLatestTransferRow, error)
	GetDepreciationPercents(ctx context.Context, houseTypes []string, maxDay int) (map[string]map[int]float64, error)
	GetLatestManualInputsByFarms(ctx context.Context, areaIDs, locationIDs, projectFlockIDs []int64) ([]FarmDepreciationManualInputRow, error)
	UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error
	DB() *gorm.DB
}

type expenseDepreciationRepository struct {
	db *gorm.DB
}

func NewExpenseDepreciationRepository(db *gorm.DB) ExpenseDepreciationRepository {
	return &expenseDepreciationRepository{db: db}
}

func (r *expenseDepreciationRepository) DB() *gorm.DB {
	return r.db
}

func (r *expenseDepreciationRepository) GetCandidateFarms(
	ctx context.Context,
	period time.Time,
	areaIDs, locationIDs, projectFlockIDs []int64,
) ([]FarmDepreciationCandidateRow, error) {
	rows := make([]FarmDepreciationCandidateRow, 0)

	query := r.db.WithContext(ctx).
		Table("project_flocks AS pf").
		Select("DISTINCT pf.id AS project_flock_id, pf.flock_name AS farm_name").
		Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
		Where("pf.deleted_at IS NULL").
		Where("pf.category = ?", utils.ProjectFlockCategoryLaying).
		Where("(pfk.closed_at IS NULL OR DATE(pfk.closed_at) >= DATE(?))", period)

	if len(areaIDs) > 0 {
		query = query.Where("pf.area_id IN ?", areaIDs)
	}
	if len(locationIDs) > 0 {
		query = query.Where("pf.location_id IN ?", locationIDs)
	}
	if len(projectFlockIDs) > 0 {
		query = query.Where("pf.id IN ?", projectFlockIDs)
	}

	if err := query.Order("pf.id ASC").Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func (r *expenseDepreciationRepository) GetSnapshotsByPeriodAndFarmIDs(
	ctx context.Context,
	period time.Time,
	farmIDs []uint,
) ([]entity.FarmDepreciationSnapshot, error) {
	if len(farmIDs) == 0 {
		return []entity.FarmDepreciationSnapshot{}, nil
	}

	rows := make([]entity.FarmDepreciationSnapshot, 0)
	if err := r.db.WithContext(ctx).
		Where("project_flock_id IN ?", farmIDs).
		Where("period_date = DATE(?)", period).
		Find(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func (r *expenseDepreciationRepository) UpsertSnapshots(ctx context.Context, rows []entity.FarmDepreciationSnapshot) error {
	if len(rows) == 0 {
		return nil
	}

	return r.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns: []clause.Column{
				{Name: "project_flock_id"},
				{Name: "period_date"},
			},
			DoUpdates: clause.AssignmentColumns([]string{
				"depreciation_percent_effective",
				"depreciation_value",
				"pullet_cost_day_n_total",
				"components",
				"updated_at",
			}),
		}).
		Create(&rows).Error
}

func (r *expenseDepreciationRepository) DeleteSnapshotsFromDate(
	ctx context.Context,
	fromDate time.Time,
	farmIDs []uint,
) error {
	if fromDate.IsZero() {
		return nil
	}

	query := r.db.WithContext(ctx).
		Table("farm_depreciation_snapshots").
		Where("period_date >= DATE(?)", fromDate)
	if len(farmIDs) > 0 {
		query = query.Where("project_flock_id IN ?", farmIDs)
	}
	return query.Delete(nil).Error
}

func (r *expenseDepreciationRepository) GetLatestTransferInputsByFarms(
	ctx context.Context,
	period time.Time,
	farmIDs []uint,
) ([]FarmDepreciationLatestTransferRow, error) {
	if len(farmIDs) == 0 {
		return []FarmDepreciationLatestTransferRow{}, nil
	}

	rows := make([]FarmDepreciationLatestTransferRow, 0)
	query := `
WITH latest_transfer_approval AS (
	SELECT a.approvable_id, a.action
	FROM approvals a
	JOIN (
		SELECT approvable_id, MAX(action_at) AS latest_action_at
		FROM approvals
		WHERE approvable_type = @approval_type
		GROUP BY approvable_id
	) la
		ON la.approvable_id = a.approvable_id
		AND la.latest_action_at = a.action_at
	WHERE a.approvable_type = @approval_type
),
approved_transfers AS (
	SELECT
		lt.id,
		lt.from_project_flock_id,
		lt.to_project_flock_id,
		COALESCE(DATE(lt.effective_move_date), DATE(lt.economic_cutoff_date), DATE(lt.transfer_date)) AS effective_date
	FROM laying_transfers lt
	JOIN latest_transfer_approval lta ON lta.approvable_id = lt.id
	WHERE lt.deleted_at IS NULL
	  AND lt.executed_at IS NOT NULL
	  AND lta.action = 'APPROVED'
)
SELECT DISTINCT ON (ltt.target_project_flock_kandang_id)
	pf.id AS project_flock_id,
	pf.flock_name AS farm_name,
	pfk.id AS project_flock_kandang_id,
	k.id AS kandang_id,
	k.name AS kandang_name,
	k.house_type::text AS house_type,
	at.from_project_flock_id AS source_project_flock_id,
	at.effective_date AS transfer_date,
	ltt.total_qty AS transfer_qty,
	at.id AS transfer_id
FROM laying_transfer_targets ltt
JOIN approved_transfers at ON at.id = ltt.laying_transfer_id
JOIN project_flock_kandangs pfk ON pfk.id = ltt.target_project_flock_kandang_id
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
JOIN kandangs k ON k.id = pfk.kandang_id
WHERE ltt.deleted_at IS NULL
  AND pf.id IN @farm_ids
  AND at.effective_date <= DATE(@period_date)
ORDER BY ltt.target_project_flock_kandang_id, at.effective_date DESC, at.id DESC
`

	if err := r.db.WithContext(ctx).Raw(query, map[string]any{
		"approval_type": utils.ApprovalWorkflowTransferToLaying.String(),
		"farm_ids":      farmIDs,
		"period_date":   period,
	}).Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func (r *expenseDepreciationRepository) GetDepreciationPercents(
	ctx context.Context,
	houseTypes []string,
	maxDay int,
) (map[string]map[int]float64, error) {
	result := make(map[string]map[int]float64)
	if len(houseTypes) == 0 || maxDay <= 0 {
		return result, nil
	}

	rows := make([]houseDepreciationPercentRow, 0)
	if err := r.db.WithContext(ctx).
		Table("house_depreciation_standards").
		Select("house_type::text AS house_type, day, depreciation_percent").
		Where("house_type::text IN ?", houseTypes).
		Where("day <= ?", maxDay).
		Order("house_type ASC, day ASC").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	for _, row := range rows {
		if _, exists := result[row.HouseType]; !exists {
			result[row.HouseType] = make(map[int]float64)
		}
		result[row.HouseType][row.Day] = row.DepreciationPercent
	}

	return result, nil
}

func (r *expenseDepreciationRepository) GetLatestManualInputsByFarms(
	ctx context.Context,
	areaIDs, locationIDs, projectFlockIDs []int64,
) ([]FarmDepreciationManualInputRow, error) {
	rows := make([]FarmDepreciationManualInputRow, 0)

	query := r.db.WithContext(ctx).
		Table("farm_depreciation_manual_inputs AS fdmi").
		Select(`
			fdmi.id AS id,
			fdmi.project_flock_id AS project_flock_id,
			pf.flock_name AS farm_name,
			fdmi.total_cost AS total_cost,
			fdmi.cutover_date AS cutover_date,
			fdmi.note AS note
		`).
		Joins("JOIN project_flocks AS pf ON pf.id = fdmi.project_flock_id").
		Where("pf.deleted_at IS NULL").
		Where("pf.category = ?", utils.ProjectFlockCategoryLaying)

	if len(areaIDs) > 0 {
		query = query.Where("pf.area_id IN ?", areaIDs)
	}
	if len(locationIDs) > 0 {
		query = query.Where("pf.location_id IN ?", locationIDs)
	}
	if len(projectFlockIDs) > 0 {
		query = query.Where("pf.id IN ?", projectFlockIDs)
	}

	if err := query.
		Order("pf.id ASC").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	return rows, nil
}

func (r *expenseDepreciationRepository) UpsertManualInput(ctx context.Context, row *entity.FarmDepreciationManualInput) error {
	if row == nil {
		return nil
	}

	now := time.Now().UTC()
	err := r.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns: []clause.Column{
				{Name: "project_flock_id"},
			},
			DoUpdates: clause.Assignments(map[string]any{
				"total_cost":   row.TotalCost,
				"cutover_date": row.CutoverDate,
				"note":         row.Note,
				"updated_at":   now,
			}),
		}).
		Create(row).Error
	if err != nil {
		return err
	}

	return r.db.WithContext(ctx).
		Table("farm_depreciation_manual_inputs").
		Select("id, project_flock_id, total_cost, cutover_date, note").
		Where("project_flock_id = ?", row.ProjectFlockId).
		Take(row).Error
}
