package ledger

import (
	"fmt"
	"time"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/ledger/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// parseObjectID converts a hex string to ObjectID
func parseObjectID(hexID string) (bson.ObjectID, error) {
	if hexID == "" {
		return bson.NilObjectID, merrors.InvalidArgument("empty object ID")
	}
	oid, err := bson.ObjectIDFromHex(hexID)
	if err != nil {
		return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("invalid object ID: %v", err))
	}
	return oid, nil
}

// parseDecimal converts a string amount to decimal
func parseDecimal(amount string) (decimal.Decimal, error) {
	if amount == "" {
		return decimal.Zero, merrors.InvalidArgument("empty amount")
	}
	dec, err := decimal.NewFromString(amount)
	if err != nil {
		return decimal.Zero, merrors.InvalidArgument(fmt.Sprintf("invalid decimal amount: %v", err))
	}
	return dec, nil
}

// validateMoney checks that a Money message is valid
func validateMoney(m *moneyv1.Money, fieldName string) error {
	if m == nil {
		return merrors.InvalidArgument(fmt.Sprintf("%s: money is required", fieldName))
	}
	if m.Amount == "" {
		return merrors.InvalidArgument(fmt.Sprintf("%s: amount is required", fieldName))
	}
	if m.Currency == "" {
		return merrors.InvalidArgument(fmt.Sprintf("%s: currency is required", fieldName))
	}
	// Validate it's a valid decimal
	if _, err := parseDecimal(m.Amount); err != nil {
		return err
	}
	return nil
}

// validatePostingLines validates charge lines
func validatePostingLines(lines []*ledgerv1.PostingLine) error {
	for i, line := range lines {
		if line == nil {
			return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: nil posting line", i))
		}
		if line.LedgerAccountRef == "" {
			return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: ledger_account_ref is required", i))
		}
		if line.Money == nil {
			return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: money is required", i))
		}
		if err := validateMoney(line.Money, fmt.Sprintf("charges[%d].money", i)); err != nil {
			return err
		}
		// Charges should not be MAIN type
		if line.LineType == ledgerv1.LineType_LINE_MAIN {
			return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: cannot have LINE_MAIN type", i))
		}
	}
	return nil
}

// getEventTime extracts event time from proto or defaults to now
func getEventTime(ts *timestamppb.Timestamp) time.Time {
	if ts != nil && ts.IsValid() {
		return ts.AsTime()
	}
	return time.Now().UTC()
}

// protoLineTypeToModel converts proto LineType to model LineType
func protoLineTypeToModel(lt ledgerv1.LineType) model.LineType {
	switch lt {
	case ledgerv1.LineType_LINE_MAIN:
		return model.LineTypeMain
	case ledgerv1.LineType_LINE_FEE:
		return model.LineTypeFee
	case ledgerv1.LineType_LINE_SPREAD:
		return model.LineTypeSpread
	case ledgerv1.LineType_LINE_REVERSAL:
		return model.LineTypeReversal
	default:
		return model.LineTypeMain
	}
}

// modelLineTypeToProto converts model LineType to proto LineType
func modelLineTypeToProto(lt model.LineType) ledgerv1.LineType {
	switch lt {
	case model.LineTypeMain:
		return ledgerv1.LineType_LINE_MAIN
	case model.LineTypeFee:
		return ledgerv1.LineType_LINE_FEE
	case model.LineTypeSpread:
		return ledgerv1.LineType_LINE_SPREAD
	case model.LineTypeReversal:
		return ledgerv1.LineType_LINE_REVERSAL
	default:
		return ledgerv1.LineType_LINE_TYPE_UNSPECIFIED
	}
}

// modelEntryTypeToProto converts model EntryType to proto EntryType
func modelEntryTypeToProto(et model.EntryType) ledgerv1.EntryType {
	switch et {
	case model.EntryTypeCredit:
		return ledgerv1.EntryType_ENTRY_CREDIT
	case model.EntryTypeDebit:
		return ledgerv1.EntryType_ENTRY_DEBIT
	case model.EntryTypeTransfer:
		return ledgerv1.EntryType_ENTRY_TRANSFER
	case model.EntryTypeFX:
		return ledgerv1.EntryType_ENTRY_FX
	case model.EntryTypeFee:
		return ledgerv1.EntryType_ENTRY_FEE
	case model.EntryTypeAdjust:
		return ledgerv1.EntryType_ENTRY_ADJUST
	case model.EntryTypeReverse:
		return ledgerv1.EntryType_ENTRY_REVERSE
	default:
		return ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED
	}
}

// calculateBalance computes net balance from a set of posting lines
func calculateBalance(lines []*model.PostingLine) (decimal.Decimal, error) {
	balance := decimal.Zero
	for _, line := range lines {
		amount, err := parseDecimal(line.Amount)
		if err != nil {
			return decimal.Zero, merrors.InvalidArgumentWrap(err, "invalid line amount")
		}
		balance = balance.Add(amount)
	}
	return balance, nil
}

// validateBalanced ensures posting lines sum to zero (double-entry accounting)
func validateBalanced(lines []*model.PostingLine) error {
	balance, err := calculateBalance(lines)
	if err != nil {
		return err
	}
	if !balance.IsZero() {
		return merrors.InvalidArgument(fmt.Sprintf("journal entry must balance (sum=0), got: %s", balance.String()))
	}
	return nil
}
