package ledger

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tech/sendico/ledger/internal/appversion"
	"github.com/tech/sendico/pkg/connector/params"
	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/ledgerconv"
	"github.com/tech/sendico/pkg/merrors"
	accountrolev1 "github.com/tech/sendico/pkg/proto/common/account_role/v1"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/structpb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const ledgerConnectorID = "ledger"

type connectorAdapter struct {
	connectorv1.UnimplementedConnectorServiceServer
	svc *Service
}

func newConnectorAdapter(svc *Service) *connectorAdapter {
	return &connectorAdapter{svc: svc}
}

func (c *connectorAdapter) GetCapabilities(_ context.Context, _ *connectorv1.GetCapabilitiesRequest) (*connectorv1.GetCapabilitiesResponse, error) {
	return &connectorv1.GetCapabilitiesResponse{
		Capabilities: &connectorv1.ConnectorCapabilities{
			ConnectorType:         ledgerConnectorID,
			Version:               appversion.Create().Short(),
			SupportedAccountKinds: []connectorv1.AccountKind{connectorv1.AccountKind_LEDGER_ACCOUNT},
			SupportedOperationTypes: []connectorv1.OperationType{
				connectorv1.OperationType_CREDIT,
				connectorv1.OperationType_DEBIT,
				connectorv1.OperationType_TRANSFER,
				connectorv1.OperationType_FX,
			},
			OpenAccountParams: ledgerOpenAccountParams(),
			OperationParams:   ledgerOperationParams(),
		},
	}, nil
}

func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.OpenAccountRequest) (*connectorv1.OpenAccountResponse, error) {
	if req == nil {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: request is required", nil, "")}, nil
	}
	if req.GetKind() != connectorv1.AccountKind_LEDGER_ACCOUNT {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_ACCOUNT_KIND, "open_account: unsupported account kind", nil, "")}, nil
	}

	reader := params.New(req.GetParams())
	orgRef := strings.TrimSpace(reader.String("organization_ref"))
	if orgRef == "" {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil, "")}, nil
	}

	accountType, err := parseLedgerAccountType(reader, "account_type")
	if err != nil {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), nil, "")}, nil
	}

	currency := strings.TrimSpace(req.GetAsset())
	if currency == "" {
		currency = strings.TrimSpace(reader.String("currency"))
	}
	if currency == "" {
		return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: asset is required", nil, "")}, nil
	}

	status := parseLedgerAccountStatus(reader, "status")
	role := accountRoleFromConnectorRole(req.GetRole())
	if role == ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
		role = parseLedgerAccountRole(reader, "role") // backward compat: accept role via params
	}
	metadata := mergeMetadata(reader.StringMap("metadata"), req.GetLabel(), req.GetOwnerRef(), req.GetCorrelationId(), req.GetParentIntentId())
	describable := describableFromLabel(req.GetLabel(), reader.String("description"))

	resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
		OrganizationRef: orgRef,
		AccountType:     accountType,
		Currency:        currency,
		Status:          status,
		AllowNegative:   reader.Bool("allow_negative"),
		Role:            role,
		Metadata:        metadata,
		Describable:     describable,
		OwnerRef:        req.GetOwnerRef(),
	})
	if err != nil {
		return &connectorv1.OpenAccountResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, "")}, nil
	}

	return &connectorv1.OpenAccountResponse{
		Account: ledgerAccountToConnector(resp.GetAccount()),
	}, nil
}

func (c *connectorAdapter) GetAccount(ctx context.Context, req *connectorv1.GetAccountRequest) (*connectorv1.GetAccountResponse, error) {
	if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
		return nil, merrors.InvalidArgument("get_account: account_ref.account_id is required")
	}
	accountRef, err := parseObjectID(strings.TrimSpace(req.GetAccountRef().GetAccountId()))
	if err != nil {
		return nil, err
	}
	if c.svc.storage == nil || c.svc.storage.Accounts() == nil {
		return nil, merrors.Internal("get_account: storage unavailable")
	}
	account, err := c.svc.storage.Accounts().Get(ctx, accountRef)
	if err != nil {
		return nil, err
	}
	return &connectorv1.GetAccountResponse{
		Account: ledgerAccountToConnector(toProtoAccount(account)),
	}, nil
}

