package gateway

import (
	"bytes"
	"context"
	"crypto/hmac"
	"encoding/json"
	"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"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

type callbackPayment struct {
	ID          string `json:"id"`
	Type        string `json:"type"`
	Status      string `json:"status"`
	Date        string `json:"date"`
	Method      string `json:"method"`
	Description string `json:"description"`
	Sum         struct {
		Amount   int64  `json:"amount"`
		Currency string `json:"currency"`
	} `json:"sum"`
}

type callbackOperation struct {
	ID          int64  `json:"id"`
	Type        string `json:"type"`
	Status      string `json:"status"`
	Date        string `json:"date"`
	CreatedDate string `json:"created_date"`
	RequestID   string `json:"request_id"`
	SumInitial  struct {
		Amount   int64  `json:"amount"`
		Currency string `json:"currency"`
	} `json:"sum_initial"`
	SumConverted struct {
		Amount   int64  `json:"amount"`
		Currency string `json:"currency"`
	} `json:"sum_converted"`
	Provider struct {
		ID         int64  `json:"id"`
		PaymentID  string `json:"payment_id"`
		AuthCode   string `json:"auth_code"`
		EndpointID int64  `json:"endpoint_id"`
		Date       string `json:"date"`
	} `json:"provider"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

type monetixCallback struct {
	ProjectID int64           `json:"project_id"`
	Payment   callbackPayment `json:"payment"`
	Account   struct {
		Number      string `json:"number"`
		Type        string `json:"type"`
		CardHolder  string `json:"card_holder"`
		ExpiryMonth string `json:"expiry_month"`
		ExpiryYear  string `json:"expiry_year"`
	} `json:"account"`
	Customer struct {
		ID string `json:"id"`
	} `json:"customer"`
	Operation callbackOperation `json:"operation"`
	Signature string            `json:"signature"`
}

// ProcessMonetixCallback ingests Monetix provider callbacks and updates payout state.
func (s *Service) ProcessMonetixCallback(ctx context.Context, payload []byte) (int, error) {
	log := s.logger.Named("callback")
	if s.card == nil {
		log.Warn("Card payout processor not initialised")
		return http.StatusInternalServerError, merrors.Internal("card payout processor not initialised")
	}
	log.Debug("Callback processing requested", zap.Int("payload_bytes", len(payload)))
	return s.card.ProcessCallback(ctx, payload)
}

func mapCallbackToState(clock clockpkg.Clock, cfg monetix.Config, cb monetixCallback) (*mntxv1.CardPayoutState, string) {
	status := strings.ToLower(strings.TrimSpace(cb.Payment.Status))
	opStatus := strings.ToLower(strings.TrimSpace(cb.Operation.Status))
	code := strings.TrimSpace(cb.Operation.Code)

	outcome := monetix.OutcomeDecline
	internalStatus := mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED

	if status == cfg.SuccessStatus() && opStatus == cfg.SuccessStatus() && (code == "" || code == "0") {
		internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS
		outcome = monetix.OutcomeSuccess
	} else if status == cfg.ProcessingStatus() || opStatus == cfg.ProcessingStatus() {
		internalStatus = mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING
		outcome = monetix.OutcomeProcessing
	}

	now := timestamppb.New(clock.Now())
	state := &mntxv1.CardPayoutState{
		PayoutId:          cb.Payment.ID,
		ProjectId:         cb.ProjectID,
		CustomerId:        cb.Customer.ID,
		AmountMinor:       cb.Payment.Sum.Amount,
		Currency:          strings.ToUpper(strings.TrimSpace(cb.Payment.Sum.Currency)),
		Status:            internalStatus,
		ProviderCode:      cb.Operation.Code,
		ProviderMessage:   cb.Operation.Message,
		ProviderPaymentId: fallbackProviderPaymentID(cb),
		OperationRef:      strings.TrimSpace(cb.Payment.ID),
		UpdatedAt:         now,
		CreatedAt:         now,
	}

	return state, outcome
}

func fallbackProviderPaymentID(cb monetixCallback) string {
	if cb.Operation.Provider.PaymentID != "" {
		return cb.Operation.Provider.PaymentID
	}
	if cb.Operation.RequestID != "" {
		return cb.Operation.RequestID
	}
	return cb.Payment.ID
}

func verifyCallbackSignature(payload []byte, secret string) (string, error) {
	root, err := decodeCallbackPayload(payload)
	if err != nil {
		return "", err
	}
	signature, ok := signatureFromPayload(root)
	if !ok || strings.TrimSpace(signature) == "" {
		return "", merrors.InvalidArgument("signature is missing")
	}
	calculated, err := monetix.SignPayload(root, secret)
	if err != nil {
		return signature, err
	}
	if subtleConstantTimeCompare(signature, calculated) {
		return signature, nil
	}
	return signature, merrors.DataConflict("signature mismatch")
}

func decodeCallbackPayload(payload []byte) (any, error) {
	var root any
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	if err := decoder.Decode(&root); err != nil {
		return nil, err
	}
	return root, nil
}

func signatureFromPayload(root any) (string, bool) {
	payload, ok := root.(map[string]any)
	if !ok {
		return "", false
	}
	for key, value := range payload {
		if !strings.EqualFold(key, "signature") {
			continue
		}
		signature, ok := value.(string)
		return signature, ok
	}
	return "", false
}

func subtleConstantTimeCompare(a, b string) bool {
	return hmac.Equal([]byte(strings.TrimSpace(a)), []byte(strings.TrimSpace(b)))
}
