package service

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

	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/validations"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/sirupsen/logrus"
	"gorm.io/gorm"
)

type TransactionService interface {
	GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Payment, int64, error)
	GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
	DeleteOne(ctx *fiber.Ctx, id uint) error
}

type transactionService struct {
	Log               *logrus.Logger
	Validate          *validator.Validate
	Repository        repository.TransactionRepository
	ApprovalSvc       commonSvc.ApprovalService
	approvalWorkflows map[string]approvalutils.ApprovalWorkflowKey
}

func NewTransactionService(
	repo repository.TransactionRepository,
	approvalSvc commonSvc.ApprovalService,
	validate *validator.Validate,
) TransactionService {
	return &transactionService{
		Log:         utils.Log,
		Validate:    validate,
		Repository:  repo,
		ApprovalSvc: approvalSvc,
		approvalWorkflows: map[string]approvalutils.ApprovalWorkflowKey{
			string(utils.TransactionTypeSaldoAwal): utils.ApprovalWorkflowInitial,
			string(utils.TransactionTypeInjection): utils.ApprovalWorkflowInjection,
		},
	}
}

func (s transactionService) withRelations(db *gorm.DB) *gorm.DB {
	return db.Preload("CreatedUser").
		Preload("BankWarehouse").
		Preload("Customer").
		Preload("Supplier")
}

func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Payment, int64, error) {
	if err := s.Validate.Struct(params); err != nil {
		return nil, 0, err
	}

	startDate, endDate, err := parseTransactionDateRange(params.StartDate, params.EndDate)
	if err != nil {
		return nil, 0, err
	}

	offset := (params.Page - 1) * params.Limit

	transactions, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
		db = s.withRelations(db)

		if params.Search != "" {
			like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
			db = db.Joins(
				"LEFT JOIN customers ON customers.id = payments.party_id AND payments.party_type = ? AND customers.deleted_at IS NULL",
				string(utils.PaymentPartyCustomer),
			).Joins(
				"LEFT JOIN suppliers ON suppliers.id = payments.party_id AND payments.party_type = ? AND suppliers.deleted_at IS NULL",
				string(utils.PaymentPartySupplier),
			).Joins(
				"LEFT JOIN banks ON banks.id = payments.bank_id AND banks.deleted_at IS NULL",
			)
			db = db.Where(
				`LOWER(payment_code) LIKE ? OR
				LOWER(COALESCE(reference_number, '')) LIKE ? OR
				LOWER(COALESCE(payment_method, '')) LIKE ? OR
				LOWER(COALESCE(transaction_type, '')) LIKE ? OR
				LOWER(COALESCE(notes, '')) LIKE ? OR
				LOWER(COALESCE(customers.name, '')) LIKE ? OR
				LOWER(COALESCE(suppliers.name, '')) LIKE ? OR
				LOWER(COALESCE(banks.name, '')) LIKE ?`,
				like, like, like, like, like, like, like, like,
			)
		}

		if len(params.TransactionTypes) > 0 {
			types := make([]string, 0, len(params.TransactionTypes))
			for _, transactionType := range params.TransactionTypes {
				normalized := strings.ToUpper(strings.TrimSpace(transactionType))
				if normalized == "" {
					continue
				}
				types = append(types, normalized)
			}
			if len(types) > 0 {
				db = db.Where("transaction_type IN ?", types)
			}
		}

		if len(params.BankIDs) > 0 {
			db = db.Where("bank_id IN ?", params.BankIDs)
		}

		customerIDs := params.CustomerIDs
		supplierIDs := params.SupplierIDs

		if len(customerIDs) > 0 && len(supplierIDs) > 0 {
			db = db.Where(
				"(party_type = ? AND party_id IN ?) OR (party_type = ? AND party_id IN ?)",
				string(utils.PaymentPartyCustomer), customerIDs,
				string(utils.PaymentPartySupplier), supplierIDs,
			)
		} else if len(customerIDs) > 0 {
			db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartyCustomer), customerIDs)
		} else if len(supplierIDs) > 0 {
			db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartySupplier), supplierIDs)
		}

		if startDate != nil {
			db = db.Where("payment_date >= ?", *startDate)
		}
		if endDate != nil {
			db = db.Where("payment_date < ?", *endDate)
		}

		return applyTransactionSort(db, params.SortDate)
	})

	if err != nil {
		s.Log.Errorf("Failed to get transactions: %+v", err)
		return nil, 0, err
	}
	s.attachApprovals(c.Context(), transactions)
	return transactions, total, nil
}