func (c *connectorAdapter) ListAccounts(ctx context.Context, req *connectorv1.ListAccountsRequest) (*connectorv1.ListAccountsResponse, error) {
	if req == nil {
		return nil, merrors.InvalidArgument("list_accounts: request is required")
	}
	orgRef := strings.TrimSpace(req.GetOrganizationRef())
	if orgRef == "" {
		return nil, merrors.InvalidArgument("list_accounts: organization_ref is required")
	}
	resp, err := c.svc.ListAccounts(ctx, &ledgerv1.ListAccountsRequest{
		OrganizationRef: orgRef,
		OwnerRefFilter:  req.GetOwnerRefFilter(),
	})
	if err != nil {
		return nil, err
	}
	accounts := make([]*connectorv1.Account, 0, len(resp.GetAccounts()))
	for _, account := range resp.GetAccounts() {
		accounts = append(accounts, ledgerAccountToConnector(account))
	}
	return &connectorv1.ListAccountsResponse{Accounts: accounts}, nil
}

func (c *connectorAdapter) GetBalance(ctx context.Context, req *connectorv1.GetBalanceRequest) (*connectorv1.GetBalanceResponse, error) {
	if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
		return nil, merrors.InvalidArgument("get_balance: account_ref.account_id is required")
	}
	resp, err := c.svc.GetBalance(ctx, &ledgerv1.GetBalanceRequest{
		LedgerAccountRef: strings.TrimSpace(req.GetAccountRef().GetAccountId()),
	})
	if err != nil {
		return nil, err
	}
	return &connectorv1.GetBalanceResponse{
		Balance: &connectorv1.Balance{
			AccountRef:      req.GetAccountRef(),
			Available:       resp.GetBalance(),
			CalculatedAt:    resp.GetLastUpdated(),
			PendingInbound:  nil,
			PendingOutbound: nil,
		},
	}, nil
}

func (c *connectorAdapter) UpdateAccountState(ctx context.Context, req *connectorv1.UpdateAccountStateRequest) (*connectorv1.UpdateAccountStateResponse, error) {
	if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
		return &connectorv1.UpdateAccountStateResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "update_account_state: account_ref.account_id is required", nil, "")}, nil
	}
	accountID := strings.TrimSpace(req.GetAccountRef().GetAccountId())

	switch req.GetTargetState() {
	case connectorv1.AccountState_ACCOUNT_SUSPENDED:
		resp, err := c.svc.BlockAccount(ctx, &ledgerv1.BlockAccountRequest{
			LedgerAccountRef: accountID,
			OrganizationRef:  "", // resolved from account itself
			Role:             accountRoleFromConnectorRole(req.GetSourceRole()),
		})
		if err != nil {
			return &connectorv1.UpdateAccountStateResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, accountID)}, nil
		}
		return &connectorv1.UpdateAccountStateResponse{Account: ledgerAccountToConnector(resp.GetAccount())}, nil

	case connectorv1.AccountState_ACCOUNT_ACTIVE:
		resp, err := c.svc.UnblockAccount(ctx, &ledgerv1.UnblockAccountRequest{
			LedgerAccountRef: accountID,
			OrganizationRef:  "", // resolved from account itself
			Role:             accountRoleFromConnectorRole(req.GetSourceRole()),
		})
		if err != nil {
			return &connectorv1.UpdateAccountStateResponse{Error: connectorError(mapErrorCode(err), err.Error(), nil, accountID)}, nil
		}
		return &connectorv1.UpdateAccountStateResponse{Account: ledgerAccountToConnector(resp.GetAccount())}, nil

	default:
		return &connectorv1.UpdateAccountStateResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "update_account_state: target_state must be ACCOUNT_ACTIVE or ACCOUNT_SUSPENDED", nil, accountID)}, nil
	}
}

