package service

import (
	"context"
	"errors"
	"fmt"
	"strings"

	commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
	commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
	repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/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 PaymentService interface {
	GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
	CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error)
	UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error)
}

type paymentService struct {
	Log              *logrus.Logger
	Validate         *validator.Validate
	Repository       repository.PaymentRepository
	ApprovalSvc      commonSvc.ApprovalService
	approvalWorkflow approvalutils.ApprovalWorkflowKey
}

func NewPaymentService(
	repo repository.PaymentRepository,
	approvalSvc commonSvc.ApprovalService,
	validate *validator.Validate,
) PaymentService {
	return &paymentService{
		Log:              utils.Log,
		Validate:         validate,
		Repository:       repo,
		ApprovalSvc:      approvalSvc,
		approvalWorkflow: utils.ApprovalWorkflowPayment,
	}
}

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

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

func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	//! CHECK PARTY TYPE
	party, err := normalizePartyType(req.PartyType)
	if err != nil {
		return nil, err
	}

	//! CHECK EXISTS
	if err := s.ensurePartyExists(c.Context(), party, req.PartyId); err != nil {
		return nil, err
	}
	if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
		return nil, err
	}

	//? NORMALIZE
	paymentDate, err := utils.ParseDateString(req.PaymentDate)
	if err != nil {
		return nil, utils.BadRequest(err.Error())
	}
	method, err := normalizePaymentMethod(req.PaymentMethod)
	if err != nil {
		return nil, err
	}
	transactionType, err := s.resolveTransactionType(c.Context(), party, req.PartyId)
	if err != nil {
		return nil, err
	}

	//? GET CREATED BY
	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}

	code, err := s.generatePaymentCode(c.Context(), party)
	if err != nil {
		return nil, err
	}

	createBody := &entity.Payment{
		PaymentCode:        code,
		ReferenceNumber:    req.ReferenceNumber,
		TransactionType:    transactionType,
		PartyType:          party,
		PartyId:            req.PartyId,
		PartyAccountNumber: req.PartyAccountNumber,
		PaymentDate:        paymentDate,
		PaymentMethod:      method,
		BankId:             req.BankId,
		Direction:          directionForParty(party),
		Nominal:            req.Nominal,
		Notes:              req.Notes,
		CreatedBy:          actorID,
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		paymentRepoTx := repository.NewPaymentRepository(dbTransaction)
		if err := paymentRepoTx.CreateOne(c.Context(), createBody, nil); err != nil {
			return err
		}

		if s.ApprovalSvc != nil {
			action := entity.ApprovalActionCreated
			approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
			_, err := approvalSvcTx.CreateApproval(
				c.Context(),
				s.approvalWorkflow,
				createBody.Id,
				utils.PaymentStepPengajuan,
				&action,
				actorID,
				nil,
			)
			if err != nil {
				return err
			}
		}

		return nil
	})
	if err != nil {
		s.Log.Errorf("Failed to create payment: %+v", err)
		return nil, err
	}

	return s.GetOne(c, createBody.Id)
}

func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
	if err := s.Validate.Struct(req); err != nil {
		return nil, err
	}

	updateBody := make(map[string]any)

	if req.PaymentDate != nil {
		parsedDate, err := utils.ParseDateString(*req.PaymentDate)
		if err != nil {
			return nil, utils.BadRequest(err.Error())
		}
		updateBody["payment_date"] = parsedDate
	}
	if req.Nominal != nil {
		updateBody["nominal"] = *req.Nominal
	}
	if req.ReferenceNumber != nil {
		updateBody["reference_number"] = *req.ReferenceNumber
	}
	if req.PartyAccountNumber != nil {
		updateBody["party_account_number"] = *req.PartyAccountNumber
	}
	if req.PaymentMethod != nil {
		method, err := normalizePaymentMethod(*req.PaymentMethod)
		if err != nil {
			return nil, err
		}
		updateBody["payment_method"] = method
	}
	if req.BankId != nil {
		if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
			return nil, err
		}
		updateBody["bank_id"] = *req.BankId
	}
	if req.Notes != nil {
		updateBody["notes"] = *req.Notes
	}

	if req.PartyType != nil || req.PartyId != nil {
		existing, err := s.Repository.GetByID(c.Context(), id, nil)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found")
		}
		if err != nil {
			s.Log.Errorf("Failed get payment by id: %+v", err)
			return nil, err
		}

		partyType := existing.PartyType
		partyId := existing.PartyId

		if req.PartyType != nil {
			normalized, err := normalizePartyType(*req.PartyType)
			if err != nil {
				return nil, err
			}
			partyType = normalized
			updateBody["party_type"] = partyType
			updateBody["direction"] = directionForParty(partyType)
		}
		if req.PartyId != nil {
			partyId = *req.PartyId
			updateBody["party_id"] = partyId
		}

		if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
			return nil, err
		}

		transactionType, err := s.resolveTransactionType(c.Context(), partyType, partyId)
		if err != nil {
			return nil, err
		}
		updateBody["transaction_type"] = transactionType
	}

	if len(updateBody) == 0 {
		return s.GetOne(c, id)
	}

	if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found")
		}
		s.Log.Errorf("Failed to update payment: %+v", err)
		return nil, err
	}

	return s.GetOne(c, id)
}

