package service

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strings"
	"time"

	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/initials/repositories"
	validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/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 InitialService 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 initialService struct {
	Log              *logrus.Logger
	Validate         *validator.Validate
	Repository       repository.InitialRepository
	ApprovalSvc      commonSvc.ApprovalService
	approvalWorkflow approvalutils.ApprovalWorkflowKey
}

func NewInitialService(
	repo repository.InitialRepository,
	approvalSvc commonSvc.ApprovalService,
	validate *validator.Validate,
) InitialService {
	return &initialService{
		Log:              utils.Log,
		Validate:         validate,
		Repository:       repo,
		ApprovalSvc:      approvalSvc,
		approvalWorkflow: utils.ApprovalWorkflowInitial,
	}
}

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

func (s initialService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
	initial, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
	}
	if err != nil {
		s.Log.Errorf("Failed get initial by id: %+v", err)
		return nil, err
	}
	if !isInitialTransaction(initial.TransactionType) {
		return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
	}
	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 initial %d: %+v", id, err)
		} else {
			initial.LatestApproval = approval
		}
	}
	return initial, nil
}

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

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

	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
	}

	balanceType, err := normalizeInitialBalanceType(req.InitialBalanceType)
	if err != nil {
		return nil, err
	}

	actorID, err := m.ActorIDFromContext(c)
	if err != nil {
		return nil, err
	}

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

	reference := req.ReferenceNumber
	createBody := &entity.Payment{
		PaymentCode:     code,
		ReferenceNumber: &reference,
		TransactionType: string(utils.TransactionTypeSaldoAwal),
		PartyType:       party,
		PartyId:         req.PartyId,
		PartyAccountNumber: nil,
		PaymentDate:     time.Now(),
		PaymentMethod:   string(utils.PaymentMethodSaldo),
		BankId:          req.BankId,
		Direction:       directionForInitialType(party, balanceType),
		Nominal:         signedNominal(balanceType, req.Nominal),
		Notes:           req.Note,
		CreatedBy:       actorID,
	}

	err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
		initialRepoTx := repository.NewInitialRepository(dbTransaction)
		if err := initialRepoTx.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.InitialStepPengajuan,
				&action,
				actorID,
				nil,
			)
			if err != nil {
				return err
			}
		}

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

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

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

	updateBody := make(map[string]any)

	if req.ReferenceNumber != nil {
		updateBody["reference_number"] = *req.ReferenceNumber
	}
	if req.Note != nil {
		updateBody["notes"] = *req.Note
	}
	if req.BankId != nil {
		if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
			return nil, err
		}
		updateBody["bank_id"] = *req.BankId
	}

	requiresExisting := req.PartyType != nil || req.PartyId != nil || req.InitialBalanceType != nil || req.Nominal != nil
	requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil
	var existing *entity.Payment
	var resolvedPartyType string
	var resolvedPartyId uint
	if requiresVerification {
		current, err := s.Repository.GetByID(c.Context(), id, nil)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
		}
		if err != nil {
			s.Log.Errorf("Failed get initial by id: %+v", err)
			return nil, err
		}
		if !isInitialTransaction(current.TransactionType) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
		}
		existing = current
		resolvedPartyType = existing.PartyType
		resolvedPartyId = existing.PartyId
	}

	if req.PartyType != nil || req.PartyId != nil {
		if req.PartyType != nil {
			normalized, err := normalizePartyType(*req.PartyType)
			if err != nil {
				return nil, err
			}
			resolvedPartyType = normalized
			updateBody["party_type"] = resolvedPartyType
		}
		if req.PartyId != nil {
			resolvedPartyId = *req.PartyId
			updateBody["party_id"] = resolvedPartyId
		}

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

	if req.InitialBalanceType != nil || req.Nominal != nil {
		balanceType := balanceTypeFromPayment(existing)
		if req.InitialBalanceType != nil {
			normalized, err := normalizeInitialBalanceType(*req.InitialBalanceType)
			if err != nil {
				return nil, err
			}
			balanceType = normalized
		}

		nominal := math.Abs(existing.Nominal)
		if req.Nominal != nil {
			nominal = *req.Nominal
		}

		updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
		updateBody["nominal"] = signedNominal(balanceType, nominal)
	} else if req.PartyType != nil {
		balanceType := balanceTypeFromPayment(existing)
		updateBody["direction"] = directionForInitialType(resolvedPartyType, balanceType)
	}

	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, "Initial not found")
		}
		s.Log.Errorf("Failed to update initial: %+v", err)
		return nil, err
	}

	return s.GetOne(c, id)
}

func isInitialTransaction(transactionType string) bool {
	return strings.EqualFold(transactionType, string(utils.TransactionTypeSaldoAwal))
}

func balanceTypeFromPayment(payment *entity.Payment) string {
	if payment.Nominal < 0 {
		return "NEGATIVE"
	}
	return "POSITIVE"
}

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 normalizeInitialBalanceType(balanceType string) (string, error) {
	normalized := strings.ToUpper(strings.TrimSpace(balanceType))
	switch normalized {
	case "NEGATIVE", "POSITIVE":
		return normalized, nil
	default:
		return "", utils.BadRequest("`initial_balance_type` must be `NEGATIVE` or `POSITIVE`")
	}
}

func directionForInitialType(partyType string, balanceType string) string {
	switch utils.PaymentParty(strings.ToUpper(strings.TrimSpace(partyType))) {
	case utils.PaymentPartySupplier:
		if strings.EqualFold(balanceType, "POSITIVE") {
			return "OUT"
		}
		return "IN"
	case utils.PaymentPartyCustomer:
		if strings.EqualFold(balanceType, "NEGATIVE") {
			return "OUT"
		}
		return "IN"
	default:
		if strings.EqualFold(balanceType, "NEGATIVE") {
			return "OUT"
		}
		return "IN"
	}
}

func signedNominal(balanceType string, nominal float64) float64 {
	normalized := math.Abs(nominal)
	if strings.EqualFold(balanceType, "NEGATIVE") {
		return -normalized
	}
	return normalized
}

func (s initialService) generateInitialCode(ctx context.Context) (string, error) {
	sequence, err := s.Repository.NextPaymentSequence(ctx)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("INIT-%05d", sequence), nil
}

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

func (s initialService) 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 initialService) ensureBankExists(ctx context.Context, bankId *uint) error {
	return commonSvc.EnsureRelations(ctx,
		commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
	)
}

func normalizeOptionalBankId(bankId **uint) {
	if bankId == nil || *bankId == nil {
		return
	}
	if **bankId == 0 {
		*bankId = nil
	}
}