func (c *connectorAdapter) 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()
	if strings.TrimSpace(op.GetIdempotencyKey()) == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: idempotency_key is required", op, "")}}, nil
	}
	reader := params.New(op.GetParams())
	orgRef := strings.TrimSpace(reader.String("organization_ref"))
	if orgRef == "" {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "submit_operation: organization_ref is required", op, "")}}, nil
	}

	metadata := mergeMetadata(reader.StringMap("metadata"), "", "", op.GetCorrelationId(), op.GetParentIntentId())
	description := strings.TrimSpace(reader.String("description"))
	eventTime := parseEventTime(reader)
	charges, err := parseLedgerCharges(reader)
	if err != nil {
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
	}
	operation := discovery.NormalizeOperation(reader.String("operation"))

	switch op.GetType() {
	case connectorv1.OperationType_CREDIT:
		accountID := operationAccountID(op.GetTo())
		if accountID == "" && op.GetToRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: to.account or to_role is required", op, "")}}, nil
		}
		if operation != "" && operation != discovery.OperationExternalCredit {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "credit: unsupported operation override", op, "")}}, nil
		}
		creditFn := c.svc.PostCreditWithCharges
		if operation == discovery.OperationExternalCredit {
			creditFn = c.svc.PostExternalCreditWithCharges
		}
		resp, err := creditFn(ctx, &ledgerv1.PostCreditRequest{
			IdempotencyKey:         strings.TrimSpace(op.GetIdempotencyKey()),
			OrganizationRef:        orgRef,
			LedgerAccountRef:       accountID,
			Money:                  op.GetMoney(),
			Description:            description,
			Charges:                charges,
			Metadata:               metadata,
			EventTime:              eventTime,
			ContraLedgerAccountRef: strings.TrimSpace(reader.String("contra_ledger_account_ref")),
			Role:                   accountRoleFromConnectorRole(op.GetToRole()),
		})
		if err != nil {
			c.svc.logger.Warn("Operation failed", zap.Error(err), zap.String("operation", operation),
				zap.String("idempotency_key", op.IdempotencyKey), zap.String("description", description),
				zap.String("organization_ref", orgRef), zap.String("ledger_account_ref", accountID),
			)
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
		}
		return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_OPERATION_SUCCESS)}, nil
	case connectorv1.OperationType_DEBIT:
		accountID := operationAccountID(op.GetFrom())
		if accountID == "" && op.GetFromRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: from.account or from_role is required", op, "")}}, nil
		}
		if operation != "" && operation != discovery.OperationExternalDebit {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "debit: unsupported operation override", op, "")}}, nil
		}
		debitFn := c.svc.PostDebitWithCharges
		if operation == discovery.OperationExternalDebit {
			debitFn = c.svc.PostExternalDebitWithCharges
		}
		resp, err := debitFn(ctx, &ledgerv1.PostDebitRequest{
			IdempotencyKey:         strings.TrimSpace(op.GetIdempotencyKey()),
			OrganizationRef:        orgRef,
			LedgerAccountRef:       accountID,
			Money:                  op.GetMoney(),
			Description:            description,
			Charges:                charges,
			Metadata:               metadata,
			EventTime:              eventTime,
			ContraLedgerAccountRef: strings.TrimSpace(reader.String("contra_ledger_account_ref")),
			Role:                   accountRoleFromConnectorRole(op.GetFromRole()),
		})
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, accountID)}}, nil
		}
		return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_OPERATION_SUCCESS)}, nil
	case connectorv1.OperationType_TRANSFER:
		fromID := operationAccountID(op.GetFrom())
		toID := operationAccountID(op.GetTo())
		if fromID == "" && op.GetFromRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: from.account or from_role is required", op, "")}}, nil
		}
		if toID == "" && op.GetToRole() == accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "transfer: to.account or to_role is required", op, "")}}, nil
		}
		resp, err := c.svc.TransferInternal(ctx, &ledgerv1.TransferRequest{
			IdempotencyKey:       strings.TrimSpace(op.GetIdempotencyKey()),
			OrganizationRef:      orgRef,
			FromLedgerAccountRef: fromID,
			ToLedgerAccountRef:   toID,
			Money:                op.GetMoney(),
			Description:          description,
			Charges:              charges,
			Metadata:             metadata,
			EventTime:            eventTime,
			FromRole:             accountRoleFromConnectorRole(op.GetFromRole()),
			ToRole:               accountRoleFromConnectorRole(op.GetToRole()),
		})
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
		}
		return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_OPERATION_SUCCESS)}, nil
	case connectorv1.OperationType_FX:
		fromID := operationAccountID(op.GetFrom())
		toID := operationAccountID(op.GetTo())
		if fromID == "" || toID == "" {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "fx: from.account and to.account are required", op, "")}}, nil
		}
		toMoney, err := parseMoneyFromMap(reader.Map("to_money"))
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, err.Error(), op, "")}}, nil
		}
		resp, err := c.svc.ApplyFXWithCharges(ctx, &ledgerv1.FXRequest{
			IdempotencyKey:       strings.TrimSpace(op.GetIdempotencyKey()),
			OrganizationRef:      orgRef,
			FromLedgerAccountRef: fromID,
			ToLedgerAccountRef:   toID,
			FromMoney:            op.GetMoney(),
			ToMoney:              toMoney,
			Rate:                 strings.TrimSpace(reader.String("rate")),
			Description:          description,
			Charges:              charges,
			Metadata:             metadata,
			EventTime:            eventTime,
		})
		if err != nil {
			return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(mapErrorCode(err), err.Error(), op, "")}}, nil
		}
		return &connectorv1.SubmitOperationResponse{Receipt: ledgerReceipt(resp.GetJournalEntryRef(), connectorv1.OperationStatus_OPERATION_SUCCESS)}, nil
	default:
		return &connectorv1.SubmitOperationResponse{Receipt: &connectorv1.OperationReceipt{Error: connectorError(connectorv1.ErrorCode_UNSUPPORTED_OPERATION, "submit_operation: unsupported operation type", op, "")}}, nil
	}
}

