package ledger

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/url"
	"strings"
	"time"

	"github.com/tech/sendico/pkg/discovery"
	"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"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/structpb"
)

const ledgerConnectorID = "ledger"

type Config struct {
	Endpoint string
	Timeout  time.Duration
	Insecure bool
}

type Account struct {
	AccountID       string
	AccountCode     string
	Currency        string
	OrganizationRef string
}

type Balance struct {
	AccountID string
	Amount    string
	Currency  string
}

type PostRequest struct {
	AccountID       string
	OrganizationRef string
	Amount          string
	Currency        string
	Reference       string
	IdempotencyKey  string
}

type OperationResult struct {
	Reference string
}

type Client interface {
	GetAccount(ctx context.Context, accountID string) (*Account, error)
	GetBalance(ctx context.Context, accountID string) (*Balance, error)
	ExternalCredit(ctx context.Context, req PostRequest) (*OperationResult, error)
	ExternalDebit(ctx context.Context, req PostRequest) (*OperationResult, error)
	Close() error
}

type grpcConnectorClient interface {
	GetAccount(ctx context.Context, in *connectorv1.GetAccountRequest, opts ...grpc.CallOption) (*connectorv1.GetAccountResponse, error)
	GetBalance(ctx context.Context, in *connectorv1.GetBalanceRequest, opts ...grpc.CallOption) (*connectorv1.GetBalanceResponse, error)
	SubmitOperation(ctx context.Context, in *connectorv1.SubmitOperationRequest, opts ...grpc.CallOption) (*connectorv1.SubmitOperationResponse, error)
}

type connectorClient struct {
	cfg    Config
	conn   *grpc.ClientConn
	client grpcConnectorClient
}

func New(cfg Config) (Client, error) {
	cfg.Endpoint = strings.TrimSpace(cfg.Endpoint)
	if cfg.Endpoint == "" {
		return nil, merrors.InvalidArgument("ledger endpoint is required", "ledger.endpoint")
	}
	if normalized, insecure := normalizeEndpoint(cfg.Endpoint); normalized != "" {
		cfg.Endpoint = normalized
		if insecure {
			cfg.Insecure = true
		}
	}
	if cfg.Timeout <= 0 {
		cfg.Timeout = 5 * time.Second
	}
	dialOpts := []grpc.DialOption{}
	if cfg.Insecure {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	} else {
		dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
	}
	conn, err := grpc.NewClient(cfg.Endpoint, dialOpts...)
	if err != nil {
		return nil, merrors.InternalWrap(err, fmt.Sprintf("ledger: dial %s", cfg.Endpoint))
	}
	return &connectorClient{
		cfg:    cfg,
		conn:   conn,
		client: connectorv1.NewConnectorServiceClient(conn),
	}, nil
}

func (c *connectorClient) Close() error {
	if c == nil || c.conn == nil {
		return nil
	}
	return c.conn.Close()
}

func (c *connectorClient) GetAccount(ctx context.Context, accountID string) (*Account, error) {
	accountID = strings.TrimSpace(accountID)
	if accountID == "" {
		return nil, merrors.InvalidArgument("ledger account_id is required", "account_id")
	}
	ctx, cancel := c.callContext(ctx)
	defer cancel()

	resp, err := c.client.GetAccount(ctx, &connectorv1.GetAccountRequest{
		AccountRef: &connectorv1.AccountRef{
			ConnectorId: ledgerConnectorID,
			AccountId:   accountID,
		},
	})
	if err != nil {
		return nil, err
	}
	account := resp.GetAccount()
	if account == nil {
		return nil, merrors.NoData("ledger account not found")
	}
	accountCode := strings.TrimSpace(account.GetLabel())
	organizationRef := strings.TrimSpace(account.GetOwnerRef())
	if organizationRef == "" && account.GetProviderDetails() != nil {
		details := account.GetProviderDetails().AsMap()
		if organizationRef == "" {
			organizationRef = firstDetailValue(details, "organization_ref", "organizationRef", "org_ref")
		}
		if accountCode == "" {
			accountCode = firstDetailValue(details, "account_code", "accountCode", "code", "ledger_account_code")
		}
	}
	return &Account{
		AccountID:       accountID,
		AccountCode:     accountCode,
		Currency:        strings.ToUpper(strings.TrimSpace(account.GetAsset())),
		OrganizationRef: organizationRef,
	}, nil
}

