package gateway

import (
	"context"
	"errors"
	"strings"

	"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"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/structpb"
)

const tgsettleConnectorID = "tgsettle"

func (s *Service) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
	return &connectorv1.GetCapabilitiesResponse{
		Capabilities: &connectorv1.ConnectorCapabilities{
			ConnectorType:           tgsettleConnectorID,
			Version:                 "",
			SupportedAccountKinds:   nil,
			SupportedOperationTypes: []connectorv1.OperationType{connectorv1.OperationType_TRANSFER},
			OperationParams:         tgsettleOperationParams(),
		},
	}, 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 {
		s.logger.Warn("Submit operation rejected", zap.String("reason", "operation is required"))
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: operation is required", nil)}}, nil
	}
	op := req.GetOperation()
	if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
		s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "idempotency_key is required"))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op)}}, nil
	}
	if op.GetType() != connectorv1.OperationType_TRANSFER {
		s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "unsupported operation type"))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op)}}, nil
	}
	reader := params.New(op.GetParams())
	metadata := reader.StringMap("metadata")
	if metadata == nil {
		metadata = map[string]string{}
	}
	paymentIntentID := strings.TrimSpace(reader.String("payment_intent_id"))
	if paymentIntentID == "" {
		paymentIntentID = strings.TrimSpace(reader.String("payment_ref"))
	}
	if paymentIntentID == "" {
		paymentIntentID = strings.TrimSpace(metadata[metadataPaymentIntentID])
	}
	if paymentIntentID == "" {
		s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "payment_intent_id is required"))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: payment_intent_id is required", op)}}, nil
	}
	source := operationAccountID(op.GetFrom())
	if source == "" {
		s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "from.account is required"))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account is required", op)}}, nil
	}
	dest, err := transferDestinationFromOperation(op)
	if err != nil {
		s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.Error(err))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op)}}, nil
	}
	amount := op.GetMoney()
	if amount == nil {
		s.logger.Warn("Submit operation rejected", append(operationLogFields(op), zap.String("reason", "money is required"))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: money is required", op)}}, nil
	}

	metadata[metadataPaymentIntentID] = paymentIntentID
	quoteRef := strings.TrimSpace(reader.String("quote_ref"))
	if quoteRef != "" {
		metadata[metadataQuoteRef] = quoteRef
	}
	targetChatID := strings.TrimSpace(reader.String("target_chat_id"))
	if targetChatID != "" {
		metadata[metadataTargetChatID] = targetChatID
	}
	outgoingLeg := normalizeRail(reader.String("outgoing_leg"))
	if outgoingLeg != "" {
		metadata[metadataOutgoingLeg] = outgoingLeg
	}

	normalizedAmount := normalizeMoneyForTransfer(amount)
	logFields := append(operationLogFields(op),
		zap.String("payment_intent_id", paymentIntentID),
		zap.String("organization_ref", strings.TrimSpace(reader.String("organization_ref"))),
		zap.String("source_wallet_ref", source),
		zap.String("amount", strings.TrimSpace(normalizedAmount.GetAmount())),
		zap.String("currency", strings.TrimSpace(normalizedAmount.GetCurrency())),
		zap.String("quote_ref", quoteRef),
		zap.String("operation_ref", req.Operation.GetOperationRef()),
		zap.String("intent_ref", op.GetIntentRef()),
		zap.String("outgoing_leg", outgoingLeg),
	)
	logFields = append(logFields, transferDestinationLogFields(dest)...)

	resp, err := s.SubmitTransfer(ctx, &chainv1.SubmitTransferRequest{
		IdempotencyKey:  strings.TrimSpace(op.GetIdempotencyKey()),
		OrganizationRef: strings.TrimSpace(reader.String("organization_ref")),
		SourceWalletRef: source,
		Destination:     dest,
		Amount:          normalizedAmount,
		Metadata:        metadata,
		PaymentRef:      paymentIntentID,
		IntentRef:       strings.TrimSpace(op.GetIntentRef()),
		OperationRef:    strings.TrimSpace(op.GetOperationRef()),
	})
	if err != nil {
		s.logger.Warn("Submit operation transfer failed", append(logFields, zap.Error(err))...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op)}}, nil
	}
	transfer := resp.GetTransfer()
	operationID := strings.TrimSpace(transfer.GetOperationRef())
	if operationID == "" {
		s.logger.Warn("Submit operation transfer response missing operation_ref", append(logFields,
			zap.String("transfer_ref", strings.TrimSpace(transfer.GetTransferRef())),
		)...)
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{
			Error: connectorError(connectorv1.ErrorCode_TEMPORARY_UNAVAILABLE, "submit_operation: operation_ref is missing in transfer response", op),
		}}, nil
	}
	s.logger.Info("Submit operation transfer submitted", append(logFields,
		zap.String("transfer_ref", strings.TrimSpace(transfer.GetTransferRef())),
		zap.String("status", transfer.GetStatus().String()),
	)...)
	return &connectorv1.SubmitOperationResponse{
		Receipt: &connectorv1.OperationReceipt{
			OperationId: operationID,
			Status:      transferStatusToOperation(transfer.GetStatus()),
			ProviderRef: strings.TrimSpace(transfer.GetTransferRef()),
		},
	}, nil
}

