package gateway

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"

	"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
	"github.com/tech/sendico/gateway/mntx/storage"
	"github.com/tech/sendico/gateway/mntx/storage/model"
	clockpkg "github.com/tech/sendico/pkg/clock"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	msg "github.com/tech/sendico/pkg/messaging"
	"github.com/tech/sendico/pkg/mlogger"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type cardPayoutProcessor struct {
	logger     mlogger.Logger
	config     monetix.Config
	clock      clockpkg.Clock
	store      storage.Repository
	httpClient *http.Client
	producer   msg.Producer
}

func newCardPayoutProcessor(
	logger mlogger.Logger,
	cfg monetix.Config,
	clock clockpkg.Clock,
	store storage.Repository,
	client *http.Client,
	producer msg.Producer,
) *cardPayoutProcessor {
	return &cardPayoutProcessor{
		logger:     logger.Named("card_payout_processor"),
		config:     cfg,
		clock:      clock,
		store:      store,
		httpClient: client,
		producer:   producer,
	}
}

func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayoutRequest) (*mntxv1.CardPayoutResponse, error) {
	if p == nil {
		return nil, merrors.Internal("card payout processor not initialised")
	}

	req = sanitizeCardPayoutRequest(req)

	p.logger.Info("Submitting card payout",
		zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
		zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
		zap.Int64("amount_minor", req.GetAmountMinor()),
		zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
		zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
		zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
	)

	if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
		p.logger.Warn("Monetix configuration is incomplete for payout submission")
		return nil, merrors.Internal("monetix configuration is incomplete")
	}

	if err := validateCardPayoutRequest(req, p.config); err != nil {
		p.logger.Warn("Card payout validation failed",
			zap.String("payout_id", req.GetPayoutId()),
			zap.String("customer_id", req.GetCustomerId()),
			zap.Error(err),
		)
		return nil, err
	}

	projectID := req.GetProjectId()
	if projectID == 0 {
		projectID = p.config.ProjectID
	}
	if projectID == 0 {
		p.logger.Warn("Monetix project_id is not configured", zap.String("payout_id", req.GetPayoutId()))
		return nil, merrors.Internal("monetix project_id is not configured")
	}

	now := p.clock.Now()

	state := &model.CardPayout{
		Base: storable.Base{
			ID: bson.NilObjectID,
		},
		PayoutID:       strings.TrimSpace(req.GetPayoutId()),
		OperationRef:   strings.TrimSpace(req.GetOperationRef()),
		IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
		IntentRef:      strings.TrimSpace(req.GetIntentRef()),
		ProjectID:      projectID,
		CustomerID:     strings.TrimSpace(req.GetCustomerId()),
		AmountMinor:    req.GetAmountMinor(),
		Currency:       strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
		Status:         model.PayoutStatusWaiting,
		CreatedAt:      now,
		UpdatedAt:      now,
	}

	// Keep CreatedAt/refs if record already exists.
	if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil {
		if !existing.CreatedAt.IsZero() {
			state.CreatedAt = existing.CreatedAt
		}
		if state.OperationRef == "" {
			state.OperationRef = existing.OperationRef
		}
		if state.IdempotencyKey == "" {
			state.IdempotencyKey = existing.IdempotencyKey
		}
	}

	client := monetix.NewClient(p.config, p.httpClient, p.logger)
	apiReq := buildCardPayoutRequest(projectID, req)

	result, err := client.CreateCardPayout(ctx, apiReq)
	if err != nil {
		state.Status = model.PayoutStatusFailed
		state.ProviderMessage = err.Error()
		state.UpdatedAt = p.clock.Now()

		if e := p.updatePayoutStatus(ctx, state); e != nil {
			p.logger.Warn("Failed to update payout status",
				zap.Error(e),
				zap.String("payout_id", state.PayoutID),
				zap.String("customer_id", state.CustomerID),
				zap.String("operation_ref", state.OperationRef),
				zap.String("idempotency_key", state.IdempotencyKey),
				zap.String("operation_ref", state.OperationRef),
				zap.String("intent_ref", state.IntentRef),
			)
		}

		p.logger.Warn("Monetix payout submission failed",
			zap.Error(err),
			zap.String("payout_id", state.PayoutID),
			zap.String("customer_id", state.CustomerID),
			zap.String("operation_ref", state.OperationRef),
			zap.String("idempotency_key", state.IdempotencyKey),
			zap.String("operation_ref", state.OperationRef),
			zap.String("intent_ref", state.IntentRef),
		)

		return nil, err
	}

	// Provider request id is the provider-side payment id in your model.
	state.ProviderPaymentID = strings.TrimSpace(result.ProviderRequestID)
	if result.Accepted {
		state.Status = model.PayoutStatusWaiting
	} else {
		state.Status = model.PayoutStatusFailed
		state.ProviderCode = strings.TrimSpace(result.ErrorCode)
		state.ProviderMessage = strings.TrimSpace(result.ErrorMessage)
	}

	state.UpdatedAt = p.clock.Now()
	if err := p.updatePayoutStatus(ctx, state); err != nil {
		p.logger.Warn("Failed to store payout",
			zap.Error(err),
			zap.String("payout_id", state.PayoutID),
			zap.String("customer_id", state.CustomerID),
			zap.String("operation_ref", state.OperationRef),
			zap.String("idempotency_key", state.IdempotencyKey),
		)
		// do not fail request here: provider already answered and client expects response
	}

	resp := &mntxv1.CardPayoutResponse{
		Payout:            StateToProto(state),
		Accepted:          result.Accepted,
		ProviderRequestId: result.ProviderRequestID,
		ErrorCode:         result.ErrorCode,
		ErrorMessage:      result.ErrorMessage,
	}

	p.logger.Info("Card payout submission stored",
		zap.String("payout_id", state.PayoutID),
		zap.String("status", string(state.Status)),
		zap.Bool("accepted", result.Accepted),
		zap.String("provider_request_id", result.ProviderRequestID),
	)

	return resp, nil
}

