package accountapiimp

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

	trongatewayclient "github.com/tech/sendico/gateway/tron/client"
	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/db/account"
	"github.com/tech/sendico/pkg/db/organization"
	"github.com/tech/sendico/pkg/db/policy"
	"github.com/tech/sendico/pkg/db/refreshtokens"
	"github.com/tech/sendico/pkg/db/transaction"
	"github.com/tech/sendico/pkg/db/verification"
	"github.com/tech/sendico/pkg/domainprovider"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/messaging"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"github.com/tech/sendico/server/interface/accountservice"
	eapi "github.com/tech/sendico/server/interface/api"
	"github.com/tech/sendico/server/interface/services/fileservice"
	mutil "github.com/tech/sendico/server/internal/mutil/param"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type AccountAPI struct {
	logger                mlogger.Logger
	db                    account.DB
	odb                   organization.DB
	tf                    transaction.Factory
	rtdb                  refreshtokens.DB
	plcdb                 policy.DB
	vdb                   verification.DB
	domain                domainprovider.DomainProvider
	avatars               mservice.MicroService
	producer              messaging.Producer
	pmanager              auth.Manager
	enf                   auth.Enforcer
	oph                   mutil.ParamHelper
	aph                   mutil.ParamHelper
	tph                   mutil.ParamHelper
	accountsPermissionRef bson.ObjectID
	accService            accountservice.AccountService
	chainGateway          chainWalletClient
	ledgerClient          ledgerAccountClient
	chainAsset            *chainv1.Asset
}

type chainWalletClient interface {
	CreateManagedWallet(ctx context.Context, req *chainv1.CreateManagedWalletRequest) (*chainv1.CreateManagedWalletResponse, error)
	Close() error
}

type ledgerAccountClient interface {
	CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
	Close() error
}

func (a *AccountAPI) Name() mservice.Type {
	return mservice.Accounts
}

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

func CreateAPI(a eapi.API) (*AccountAPI, error) {
	p := new(AccountAPI)
	p.logger = a.Logger().Named(p.Name())
	var err error
	if p.db, err = a.DBFactory().NewAccountDB(); err != nil {
		p.logger.Error("Failed to create accounts database", zap.Error(err))
		return nil, err
	}
	if p.rtdb, err = a.DBFactory().NewRefreshTokensDB(); err != nil {
		p.logger.Error("Failed to create refresh tokens database", zap.Error(err))
		return nil, err
	}
	if p.odb, err = a.DBFactory().NewOrganizationDB(); err != nil {
		p.logger.Error("Failed to create organizations database", zap.Error(err))
		return nil, err
	}
	if p.plcdb, err = a.DBFactory().NewPoliciesDB(); err != nil {
		p.logger.Error("Failed to create policies database", zap.Error(err))
		return nil, err
	}
	if p.vdb, err = a.DBFactory().NewVerificationsDB(); err != nil {
		p.logger.Error("Failed to create verification database", zap.Error(err))
		return nil, err
	}

	p.domain = a.DomainProvider()
	p.producer = a.Register().Messaging().Producer()
	p.tf = a.DBFactory().TransactionFactory()
	p.pmanager = a.Permissions().Manager()
	p.enf = a.Permissions().Enforcer()
	p.oph = mutil.CreatePH(mservice.Organizations)
	p.aph = mutil.CreatePH(mservice.Accounts)
	p.tph = mutil.CreatePH("token")

	if p.accService, err = accountservice.NewAccountService(p.logger, a.DBFactory(), p.enf, p.pmanager.Role(), &a.Config().Mw.Password); err != nil {
		p.logger.Error("Failed to create account manager", zap.Error(err))
		return nil, err
	}

	// Account related api endpoints
	a.Register().Handler(mservice.Accounts, "/signup", api.Post, p.signup)
	a.Register().Handler(mservice.Accounts, "/signup/availability", api.Get, p.signupAvailability)

	a.Register().AccountHandler(mservice.Accounts, "", api.Put, p.updateProfile)
	a.Register().AccountHandler(mservice.Accounts, "", api.Get, p.getProfile)

	a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/employee"), api.Put, p.updateEmployee)

	a.Register().AccountHandler(mservice.Accounts, "/dzone", api.Get, p.dzone)
	a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/profile"), api.Delete, p.deleteProfile)
	a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/organization"), api.Delete, p.deleteOrganization)
	a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/all"), api.Delete, p.deleteAll)

	a.Register().AccountHandler(mservice.Accounts, p.oph.AddRef("/list"), api.Get, p.getEmployees)

	a.Register().AccountHandler(mservice.Accounts, "/password", api.Post, p.checkPassword)
	a.Register().AccountHandler(mservice.Accounts, "/password", api.Patch, p.changePassword)
	a.Register().Handler(mservice.Accounts, "/password", api.Put, p.forgotPassword)
	a.Register().Handler(mservice.Accounts, p.tph.AddRef(p.aph.AddRef("/password/reset")), api.Post, p.resetPassword)

	a.Register().Handler(mservice.Accounts, mutil.AddToken("/verify"), api.Get, p.verify)
	a.Register().Handler(mservice.Accounts, "/email", api.Post, p.resendVerificationMail)
	a.Register().Handler(mservice.Accounts, "/email", api.Put, p.resendVerification)

	if p.avatars, err = fileservice.CreateAPI(a, p.Name()); err != nil {
		p.logger.Error("Failed to create image server", zap.Error(err))
		return nil, err
	}

	accountsPolicy, err := a.Permissions().GetPolicyDescription(context.Background(), mservice.Accounts)
	if err != nil {
		p.logger.Warn("Failed to fetch account permission policy description", zap.Error(err))
		return nil, err
	}
	p.accountsPermissionRef = accountsPolicy.ID

	cfg := a.Config()
	if cfg == nil {
		p.logger.Error("Failed to fetch service configuration")
		return nil, merrors.InvalidArgument("No configuration provided")
	}
	if err := p.initChainGateway(cfg.ChainGateway); err != nil {
		p.logger.Error("Failed to initialize chain gateway client", zap.Error(err))
		return nil, err
	}
	if err := p.initLedgerClient(cfg.Ledger); err != nil {
		p.logger.Error("Failed to initialize ledger client", zap.Error(err))
		return nil, err
	}

	return p, nil
}

