package gateway

import (
	"context"
	"errors"
	"strings"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/gateway/aurora/internal/appversion"
	"github.com/tech/sendico/pkg/connector/params"
	"github.com/tech/sendico/pkg/merrors"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	"google.golang.org/protobuf/types/known/structpb"
)

const connectorTypeID = "mntx"

func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
	return &connectorv1.GetCapabilitiesResponse{
		Capabilities: &connectorv1.ConnectorCapabilities{
			ConnectorType:           connectorTypeID,
			Version:                 appversion.Create().Short(),
			SupportedAccountKinds:   nil,
			SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_PAYOUT},
			OperationParams:         connectorOperationParams(),
		},
	}, nil
}

func (s *Service) OpenAccount(_ context.Context, _ *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
	return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported", nil, "")}, nil
}

func (s *Service) GetAccount(_ context.Context, _ *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
	return nil, merrors.NotImplemented("get_account: unsupported")
}

func (s *Service) ListAccounts(_ context.Context, _ *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
	return nil, merrors.NotImplemented("list_accounts: unsupported")
}

func (s *Service) GetBalance(_ context.Context, _ *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
	return nil, merrors.NotImplemented("get_balance: unsupported")
}

func (s *Service) SubmitOperation(ctx context.Context, req *connectorv1.SubmitOperationRequest) (*connectorv1.SubmitOperationResponse, error) {
	if req == nil || req.GetOperation() == nil {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil, "")}}, nil
	}
	op := req.GetOperation()
	idempotencyKey := strings.TrimSpace(op.GetIdempotencyKey())
	if idempotencyKey == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
	}
	operationRef := strings.TrimSpace(op.GetOperationRef())
	if operationRef == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation_ref is required", op, "")}}, nil
	}
	intentRef := strings.TrimSpace(op.GetIntentRef())
	if intentRef == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: intent_ref is required", op, "")}}, nil
	}
	if op.GetType() != connectorv1.OperationType_PAYOUT {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
	}
	reader := params.New(op.GetParams())
	amountMinor, currency, err := payoutAmount(op, reader)
	if err != nil {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
	}
	parentPaymentRef := strings.TrimSpace(reader.String("parent_payment_ref"))

	payoutID := operationIDForRequest(operationRef)

	if strings.TrimSpace(reader.String("card_token")) != "" {
		resp, err := s.CreateCardTokenPayout(ctx, buildCardTokenPayoutRequestFromParams(reader, payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef, amountMinor, currency))
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
		}
		return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
	}
	cr := buildCardPayoutRequestFromParams(reader, payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef, amountMinor, currency)
	resp, err := s.CreateCardPayout(ctx, cr)
	if err != nil {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
	}
	return &connectorv1.SubmitOperationResponse{Receipt: payoutReceipt(resp.GetPayout())}, nil
}

func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
	if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
		return nil, merrors.InvalidArgument("get_operation: operation_id is required")
	}

	operationRef := strings.TrimSpace(req.GetOperationId())
	if s.storage == nil || s.storage.Payouts() == nil {
		return nil, merrors.Internal("get_operation: storage is not configured")
	}

	payout, err := s.storage.Payouts().FindByOperationRef(ctx, operationRef)
	if err != nil {
		return nil, err
	}
	if payout == nil {
		return nil, merrors.NoData("payout not found")
	}

	return &connectorv1.GetOperationResponse{Operation: payoutToOperation(StateToProto(payout))}, nil
}

func (s *Service) ListOperations(_ context.Context, _ *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
	return nil, merrors.NotImplemented("list_operations: unsupported")
}