func (c *connectorAdapter) 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")
	}
	entry, err := c.svc.GetJournalEntry(ctx, &ledgerv1.GetEntryRequest{EntryRef: strings.TrimSpace(req.GetOperationId())})
	if err != nil {
		return nil, err
	}
	return &connectorv1.GetOperationResponse{Operation: ledgerEntryToOperation(entry)}, nil
}

func (c *connectorAdapter) ListOperations(ctx context.Context, req *connectorv1.ListOperationsRequest) (*connectorv1.ListOperationsResponse, error) {
	if req == nil || req.GetAccountRef() == nil || strings.TrimSpace(req.GetAccountRef().GetAccountId()) == "" {
		return nil, merrors.InvalidArgument("list_operations: account_ref.account_id is required")
	}
	resp, err := c.svc.GetStatement(ctx, &ledgerv1.GetStatementRequest{
		LedgerAccountRef: strings.TrimSpace(req.GetAccountRef().GetAccountId()),
		Cursor:           "",
		Limit:            0,
	})
	if err != nil {
		return nil, err
	}
	ops := make([]*connectorv1.Operation, 0, len(resp.GetEntries()))
	for _, entry := range resp.GetEntries() {
		ops = append(ops, ledgerEntryToOperation(entry))
	}
	return &connectorv1.ListOperationsResponse{Operations: ops}, nil
}