func (s transactionService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
	transaction, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Transaction not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get transaction by id: %+v", err)
		return nil, err
	}
	if s.ApprovalSvc != nil {
		approval, err := s.ApprovalSvc.LatestByTarget(
			c.Context(),
			s.workflowForTransaction(transaction),
			id,
			s.approvalQueryModifier(),
		)
		if err != nil {
			s.Log.Warnf("Unable to load latest approval for transaction %d: %+v", id, err)
		} else {
			transaction.LatestApproval = approval
		}
	}
	return transaction, nil
}

func (s transactionService) DeleteOne(c *fiber.Ctx, id uint) error {
	if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return fiber.NewError(fiber.StatusNotFound, "Transaction not found")
		}
		s.Log.Errorf("Failed to delete transaction: %+v", err)
		return err
	}
	return nil
}

func (s transactionService) attachApprovals(ctx context.Context, transactions []entity.Payment) {
	if s.ApprovalSvc == nil || len(transactions) == 0 {
		return
	}

	workflowIDs := map[approvalutils.ApprovalWorkflowKey][]uint{}
	for _, transaction := range transactions {
		workflow := s.workflowForTransaction(&transaction)
		workflowIDs[workflow] = append(workflowIDs[workflow], transaction.Id)
	}

	approvalByWorkflow := make(map[approvalutils.ApprovalWorkflowKey]map[uint]*entity.Approval, len(workflowIDs))
	for workflow, ids := range workflowIDs {
		if len(ids) == 0 {
			continue
		}
		approvals, err := s.ApprovalSvc.LatestByTargets(ctx, workflow, ids, s.approvalQueryModifier())
		if err != nil {
			s.Log.Warnf("Unable to load latest approvals for transactions: %+v", err)
			continue
		}
		approvalByWorkflow[workflow] = approvals
	}

	for i := range transactions {
		workflow := s.workflowForTransaction(&transactions[i])
		if approvals, ok := approvalByWorkflow[workflow]; ok {
			transactions[i].LatestApproval = approvals[transactions[i].Id]
		}
	}
}

func (s transactionService) workflowForTransaction(transaction *entity.Payment) approvalutils.ApprovalWorkflowKey {
	if transaction == nil {
		return utils.ApprovalWorkflowPayment
	}
	transactionType := strings.TrimSpace(strings.ToUpper(transaction.TransactionType))
	if transactionType == "" {
		return utils.ApprovalWorkflowPayment
	}
	if workflow, ok := s.approvalWorkflows[transactionType]; ok {
		return workflow
	}
	return utils.ApprovalWorkflowPayment
}

func (s transactionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Preload("ActionUser")
	}
}

func parseTransactionDateRange(startDate, endDate string) (*time.Time, *time.Time, error) {
	start := strings.TrimSpace(startDate)
	end := strings.TrimSpace(endDate)

	var startPtr *time.Time
	var endPtr *time.Time
	var endValue *time.Time

	if start != "" {
		parsed, err := utils.ParseDateString(start)
		if err != nil {
			return nil, nil, utils.BadRequest("start_date must use format YYYY-MM-DD")
		}
		startPtr = &parsed
	}

	if end != "" {
		parsed, err := utils.ParseDateString(end)
		if err != nil {
			return nil, nil, utils.BadRequest("end_date must use format YYYY-MM-DD")
		}
		endValue = &parsed
		nextDay := parsed.AddDate(0, 0, 1)
		endPtr = &nextDay
	}

	if startPtr != nil && endValue != nil && startPtr.After(*endValue) {
		return nil, nil, utils.BadRequest("start_date must be earlier than end_date")
	}

	return startPtr, endPtr, nil
}

func applyTransactionSort(db *gorm.DB, sortDate string) *gorm.DB {
	switch strings.ToLower(strings.TrimSpace(sortDate)) {
	case "created_at":
		return db.Order("created_at DESC").Order("payment_date DESC")
	case "payment_date":
		return db.Order("payment_date DESC").Order("created_at DESC")
	default:
		return db.Order("payment_date DESC").Order("created_at DESC")
	}
}