func connectorOperationParams() []*connectorv1.OperationParamSpec {
	return []*connectorv1.OperationParamSpec{
		{OperationType: connectorv1.OperationType_PAYOUT, Params: []*connectorv1.ParamSpec{
			{Key: "customer_id", Type: connectorv1.ParamType_STRING, Required: true},
			{Key: "customer_first_name", Type: connectorv1.ParamType_STRING, Required: true},
			{Key: "customer_last_name", Type: connectorv1.ParamType_STRING, Required: true},
			{Key: "customer_ip", Type: connectorv1.ParamType_STRING, Required: true},
			{Key: "card_token", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "card_pan", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "card_exp_year", Type: connectorv1.ParamType_INT, Required: false},
			{Key: "card_exp_month", Type: connectorv1.ParamType_INT, Required: false},
			{Key: "card_holder", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "amount_minor", Type: connectorv1.ParamType_INT, Required: false},
			{Key: "project_id", Type: connectorv1.ParamType_INT, Required: false},
			{Key: "parent_payment_ref", Type: connectorv1.ParamType_STRING, Required: true},
			{Key: "comment", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "customer_middle_name", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "customer_country", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "customer_state", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "customer_city", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "customer_address", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "customer_zip", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "masked_pan", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false},
		}},
	}
}

func payoutAmount(op *connectorv1.Operation, reader params.Reader) (int64, string, error) {
	if op == nil {
		return 0, "", merrors.InvalidArgument("payout: operation is required")
	}
	currency := currencyFromOperation(op)
	if currency == "" {
		return 0, "", merrors.InvalidArgument("payout: currency is required")
	}
	if minor, ok := reader.Int64("amount_minor"); ok && minor > 0 {
		return minor, currency, nil
	}
	money := op.GetMoney()
	if money == nil {
		return 0, "", merrors.InvalidArgument("payout: money is required")
	}
	amount := strings.TrimSpace(money.GetAmount())
	if amount == "" {
		return 0, "", merrors.InvalidArgument("payout: amount is required")
	}
	dec, err := decimal.NewFromString(amount)
	if err != nil {
		return 0, "", merrors.InvalidArgument("payout: invalid amount")
	}
	minor := dec.Mul(decimal.NewFromInt(100)).IntPart()
	return minor, currency, nil
}

func currencyFromOperation(op *connectorv1.Operation) string {
	if op == nil || op.GetMoney() == nil {
		return ""
	}
	currency := strings.TrimSpace(op.GetMoney().GetCurrency())
	if idx := strings.Index(currency, "-"); idx > 0 {
		currency = currency[:idx]
	}
	return strings.ToUpper(currency)
}

func operationIDForRequest(operationRef string) string {
	return strings.TrimSpace(operationRef)
}

