package ledger

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mutil/mzap"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// getBalanceResponder implements balance query logic
func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanceRequest) gsresponse.Responder[ledgerv1.BalanceResponse] {
	return func(ctx context.Context) (*ledgerv1.BalanceResponse, error) {
		if req.LedgerAccountRef == "" {
			return nil, merrors.InvalidArgument("ledger_account_ref is required")
		}

		accountRef, err := parseObjectID(req.LedgerAccountRef)
		if err != nil {
			return nil, err
		}
		logger := s.logger.With(mzap.ObjRef("ledger_account_ref", accountRef))

		// Get account to verify it exists
		account, err := s.storage.Accounts().Get(ctx, accountRef)
		if err != nil {
			if errors.Is(err, storage.ErrAccountNotFound) {
				return nil, merrors.NoData("account not found")
			}
			logger.Warn("Failed to get account", zap.Error(err))
			return nil, merrors.Internal("failed to get account")
		}

		// Get balance
		balance, err := s.storage.Balances().Get(ctx, accountRef)
		if err != nil {
			if errors.Is(err, storage.ErrBalanceNotFound) {
				// Return zero balance if account exists but has no balance yet
				return &ledgerv1.BalanceResponse{
					LedgerAccountRef: req.LedgerAccountRef,
					Balance: &moneyv1.Money{
						Amount:   "0",
						Currency: account.Currency,
					},
					Version:     0,
					LastUpdated: timestamppb.Now(),
				}, nil
			}
			logger.Warn("Failed to get balance", zap.Error(err))
			return nil, merrors.Internal("failed to get balance")
		}

		recordBalanceQuery("success", 0)

		return &ledgerv1.BalanceResponse{
			LedgerAccountRef: req.LedgerAccountRef,
			Balance: &moneyv1.Money{
				Amount:   balance.Balance,
				Currency: account.Currency,
			},
			Version:     balance.Version,
			LastUpdated: timestamppb.New(balance.UpdatedAt),
		}, nil
	}
}

// getJournalEntryResponder implements journal entry query logic
func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetEntryRequest) gsresponse.Responder[ledgerv1.JournalEntryResponse] {
	return func(ctx context.Context) (*ledgerv1.JournalEntryResponse, error) {
		if req.EntryRef == "" {
			return nil, merrors.InvalidArgument("entry_ref is required")
		}

		entryRef, err := parseObjectID(req.EntryRef)
		if err != nil {
			return nil, err
		}
		logger := s.logger.With(mzap.ObjRef("entry_ref", entryRef))

		// Get journal entry
		entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
		if err != nil {
			if errors.Is(err, storage.ErrJournalEntryNotFound) {
				return nil, merrors.NoData("journal entry not found")
			}
			logger.Warn("Failed to get journal entry", zap.Error(err))
			return nil, merrors.Internal("failed to get journal entry")
		}

		// Get posting lines for this entry
		lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
		if err != nil {
			logger.Warn("Failed to get posting lines", zap.Error(err))
			return nil, merrors.Internal("failed to get posting lines")
		}

		// Convert to proto
		protoLines := make([]*ledgerv1.PostingLine, 0, len(lines))
		accountRefs := make([]string, 0, len(lines))
		for _, line := range lines {
			protoLines = append(protoLines, &ledgerv1.PostingLine{
				LedgerAccountRef: line.AccountRef.Hex(),
				Money: &moneyv1.Money{
					Amount:   line.Amount,
					Currency: line.Currency,
				},
				LineType: modelLineTypeToProto(line.LineType),
			})
			accountRefs = append(accountRefs, line.AccountRef.Hex())
		}

		return &ledgerv1.JournalEntryResponse{
			EntryRef:          req.EntryRef,
			IdempotencyKey:    entry.IdempotencyKey,
			EntryType:         modelEntryTypeToProto(entry.EntryType),
			Description:       entry.Description,
			EventTime:         timestamppb.New(entry.EventTime),
			Version:           entry.Version,
			Lines:             protoLines,
			Metadata:          entry.Metadata,
			LedgerAccountRefs: accountRefs,
		}, nil
	}
}