func (c *connectorClient) GetBalance(ctx context.Context, accountID string) (*Balance, error) {
	accountID = strings.TrimSpace(accountID)
	if accountID == "" {
		return nil, merrors.InvalidArgument("ledger account_id is required", "account_id")
	}
	ctx, cancel := c.callContext(ctx)
	defer cancel()

	resp, err := c.client.GetBalance(ctx, &connectorv1.GetBalanceRequest{
		AccountRef: &connectorv1.AccountRef{
			ConnectorId: ledgerConnectorID,
			AccountId:   accountID,
		},
	})
	if err != nil {
		return nil, err
	}
	balance := resp.GetBalance()
	if balance == nil || balance.GetAvailable() == nil {
		return nil, merrors.Internal("ledger balance is unavailable")
	}
	return &Balance{
		AccountID: accountID,
		Amount:    strings.TrimSpace(balance.GetAvailable().GetAmount()),
		Currency:  strings.ToUpper(strings.TrimSpace(balance.GetAvailable().GetCurrency())),
	}, nil
}

func (c *connectorClient) ExternalCredit(ctx context.Context, req PostRequest) (*OperationResult, error) {
	return c.submitExternalOperation(ctx, connectorv1.OperationType_CREDIT, discovery.OperationExternalCredit, req)
}

func (c *connectorClient) ExternalDebit(ctx context.Context, req PostRequest) (*OperationResult, error) {
	return c.submitExternalOperation(ctx, connectorv1.OperationType_DEBIT, discovery.OperationExternalDebit, req)
}

func (c *connectorClient) submitExternalOperation(ctx context.Context, opType connectorv1.OperationType, operation string, req PostRequest) (*OperationResult, error) {
	req.AccountID = strings.TrimSpace(req.AccountID)
	req.OrganizationRef = strings.TrimSpace(req.OrganizationRef)
	req.Amount = strings.TrimSpace(req.Amount)
	req.Currency = strings.ToUpper(strings.TrimSpace(req.Currency))
	req.Reference = strings.TrimSpace(req.Reference)
	req.IdempotencyKey = strings.TrimSpace(req.IdempotencyKey)

	if req.AccountID == "" {
		return nil, merrors.InvalidArgument("ledger account_id is required", "account_id")
	}
	if req.OrganizationRef == "" {
		return nil, merrors.InvalidArgument("ledger organization_ref is required", "organization_ref")
	}
	if req.Amount == "" || req.Currency == "" {
		return nil, merrors.InvalidArgument("ledger amount is required", "amount")
	}
	if req.IdempotencyKey == "" {
		return nil, merrors.InvalidArgument("ledger idempotency_key is required", "idempotency_key")
	}

	params := map[string]any{
		"organization_ref": req.OrganizationRef,
		"operation":        operation,
		"description":      "tgsettle treasury operation",
		"metadata": map[string]any{
			"reference": req.Reference,
		},
	}
	operationReq := &connectorv1.Operation{
		Type:           opType,
		IdempotencyKey: req.IdempotencyKey,
		Money: &moneyv1.Money{
			Amount:   req.Amount,
			Currency: req.Currency,
		},
		Params: structFromMap(params),
	}
	account := &connectorv1.AccountRef{ConnectorId: ledgerConnectorID, AccountId: req.AccountID}
	switch opType {
	case connectorv1.OperationType_CREDIT:
		operationReq.To = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: account}}
	case connectorv1.OperationType_DEBIT:
		operationReq.From = &connectorv1.OperationParty{Ref: &connectorv1.OperationParty_Account{Account: account}}
	}

	ctx, cancel := c.callContext(ctx)
	defer cancel()

	resp, err := c.client.SubmitOperation(ctx, &connectorv1.SubmitOperationRequest{Operation: operationReq})
	if err != nil {
		return nil, err
	}
	if resp.GetReceipt() == nil {
		return nil, merrors.Internal("ledger receipt is unavailable")
	}
	if receiptErr := resp.GetReceipt().GetError(); receiptErr != nil {
		message := strings.TrimSpace(receiptErr.GetMessage())
		if message == "" {
			message = "ledger operation failed"
		}
		return nil, merrors.InvalidArgument(message)
	}
	reference := strings.TrimSpace(resp.GetReceipt().GetOperationId())
	if reference == "" {
		reference = req.Reference
	}
	return &OperationResult{Reference: reference}, nil
}

func (c *connectorClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
	if ctx == nil {
		ctx = context.Background()
	}
	return context.WithTimeout(ctx, c.cfg.Timeout) //nolint:gosec // cancel func is always invoked by call sites
}

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

func normalizeEndpoint(raw string) (string, bool) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return "", false
	}
	parsed, err := url.Parse(raw)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return raw, false
	}
	switch strings.ToLower(strings.TrimSpace(parsed.Scheme)) {
	case "http", "grpc":
		return parsed.Host, true
	case "https", "grpcs":
		return parsed.Host, false
	default:
		return raw, false
	}
}

func firstDetailValue(values map[string]any, keys ...string) string {
	if len(values) == 0 || len(keys) == 0 {
		return ""
	}
	for _, key := range keys {
		key = strings.TrimSpace(key)
		if key == "" {
			continue
		}
		if value, ok := values[key]; ok {
			if text := strings.TrimSpace(fmt.Sprint(value)); text != "" {
				return text
			}
		}
	}
	return ""
}
