package repositories

import (
	"context"
	"fmt"
	"strings"
	"time"

	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"

	"gorm.io/gorm"
)

type DebtSupplierRepository interface {
	GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
	GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
	GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
	GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
	GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error)
	GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error)
	GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
	GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
}

type debtSupplierRepositoryImpl struct {
	db *gorm.DB
}

type PaymentReferenceSummary struct {
	Total             float64
	LatestPaymentDate time.Time
}

func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
	return &debtSupplierRepositoryImpl{db: db}
}

func (r *debtSupplierRepositoryImpl) latestPurchaseApproval(ctx context.Context) *gorm.DB {
	return r.db.WithContext(ctx).
		Table("approvals AS a").
		Select("a.approvable_id, a.step_number, a.action").
		Joins(`
			JOIN (
				SELECT approvable_id, MAX(action_at) AS latest_action_at
				FROM approvals
				WHERE approvable_type = ?
				GROUP BY approvable_id
			) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
			string(utils.ApprovalWorkflowPurchase),
		)
}

func resolveDebtSupplierDateColumn(filterBy string) string {
	switch strings.ToLower(strings.TrimSpace(filterBy)) {
	case "po_date":
		return "purchases.po_date"
	case "received_date", "":
		return "purchase_items.received_date"
	default:
		return "purchase_items.received_date"
	}
}

func (r *debtSupplierRepositoryImpl) baseSupplierQuery(ctx context.Context, filters *validation.DebtSupplierQuery) *gorm.DB {
	dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)

	db := r.db.WithContext(ctx).
		Model(&entity.Supplier{}).
		Joins("JOIN purchases ON purchases.supplier_id = suppliers.id").
		Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
		Joins("JOIN warehouses w ON w.id = purchase_items.warehouse_id").
		Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
		Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
		Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
		Where("purchase_items.received_date IS NOT NULL")

	if len(filters.SupplierIDs) > 0 {
		db = db.Where("suppliers.id IN ?", filters.SupplierIDs)
	}

	if filters.AllowedAreaIDs != nil {
		if len(filters.AllowedAreaIDs) == 0 {
			db = db.Where("1 = 0")
		} else {
			db = db.Where("w.area_id IN ?", filters.AllowedAreaIDs)
		}
	}

	if filters.AllowedLocationIDs != nil {
		if len(filters.AllowedLocationIDs) == 0 {
			db = db.Where("1 = 0")
		} else {
			db = db.Where("w.location_id IN ?", filters.AllowedLocationIDs)
		}
	}

	if filters.StartDate != "" {
		if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
			db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom)
		}
	}

	if filters.EndDate != "" {
		if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
			db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo)
		}
	}

	return db
}

func (r *debtSupplierRepositoryImpl) GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error) {
	query := r.baseSupplierQuery(ctx, filters)

	var totalSuppliers int64
	if err := query.
		Distinct("suppliers.id").
		Count(&totalSuppliers).Error; err != nil {
		return nil, 0, err
	}

	if totalSuppliers == 0 {
		return []entity.Supplier{}, 0, nil
	}

	if offset < 0 {
		offset = 0
	}

	var supplierIDs []uint
	if err := query.
		Select("suppliers.id").
		Order("suppliers.id ASC").
		Offset(offset).
		Limit(limit).
		Pluck("suppliers.id", &supplierIDs).Error; err != nil {
		return nil, 0, err
	}

	if len(supplierIDs) == 0 {
		return []entity.Supplier{}, totalSuppliers, nil
	}

	var suppliers []entity.Supplier
	if err := r.db.WithContext(ctx).
		Where("id IN ?", supplierIDs).
		Find(&suppliers).Error; err != nil {
		return nil, 0, err
	}

	return suppliers, totalSuppliers, nil
}

func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error) {
	if len(supplierIDs) == 0 {
		return []entity.Purchase{}, nil
	}

	purchaseIDs, err := r.getPurchaseIDs(ctx, supplierIDs, filters)
	if err != nil {
		return nil, err
	}
	if len(purchaseIDs) == 0 {
		return []entity.Purchase{}, nil
	}

	preloadItems := func(db *gorm.DB) *gorm.DB {
		db = db.
			Preload("Warehouse").
			Preload("Warehouse.Area").
			Order("purchase_items.id ASC")

		if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" {
			if filters.StartDate != "" {
				if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
					db = db.Where("DATE(purchase_items.received_date) >= ?", dateFrom)
				}
			}
			if filters.EndDate != "" {
				if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
					db = db.Where("DATE(purchase_items.received_date) <= ?", dateTo)
				}
			}
		}

		return db
	}

	var purchases []entity.Purchase
	if err := r.db.WithContext(ctx).
		Model(&entity.Purchase{}).
		Preload("Supplier").
		Preload("Items", preloadItems).
		Where("purchases.id IN ?", purchaseIDs).
		Order("purchases.id ASC").
		Find(&purchases).Error; err != nil {
		return nil, err
	}

	return purchases, nil
}

func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) {
	if len(supplierIDs) == 0 {
		return []entity.Payment{}, nil
	}

	db := r.db.WithContext(ctx).
		Model(&entity.Payment{}).
		Where("party_type = ?", string(utils.PaymentPartySupplier)).
		Where("direction = ?", "OUT").
		Where("party_id IN ?", supplierIDs).
		Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal))

	if strings.TrimSpace(filters.StartDate) != "" {
		if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
			db = db.Where("DATE(payment_date) >= ?", dateFrom)
		}
	}

	if strings.TrimSpace(filters.EndDate) != "" {
		if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
			db = db.Where("DATE(payment_date) <= ?", dateTo)
		}
	}

	var payments []entity.Payment
	if err := db.
		Order("payment_date ASC, id ASC").
		Find(&payments).Error; err != nil {
		return nil, err
	}

	return payments, nil
}

func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) {
	dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)

	db := r.db.WithContext(ctx).
		Table("purchases").
		Select("DISTINCT purchases.id").
		Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
		Joins("JOIN warehouses w ON w.id = purchase_items.warehouse_id").
		Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
		Where("purchases.supplier_id IN ?", supplierIDs).
		Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
		Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
		Where("purchase_items.received_date IS NOT NULL")

	if filters.AllowedAreaIDs != nil {
		if len(filters.AllowedAreaIDs) == 0 {
			db = db.Where("1 = 0")
		} else {
			db = db.Where("w.area_id IN ?", filters.AllowedAreaIDs)
		}
	}

	if filters.AllowedLocationIDs != nil {
		if len(filters.AllowedLocationIDs) == 0 {
			db = db.Where("1 = 0")
		} else {
			db = db.Where("w.location_id IN ?", filters.AllowedLocationIDs)
		}
	}

	if filters.StartDate != "" {
		if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
			db = db.Where(fmt.Sprintf("DATE(%s) >= ?", dateColumn), dateFrom)
		}
	}

	if filters.EndDate != "" {
		if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
			db = db.Where(fmt.Sprintf("DATE(%s) <= ?", dateColumn), dateTo)
		}
	}

	var purchaseIDs []uint
	if err := db.Order("purchases.id ASC").Pluck("purchases.id", &purchaseIDs).Error; err != nil {
		return nil, err
	}

	return purchaseIDs, nil
}

func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error) {
	if len(supplierIDs) == 0 || len(references) == 0 {
		return map[string]float64{}, nil
	}

	type paymentRow struct {
		ReferenceNumber *string `gorm:"column:reference_number"`
		Total           float64 `gorm:"column:total"`
	}

	rows := make([]paymentRow, 0)
	if err := r.db.WithContext(ctx).
		Model(&entity.Payment{}).
		Select("reference_number, SUM(nominal) AS total").
		Where("party_type = ?", string(utils.PaymentPartySupplier)).
		Where("direction = ?", "OUT").
		Where("party_id IN ?", supplierIDs).
		Where("reference_number IN ?", references).
		Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
		Group("reference_number").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make(map[string]float64, len(rows))
	for _, row := range rows {
		if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" {
			continue
		}
		result[*row.ReferenceNumber] = row.Total
	}

	return result, nil
}

func (r *debtSupplierRepositoryImpl) GetPaymentSummariesByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]PaymentReferenceSummary, error) {
	if len(supplierIDs) == 0 || len(references) == 0 {
		return map[string]PaymentReferenceSummary{}, nil
	}

	type paymentRow struct {
		ReferenceNumber   *string   `gorm:"column:reference_number"`
		Total             float64   `gorm:"column:total"`
		LatestPaymentDate time.Time `gorm:"column:latest_payment_date"`
	}

	rows := make([]paymentRow, 0)
	if err := r.db.WithContext(ctx).
		Model(&entity.Payment{}).
		Select("reference_number, SUM(nominal) AS total, MAX(payment_date) AS latest_payment_date").
		Where("party_type = ?", string(utils.PaymentPartySupplier)).
		Where("direction = ?", "OUT").
		Where("party_id IN ?", supplierIDs).
		Where("reference_number IN ?", references).
		Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
		Group("reference_number").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make(map[string]PaymentReferenceSummary, len(rows))
	for _, row := range rows {
		if row.ReferenceNumber == nil || strings.TrimSpace(*row.ReferenceNumber) == "" {
			continue
		}
		result[*row.ReferenceNumber] = PaymentReferenceSummary{
			Total:             row.Total,
			LatestPaymentDate: row.LatestPaymentDate,
		}
	}

	return result, nil
}

func (r *debtSupplierRepositoryImpl) GetInitialBalanceTotals(ctx context.Context, supplierIDs []uint) (map[uint]float64, error) {
	if len(supplierIDs) == 0 {
		return map[uint]float64{}, nil
	}

	type balanceRow struct {
		SupplierID uint    `gorm:"column:supplier_id"`
		Total      float64 `gorm:"column:total"`
	}

	rows := make([]balanceRow, 0)
	if err := r.db.WithContext(ctx).
		Model(&entity.Payment{}).
		Select("party_id AS supplier_id, SUM(nominal) AS total").
		Where("party_type = ?", string(utils.PaymentPartySupplier)).
		Where("party_id IN ?", supplierIDs).
		Where("transaction_type = ?", string(utils.TransactionTypeSaldoAwal)).
		Group("party_id").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make(map[uint]float64, len(rows))
	for _, row := range rows {
		result[row.SupplierID] = row.Total
	}

	return result, nil
}

func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
	if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
		return map[uint]float64{}, nil
	}

	dateFrom, err := utils.ParseDateString(filters.StartDate)
	if err != nil {
		return map[uint]float64{}, nil
	}

	dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)

	type purchaseTotalRow struct {
		SupplierID uint    `gorm:"column:supplier_id"`
		Total      float64 `gorm:"column:total"`
	}

	rows := make([]purchaseTotalRow, 0)
	if err := r.db.WithContext(ctx).
		Table("purchases").
		Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
		Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
		Joins("JOIN (?) AS la ON la.approvable_id = purchases.id", r.latestPurchaseApproval(ctx)).
		Where("purchases.supplier_id IN ?", supplierIDs).
		Where("la.step_number >= ?", uint16(utils.PurchaseStepReceiving)).
		Where("(la.action IS NULL OR la.action != ?)", string(entity.ApprovalActionRejected)).
		Where("purchase_items.received_date IS NOT NULL").
		Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
		Group("purchases.supplier_id").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make(map[uint]float64, len(rows))
	for _, row := range rows {
		result[row.SupplierID] = row.Total
	}

	return result, nil
}

func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
	if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
		return map[uint]float64{}, nil
	}

	dateFrom, err := utils.ParseDateString(filters.StartDate)
	if err != nil {
		return map[uint]float64{}, nil
	}

	type paymentTotalRow struct {
		SupplierID uint    `gorm:"column:supplier_id"`
		Total      float64 `gorm:"column:total"`
	}

	rows := make([]paymentTotalRow, 0)
	if err := r.db.WithContext(ctx).
		Model(&entity.Payment{}).
		Select("party_id AS supplier_id, SUM(nominal) AS total").
		Where("party_type = ?", string(utils.PaymentPartySupplier)).
		Where("direction = ?", "OUT").
		Where("party_id IN ?", supplierIDs).
		Where("transaction_type <> ?", string(utils.TransactionTypeSaldoAwal)).
		Where("DATE(payment_date) < ?", dateFrom).
		Group("party_id").
		Scan(&rows).Error; err != nil {
		return nil, err
	}

	result := make(map[uint]float64, len(rows))
	for _, row := range rows {
		result[row.SupplierID] = row.Total
	}

	return result, nil
}
