package psvc

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tech/sendico/pkg/discovery"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/batchmeta"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/idem"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/opagg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/qsnap"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/reqval"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	"go.uber.org/zap"
)

const (
	attrAggregatedByRecipient   = "orchestrator.v2.aggregated_by_recipient"
	attrAggregatedItems         = "orchestrator.v2.aggregated_items"
	attrBatchOptimizerMode      = "orchestrator.v2.batch_optimizer_mode"
	attrBatchOptimizerGrouping  = "orchestrator.v2.batch_optimizer_group_by"
	attrBatchOptimizerRuleID    = "orchestrator.v2.batch_optimizer_rule_id"
	attrBatchOptimizerRuleMatch = "orchestrator.v2.batch_optimizer_rule_matched"
)

func (s *svc) ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (resp *orchestrationv2.ExecuteBatchPaymentResponse, err error) {
	logger := s.logger
	orgRef := ""
	if req != nil && req.GetMeta() != nil {
		orgRef = strings.TrimSpace(req.GetMeta().GetOrganizationRef())
	}
	logger.Debug("Starting batch payment execution",
		zap.String("organization_ref", orgRef),
		zap.String("quotation_ref", strings.TrimSpace(req.GetQuotationRef())),
		zap.Bool("has_client_payment_ref", strings.TrimSpace(req.GetClientPaymentRef()) != ""),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if resp != nil {
			fields = append(fields, zap.Int("payments_count", len(resp.GetPayments())))
		}
		if err != nil {
			logger.Warn("Failed to execute batch payment", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed batch payment execution", fields...)
	}(time.Now())

	requestCtx, err := s.prepareBatchExecute(req)
	if err != nil {
		return nil, err
	}

	resolved, err := s.quote.ResolveAll(ctx, s.quoteStore, qsnap.ResolveAllInput{
		OrganizationRef: requestCtx.OrganizationID,
		QuotationRef:    requestCtx.QuotationRef,
	})
	if err != nil {
		return nil, remapResolveError(err)
	}
	if resolved == nil || len(resolved.Items) == 0 {
		return nil, merrors.InvalidArgument("quotation has no executable items")
	}

	optimized, err := s.batchOptimizer.Optimize(BatchOptimizeInput{Items: resolved.Items})
	if err != nil {
		return nil, err
	}
	groups, err := s.buildBatchOperationGroups(optimized.Groups)
	if err != nil {
		return nil, err
	}
	payments := make([]*orchestrationv2.Payment, 0, len(groups))
	for i := range groups {
		payment, groupErr := s.executeGroup(ctx, requestCtx, resolved.QuotationRef, groups[i])
		if groupErr != nil {
			return nil, groupErr
		}
		protoPayment, mapErr := s.mapPayment(payment)
		if mapErr != nil {
			return nil, mapErr
		}
		payments = append(payments, protoPayment)
	}
	return &orchestrationv2.ExecuteBatchPaymentResponse{Payments: payments}, nil
}

func (s *svc) prepareBatchExecute(req *orchestrationv2.ExecuteBatchPaymentRequest) (*reqval.Ctx, error) {
	return s.validator.Validate(mapBatchExecuteReq(req))
}

func mapBatchExecuteReq(req *orchestrationv2.ExecuteBatchPaymentRequest) *reqval.Req {
	if req == nil {
		return nil
	}
	out := &reqval.Req{
		QuotationRef:     req.GetQuotationRef(),
		ClientPaymentRef: req.GetClientPaymentRef(),
	}
	meta := req.GetMeta()
	if meta == nil {
		return out
	}
	out.Meta = &reqval.Meta{OrganizationRef: meta.GetOrganizationRef()}
	if meta.GetTrace() != nil {
		out.Meta.Trace = &reqval.Trace{IdempotencyKey: meta.GetTrace().GetIdempotencyKey()}
	}
	return out
}

func (s *svc) executeGroup(ctx context.Context, requestCtx *reqval.Ctx, quotationRef string, group opagg.Group) (*agg.Payment, error) {
	normalizedIntentRefs := normalizeIntentRefs(group.IntentRefs)
	if len(normalizedIntentRefs) == 0 {
		return nil, merrors.InvalidArgument("aggregated group has no intent refs")
	}
	groupIdempotencyKey := deriveGroupIdempotencyKey(requestCtx.IdempotencyKey, normalizedIntentRefs)
	intentRefJoined := strings.Join(normalizedIntentRefs, ",")

	fingerprint, err := s.idempotency.Fingerprint(idem.FPInput{
		OrganizationRef:  requestCtx.OrganizationRef,
		QuotationRef:     requestCtx.QuotationRef,
		IntentRef:        intentRefJoined,
		ClientPaymentRef: requestCtx.ClientPaymentRef,
	})
	if err != nil {
		return nil, err
	}

	existing, err := s.repository.GetByIdempotencyKey(ctx, requestCtx.OrganizationID, groupIdempotencyKey)
	if err != nil && !errors.Is(err, prepo.ErrPaymentNotFound) && !errors.Is(err, merrors.ErrNoData) {
		return nil, err
	}
	var payment *agg.Payment
	if existing != nil {
		existingFP, err := s.idempotency.Fingerprint(idem.FPInput{
			OrganizationRef:  requestCtx.OrganizationRef,
			QuotationRef:     existing.QuotationRef,
			IntentRef:        strings.TrimSpace(existing.IntentSnapshot.Ref),
			ClientPaymentRef: existing.ClientPaymentRef,
		})
		if err != nil {
			return nil, err
		}
		if strings.TrimSpace(existingFP) != strings.TrimSpace(fingerprint) {
			return nil, idem.ErrIdempotencyParamMismatch
		}
		payment = existing
	} else {
		payment, err = s.createGroupPayment(ctx, requestCtx, quotationRef, groupIdempotencyKey, intentRefJoined, fingerprint, group)
		if err != nil {
			return nil, remapIdempotencyError(err)
		}
	}

	payment, err = s.runRuntime(ctx, payment)
	if err != nil {
		return nil, err
	}
	return payment, nil
}

func (s *svc) createGroupPayment(
	ctx context.Context,
	requestCtx *reqval.Ctx,
	quotationRef string,
	groupIdempotencyKey string,
	intentRefJoined string,
	expectedFingerprint string,
	group opagg.Group,
) (*agg.Payment, error) {
	intentSnapshot := group.IntentSnapshot
	intentSnapshot.Ref = intentRefJoined

	graph, err := s.planner.Compile(xplan.Input{
		IntentSnapshot: intentSnapshot,
		QuoteSnapshot:  group.QuoteSnapshot,
	})
	if err != nil {
		return nil, err
	}

	payment, err := s.aggregate.Create(agg.Input{
		OrganizationRef:  requestCtx.OrganizationID,
		IdempotencyKey:   groupIdempotencyKey,
		QuotationRef:     quotationRef,
		ClientPaymentRef: requestCtx.ClientPaymentRef,
		IntentSnapshot:   intentSnapshot,
		QuoteSnapshot:    group.QuoteSnapshot,
		Steps:            toStepShells(graph, intentSnapshot, group.QuoteSnapshot),
	})
	if err != nil {
		return nil, err
	}

	if err := s.repository.Create(ctx, payment); err != nil {
		if !errors.Is(err, prepo.ErrDuplicatePayment) {
			return nil, err
		}
		existing, getErr := s.repository.GetByIdempotencyKey(ctx, requestCtx.OrganizationID, groupIdempotencyKey)
		if getErr != nil {
			return nil, getErr
		}
		if existing != nil {
			existingFP, fpErr := s.idempotency.Fingerprint(idem.FPInput{
				OrganizationRef:  requestCtx.OrganizationRef,
				QuotationRef:     existing.QuotationRef,
				IntentRef:        strings.TrimSpace(existing.IntentSnapshot.Ref),
				ClientPaymentRef: existing.ClientPaymentRef,
			})
			if fpErr != nil {
				return nil, fpErr
			}
			if strings.TrimSpace(existingFP) != strings.TrimSpace(expectedFingerprint) {
				return nil, idem.ErrIdempotencyParamMismatch
			}
			return existing, nil
		}
		return nil, err
	}

	if err := s.recordPaymentCreated(ctx, payment, graph); err != nil {
		return nil, err
	}
	return payment, nil
}

func deriveGroupIdempotencyKey(baseKey string, intentRefs []string) string {
	normalized := normalizeIntentRefs(intentRefs)
	h := sha256.New()
	h.Write([]byte(strings.TrimSpace(baseKey)))
	h.Write([]byte{0x1f})
	h.Write([]byte(strings.Join(normalized, ",")))
	return hex.EncodeToString(h.Sum(nil))
}

func normalizeIntentRefs(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for i := range values {
		token := strings.TrimSpace(values[i])
		if token == "" {
			continue
		}
		if _, exists := seen[token]; exists {
			continue
		}
		seen[token] = struct{}{}
		out = append(out, token)
	}
	sort.Strings(out)
	if len(out) == 0 {
		return nil
	}
	return out
}

func (s *svc) buildBatchOperationGroups(groups []opagg.Group) ([]opagg.Group, error) {
	if len(groups) == 0 {
		return nil, merrors.InvalidArgument("aggregation produced no groups")
	}

	out := make([]opagg.Group, 0, len(groups))
	for i := range groups {
		group := groups[i]
		group.IntentRefs = normalizeIntentRefs(group.IntentRefs)
		if len(group.IntentRefs) == 0 {
			return nil, merrors.InvalidArgument("aggregated group has no intent refs")
		}
		if group.IntentSnapshot.Attributes == nil {
			group.IntentSnapshot.Attributes = map[string]string{}
		}
		if len(group.IntentRefs) > 1 {
			group.IntentSnapshot.Attributes[attrAggregatedByRecipient] = "true"
		} else {
			delete(group.IntentSnapshot.Attributes, attrAggregatedByRecipient)
		}
		group.IntentSnapshot.Attributes[attrAggregatedItems] = strconv.Itoa(len(group.IntentRefs))

		targets := buildBatchPayoutTargets(group)
		if routeContainsCardPayout(group.QuoteSnapshot) && len(targets) > 0 {
			raw, err := batchmeta.EncodePayoutTargets(targets)
			if err != nil {
				return nil, err
			}
			if strings.TrimSpace(raw) != "" {
				group.IntentSnapshot.Attributes[batchmeta.AttrPayoutTargets] = raw
			}
		} else {
			delete(group.IntentSnapshot.Attributes, batchmeta.AttrPayoutTargets)
		}
		out = append(out, group)
	}
	if len(out) == 0 {
		return nil, merrors.InvalidArgument("aggregation produced no groups")
	}
	return out, nil
}

func buildBatchPayoutTargets(group opagg.Group) []batchmeta.PayoutTarget {
	if len(group.Members) > 0 {
		targets := make([]batchmeta.PayoutTarget, 0, len(group.Members))
		for i := range group.Members {
			member := group.Members[i]
			target := batchmeta.PayoutTarget{
				TargetRef: firstNonEmpty(strings.TrimSpace(member.IntentRef), "recipient-"+strconv.Itoa(i+1)),
				IntentRef: normalizeIntentRefs([]string{member.IntentRef}),
				Comment:   strings.TrimSpace(member.IntentSnapshot.Comment),
				Amount:    batchPayoutAmountFromMember(member),
			}
			if member.IntentSnapshot.Destination.Type == model.EndpointTypeCard && member.IntentSnapshot.Destination.Card != nil {
				card := *member.IntentSnapshot.Destination.Card
				target.Card = &card
			}
			if member.IntentSnapshot.Customer != nil {
				customer := *member.IntentSnapshot.Customer
				target.Customer = &customer
			}
			targets = append(targets, target)
		}
		if len(targets) == 0 {
			return nil
		}
		return targets
	}

	if isEmptyIntentSnapshot(group.IntentSnapshot) {
		return nil
	}
	target := batchmeta.PayoutTarget{
		TargetRef: firstNonEmpty(strings.TrimSpace(group.RecipientKey), "recipient-1"),
		IntentRef: normalizeIntentRefs(group.IntentRefs),
		Comment:   strings.TrimSpace(group.IntentSnapshot.Comment),
		Amount:    batchPayoutAmount(group),
	}
	if group.IntentSnapshot.Destination.Type == model.EndpointTypeCard && group.IntentSnapshot.Destination.Card != nil {
		card := *group.IntentSnapshot.Destination.Card
		target.Card = &card
	}
	if group.IntentSnapshot.Customer != nil {
		customer := *group.IntentSnapshot.Customer
		target.Customer = &customer
	}
	return []batchmeta.PayoutTarget{target}
}

func batchPayoutAmount(group opagg.Group) *paymenttypes.Money {
	if group.QuoteSnapshot != nil && group.QuoteSnapshot.ExpectedSettlementAmount != nil {
		return &paymenttypes.Money{
			Amount:   strings.TrimSpace(group.QuoteSnapshot.ExpectedSettlementAmount.Amount),
			Currency: strings.TrimSpace(group.QuoteSnapshot.ExpectedSettlementAmount.Currency),
		}
	}
	if group.IntentSnapshot.Amount == nil {
		return nil
	}
	return &paymenttypes.Money{
		Amount:   strings.TrimSpace(group.IntentSnapshot.Amount.Amount),
		Currency: strings.TrimSpace(group.IntentSnapshot.Amount.Currency),
	}
}

func batchPayoutAmountFromMember(member opagg.GroupMember) *paymenttypes.Money {
	if member.QuoteSnapshot != nil && member.QuoteSnapshot.ExpectedSettlementAmount != nil {
		return &paymenttypes.Money{
			Amount:   strings.TrimSpace(member.QuoteSnapshot.ExpectedSettlementAmount.Amount),
			Currency: strings.TrimSpace(member.QuoteSnapshot.ExpectedSettlementAmount.Currency),
		}
	}
	if member.IntentSnapshot.Amount == nil {
		return nil
	}
	return &paymenttypes.Money{
		Amount:   strings.TrimSpace(member.IntentSnapshot.Amount.Amount),
		Currency: strings.TrimSpace(member.IntentSnapshot.Amount.Currency),
	}
}

func isEmptyIntentSnapshot(intent model.PaymentIntent) bool {
	return intent.Amount == nil && (intent.Kind == "" || intent.Kind == model.PaymentKindUnspecified)
}

func routeContainsCardPayout(snapshot *model.PaymentQuoteSnapshot) bool {
	if snapshot == nil || snapshot.Route == nil {
		return false
	}
	for i := range snapshot.Route.Hops {
		hop := snapshot.Route.Hops[i]
		if hop == nil {
			continue
		}
		if model.ParseRail(hop.Rail) == discovery.RailCardPayout {
			return true
		}
	}
	return model.ParseRail(snapshot.Route.Rail) == discovery.RailCardPayout
}
