package psvc

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

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

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oobs"
	"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"
	orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
	"go.uber.org/zap"
)

func (s *svc) ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (resp *orchestrationv2.ExecutePaymentResponse, err error) {
	logger := s.logger
	orgRef := ""
	if req != nil && req.GetMeta() != nil {
		orgRef = strings.TrimSpace(req.GetMeta().GetOrganizationRef())
	}
	logger.Debug("Starting 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 && resp.Payment != nil {
			fields = append(fields,
				zap.String("payment_ref", strings.TrimSpace(resp.Payment.GetPaymentRef())),
				zap.String("state", resp.Payment.GetState().String()),
				zap.Uint64("version", resp.Payment.GetVersion()),
			)
		}
		if err != nil {
			logger.Warn("Failed to execute payment", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed payment execution", fields...)
	}(time.Now())

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

	payment, reused, err := s.tryReuse(ctx, requestCtx, fingerprint)
	if err != nil {
		return nil, remapIdempotencyError(err)
	}
	if !reused {
		payment, err = s.createNewPayment(ctx, requestCtx)
		if err != nil {
			return nil, remapIdempotencyError(err)
		}
	}
	if payment != nil {
		logger.Debug("Loaded payment for execution",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.Bool("reused", reused),
			zap.String("state", string(payment.State)),
		)
	}

	payment, err = s.runRuntime(ctx, payment)
	if err != nil {
		return nil, err
	}
	if payment != nil {
		logger.Debug("Completed runtime loop for payment execution",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("state", string(payment.State)),
			zap.Uint64("version", payment.Version),
		)
	}
	protoPayment, err := s.mapPayment(payment)
	if err != nil {
		return nil, err
	}
	if payment != nil {
		logger.Debug("Prepared finalized payment response",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("state", string(payment.State)),
			zap.Uint64("version", payment.Version),
		)
	}
	resp = &orchestrationv2.ExecutePaymentResponse{Payment: protoPayment}
	return resp, nil
}

func (s *svc) prepareExecute(req *orchestrationv2.ExecutePaymentRequest) (*reqval.Ctx, string, error) {
	requestCtx, err := s.validator.Validate(mapExecuteReq(req))
	if err != nil {
		return nil, "", err
	}
	fingerprint, err := s.idempotency.Fingerprint(idem.FPInput{
		OrganizationRef:  requestCtx.OrganizationRef,
		QuotationRef:     requestCtx.QuotationRef,
		ClientPaymentRef: requestCtx.ClientPaymentRef,
	})
	if err != nil {
		return nil, "", err
	}
	return requestCtx, fingerprint, nil
}

func mapExecuteReq(req *orchestrationv2.ExecutePaymentRequest) *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) tryReuse(ctx context.Context, requestCtx *reqval.Ctx, requestFingerprint string) (*agg.Payment, bool, error) {
	existing, err := s.repository.GetByIdempotencyKey(ctx, requestCtx.OrganizationID, requestCtx.IdempotencyKey)
	if err != nil {
		if errors.Is(err, prepo.ErrPaymentNotFound) || errors.Is(err, merrors.ErrNoData) {
			return nil, false, nil
		}
		return nil, false, err
	}
	if existing == nil {
		return nil, false, nil
	}

	existingFingerprint, err := s.idempotency.Fingerprint(idem.FPInput{
		OrganizationRef:  requestCtx.OrganizationRef,
		QuotationRef:     existing.QuotationRef,
		ClientPaymentRef: existing.ClientPaymentRef,
	})
	if err != nil {
		return nil, false, err
	}
	if strings.TrimSpace(existingFingerprint) != strings.TrimSpace(requestFingerprint) {
		return nil, false, idem.ErrIdempotencyParamMismatch
	}
	return existing, true, nil
}