// getStatementResponder implements account statement query logic
func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStatementRequest) gsresponse.Responder[ledgerv1.StatementResponse] {
	return func(ctx context.Context) (*ledgerv1.StatementResponse, error) {
		if req.LedgerAccountRef == "" {
			return nil, merrors.InvalidArgument("ledger_account_ref is required")
		}

		accountRef, err := parseObjectID(req.LedgerAccountRef)
		if err != nil {
			return nil, err
		}
		logger := s.logger.With(mzap.ObjRef("ledger_account_ref", accountRef))

		// Verify account exists
		_, err = s.storage.Accounts().Get(ctx, accountRef)
		if err != nil {
			if errors.Is(err, storage.ErrAccountNotFound) {
				return nil, merrors.NoData("account not found")
			}
			logger.Warn("Failed to get account", zap.Error(err))
			return nil, merrors.Internal("failed to get account")
		}

		// Parse pagination
		limit := int(req.Limit)
		if limit <= 0 {
			limit = 50 // default
		}
		if limit > 100 {
			limit = 100 // max
		}

		offset := 0
		if req.Cursor != "" {
			offset, err = parseCursor(req.Cursor)
			if err != nil {
				return nil, merrors.InvalidArgument(fmt.Sprintf("invalid cursor: %v", err))
			}
		}
		logger = logger.With(zap.Int("limit", limit), zap.Int("offset", offset))

		// Get posting lines for account
		postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset)
		if err != nil {
			logger.Warn("Failed to get posting lines", zap.Error(err))
			return nil, merrors.Internal("failed to get posting lines")
		}

		// Check if there are more results
		hasMore := len(postingLines) > limit
		if hasMore {
			postingLines = postingLines[:limit]
		}

		// Group by journal entry and fetch entry details
		entryMap := make(map[string]bool)
		for _, line := range postingLines {
			entryMap[line.JournalEntryRef.Hex()] = true
		}

		entries := make([]*ledgerv1.JournalEntryResponse, 0)
		for entryRefHex := range entryMap {
			entryRef, err := parseObjectID(entryRefHex)
			if err != nil {
				s.logger.Warn("Invalid journal entry ref in posting lines", zap.String("entry_ref", entryRefHex), zap.Error(err))
				return nil, err
			}

			entry, err := s.storage.JournalEntries().Get(ctx, entryRef)
			if err != nil {
				logger.Warn("Failed to get journal entry for statement", zap.Error(err), zap.String("entry_ref", entryRefHex))
				continue
			}

			// Get all lines for this entry
			lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef)
			if err != nil {
				logger.Warn("Failed to get posting lines for entry", zap.Error(err), zap.String("entry_ref", entryRefHex))
				continue
			}

			// Convert to proto
			protoLines := make([]*ledgerv1.PostingLine, 0, len(lines))
			accountRefs := make([]string, 0, len(lines))
			for _, line := range lines {
				protoLines = append(protoLines, &ledgerv1.PostingLine{
					LedgerAccountRef: line.AccountRef.Hex(),
					Money: &moneyv1.Money{
						Amount:   line.Amount,
						Currency: line.Currency,
					},
					LineType: modelLineTypeToProto(line.LineType),
				})
				accountRefs = append(accountRefs, line.AccountRef.Hex())
			}

			entries = append(entries, &ledgerv1.JournalEntryResponse{
				EntryRef:          entryRefHex,
				IdempotencyKey:    entry.IdempotencyKey,
				EntryType:         modelEntryTypeToProto(entry.EntryType),
				Description:       entry.Description,
				EventTime:         timestamppb.New(entry.EventTime),
				Version:           entry.Version,
				Lines:             protoLines,
				Metadata:          entry.Metadata,
				LedgerAccountRefs: accountRefs,
			})
		}

		// Generate next cursor
		nextCursor := ""
		if hasMore {
			nextCursor = encodeCursor(offset + limit)
		}

		return &ledgerv1.StatementResponse{
			Entries:    entries,
			NextCursor: nextCursor,
		}, nil
	}
}

// parseCursor decodes a pagination cursor
func parseCursor(cursor string) (int, error) {
	decoded, err := base64.StdEncoding.DecodeString(cursor)
	if err != nil {
		return 0, merrors.InvalidArgumentWrap(err, "invalid base64")
	}
	parts := strings.Split(string(decoded), ":")
	if len(parts) != 2 || parts[0] != "offset" {
		return 0, merrors.InvalidArgument("invalid cursor format")
	}
	offset, err := strconv.Atoi(parts[1])
	if err != nil {
		return 0, merrors.InvalidArgumentWrap(err, "invalid offset")
	}
	return offset, nil
}

// encodeCursor encodes an offset into a pagination cursor
func encodeCursor(offset int) string {
	cursor := fmt.Sprintf("offset:%d", offset)
	return base64.StdEncoding.EncodeToString([]byte(cursor))
}