func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error {
	if cfg == nil {
		return merrors.InvalidArgument("chain gateway configuration is not provided")
	}

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

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

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

	asset, err := buildGatewayAsset(cfg.DefaultAsset)
	if err != nil {
		_ = client.Close()
		return err
	}

	a.chainGateway = client
	a.chainAsset = asset
	return nil
}

func (a *AccountAPI) 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.ledgerClient = client
	return nil
}

func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*chainv1.Asset, error) {
	chain, err := parseChainNetwork(cfg.Chain)
	if err != nil {
		return nil, err
	}
	tokenSymbol := strings.TrimSpace(cfg.TokenSymbol)
	if tokenSymbol == "" {
		return nil, merrors.InvalidArgument("chain gateway token symbol is required")
	}
	return &chainv1.Asset{
		Chain:           chain,
		TokenSymbol:     strings.ToUpper(tokenSymbol),
		ContractAddress: strings.TrimSpace(cfg.ContractAddress),
	}, nil
}

func parseChainNetwork(value string) (chainv1.ChainNetwork, error) {
	switch strings.ToUpper(strings.TrimSpace(value)) {
	case "ETHEREUM_MAINNET", "CHAIN_NETWORK_ETHEREUM_MAINNET":
		return chainv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, nil
	case "ARBITRUM_ONE", "CHAIN_NETWORK_ARBITRUM_ONE":
		return chainv1.ChainNetwork_CHAIN_NETWORK_ARBITRUM_ONE, nil
	case "TRON_MAINNET", "CHAIN_NETWORK_TRON_MAINNET":
		return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_MAINNET, nil
	case "TRON_NILE", "CHAIN_NETWORK_TRON_NILE":
		return chainv1.ChainNetwork_CHAIN_NETWORK_TRON_NILE, nil
	case "", "CHAIN_NETWORK_UNSPECIFIED":
		return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument("chain network must be specified")
	default:
		return chainv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported chain network %s", value))
	}
}
