package ledgerapiimp

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	ledgerclient "github.com/tech/sendico/ledger/client"
	api "github.com/tech/sendico/pkg/api/http"
	"github.com/tech/sendico/pkg/auth"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	eapi "github.com/tech/sendico/server/interface/api"
	mutil "github.com/tech/sendico/server/internal/mutil/param"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type ledgerClient interface {
	CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
	ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
	GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
	Close() error
}

type LedgerAPI struct {
	logger        mlogger.Logger
	client        ledgerClient
	enf           auth.Enforcer
	oph           mutil.ParamHelper
	aph           mutil.ParamHelper
	permissionRef bson.ObjectID
	balancePerm   bson.ObjectID
}

func (a *LedgerAPI) Name() mservice.Type { return mservice.LedgerAccounts }

func (a *LedgerAPI) Finish(ctx context.Context) error {
	if a.client != nil {
		if err := a.client.Close(); err != nil {
			a.logger.Warn("Failed to close ledger client", zap.Error(err))
		}
	}
	return nil
}

func CreateAPI(apiCtx eapi.API) (*LedgerAPI, error) {
	p := &LedgerAPI{
		logger: apiCtx.Logger().Named(mservice.LedgerAccounts),
		enf:    apiCtx.Permissions().Enforcer(),
		oph:    mutil.CreatePH(mservice.Organizations),
		aph:    mutil.CreatePH("ledger_account"),
	}

	desc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.LedgerAccounts)
	if err != nil {
		p.logger.Warn("Failed to fetch ledger accounts permission description", zap.Error(err))
		return nil, err
	}
	p.permissionRef = desc.ID

	bdesc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.LedgerBalances)
	if err != nil {
		p.logger.Warn("Failed to fetch ledger balances permission description", zap.Error(err))
		return nil, err
	}
	p.balancePerm = bdesc.ID

	if err := p.initLedgerClient(apiCtx.Config().Ledger); err != nil {
		p.logger.Error("Failed to initialize ledger client", zap.Error(err))
		return nil, err
	}

	apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listAccounts)
	apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Post, p.createAccount)
	apiCtx.Register().AccountHandler(p.Name(), p.aph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getBalance)

	return p, nil
}

func (a *LedgerAPI) initLedgerClient(cfg *eapi.LedgerConfig) error {
	if cfg == nil {
		return merrors.InvalidArgument("ledger configuration is not provided")
	}

	address := strings.TrimSpace(cfg.Address)
	if address == "" {
		address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
	}
	if address == "" {
		return merrors.InvalidArgument(fmt.Sprintf("ledger address is not specified and address env %s is empty", cfg.AddressEnv))
	}

	clientCfg := ledgerclient.Config{
		Address:     address,
		DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
		CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
		Insecure:    cfg.Insecure,
	}

	client, err := ledgerclient.New(context.Background(), clientCfg)
	if err != nil {
		return err
	}

	a.client = client
	return nil
}