func metadataFromReader(reader params.Reader) map[string]string {
	metadata := reader.StringMap("metadata")
	if len(metadata) == 0 {
		return nil
	}
	out := make(map[string]string, len(metadata))
	for key, value := range metadata {
		k := strings.TrimSpace(key)
		if k == "" || strings.EqualFold(k, "comment") {
			continue
		}
		out[k] = strings.TrimSpace(value)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func buildCardTokenPayoutRequestFromParams(reader params.Reader,
	payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef string,
	amountMinor int64, currency string) *mntxv1.CardTokenPayoutRequest {
	operationRef = strings.TrimSpace(operationRef)
	payoutID = strings.TrimSpace(payoutID)
	if operationRef != "" {
		payoutID = ""
	}
	req := &mntxv1.CardTokenPayoutRequest{
		PayoutId:           payoutID,
		ParentPaymentRef:   strings.TrimSpace(parentPaymentRef),
		ProjectId:          readerInt64(reader, "project_id"),
		CustomerId:         strings.TrimSpace(reader.String("customer_id")),
		CustomerFirstName:  strings.TrimSpace(reader.String("customer_first_name")),
		CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
		CustomerLastName:   strings.TrimSpace(reader.String("customer_last_name")),
		CustomerIp:         strings.TrimSpace(reader.String("customer_ip")),
		CustomerZip:        strings.TrimSpace(reader.String("customer_zip")),
		CustomerCountry:    strings.TrimSpace(reader.String("customer_country")),
		CustomerState:      strings.TrimSpace(reader.String("customer_state")),
		CustomerCity:       strings.TrimSpace(reader.String("customer_city")),
		CustomerAddress:    strings.TrimSpace(reader.String("customer_address")),
		AmountMinor:        amountMinor,
		Currency:           currency,
		CardToken:          strings.TrimSpace(reader.String("card_token")),
		CardHolder:         strings.TrimSpace(reader.String("card_holder")),
		MaskedPan:          strings.TrimSpace(reader.String("masked_pan")),
		Comment:            strings.TrimSpace(reader.String("comment")),
		Metadata:           metadataFromReader(reader),
		OperationRef:       operationRef,
		IdempotencyKey:     strings.TrimSpace(idempotencyKey),
		IntentRef:          strings.TrimSpace(intentRef),
	}
	return req
}

func buildCardPayoutRequestFromParams(reader params.Reader,
	payoutID, parentPaymentRef, idempotencyKey, operationRef, intentRef string,
	amountMinor int64, currency string) *mntxv1.CardPayoutRequest {
	operationRef = strings.TrimSpace(operationRef)
	payoutID = strings.TrimSpace(payoutID)
	if operationRef != "" {
		payoutID = ""
	}
	return &mntxv1.CardPayoutRequest{
		PayoutId:           payoutID,
		ParentPaymentRef:   strings.TrimSpace(parentPaymentRef),
		ProjectId:          readerInt64(reader, "project_id"),
		CustomerId:         strings.TrimSpace(reader.String("customer_id")),
		CustomerFirstName:  strings.TrimSpace(reader.String("customer_first_name")),
		CustomerMiddleName: strings.TrimSpace(reader.String("customer_middle_name")),
		CustomerLastName:   strings.TrimSpace(reader.String("customer_last_name")),
		CustomerIp:         strings.TrimSpace(reader.String("customer_ip")),
		CustomerZip:        strings.TrimSpace(reader.String("customer_zip")),
		CustomerCountry:    strings.TrimSpace(reader.String("customer_country")),
		CustomerState:      strings.TrimSpace(reader.String("customer_state")),
		CustomerCity:       strings.TrimSpace(reader.String("customer_city")),
		CustomerAddress:    strings.TrimSpace(reader.String("customer_address")),
		AmountMinor:        amountMinor,
		Currency:           currency,
		CardPan:            strings.TrimSpace(reader.String("card_pan")),
		CardExpYear:        uint32(readerInt64(reader, "card_exp_year")),  //nolint:gosec // values are validated by request validators
		CardExpMonth:       uint32(readerInt64(reader, "card_exp_month")), //nolint:gosec // values are validated by request validators
		CardHolder:         strings.TrimSpace(reader.String("card_holder")),
		Comment:            strings.TrimSpace(reader.String("comment")),
		Metadata:           metadataFromReader(reader),
		OperationRef:       operationRef,
		IdempotencyKey:     strings.TrimSpace(idempotencyKey),
		IntentRef:          strings.TrimSpace(intentRef),
	}
}

func readerInt64(reader params.Reader, key string) int64 {
	if v, ok := reader.Int64(key); ok {
		return v
	}
	return 0
}

func payoutReceipt(state *mntxv1.CardPayoutState) *connectorv1.OperationReceipt {
	if state == nil {
		return &connectorv1.OperationReceipt{
			Status: connectorv1.OperationStatus_OPERATION_PROCESSING,
		}
	}
	return &connectorv1.OperationReceipt{
		OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
		Status:      payoutStatusToOperation(state.GetStatus()),
		ProviderRef: strings.TrimSpace(state.GetProviderPaymentId()),
	}
}

func payoutToOperation(state *mntxv1.CardPayoutState) *connectorv1.Operation {
	if state == nil {
		return nil
	}
	op := &connectorv1.Operation{
		OperationId: firstNonEmpty(strings.TrimSpace(state.GetOperationRef()), strings.TrimSpace(state.GetPayoutId())),
		Type:        connectorv1.OperationType_PAYOUT,
		Status:      payoutStatusToOperation(state.GetStatus()),
		Money: &moneyv1.Money{
			Amount:   minorToDecimal(state.GetAmountMinor()),
			Currency: strings.ToUpper(strings.TrimSpace(state.GetCurrency())),
		},
		ProviderRef:  strings.TrimSpace(state.GetProviderPaymentId()),
		IntentRef:    strings.TrimSpace(state.GetIntentRef()),
		OperationRef: strings.TrimSpace(state.GetOperationRef()),
		CreatedAt:    state.GetCreatedAt(),
		UpdatedAt:    state.GetUpdatedAt(),
	}

	params := map[string]interface{}{}
	if paymentRef := strings.TrimSpace(state.GetParentPaymentRef()); paymentRef != "" {
		params["payment_ref"] = paymentRef
		params["parent_payment_ref"] = paymentRef
	}
	if comment := strings.TrimSpace(state.GetComment()); comment != "" {
		params["comment"] = comment
	}
	if providerCode := strings.TrimSpace(state.GetProviderCode()); providerCode != "" {
		params["provider_code"] = providerCode
	}
	if providerMessage := strings.TrimSpace(state.GetProviderMessage()); providerMessage != "" {
		params["provider_message"] = providerMessage
		params["failure_reason"] = providerMessage
	}
	if len(params) > 0 {
		op.Params = structFromMap(params)
	}

	return op
}

func minorToDecimal(amount int64) string {
	dec := decimal.NewFromInt(amount).Div(decimal.NewFromInt(100))
	return dec.StringFixed(2)
}

func payoutStatusToOperation(status mntxv1.PayoutStatus) connectorv1.OperationStatus {
	switch status {

	case mntxv1.PayoutStatus_PAYOUT_STATUS_CREATED:
		return connectorv1.OperationStatus_OPERATION_CREATED

	case mntxv1.PayoutStatus_PAYOUT_STATUS_WAITING:
		return connectorv1.OperationStatus_OPERATION_WAITING

	case mntxv1.PayoutStatus_PAYOUT_STATUS_SUCCESS:
		return connectorv1.OperationStatus_OPERATION_SUCCESS

	case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
		return connectorv1.OperationStatus_OPERATION_FAILED

	case mntxv1.PayoutStatus_PAYOUT_STATUS_CANCELLED:
		return connectorv1.OperationStatus_OPERATION_CANCELLED

	default:
		return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
	}
}

func structFromMap(values map[string]interface{}) *structpb.Struct {
	if len(values) == 0 {
		return nil
	}
	result, err := structpb.NewStruct(values)
	if err != nil {
		return nil
	}
	return result
}

func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation, accountID string) *connectorv1.ConnectorError {
	err := &connectorv1.ConnectorError{
		Code:      code,
		Message:   strings.TrimSpace(message),
		AccountId: strings.TrimSpace(accountID),
	}
	if op != nil {
		err.CorrelationId = strings.TrimSpace(op.GetCorrelationId())
		err.ParentIntentId = strings.TrimSpace(op.GetParentIntentId())
		err.OperationId = strings.TrimSpace(op.GetOperationId())
	}
	return err
}

func mapErrorCode(err error) connectorv1.ErrorCode {
	switch {
	case errors.Is(err, merrors.ErrInvalidArg):
		return connectorv1.ErrorCode_INVALID_PARAMS
	case errors.Is(err, merrors.ErrNoData):
		return connectorv1.ErrorCode_NOT_FOUND
	case errors.Is(err, merrors.ErrNotImplemented):
		return connectorv1.ErrorCode_UNSUPPORTED_OPERATION
	case errors.Is(err, merrors.ErrInternal):
		return connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE
	default:
		return connectorv1.ErrorCode_PROVIDER_ERROR
	}
}