func (s *Service) GetOperation(ctx context.Context, req *connectorv1.GetOperationRequest) (*connectorv1.GetOperationResponse, error) {
	if req == nil || strings.TrimSpace(req.GetOperationId()) == "" {
		s.logger.Warn("Get operation rejected", zap.String("reason", "operation_id is required"))
		return nil, merrors.InvalidArgument("get_operation: operation_id is required")
	}
	operationID := strings.TrimSpace(req.GetOperationId())

	if s.repo == nil || s.repo.Payments() == nil {
		s.logger.Warn("Get operation storage unavailable", zap.String("operation_id", operationID))
		return nil, merrors.Internal("get_operation: storage is not configured")
	}

	record, err := s.repo.Payments().FindByOperationRef(ctx, operationID)
	if err != nil {
		s.logger.Warn("Get operation lookup by operation_ref failed", zap.String("operation_id", operationID), zap.Error(err))
		return nil, err
	}
	if record == nil {
		return nil, status.Error(codes.NotFound, "operation not found")
	}

	return &connectorv1.GetOperationResponse{Operation: transferToOperation(transferFromPayment(record, nil))}, nil
}

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

func tgsettleOperationParams() []*connectorv1.OperationParamSpec {
	return []*connectorv1.OperationParamSpec{
		{OperationType: connectorv1.OperationType_TRANSFER, Params: []*connectorv1.ParamSpec{
			{Key: "payment_intent_id", Type: connectorv1.ParamType_STRING, Required: true},
			{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "quote_ref", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "target_chat_id", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "outgoing_leg", Type: connectorv1.ParamType_STRING, Required: false},
			{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false},
		}},
	}
}

func transferDestinationFromOperation(op *connectorv1.Operation) (*chainv1.TransferDestination, error) {
	if op == nil {
		return nil, merrors.InvalidArgument("transfer: operation is required")
	}
	if to := op.GetTo(); to != nil {
		if account := to.GetAccount(); account != nil {
			return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(account.GetAccountId())}}, nil
		}
		if ext := to.GetExternal(); ext != nil {
			return &chainv1.TransferDestination{Destination: &chainv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(ext.GetExternalRef())}}, nil
		}
	}
	return nil, merrors.InvalidArgument("transfer: to.account or to.external is required")
}

func normalizeMoneyForTransfer(m *moneyv1.Money) *moneyv1.Money {
	if m == nil {
		return nil
	}
	currency := strings.TrimSpace(m.GetCurrency())
	if idx := strings.Index(currency, "-"); idx > 0 {
		currency = currency[:idx]
	}
	return &moneyv1.Money{
		Amount:   strings.TrimSpace(m.GetAmount()),
		Currency: currency,
	}
}

