package gateway

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

	"github.com/tech/sendico/gateway/mntx/internal/service/monetix"
	clockpkg "github.com/tech/sendico/pkg/clock"
	"github.com/tech/sendico/pkg/merrors"
	msg "github.com/tech/sendico/pkg/messaging"
	messaging "github.com/tech/sendico/pkg/messaging/envelope"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	nm "github.com/tech/sendico/pkg/model/notification"
	"github.com/tech/sendico/pkg/mservice"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/timestamppb"
)

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

func newCardPayoutProcessor(logger mlogger.Logger, cfg monetix.Config, clock clockpkg.Clock, store *cardPayoutStore, 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")
	}
	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()))),
	)
	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")
	}

	req = sanitizeCardPayoutRequest(req)
	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 := timestamppb.New(p.clock.Now())
	state := &mntxv1.CardPayoutState{
		PayoutId:    req.GetPayoutId(),
		ProjectId:   projectID,
		CustomerId:  req.GetCustomerId(),
		AmountMinor: req.GetAmountMinor(),
		Currency:    strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
		Status:      mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
		CreatedAt:   now,
		UpdatedAt:   now,
	}

	if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
		if existing.GetCreatedAt() != nil {
			state.CreatedAt = existing.GetCreatedAt()
		}
	}

	client := monetix.NewClient(p.config, p.httpClient, p.logger)
	apiReq := buildCardPayoutRequest(projectID, req)
	result, err := client.CreateCardPayout(ctx, apiReq)
	if err != nil {
		state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
		state.ProviderMessage = err.Error()
		state.UpdatedAt = timestamppb.New(p.clock.Now())
		p.store.Save(state)
		p.logger.Warn("Monetix payout submission failed",
			zap.String("payout_id", req.GetPayoutId()),
			zap.String("customer_id", req.GetCustomerId()),
			zap.Error(err),
		)
		return nil, err
	}

	state.ProviderPaymentId = result.ProviderRequestID
	if result.Accepted {
		state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
	} else {
		state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
		state.ProviderCode = result.ErrorCode
		state.ProviderMessage = result.ErrorMessage
	}
	state.UpdatedAt = timestamppb.New(p.clock.Now())
	p.store.Save(state)

	resp := &mntxv1.CardPayoutResponse{
		Payout:            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.GetPayoutId()),
		zap.String("status", state.GetStatus().String()),
		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")
	}
	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()))),
	)
	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")
	}

	req = sanitizeCardTokenPayoutRequest(req)
	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 := timestamppb.New(p.clock.Now())
	state := &mntxv1.CardPayoutState{
		PayoutId:    req.GetPayoutId(),
		ProjectId:   projectID,
		CustomerId:  req.GetCustomerId(),
		AmountMinor: req.GetAmountMinor(),
		Currency:    strings.ToUpper(strings.TrimSpace(req.GetCurrency())),
		Status:      mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING,
		CreatedAt:   now,
		UpdatedAt:   now,
	}

	if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
		if existing.GetCreatedAt() != nil {
			state.CreatedAt = existing.GetCreatedAt()
		}
	}

	client := monetix.NewClient(p.config, p.httpClient, p.logger)
	apiReq := buildCardTokenPayoutRequest(projectID, req)
	result, err := client.CreateCardTokenPayout(ctx, apiReq)
	if err != nil {
		state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
		state.ProviderMessage = err.Error()
		state.UpdatedAt = timestamppb.New(p.clock.Now())
		p.store.Save(state)
		p.logger.Warn("Monetix token payout submission failed",
			zap.String("payout_id", req.GetPayoutId()),
			zap.String("customer_id", req.GetCustomerId()),
			zap.Error(err),
		)
		return nil, err
	}

	state.ProviderPaymentId = result.ProviderRequestID
	if result.Accepted {
		state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_PENDING
	} else {
		state.Status = mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED
		state.ProviderCode = result.ErrorCode
		state.ProviderMessage = result.ErrorMessage
	}
	state.UpdatedAt = timestamppb.New(p.clock.Now())
	p.store.Save(state)

	resp := &mntxv1.CardTokenPayoutResponse{
		Payout:            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.GetPayoutId()),
		zap.String("status", state.GetStatus().String()),
		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(_ 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, ok := p.store.Get(id)
	if !ok || state == nil {
		p.logger.Warn("Payout status not found", zap.String("payout_id", id))
		return nil, merrors.NoData("payout not found")
	}
	p.logger.Info("Card payout status resolved", zap.String("payout_id", state.GetPayoutId()), zap.String("status", state.GetStatus().String()))
	return 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
	}

	state, statusLabel := mapCallbackToState(p.clock, p.config, cb)
	if existing, ok := p.store.Get(state.GetPayoutId()); ok && existing != nil {
		if existing.GetCreatedAt() != nil {
			state.CreatedAt = existing.GetCreatedAt()
		}
	}
	p.store.Save(state)
	p.emitCardPayoutEvent(state)
	monetix.ObserveCallback(statusLabel)

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

	return http.StatusOK, nil
}

func (p *cardPayoutProcessor) emitCardPayoutEvent(state *mntxv1.CardPayoutState) {
	if state == nil || p.producer == nil {
		return
	}

	event := &mntxv1.CardPayoutStatusChangedEvent{Payout: state}
	payload, err := protojson.Marshal(event)
	if err != nil {
		p.logger.Warn("Failed to marshal payout callback event", zap.Error(err))
		return
	}

	env := messaging.CreateEnvelope(string(mservice.MntxGateway), model.NewNotification(mservice.MntxGateway, nm.NAUpdated))
	if _, err := env.Wrap(payload); err != nil {
		p.logger.Warn("Failed to wrap payout callback event payload", zap.Error(err))
		return
	}
	if err := p.producer.SendMessage(env); err != nil {
		p.logger.Warn("Failed to publish payout callback event", zap.Error(err))
	}
}