func ledgerOpenAccountParams() []*connectorv1.ParamSpec {
	return []*connectorv1.ParamSpec{
		{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the ledger account."},
		{Key: "account_type", Type: connectorv1.ParamType_STRING, Required: true, Description: "ASSET | LIABILITY | REVENUE | EXPENSE."},
		{Key: "status", Type: connectorv1.ParamType_STRING, Required: false, Description: "ACTIVE | FROZEN."},
		{Key: "allow_negative", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Allow negative balance."},
		{Key: "role", Type: connectorv1.ParamType_STRING, Required: false, Description: "OPERATING | HOLD | TRANSIT | SETTLEMENT | CLEARING | PENDING | RESERVE | LIQUIDITY | FEE | CHARGEBACK | ADJUSTMENT."},
		{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Additional metadata map."},
	}
}

func ledgerOperationParams() []*connectorv1.OperationParamSpec {
	common := []*connectorv1.ParamSpec{
		{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference."},
		{Key: "description", Type: connectorv1.ParamType_STRING, Required: false, Description: "Ledger entry description."},
		{Key: "metadata", Type: connectorv1.ParamType_JSON, Required: false, Description: "Entry metadata map."},
		{Key: "charges", Type: connectorv1.ParamType_JSON, Required: false, Description: "Posting line charges."},
		{Key: "event_time", Type: connectorv1.ParamType_STRING, Required: false, Description: "RFC3339 timestamp."},
	}
	externalCredit := &connectorv1.ParamSpec{
		Key:           "operation",
		Type:          connectorv1.ParamType_STRING,
		Required:      false,
		Description:   "Optional ledger operation override (external.credit).",
		AllowedValues: []string{discovery.OperationExternalCredit},
	}
	externalDebit := &connectorv1.ParamSpec{
		Key:           "operation",
		Type:          connectorv1.ParamType_STRING,
		Required:      false,
		Description:   "Optional ledger operation override (external.debit).",
		AllowedValues: []string{discovery.OperationExternalDebit},
	}
	return []*connectorv1.OperationParamSpec{
		{OperationType: connectorv1.OperationType_CREDIT, Params: append(common, externalCredit, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
		{OperationType: connectorv1.OperationType_DEBIT, Params: append(common, externalDebit, &connectorv1.ParamSpec{Key: "contra_ledger_account_ref", Type: connectorv1.ParamType_STRING, Required: false})},
		{OperationType: connectorv1.OperationType_TRANSFER, Params: common},
		{OperationType: connectorv1.OperationType_FX, Params: append(common,
			&connectorv1.ParamSpec{Key: "to_money", Type: connectorv1.ParamType_JSON, Required: true, Description: "Target amount {amount,currency}."},
			&connectorv1.ParamSpec{Key: "rate", Type: connectorv1.ParamType_STRING, Required: false, Description: "FX rate snapshot."},
		)},
	}
}

func ledgerAccountToConnector(account *ledgerv1.LedgerAccount) *connectorv1.Account {
	if account == nil {
		return nil
	}
	details, _ := structpb.NewStruct(map[string]interface{}{
		"account_code":     account.GetAccountCode(),
		"account_type":     account.GetAccountType().String(),
		"status":           account.GetStatus().String(),
		"allow_negative":   account.GetAllowNegative(),
		"role":             account.GetRole().String(),
		"organization_ref": strings.TrimSpace(account.GetOrganizationRef()),
	})
	describable := ledgerAccountDescribable(account)
	return &connectorv1.Account{
		Ref: &connectorv1.AccountRef{
			ConnectorId: ledgerConnectorID,
			AccountId:   strings.TrimSpace(account.GetLedgerAccountRef()),
		},
		Kind:            connectorv1.AccountKind_LEDGER_ACCOUNT,
		Asset:           strings.TrimSpace(account.GetCurrency()),
		State:           ledgerAccountState(account.GetStatus()),
		Label:           strings.TrimSpace(account.GetAccountCode()),
		OwnerRef:        "",
		ProviderDetails: details,
		CreatedAt:       account.GetCreatedAt(),
		UpdatedAt:       account.GetUpdatedAt(),
		Describable:     describable,
		Role:            ledgerRoleToConnectorRole(account.GetRole()),
	}
}

func ledgerAccountState(status ledgerv1.AccountStatus) connectorv1.AccountState {
	switch status {
	case ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE:
		return connectorv1.AccountState_ACCOUNT_ACTIVE
	case ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN:
		return connectorv1.AccountState_ACCOUNT_SUSPENDED
	default:
		return connectorv1.AccountState_ACCOUNT_STATE_UNSPECIFIED
	}
}

func ledgerAccountDescribable(account *ledgerv1.LedgerAccount) *describablev1.Describable {
	if account == nil {
		return nil
	}
	if desc := cleanedDescribable(account.GetDescribable()); desc != nil {
		return desc
	}
	metadata := account.GetMetadata()
	name := ""
	if metadata != nil {
		if v := strings.TrimSpace(metadata["name"]); v != "" {
			name = v
		} else if v := strings.TrimSpace(metadata["label"]); v != "" {
			name = v
		}
	}
	if name == "" {
		name = strings.TrimSpace(account.GetAccountCode())
	}
	desc := ""
	if metadata != nil {
		desc = strings.TrimSpace(metadata["description"])
	}
	if name == "" && desc == "" {
		return nil
	}
	if desc == "" {
		return &describablev1.Describable{Name: name}
	}
	return &describablev1.Describable{Name: name, Description: &desc}
}

func describableFromLabel(label, description string) *describablev1.Describable {
	label = strings.TrimSpace(label)
	description = strings.TrimSpace(description)
	if label == "" && description == "" {
		return nil
	}
	if description == "" {
		return &describablev1.Describable{Name: label}
	}
	return &describablev1.Describable{Name: label, Description: &description}
}

func cleanedDescribable(desc *describablev1.Describable) *describablev1.Describable {
	if desc == nil {
		return nil
	}
	name := strings.TrimSpace(desc.GetName())
	var description *string
	if desc.Description != nil {
		trimmed := strings.TrimSpace(desc.GetDescription())
		if trimmed != "" {
			description = &trimmed
		}
	}
	if name == "" && description == nil {
		return nil
	}
	return &describablev1.Describable{
		Name:        name,
		Description: description,
	}
}

func ledgerReceipt(ref string, status connectorv1.OperationStatus) *connectorv1.OperationReceipt {
	return &connectorv1.OperationReceipt{
		OperationId: strings.TrimSpace(ref),
		Status:      status,
		ProviderRef: strings.TrimSpace(ref),
	}
}

func ledgerEntryToOperation(entry *ledgerv1.JournalEntryResponse) *connectorv1.Operation {
	if entry == nil {
		return nil
	}
	op := &connectorv1.Operation{
		OperationId: strings.TrimSpace(entry.GetEntryRef()),
		Type:        ledgerEntryType(entry.GetEntryType()),
		Status:      connectorv1.OperationStatus_OPERATION_SUCCESS,
		CreatedAt:   entry.GetEventTime(),
		UpdatedAt:   entry.GetEventTime(),
	}
	mainLines := ledgerMainLines(entry.GetLines())
	if len(mainLines) > 0 {
		op.Money = mainLines[0].GetMoney()
	}
	switch op.Type {
	case connectorv1.OperationType_CREDIT:
		if len(mainLines) > 0 {
			op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
		}
	case connectorv1.OperationType_DEBIT:
		if len(mainLines) > 0 {
			op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
		}
	case connectorv1.OperationType_TRANSFER, connectorv1.OperationType_FX:
		if len(mainLines) > 0 {
			op.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[0].GetLedgerAccountRef()}}}
		}
		if len(mainLines) > 1 {
			op.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: mainLines[1].GetLedgerAccountRef()}}}
		}
	}
	return op
}

