package pquery

import (
	"bytes"
	"context"
	"sort"
	"strings"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

const (
	defaultLimit int32 = 50
	maxLimit     int32 = 200
)

var allStates = []agg.State{
	agg.StateCreated,
	agg.StateExecuting,
	agg.StateNeedsAttention,
	agg.StateSettled,
	agg.StateFailed,
}

type svc struct {
	logger mlogger.Logger
	repo   prepo.Repository
}

type normalizedInput struct {
	organizationRef bson.ObjectID
	quotationRef    string
	states          []agg.State
	createdFrom     *time.Time
	createdTo       *time.Time
	cursor          *prepo.ListCursor
	limit           int32
}

func newService(deps Dependencies) (Service, error) {
	if deps.Repository == nil {
		return nil, merrors.InvalidArgument("payment repository v2 is required")
	}
	logger := deps.Logger
	if logger == nil {
		logger = zap.NewNop()
	}
	return &svc{
		logger: logger.Named("repository"),
		repo:   deps.Repository,
	}, nil
}

func (s *svc) GetPayment(ctx context.Context, in GetPaymentInput) (payment *agg.Payment, err error) {
	logger := s.logger
	logger.Debug("Starting Get payment",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if payment != nil {
			fields = append(fields,
				zap.String("state", string(payment.State)),
				zap.Uint64("version", payment.Version),
			)
		}
		if err != nil {
			logger.Warn("Failed to get payment", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Get payment", fields...)
	}(time.Now())

	paymentRef := strings.TrimSpace(in.PaymentRef)
	if paymentRef == "" {
		return nil, merrors.InvalidArgument("payment_ref is required")
	}
	if in.OrganizationRef.IsZero() {
		payment, err = s.repo.GetByPaymentRefGlobal(ctx, paymentRef)
		return payment, err
	}
	payment, err = s.repo.GetByPaymentRef(ctx, in.OrganizationRef, paymentRef)
	return payment, err
}

func (s *svc) ListPayments(ctx context.Context, in ListPaymentsInput) (out *ListPaymentsOutput, err error) {
	logger := s.logger
	logger.Debug("Starting List payments",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
		zap.Int("states_count", len(in.States)),
		zap.Int32("limit", in.Limit),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("items_count", len(out.Items)))
			if out.NextCursor != nil {
				fields = append(fields,
					zap.String("next_cursor_id", out.NextCursor.ID.Hex()),
					zap.Time("next_cursor_created_at", out.NextCursor.CreatedAt),
				)
			}
		}
		if err != nil {
			logger.Warn("Failed to list payments", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed List payments", fields...)
	}(time.Now())

	norm, err := normalizeInput(in)
	if err != nil {
		return nil, err
	}
	if norm.quotationRef != "" {
		out, err = s.listByQuotationRef(ctx, norm)
		return out, err
	}
	out, err = s.listByStates(ctx, norm)
	return out, err
}

func normalizeInput(in ListPaymentsInput) (*normalizedInput, error) {
	if in.OrganizationRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	if in.CreatedFrom != nil && in.CreatedTo != nil {
		from := in.CreatedFrom.UTC()
		to := in.CreatedTo.UTC()
		if !from.Before(to) {
			return nil, merrors.InvalidArgument("created_from must be before created_to")
		}
	}
	states, err := normalizeStates(in.States)
	if err != nil {
		return nil, err
	}
	var createdFrom *time.Time
	if in.CreatedFrom != nil {
		from := in.CreatedFrom.UTC()
		createdFrom = &from
	}
	var createdTo *time.Time
	if in.CreatedTo != nil {
		to := in.CreatedTo.UTC()
		createdTo = &to
	}

	return &normalizedInput{
		organizationRef: in.OrganizationRef,
		quotationRef:    strings.TrimSpace(in.QuotationRef),
		states:          states,
		createdFrom:     createdFrom,
		createdTo:       createdTo,
		cursor:          in.Cursor,
		limit:           sanitizeLimit(in.Limit),
	}, nil
}

func normalizeStates(src []agg.State) ([]agg.State, error) {
	if len(src) == 0 {
		return append([]agg.State(nil), allStates...), nil
	}
	out := make([]agg.State, 0, len(src))
	seen := map[agg.State]struct{}{}
	for i := range src {
		state, ok := normalizeState(src[i])
		if !ok {
			return nil, merrors.InvalidArgument("states contains invalid value")
		}
		if _, exists := seen[state]; exists {
			continue
		}
		seen[state] = struct{}{}
		out = append(out, state)
	}
	if len(out) == 0 {
		return append([]agg.State(nil), allStates...), nil
	}
	return out, nil
}

func normalizeState(state agg.State) (agg.State, bool) {
	switch strings.ToLower(strings.TrimSpace(string(state))) {
	case string(agg.StateCreated):
		return agg.StateCreated, true
	case string(agg.StateExecuting):
		return agg.StateExecuting, true
	case string(agg.StateNeedsAttention):
		return agg.StateNeedsAttention, true
	case string(agg.StateSettled):
		return agg.StateSettled, true
	case string(agg.StateFailed):
		return agg.StateFailed, true
	default:
		return agg.StateUnspecified, false
	}
}

func sanitizeLimit(limit int32) int32 {
	if limit <= 0 {
		return defaultLimit
	}
	if limit > maxLimit {
		return maxLimit
	}
	return limit
}

func (s *svc) listByQuotationRef(ctx context.Context, in *normalizedInput) (*ListPaymentsOutput, error) {
	cursor := in.cursor
	out := make([]*agg.Payment, 0, in.limit)

	for len(out) < int(in.limit) {
		page, err := s.repo.ListByQuotationRef(ctx, prepo.ListByQuotationRefInput{
			OrganizationRef: in.organizationRef,
			QuotationRef:    in.quotationRef,
			Limit:           in.limit,
			Cursor:          cursor,
		})
		if err != nil {
			return nil, err
		}
		if page == nil || len(page.Items) == 0 {
			break
		}

		for i := range page.Items {
			item := page.Items[i]
			if !matchesFilters(item, in) {
				continue
			}
			out = append(out, item)
			if len(out) == int(in.limit) {
				break
			}
		}
		if len(out) == int(in.limit) || page.NextCursor == nil {
			break
		}
		cursor = page.NextCursor
	}

	return buildOutput(out, in.limit), nil
}

func (s *svc) listByStates(ctx context.Context, in *normalizedInput) (*ListPaymentsOutput, error) {
	cursor := in.cursor
	out := make([]*agg.Payment, 0, in.limit)

	for len(out) < int(in.limit) {
		merged, next, err := s.fetchStatesPage(ctx, in, cursor)
		if err != nil {
			return nil, err
		}
		if len(merged) == 0 {
			break
		}

		for i := range merged {
			if !matchesFilters(merged[i], in) {
				continue
			}
			out = append(out, merged[i])
			if len(out) == int(in.limit) {
				break
			}
		}
		if len(out) == int(in.limit) || next == nil {
			break
		}
		cursor = next
	}

	return buildOutput(out, in.limit), nil
}

func (s *svc) fetchStatesPage(
	ctx context.Context,
	in *normalizedInput,
	cursor *prepo.ListCursor,
) ([]*agg.Payment, *prepo.ListCursor, error) {
	batch := in.limit
	if batch < 20 {
		batch = 20
	}

	merged := make([]*agg.Payment, 0, len(in.states)*int(batch))
	var next *prepo.ListCursor
	for i := range in.states {
		page, err := s.repo.ListByState(ctx, prepo.ListByStateInput{
			OrganizationRef: in.organizationRef,
			State:           in.states[i],
			Limit:           batch,
			Cursor:          cursor,
		})
		if err != nil {
			return nil, nil, err
		}
		if page == nil || len(page.Items) == 0 {
			continue
		}
		merged = append(merged, page.Items...)
		if next == nil || cursorLess(page.NextCursor, next) {
			next = page.NextCursor
		}
	}
	if len(merged) == 0 {
		return nil, nil, nil
	}

	sortPaymentsDesc(merged)
	merged = dedupeByPaymentRef(merged)
	if len(merged) == 0 {
		return nil, next, nil
	}
	oldest := cursorFromPayment(merged[len(merged)-1])
	if cursorLess(oldest, next) {
		next = oldest
	}
	return merged, next, nil
}

func cursorFromPayment(payment *agg.Payment) *prepo.ListCursor {
	if payment == nil || payment.ID.IsZero() || payment.CreatedAt.IsZero() {
		return nil
	}
	return &prepo.ListCursor{
		CreatedAt: payment.CreatedAt.UTC(),
		ID:        payment.ID,
	}
}

func cursorLess(left *prepo.ListCursor, right *prepo.ListCursor) bool {
	if left == nil {
		return false
	}
	if right == nil {
		return true
	}
	if left.CreatedAt.Before(right.CreatedAt) {
		return true
	}
	if left.CreatedAt.After(right.CreatedAt) {
		return false
	}
	return bytes.Compare(left.ID[:], right.ID[:]) < 0
}

func buildOutput(items []*agg.Payment, limit int32) *ListPaymentsOutput {
	if len(items) == 0 {
		return &ListPaymentsOutput{}
	}
	if limit > 0 && len(items) > int(limit) {
		items = items[:int(limit)]
	}
	var nextCursor *prepo.ListCursor
	if limit > 0 && len(items) == int(limit) {
		nextCursor = cursorFromPayment(items[len(items)-1])
	}
	return &ListPaymentsOutput{
		Items:      items,
		NextCursor: nextCursor,
	}
}

func matchesFilters(payment *agg.Payment, in *normalizedInput) bool {
	if payment == nil {
		return false
	}
	if in.quotationRef != "" && !strings.EqualFold(strings.TrimSpace(payment.QuotationRef), in.quotationRef) {
		return false
	}
	if in.createdFrom != nil && payment.CreatedAt.Before(*in.createdFrom) {
		return false
	}
	if in.createdTo != nil && !payment.CreatedAt.Before(*in.createdTo) {
		return false
	}
	return containsState(in.states, payment.State)
}

func containsState(states []agg.State, state agg.State) bool {
	for i := range states {
		if states[i] == state {
			return true
		}
	}
	return false
}

func sortPaymentsDesc(items []*agg.Payment) {
	sort.Slice(items, func(i, j int) bool {
		left := items[i]
		right := items[j]
		if !left.CreatedAt.Equal(right.CreatedAt) {
			return left.CreatedAt.After(right.CreatedAt)
		}
		return bytes.Compare(left.ID[:], right.ID[:]) > 0
	})
}

func dedupeByPaymentRef(items []*agg.Payment) []*agg.Payment {
	if len(items) == 0 {
		return nil
	}
	out := make([]*agg.Payment, 0, len(items))
	seen := map[string]struct{}{}
	for i := range items {
		ref := strings.TrimSpace(items[i].PaymentRef)
		if ref == "" {
			continue
		}
		if _, ok := seen[ref]; ok {
			continue
		}
		seen[ref] = struct{}{}
		out = append(out, items[i])
	}
	return out
}