func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardTokenPayoutRequest) (*mntxv1.CardTokenPayoutResponse, error) {
	if p == nil {
		return nil, merrors.Internal("card payout processor not initialised")
	}

	req = sanitizeCardTokenPayoutRequest(req)

	p.logger.Info("Submitting card token payout",
		zap.String("payout_id", strings.TrimSpace(req.GetPayoutId())),
		zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
		zap.Int64("amount_minor", req.GetAmountMinor()),
		zap.String("currency", strings.ToUpper(strings.TrimSpace(req.GetCurrency()))),
		zap.String("operation_ref", strings.TrimSpace(req.GetOperationRef())),
		zap.String("idempotency_key", strings.TrimSpace(req.GetIdempotencyKey())),
	)

	if strings.TrimSpace(p.config.BaseURL) == "" || strings.TrimSpace(p.config.SecretKey) == "" {
		p.logger.Warn("Monetix configuration is incomplete for token payout submission")
		return nil, merrors.Internal("monetix configuration is incomplete")
	}

	if err := validateCardTokenPayoutRequest(req, p.config); err != nil {
		p.logger.Warn("Card token payout validation failed",
			zap.String("payout_id", req.GetPayoutId()),
			zap.String("customer_id", req.GetCustomerId()),
			zap.Error(err),
		)
		return nil, err
	}

	projectID := req.GetProjectId()
	if projectID == 0 {
		projectID = p.config.ProjectID
	}
	if projectID == 0 {
		p.logger.Warn("Monetix project_id is not configured", zap.String("payout_id", req.GetPayoutId()))
		return nil, merrors.Internal("monetix project_id is not configured")
	}

	now := p.clock.Now()
	state := &model.CardPayout{
		PayoutID:       strings.TrimSpace(req.GetPayoutId()),
		OperationRef:   strings.TrimSpace(req.GetOperationRef()),
		IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
		ProjectID:      projectID,
		CustomerID:     strings.TrimSpace(req.GetCustomerId()),
		AmountMinor:    req.GetAmountMinor(),
		Currency:       strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
		Status:         model.PayoutStatusWaiting,
		CreatedAt:      now,
		UpdatedAt:      now,
	}

	if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil {
		if !existing.CreatedAt.IsZero() {
			state.CreatedAt = existing.CreatedAt
		}
		if state.OperationRef == "" {
			state.OperationRef = existing.OperationRef
		}
		if state.IdempotencyKey == "" {
			state.IdempotencyKey = existing.IdempotencyKey
		}
	}

	client := monetix.NewClient(p.config, p.httpClient, p.logger)
	apiReq := buildCardTokenPayoutRequest(projectID, req)

	result, err := client.CreateCardTokenPayout(ctx, apiReq)
	if err != nil {
		state.Status = model.PayoutStatusFailed
		state.ProviderMessage = err.Error()
		state.UpdatedAt = p.clock.Now()

		_ = p.updatePayoutStatus(ctx, state)

		p.logger.Warn("Monetix token payout submission failed",
			zap.String("payout_id", state.PayoutID),
			zap.String("customer_id", state.CustomerID),
			zap.Error(err),
		)

		return nil, err
	}

	state.ProviderPaymentID = strings.TrimSpace(result.ProviderRequestID)
	if result.Accepted {
		state.Status = model.PayoutStatusWaiting
	} else {
		state.Status = model.PayoutStatusFailed
		state.ProviderCode = strings.TrimSpace(result.ErrorCode)
		state.ProviderMessage = strings.TrimSpace(result.ErrorMessage)
	}

	state.UpdatedAt = p.clock.Now()
	if err := p.updatePayoutStatus(ctx, state); err != nil {
		p.logger.Warn("Failed to update payout status", zap.Error(err))
		return nil, err
	}

	resp := &mntxv1.CardTokenPayoutResponse{
		Payout:            StateToProto(state),
		Accepted:          result.Accepted,
		ProviderRequestId: result.ProviderRequestID,
		ErrorCode:         result.ErrorCode,
		ErrorMessage:      result.ErrorMessage,
	}

	p.logger.Info("Card token payout submission stored",
		zap.String("payout_id", state.PayoutID),
		zap.String("status", string(state.Status)),
		zap.Bool("accepted", result.Accepted),
		zap.String("provider_request_id", result.ProviderRequestID),
	)

	return resp, nil
}