func normalizePartyType(partyType string) (string, error) {
	party := strings.ToUpper(strings.TrimSpace(partyType))
	if !utils.IsValidPaymentParty(party) {
		return "", utils.BadRequest("`party_type` must be `customer` or `supplier`")
	}
	return party, nil
}

func normalizePaymentMethod(method string) (string, error) {
	normalized := strings.ToUpper(strings.TrimSpace(method))
	if !utils.IsValidPaymentMethod(normalized) {
		return "", utils.BadRequest("Invalid payment_method")
	}
	return normalized, nil
}

func directionForParty(partyType string) string {
	if utils.PaymentParty(partyType) == utils.PaymentPartyCustomer {
		return "IN"
	}
	return "OUT"
}

func (s paymentService) resolveTransactionType(ctx context.Context, partyType string, partyId uint) (string, error) {
	switch utils.PaymentParty(partyType) {
	case utils.PaymentPartyCustomer:
		return string(utils.TransactionTypePenjualan), nil
	case utils.PaymentPartySupplier:
		category, err := s.getSupplierCategory(ctx, partyId)
		if err != nil {
			return "", err
		}
		if isSupplierCategoryBiaya(category) {
			return string(utils.TransactionTypeBiaya), nil
		}
		return string(utils.TransactionTypePembelian), nil
	default:
		return "", utils.BadRequest("`party_type` must be `customer` or `supplier`")
	}
}

func (s paymentService) generatePaymentCode(ctx context.Context, partyType string) (string, error) {
	prefix := "PAY"
	switch utils.PaymentParty(partyType) {
	case utils.PaymentPartyCustomer:
		prefix = "PAY-IN"
	case utils.PaymentPartySupplier:
		prefix = "PAY-OUT"
	}
	sequence, err := s.Repository.NextPaymentSequence(ctx)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s-%05d", prefix, sequence), nil
}

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

func (s paymentService) ensurePartyExists(ctx context.Context, partyType string, partyId uint) error {
	switch utils.PaymentParty(partyType) {
	case utils.PaymentPartyCustomer:
		return commonSvc.EnsureRelations(ctx,
			commonSvc.RelationCheck{Name: "Customer", ID: &partyId, Exists: s.Repository.CustomerExists},
		)
	case utils.PaymentPartySupplier:
		return commonSvc.EnsureRelations(ctx,
			commonSvc.RelationCheck{Name: "Supplier", ID: &partyId, Exists: s.Repository.SupplierExists},
		)
	default:
		return utils.BadRequest("`party_type` must be `customer` or `supplier`")
	}
}

func (s paymentService) ensureBankExists(ctx context.Context, bankId *uint) error {
	return commonSvc.EnsureRelations(ctx,
		commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
	)
}

func (s paymentService) getSupplierCategory(ctx context.Context, supplierId uint) (string, error) {
	category, err := s.Repository.SupplierCategory(ctx, supplierId)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return "", utils.NotFound("Supplier not found")
		}
		return "", err
	}
	return strings.ToUpper(strings.TrimSpace(category)), nil
}

func isSupplierCategoryBiaya(category string) bool {
	switch strings.ToUpper(strings.TrimSpace(category)) {
	case string(utils.SupplierCategoryBOP), "BIAYA":
		return true
	default:
		return false
	}
}