func transferToOperation(transfer *chainv1.Transfer) *connectorv1.Operation {
	if transfer == nil {
		return nil
	}
	op := &connectorv1.Operation{
		OperationId:  strings.TrimSpace(transfer.GetOperationRef()),
		Type:         connectorv1.OperationType_TRANSFER,
		Status:       transferStatusToOperation(transfer.GetStatus()),
		Money:        transfer.GetRequestedAmount(),
		ProviderRef:  strings.TrimSpace(transfer.GetTransferRef()),
		IntentRef:    strings.TrimSpace(transfer.GetIntentRef()),
		OperationRef: strings.TrimSpace(transfer.GetOperationRef()),
		CreatedAt:    transfer.GetCreatedAt(),
		UpdatedAt:    transfer.GetUpdatedAt(),
	}
	params := map[string]interface{}{}
	if paymentRef := strings.TrimSpace(transfer.GetPaymentRef()); paymentRef != "" {
		params["payment_ref"] = paymentRef
	}
	if organizationRef := strings.TrimSpace(transfer.GetOrganizationRef()); organizationRef != "" {
		params["organization_ref"] = organizationRef
	}
	if failureReason := strings.TrimSpace(transfer.GetFailureReason()); failureReason != "" {
		params["failure_reason"] = failureReason
	}
	if len(params) > 0 {
		op.Params = structFromMap(params)
	}
	if source := strings.TrimSpace(transfer.GetSourceWalletRef()); source != "" {
		op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
			ConnectorId: tgsettleConnectorID,
			AccountId:   source,
		}}}
	}
	if dest := transfer.GetDestination(); dest != nil {
		switch d := dest.GetDestination().(type) {
		case *chainv1.TransferDestination_ManagedWalletRef:
			op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{
				ConnectorId: tgsettleConnectorID,
				AccountId:   strings.TrimSpace(d.ManagedWalletRef),
			}}}
		case *chainv1.TransferDestination_ExternalAddress:
			op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_External{External: &connectorv1.ExternalRef{
				ExternalRef: strings.TrimSpace(d.ExternalAddress),
			}}}
		}
	}
	return op
}

func transferStatusToOperation(status chainv1.TransferStatus) connectorv1.OperationStatus {
	switch status {

	case chainv1.TransferStatus_TRANSFER_CREATED:
		return connectorv1.OperationStatus_OPERATION_CREATED

	case chainv1.TransferStatus_TRANSFER_PROCESSING:
		return connectorv1.OperationStatus_OPERATION_PROCESSING

	case chainv1.TransferStatus_TRANSFER_WAITING:
		return connectorv1.OperationStatus_OPERATION_WAITING

	case chainv1.TransferStatus_TRANSFER_SUCCESS:
		return connectorv1.OperationStatus_OPERATION_SUCCESS

	case chainv1.TransferStatus_TRANSFER_FAILED:
		return connectorv1.OperationStatus_OPERATION_FAILED

	case chainv1.TransferStatus_TRANSFER_CANCELLED:
		return connectorv1.OperationStatus_OPERATION_CANCELLED

	case chainv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED:
		fallthrough
	default:
		return connectorv1.OperationStatus_OPERATION_STATUS_UNSPECIFIED
	}
}

func operationAccountID(party *connectorv1.OperationParty) string {
	if party == nil {
		return ""
	}
	if account := party.GetAccount(); account != nil {
		return strings.TrimSpace(account.GetAccountId())
	}
	return ""
}

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 operationLogFields(op *connectorv1.Operation) []zap.Field {
	if op == nil {
		return nil
	}
	return []zap.Field{
		zap.String("operation_id", strings.TrimSpace(op.GetOperationId())),
		zap.String("idempotency_key", strings.TrimSpace(op.GetIdempotencyKey())),
		zap.String("correlation_id", strings.TrimSpace(op.GetCorrelationId())),
		zap.String("parent_intent_id", strings.TrimSpace(op.GetParentIntentId())),
		zap.String("operation_type", op.GetType().String()),
		zap.String("intent_ref", strings.TrimSpace(op.GetIntentRef())),
	}
}

func transferDestinationLogFields(dest *chainv1.TransferDestination) []zap.Field {
	if dest == nil {
		return nil
	}
	switch d := dest.GetDestination().(type) {
	case *chainv1.TransferDestination_ManagedWalletRef:
		return []zap.Field{
			zap.String("destination_type", "managed_wallet"),
			zap.String("destination_ref", strings.TrimSpace(d.ManagedWalletRef)),
		}
	case *chainv1.TransferDestination_ExternalAddress:
		return []zap.Field{
			zap.String("destination_type", "external_address"),
			zap.String("destination_ref", strings.TrimSpace(d.ExternalAddress)),
		}
	default:
		return []zap.Field{zap.String("destination_type", "unknown")}
	}
}

func connectorError(code connectorv1.ErrorCode, message string, op *connectorv1.Operation) *connectorv1.ConnectorError {
	err := &connectorv1.ConnectorError{
		Code:    code,
		Message: strings.TrimSpace(message),
	}
	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
	}
}