func (p *cardPayoutProcessor) Tokenize(ctx context.Context, req *mntxv1.CardTokenizeRequest) (*mntxv1.CardTokenizeResponse, error) {
	if p == nil {
		return nil, merrors.Internal("card payout processor not initialised")
	}

	p.logger.Info("Submitting card tokenization",
		zap.String("request_id", strings.TrimSpace(req.GetRequestId())),
		zap.String("customer_id", strings.TrimSpace(req.GetCustomerId())),
	)

	cardInput, err := validateCardTokenizeRequest(req, p.config)
	if err != nil {
		p.logger.Warn("Card tokenization validation failed",
			zap.String("request_id", req.GetRequestId()),
			zap.String("customer_id", req.GetCustomerId()),
			zap.Error(err),
		)
		return nil, err
	}

	projectID := req.GetProjectId()
	if projectID == 0 {
		projectID = p.config.ProjectID
	}
	if projectID == 0 {
		p.logger.Warn("Monetix project_id is not configured", zap.String("request_id", req.GetRequestId()))
		return nil, merrors.Internal("monetix project_id is not configured")
	}

	req = sanitizeCardTokenizeRequest(req)
	cardInput = extractTokenizeCard(req)

	client := monetix.NewClient(p.config, p.httpClient, p.logger)
	apiReq := buildCardTokenizeRequest(projectID, req, cardInput)

	result, err := client.CreateCardTokenization(ctx, apiReq)
	if err != nil {
		p.logger.Warn("Monetix tokenization request failed",
			zap.String("request_id", req.GetRequestId()),
			zap.String("customer_id", req.GetCustomerId()),
			zap.Error(err),
		)
		return nil, err
	}

	resp := &mntxv1.CardTokenizeResponse{
		RequestId:    req.GetRequestId(),
		Success:      result.Accepted,
		ErrorCode:    result.ErrorCode,
		ErrorMessage: result.ErrorMessage,
	}
	resp.Token = result.Token
	resp.MaskedPan = result.MaskedPAN
	resp.ExpiryMonth = result.ExpiryMonth
	resp.ExpiryYear = result.ExpiryYear
	resp.CardBrand = result.CardBrand

	p.logger.Info("Card tokenization completed",
		zap.String("request_id", resp.GetRequestId()),
		zap.Bool("success", resp.GetSuccess()),
		zap.String("provider_request_id", result.ProviderRequestID),
	)

	return resp, nil
}