func (s *svc) createNewPayment(ctx context.Context, requestCtx *reqval.Ctx) (*agg.Payment, error) {
	resolved, graph, err := s.resolveAndPlan(ctx, requestCtx)
	if err != nil {
		return nil, err
	}
	payment, err := s.aggregate.Create(agg.Input{
		OrganizationRef:  requestCtx.OrganizationID,
		IdempotencyKey:   requestCtx.IdempotencyKey,
		QuotationRef:     resolved.QuotationRef,
		ClientPaymentRef: requestCtx.ClientPaymentRef,
		IntentSnapshot:   resolved.IntentSnapshot,
		QuoteSnapshot:    resolved.QuoteSnapshot,
		Steps:            toStepShells(graph, resolved.IntentSnapshot, resolved.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
		}
		reused, ok, reuseErr := s.tryReuse(ctx, requestCtx, mustFingerprint(s.idempotency, requestCtx))
		if reuseErr != nil {
			return nil, reuseErr
		}
		if ok {
			return reused, nil
		}
		return nil, err
	}

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

func (s *svc) resolveAndPlan(ctx context.Context, requestCtx *reqval.Ctx) (*qsnap.Output, *xplan.Graph, error) {
	resolved, err := s.quote.Resolve(ctx, s.quoteStore, qsnap.Input{
		OrganizationID: requestCtx.OrganizationID,
		QuotationRef:   requestCtx.QuotationRef,
	})
	if err != nil {
		return nil, nil, remapResolveError(err)
	}
	graph, err := s.planner.Compile(xplan.Input{
		IntentSnapshot: resolved.IntentSnapshot,
		QuoteSnapshot:  resolved.QuoteSnapshot,
	})
	if err != nil {
		return nil, nil, err
	}
	return resolved, graph, nil
}

func toStepShells(graph *xplan.Graph, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) []agg.StepShell {
	if graph == nil || len(graph.Steps) == 0 {
		return nil
	}
	out := make([]agg.StepShell, 0, len(graph.Steps))
	for i := range graph.Steps {
		plannedMoney, plannedConverted := plannedMoneyForStep(graph.Steps[i], intent, quote)
		out = append(out, agg.StepShell{
			StepRef:               graph.Steps[i].StepRef,
			StepCode:              graph.Steps[i].StepCode,
			Rail:                  graph.Steps[i].Rail,
			Gateway:               graph.Steps[i].Gateway,
			InstanceID:            graph.Steps[i].InstanceID,
			ReportVisibility:      graph.Steps[i].Visibility,
			UserLabel:             graph.Steps[i].UserLabel,
			Comment:               graph.Steps[i].Comment,
			PlannedMoney:          plannedMoney,
			PlannedConvertedMoney: plannedConverted,
		})
	}
	return out
}

func (s *svc) recordPaymentCreated(ctx context.Context, payment *agg.Payment, graph *xplan.Graph) error {
	if err := s.observer.RecordPayment(ctx, oobs.RecordPaymentInput{
		Payment: payment,
		Event:   oobs.PaymentEventCreated,
		Fields: map[string]string{
			"route_ref": graphRouteRef(graph),
		},
	}); err != nil {
		return err
	}
	if err := s.statuses.Publish(ctx, paymentStatusPublishInput{
		Payment:       payment,
		PreviousState: agg.StateUnspecified,
		CurrentState:  payment.State,
		OccurredAt:    s.nowUTC(),
		Event:         "created",
	}); err != nil {
		s.logger.Warn("Failed to publish created payment status update",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.Error(err),
		)
	}
	for i := range payment.StepExecutions {
		if err := s.observer.RecordStep(ctx, oobs.RecordStepInput{
			PaymentRef: payment.PaymentRef,
			Step:       payment.StepExecutions[i],
			Event:      oobs.StepEventScheduled,
		}); err != nil {
			return err
		}
	}
	return nil
}

func graphRouteRef(graph *xplan.Graph) string {
	if graph == nil {
		return ""
	}
	return strings.TrimSpace(graph.RouteRef)
}

func remapIdempotencyError(err error) error {
	if errors.Is(err, idem.ErrIdempotencyParamMismatch) {
		return merrors.InvalidArgument(err.Error())
	}
	return err
}

func remapResolveError(err error) error {
	switch {
	case errors.Is(err, qsnap.ErrBatchQuoteUnsupported):
		return merrors.InvalidArgument(err.Error())
	default:
		return err
	}
}

func mustFingerprint(idemSvc idem.Service, requestCtx *reqval.Ctx) string {
	if idemSvc == nil || requestCtx == nil {
		return ""
	}
	value, err := idemSvc.Fingerprint(idem.FPInput{
		OrganizationRef:  requestCtx.OrganizationRef,
		QuotationRef:     requestCtx.QuotationRef,
		ClientPaymentRef: requestCtx.ClientPaymentRef,
	})
	if err != nil {
		return ""
	}
	return value
}