func ledgerMainLines(lines []*ledgerv1.PostingLine) []*ledgerv1.PostingLine {
	if len(lines) == 0 {
		return nil
	}
	result := make([]*ledgerv1.PostingLine, 0, len(lines))
	for _, line := range lines {
		if line == nil {
			continue
		}
		if line.GetLineType() == ledgerv1.LineType_LINE_MAIN {
			result = append(result, line)
		}
	}
	return result
}

func ledgerEntryType(entryType ledgerv1.EntryType) connectorv1.OperationType {
	switch entryType {
	case ledgerv1.EntryType_ENTRY_CREDIT:
		return connectorv1.OperationType_CREDIT
	case ledgerv1.EntryType_ENTRY_DEBIT:
		return connectorv1.OperationType_DEBIT
	case ledgerv1.EntryType_ENTRY_TRANSFER:
		return connectorv1.OperationType_TRANSFER
	case ledgerv1.EntryType_ENTRY_FX:
		return connectorv1.OperationType_FX
	default:
		return connectorv1.OperationType_OPERATION_TYPE_UNSPECIFIED
	}
}

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

func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountType, error) {
	value, ok := reader.Value(key)
	if !ok {
		return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
	}
	switch v := value.(type) {
	case string:
		return parseLedgerAccountTypeString(v)
	case float64:
		truncated := int64(v)
		if v != float64(truncated) {
			return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
		}
		return parseLedgerAccountTypeInt64(truncated)
	case int:
		return parseLedgerAccountTypeInt64(int64(v))
	case int64:
		return parseLedgerAccountTypeInt64(v)
	default:
		return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: account_type is required")
	}
}

func parseLedgerAccountTypeInt64(value int64) (ledgerv1.AccountType, error) {
	if value < -1<<31 || value > 1<<31-1 {
		return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
	}
	return ledgerv1.AccountType(int32(value)), nil
}

func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
	accountType, ok := ledgerconv.ParseAccountType(value)
	if !ok || ledgerconv.IsAccountTypeUnspecified(value) {
		return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
	}
	return accountType, nil
}

func parseLedgerAccountStatus(reader params.Reader, key string) ledgerv1.AccountStatus {
	status, _ := ledgerconv.ParseAccountStatus(reader.String(key))
	return status
}

func parseLedgerAccountRole(reader params.Reader, key string) ledgerv1.AccountRole {
	role, _ := ledgerconv.ParseAccountRole(reader.String(key))
	return role
}

func parseEventTime(reader params.Reader) *timestamppb.Timestamp {
	raw := strings.TrimSpace(reader.String("event_time"))
	if raw == "" {
		return nil
	}
	parsed, err := time.Parse(time.RFC3339Nano, raw)
	if err != nil {
		return nil
	}
	return timestamppb.New(parsed)
}

