package qsnap

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

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xerr"
	"github.com/tech/sendico/payments/storage/model"
	quotestorage "github.com/tech/sendico/payments/storage/quote"
	"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"
)

type svc struct {
	logger mlogger.Logger
	now    func() time.Time
}

type resolvedQuoteItem struct {
	Intent model.PaymentIntent
	Quote  *model.PaymentQuoteSnapshot
	Status *model.QuoteStatusV2
}

func (s *svc) Resolve(
	ctx context.Context,
	store Store,
	in Input,
) (out *Output, err error) {
	logger := s.logger
	logger.Debug("Starting Resolve",
		mzap.ObjRef("organization_ref", in.OrganizationID),
		zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields,
				zap.String("quotation_ref", strings.TrimSpace(out.QuotationRef)),
			)
		}
		if err != nil {
			logger.Warn("Failed to resolve", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Resolve", fields...)
	}(time.Now())

	if store == nil {
		return nil, merrors.InvalidArgument("quotes store is required")
	}
	if in.OrganizationID.IsZero() {
		return nil, merrors.InvalidArgument("organization_id is required")
	}

	quoteRef := strings.TrimSpace(in.QuotationRef)
	if quoteRef == "" {
		return nil, merrors.InvalidArgument("quotation_ref is required")
	}

	record, err := store.GetByRef(ctx, in.OrganizationID, quoteRef)
	if err != nil {
		if errors.Is(err, quotestorage.ErrQuoteNotFound) || errors.Is(err, merrors.ErrNoData) {
			return nil, ErrQuoteNotFound
		}
		return nil, err
	}
	if record == nil {
		return nil, ErrQuoteNotFound
	}

	item, err := resolveRecordItem(record)
	if err != nil {
		return nil, err
	}

	if err := ensureExecutable(record, item.Status, s.now().UTC()); err != nil {
		return nil, err
	}

	outputRef := strings.TrimSpace(record.QuoteRef)
	if outputRef == "" {
		outputRef = quoteRef
	}
	if item.Quote != nil && strings.TrimSpace(item.Quote.QuoteRef) == "" {
		item.Quote.QuoteRef = outputRef
	}

	out = &Output{
		QuotationRef:   outputRef,
		IntentSnapshot: item.Intent,
		QuoteSnapshot:  item.Quote,
	}
	return out, nil
}

func (s *svc) ResolveAll(
	ctx context.Context,
	store Store,
	in ResolveAllInput,
) (out *ResolveAllOutput, err error) {
	logger := s.logger
	logger.Debug("Starting ResolveAll",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields,
				zap.String("quotation_ref", strings.TrimSpace(out.QuotationRef)),
				zap.Int("items_count", len(out.Items)),
			)
		}
		if err != nil {
			logger.Warn("Failed to resolve all", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed ResolveAll", fields...)
	}(time.Now())

	if store == nil {
		return nil, merrors.InvalidArgument("quotes store is required")
	}
	if in.OrganizationRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_id is required")
	}
	quoteRef := strings.TrimSpace(in.QuotationRef)
	if quoteRef == "" {
		return nil, merrors.InvalidArgument("quotation_ref is required")
	}

	record, err := store.GetByRef(ctx, in.OrganizationRef, quoteRef)
	if err != nil {
		if errors.Is(err, quotestorage.ErrQuoteNotFound) || errors.Is(err, merrors.ErrNoData) {
			return nil, ErrQuoteNotFound
		}
		return nil, err
	}
	if record == nil {
		return nil, ErrQuoteNotFound
	}
	if len(record.Items) == 0 {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "items are empty")
	}

	outputRef := strings.TrimSpace(record.QuoteRef)
	if outputRef == "" {
		outputRef = quoteRef
	}

	items := make([]ResolvedItem, 0, len(record.Items))
	for i, item := range record.Items {
		if item == nil {
			return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "items[%d] is nil", i)
		}
		if err := ensureExecutable(record, item.Status, s.now().UTC()); err != nil {
			return nil, err
		}
		resolved, err := resolveItem(item)
		if err != nil {
			return nil, xerr.Wrapf(err, "items[%d]", i)
		}
		intentRef := strings.TrimSpace(resolved.Intent.Ref)
		if resolved.Quote != nil && strings.TrimSpace(resolved.Quote.QuoteRef) == "" {
			resolved.Quote.QuoteRef = outputRef
		}
		items = append(items, ResolvedItem{
			IntentRef:      intentRef,
			IntentSnapshot: resolved.Intent,
			QuoteSnapshot:  resolved.Quote,
		})
	}

	out = &ResolveAllOutput{
		QuotationRef: outputRef,
		Items:        items,
	}
	return out, nil
}