func (p *cardPayoutProcessor) Status(ctx context.Context, payoutID string) (*mntxv1.CardPayoutState, error) {
	if p == nil {
		return nil, merrors.Internal("card payout processor not initialised")
	}

	id := strings.TrimSpace(payoutID)
	p.logger.Info("Card payout status requested", zap.String("payout_id", id))

	if id == "" {
		p.logger.Warn("Payout status requested with empty payout_id")
		return nil, merrors.InvalidArgument("payout_id is required", "payout_id")
	}

	state, err := p.store.Payouts().FindByPaymentID(ctx, id)
	if err != nil || state == nil {
		p.logger.Warn("Payout status not found", zap.String("payout_id", id), zap.Error(err))
		return nil, merrors.NoData("payout not found")
	}

	p.logger.Info("Card payout status resolved",
		zap.String("payout_id", state.PayoutID),
		zap.String("status", string(state.Status)),
	)

	return StateToProto(state), nil
}

func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byte) (int, error) {
	if p == nil {
		return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
	}

	p.logger.Debug("Processing Monetix callback", zap.Int("payload_bytes", len(payload)))

	if len(payload) == 0 {
		p.logger.Warn("Received empty Monetix callback payload")
		return http.StatusBadRequest, merrors.InvalidArgument("callback body is empty")
	}

	if strings.TrimSpace(p.config.SecretKey) == "" {
		p.logger.Warn("Monetix secret key is not configured; cannot verify callback")
		return http.StatusInternalServerError, merrors.Internal("monetix secret key is not configured")
	}

	var cb monetixCallback
	if err := json.Unmarshal(payload, &cb); err != nil {
		p.logger.Warn("Failed to unmarshal Monetix callback", zap.Error(err))
		return http.StatusBadRequest, err
	}

	signature, err := verifyCallbackSignature(payload, p.config.SecretKey)
	if err != nil {
		status := http.StatusBadRequest
		if errors.Is(err, merrors.ErrDataConflict) {
			status = http.StatusForbidden
		}
		p.logger.Warn("Monetix callback signature check failed",
			zap.String("payout_id", cb.Payment.ID),
			zap.String("signature", signature),
			zap.String("payload", string(payload)),
			zap.Error(err),
		)
		return status, err
	}

	// mapCallbackToState currently returns proto-state in your code.
	// Convert it to mongo model and preserve internal refs if record exists.
	pbState, statusLabel := mapCallbackToState(p.clock, p.config, cb)

	// Convert proto -> mongo (operationRef/idempotencyKey are internal; keep empty for now)
	state := CardPayoutStateFromProto(p.clock, pbState)

	// Preserve CreatedAt + internal keys from existing record if present.
	if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err != nil {
		p.logger.Warn("Failed to fetch payout state while processing callback",
			zap.Error(err),
			zap.String("payout_id", state.PayoutID),
		)
		return http.StatusInternalServerError, err
	} else if existing != nil {
		if !existing.CreatedAt.IsZero() {
			state.CreatedAt = existing.CreatedAt
		}
		if state.OperationRef == "" {
			state.OperationRef = existing.OperationRef
		}
		if state.IdempotencyKey == "" {
			state.IdempotencyKey = existing.IdempotencyKey
		}
		// keep failure reason if you want, or override depending on callback semantics
		if state.FailureReason == "" {
			state.FailureReason = existing.FailureReason
		}
	}

	if err := p.updatePayoutStatus(ctx, state); err != nil {
		p.logger.Warn("Failed to update payout state while processing callback", zap.Error(err))
	}
	monetix.ObserveCallback(statusLabel)

	p.logger.Info("Monetix payout callback processed",
		zap.String("payout_id", state.PayoutID),
		zap.String("status", statusLabel),
		zap.String("provider_code", state.ProviderCode),
		zap.String("provider_message", state.ProviderMessage),
		zap.String("masked_account", cb.Account.Number),
	)

	return http.StatusOK, nil
}
