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

func NewInjectionService(
	repo repository.InjectionRepository,
	approvalSvc commonSvc.ApprovalService,
	validate *validator.Validate,
) InjectionService {
	return &injectionService{
		Log:              utils.Log,
		Validate:         validate,
		Repository:       repo,
		ApprovalSvc:      approvalSvc,
		approvalWorkflow: utils.ApprovalWorkflowInjection,
	}
}

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

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

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

	if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
		return nil, err
	}

	adjustmentDate, err := utils.ParseDateString(req.AdjustmentDate)
	if err != nil {
		return nil, utils.BadRequest(err.Error())
	}

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

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

	createBody := &entity.Payment{
		PaymentCode:     code,
		TransactionType: string(utils.TransactionTypeInjection),
		PartyType:       string(utils.PaymentPartyCustomer),
		PartyId:         0,
		PartyAccountNumber: nil,
		PaymentDate:     adjustmentDate,
		PaymentMethod:   string(utils.PaymentMethodSaldo),
		BankId:          req.BankId,
		Direction:       directionForInjectionNominal(req.Nominal),
		Nominal:         req.Nominal,
		Notes:           req.Notes,
		CreatedBy:       actorID,
	}

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

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

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

func (s injectionService) 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)

	requiresVerification := req.BankId != nil || req.AdjustmentDate != nil || req.Nominal != nil || req.Notes != nil
	if requiresVerification {
		current, err := s.Repository.GetByID(c.Context(), id, nil)
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
		}
		if err != nil {
			s.Log.Errorf("Failed get injection by id: %+v", err)
			return nil, err
		}
		if !isInjectionTransaction(current.TransactionType) {
			return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
		}
	}

	if req.BankId != nil {
		if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
			return nil, err
		}
		updateBody["bank_id"] = *req.BankId
	}
	if req.AdjustmentDate != nil {
		parsedDate, err := utils.ParseDateString(*req.AdjustmentDate)
		if err != nil {
			return nil, utils.BadRequest(err.Error())
		}
		updateBody["payment_date"] = parsedDate
	}
	if req.Nominal != nil {
		updateBody["nominal"] = *req.Nominal
		updateBody["direction"] = directionForInjectionNominal(*req.Nominal)
	}
	if req.Notes != nil {
		updateBody["notes"] = *req.Notes
	}

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

	return s.GetOne(c, id)
}

func isInjectionTransaction(transactionType string) bool {
	return strings.EqualFold(transactionType, string(utils.TransactionTypeInjection))
}

func directionForInjectionNominal(nominal float64) string {
	if nominal < 0 {
		return "OUT"
	}
	return "IN"
}

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

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

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