func ensureExecutable(
	record *model.PaymentQuoteRecord,
	status *model.QuoteStatusV2,
	now time.Time,
) error {
	if record == nil {
		return ErrQuoteNotFound
	}
	if !record.ExpiresAt.IsZero() && now.After(record.ExpiresAt.UTC()) {
		return ErrQuoteExpired
	}

	if status == nil {
		return xerr.Wrapf(ErrQuoteShapeMismatch, "status is nil")
	}

	switch status.State {
	case model.QuoteStateExecutable:
		return nil
	case model.QuoteStateExpired:
		return ErrQuoteExpired
	case model.QuoteStateBlocked:
		reason := strings.TrimSpace(string(status.BlockReason))
		if reason != "" && reason != string(model.QuoteBlockReasonUnspecified) {
			return xerr.Wrapf(ErrQuoteNotExecutable, "blocked (%s)", reason)
		}
		return xerr.Wrapf(ErrQuoteNotExecutable, "blocked")
	case model.QuoteStateIndicative:
		return xerr.Wrapf(ErrQuoteNotExecutable, "indicative")
	default:
		state := strings.TrimSpace(string(status.State))
		if state == "" {
			return xerr.Wrapf(ErrQuoteNotExecutable, "unspecified status")
		}
		return xerr.Wrapf(ErrQuoteNotExecutable, "state=%s", state)
	}
}

func resolveRecordItem(record *model.PaymentQuoteRecord) (*resolvedQuoteItem, error) {
	if record == nil {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "record is nil")
	}

	if len(record.Items) == 0 {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "items are empty")
	}
	switch record.RequestShape {
	case model.QuoteRequestShapeSingle:
		if len(record.Items) != 1 {
			return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "single shape requires exactly one item")
		}
		return resolveItem(record.Items[0])
	case model.QuoteRequestShapeBatch:
		if len(record.Items) != 1 {
			return nil, ErrBatchQuoteUnsupported
		}
		return resolveItem(record.Items[0])
	default:
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "request shape is invalid")
	}
}

func resolveItem(item *model.PaymentQuoteItemV2) (*resolvedQuoteItem, error) {
	if item == nil {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "item is nil")
	}
	if item.Intent == nil {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "intent snapshot is nil")
	}
	if item.Quote == nil {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "quote snapshot is nil")
	}
	if item.Status == nil {
		return nil, xerr.Wrapf(ErrQuoteShapeMismatch, "status is nil")
	}

	intentSnapshot, err := cloneIntentSnapshot(*item.Intent)
	if err != nil {
		return nil, err
	}
	quoteSnapshot, err := cloneQuoteSnapshot(item.Quote)
	if err != nil {
		return nil, err
	}
	statusSnapshot, err := cloneStatusSnapshot(item.Status)
	if err != nil {
		return nil, err
	}

	return &resolvedQuoteItem{
		Intent: intentSnapshot,
		Quote:  quoteSnapshot,
		Status: statusSnapshot,
	}, nil
}

func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) {
	var dst model.PaymentIntent
	if err := bsonClone(src, &dst); err != nil {
		return model.PaymentIntent{}, err
	}
	return dst, nil
}

func cloneQuoteSnapshot(src *model.PaymentQuoteSnapshot) (*model.PaymentQuoteSnapshot, error) {
	if src == nil {
		return nil, nil //nolint:nilnil // nil snapshot means quote data is intentionally absent
	}
	dst := &model.PaymentQuoteSnapshot{}
	if err := bsonClone(src, dst); err != nil {
		return nil, err
	}
	return dst, nil
}

func cloneStatusSnapshot(src *model.QuoteStatusV2) (*model.QuoteStatusV2, error) {
	if src == nil {
		return nil, nil //nolint:nilnil // nil snapshot means status data is intentionally absent
	}
	dst := &model.QuoteStatusV2{}
	if err := bsonClone(src, dst); err != nil {
		return nil, err
	}
	return dst, nil
}

func bsonClone(src any, dst any) error {
	data, err := bson.Marshal(src)
	if err != nil {
		return err
	}
	return bson.Unmarshal(data, dst)
}
