package ledger

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

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/ledger/storage/model"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	pmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/mutil/mzap"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

// transferResponder implements internal transfer between accounts
func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferRequest) gsresponse.Responder[ledgerv1.PostResponse] {
	return func(ctx context.Context) (*ledgerv1.PostResponse, error) {
		// Validate request
		if req.IdempotencyKey == "" {
			return nil, merrors.InvalidArgument("idempotency_key is required")
		}
		if req.OrganizationRef == "" {
			return nil, merrors.InvalidArgument("organization_ref is required")
		}
		fromRoleModel := account_role.AccountRole("")
		if req.FromRole != ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
			var err error
			fromRoleModel, err = protoAccountRoleToModel(req.FromRole)
			if err != nil {
				return nil, err
			}
		} else if strings.TrimSpace(req.FromLedgerAccountRef) == "" {
			fromRoleModel = account_role.AccountRoleOperating
		}
		toRoleModel := account_role.AccountRole("")
		if req.ToRole != ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
			var err error
			toRoleModel, err = protoAccountRoleToModel(req.ToRole)
			if err != nil {
				return nil, err
			}
		} else if strings.TrimSpace(req.ToLedgerAccountRef) == "" {
			toRoleModel = account_role.AccountRoleOperating
		}
		if strings.TrimSpace(req.FromLedgerAccountRef) == "" && fromRoleModel == "" {
			return nil, merrors.InvalidArgument("from_ledger_account_ref or from_role is required")
		}
		if strings.TrimSpace(req.ToLedgerAccountRef) == "" && toRoleModel == "" {
			return nil, merrors.InvalidArgument("to_ledger_account_ref or to_role is required")
		}
		// Early self-transfer check when both refs are provided explicitly
		if strings.TrimSpace(req.FromLedgerAccountRef) != "" && req.FromLedgerAccountRef == req.ToLedgerAccountRef {
			return nil, merrors.InvalidArgument("cannot transfer to same account")
		}
		if err := validateMoney(req.Money, "money"); err != nil {
			return nil, err
		}
		if err := validatePostingLines(req.Charges); err != nil {
			return nil, err
		}

		orgRef, err := parseObjectID(req.OrganizationRef)
		if err != nil {
			return nil, err
		}
		logger := s.logger.With(
			zap.String("idempotency_key", req.IdempotencyKey),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("from_account_ref", strings.TrimSpace(req.FromLedgerAccountRef)),
			zap.String("to_account_ref", strings.TrimSpace(req.ToLedgerAccountRef)),
			zap.String("currency", req.Money.Currency),
		)
		if fromRoleModel != "" {
			logger = logger.With(zap.String("from_role", string(fromRoleModel)))
		}
		if toRoleModel != "" {
			logger = logger.With(zap.String("to_role", string(toRoleModel)))
		}

		// Check for duplicate idempotency key
		existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey)
		if err == nil && existingEntry != nil {
			recordDuplicateRequest(journalEntryTypeTransfer)
			logger.Info("Duplicate transfer request (idempotency)", mzap.StorableRef(existingEntry))
			return &ledgerv1.PostResponse{
				JournalEntryRef: existingEntry.GetID().Hex(),
				Version:         existingEntry.Version,
				EntryType:       ledgerv1.EntryType_ENTRY_TRANSFER,
			}, nil
		}
		if err != nil && !errors.Is(err, storage.ErrJournalEntryNotFound) {
			logger.Warn("Failed to check idempotency", zap.Error(err))
			return nil, merrors.Internal("failed to check idempotency")
		}

		// Resolve both accounts — by ref, by role, or ref+role assertion
		fromAccount, fromAccountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.FromLedgerAccountRef), fromRoleModel, orgRef, req.Money.Currency, "from_account")
		if err != nil {
			return nil, err
		}
		if err := validateAccountForOrg(fromAccount, orgRef, req.Money.Currency); err != nil {
			return nil, merrors.InvalidArgument(fmt.Sprintf("from_account: %s", err.Error()))
		}

		toAccount, toAccountRef, err := s.resolveAccount(ctx, strings.TrimSpace(req.ToLedgerAccountRef), toRoleModel, orgRef, req.Money.Currency, "to_account")
		if err != nil {
			return nil, err
		}
		if err := validateAccountForOrg(toAccount, orgRef, req.Money.Currency); err != nil {
			return nil, merrors.InvalidArgument(fmt.Sprintf("to_account: %s", err.Error()))
		}

		// Post-resolution self-transfer check (catches role-resolved collisions)
		if fromAccountRef == toAccountRef {
			return nil, merrors.InvalidArgument("cannot transfer to same account")
		}

		accountsByRef := map[bson.ObjectID]*pmodel.LedgerAccount{
			fromAccountRef: fromAccount,
			toAccountRef:   toAccount,
		}

		eventTime := getEventTime(req.EventTime)
		transferAmount, _ := parseDecimal(req.Money.Amount)

		// Create posting lines for transfer
		// Dr From Account (debit = negative)
		// Cr To Account (credit = positive)
		postingLines := make([]*model.PostingLine, 0, 2+len(req.Charges))

		// Debit from account
		fromLine := &model.PostingLine{
			JournalEntryRef: bson.NilObjectID,
			AccountRef:      fromAccountRef,
			Amount:          transferAmount.Neg().String(), // negative = debit
			Currency:        req.Money.Currency,
			LineType:        model.LineTypeMain,
		}
		fromLine.OrganizationRef = orgRef
		postingLines = append(postingLines, fromLine)

		// Credit to account
		toLine := &model.PostingLine{
			JournalEntryRef: bson.NilObjectID,
			AccountRef:      toAccountRef,
			Amount:          transferAmount.String(), // positive = credit
			Currency:        req.Money.Currency,
			LineType:        model.LineTypeMain,
		}
		toLine.OrganizationRef = orgRef
		postingLines = append(postingLines, toLine)

		// Process charges (fees/spreads)
		for i, charge := range req.Charges {
			chargeAccountRef, err := parseObjectID(charge.LedgerAccountRef)
			if err != nil {
				return nil, err
			}
			if charge.Money.Currency != req.Money.Currency {
				return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: currency mismatch", i))
			}

			chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef)
			if err != nil {
				if errors.Is(err, storage.ErrAccountNotFound) {
					return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i))
				}
				logger.Warn("Failed to get charge account", zap.Error(err), mzap.ObjRef("charge_account_ref", chargeAccountRef))
				return nil, merrors.Internal("failed to get charge account")
			}
			if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil {
				return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: %s", i, err.Error()))
			}

			chargeAmount, err := parseDecimal(charge.Money.Amount)
			if err != nil {
				return nil, err
			}

			chargeLine := &model.PostingLine{
				JournalEntryRef: bson.NilObjectID,
				AccountRef:      chargeAccountRef,
				Amount:          chargeAmount.String(),
				Currency:        charge.Money.Currency,
				LineType:        protoLineTypeToModel(charge.LineType),
			}
			chargeLine.OrganizationRef = orgRef
			postingLines = append(postingLines, chargeLine)
		}

		// Execute in transaction
		result, err := s.executeTransaction(ctx, func(txCtx context.Context) (any, error) {
			entry := &model.JournalEntry{
				IdempotencyKey: req.IdempotencyKey,
				EventTime:      eventTime,
				EntryType:      model.EntryTypeTransfer,
				Description:    req.Description,
				Metadata:       req.Metadata,
				Version:        time.Now().UnixNano(),
			}
			entry.OrganizationRef = orgRef

			if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil {
				logger.Warn("Failed to create journal entry", zap.Error(err))
				return nil, merrors.Internal("failed to create journal entry")
			}

			entryRef := entry.GetID()
			if entryRef == nil {
				return nil, merrors.Internal("journal entry missing identifier")
			}

			for _, line := range postingLines {
				line.JournalEntryRef = *entryRef
			}

			if err := validateBalanced(postingLines); err != nil {
				return nil, err
			}

			if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil {
				logger.Warn("Failed to create posting lines", zap.Error(err))
				return nil, merrors.Internal("failed to create posting lines")
			}

			if err := s.upsertBalances(txCtx, postingLines, accountsByRef); err != nil {
				return nil, err
			}

			if err := s.enqueueOutbox(txCtx, entry, postingLines); err != nil {
				return nil, err
			}

			return &ledgerv1.PostResponse{
				JournalEntryRef: entryRef.Hex(),
				Version:         entry.Version,
				EntryType:       ledgerv1.EntryType_ENTRY_TRANSFER,
			}, nil
		})

		if err != nil {
			recordJournalEntryError(journalEntryTypeTransfer, journalEntryErrorFailed)
			return nil, err
		}

		amountFloat, _ := transferAmount.Float64()
		recordTransactionAmount(req.Money.Currency, journalEntryTypeTransfer, amountFloat)
		recordJournalEntry(journalEntryTypeTransfer, journalEntryStatusSuccess, 0)
		return result.(*ledgerv1.PostResponse), nil
	}
}