func parseLedgerCharges(reader params.Reader) ([]*ledgerv1.PostingLine, error) {
	items := reader.List("charges")
	if len(items) == 0 {
		return nil, nil //nolint:nilnil // nil charges means no extra posting lines were provided
	}
	result := make([]*ledgerv1.PostingLine, 0, len(items))
	for i, item := range items {
		raw, ok := item.(map[string]interface{})
		if !ok {
			return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: invalid charge entry", i))
		}
		accountRef := strings.TrimSpace(fmt.Sprint(raw["ledger_account_ref"]))
		if accountRef == "" {
			return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: ledger_account_ref is required", i))
		}
		money, err := parseMoneyFromMap(raw)
		if err != nil {
			return nil, merrors.InvalidArgumentWrap(err, fmt.Sprintf("charges[%d]: invalid money", i))
		}
		lineType := parseLedgerLineType(fmt.Sprint(raw["line_type"]))
		result = append(result, &ledgerv1.PostingLine{
			LedgerAccountRef: accountRef,
			Money:            money,
			LineType:         lineType,
		})
	}
	return result, nil
}

func parseLedgerLineType(value string) ledgerv1.LineType {
	switch strings.ToUpper(strings.TrimSpace(value)) {
	case "LINE_TYPE_FEE", "FEE":
		return ledgerv1.LineType_LINE_FEE
	case "LINE_TYPE_SPREAD", "SPREAD":
		return ledgerv1.LineType_LINE_SPREAD
	case "LINE_TYPE_REVERSAL", "REVERSAL":
		return ledgerv1.LineType_LINE_REVERSAL
	default:
		return ledgerv1.LineType_LINE_FEE
	}
}

func parseMoneyFromMap(raw map[string]interface{}) (*moneyv1.Money, error) {
	if raw == nil {
		return nil, merrors.InvalidArgument("money is required")
	}
	amount := strings.TrimSpace(fmt.Sprint(raw["amount"]))
	currency := strings.TrimSpace(fmt.Sprint(raw["currency"]))
	if amount == "" || currency == "" {
		return nil, merrors.InvalidArgument("money is required")
	}
	return &moneyv1.Money{
		Amount:   amount,
		Currency: currency,
	}, nil
}

func mergeMetadata(base map[string]string, label, ownerRef, correlationID, parentIntentID string) map[string]string {
	metadata := map[string]string{}
	for k, v := range base {
		metadata[strings.TrimSpace(k)] = strings.TrimSpace(v)
	}
	if label != "" {
		if _, ok := metadata["label"]; !ok {
			metadata["label"] = label
		}
	}
	if ownerRef != "" {
		if _, ok := metadata["owner_ref"]; !ok {
			metadata["owner_ref"] = ownerRef
		}
	}
	if correlationID != "" {
		metadata["correlation_id"] = correlationID
	}
	if parentIntentID != "" {
		metadata["parent_intent_id"] = parentIntentID
	}
	if len(metadata) == 0 {
		return nil
	}
	return metadata
}

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 accountRoleFromConnectorRole(role accountrolev1.AccountRole) ledgerv1.AccountRole {
	switch role {
	case accountrolev1.AccountRole_OPERATING:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING
	case accountrolev1.AccountRole_HOLD:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD
	case accountrolev1.AccountRole_TRANSIT:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT
	case accountrolev1.AccountRole_SETTLEMENT:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
	case accountrolev1.AccountRole_CLEARING:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING
	case accountrolev1.AccountRole_PENDING:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING
	case accountrolev1.AccountRole_RESERVE:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE
	case accountrolev1.AccountRole_LIQUIDITY:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY
	case accountrolev1.AccountRole_FEE:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_FEE
	case accountrolev1.AccountRole_CHARGEBACK:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK
	case accountrolev1.AccountRole_ADJUSTMENT:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT
	default:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
	}
}

func ledgerRoleToConnectorRole(role ledgerv1.AccountRole) accountrolev1.AccountRole {
	switch role {
	case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING:
		return accountrolev1.AccountRole_OPERATING
	case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
		return accountrolev1.AccountRole_HOLD
	case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
		return accountrolev1.AccountRole_TRANSIT
	case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
		return accountrolev1.AccountRole_SETTLEMENT
	case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
		return accountrolev1.AccountRole_CLEARING
	case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
		return accountrolev1.AccountRole_PENDING
	case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
		return accountrolev1.AccountRole_RESERVE
	case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
		return accountrolev1.AccountRole_LIQUIDITY
	case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
		return accountrolev1.AccountRole_FEE
	case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
		return accountrolev1.AccountRole_CHARGEBACK
	case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
		return accountrolev1.AccountRole_ADJUSTMENT
	default:
		return accountrolev1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
	}
}

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
	}
}
