package app

import (
	"context"
	"errors"
	"net"
	"sync"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/grpchealth"
	"hyapp/pkg/healthhttp"
	"hyapp/pkg/logx"
	"hyapp/pkg/outboxpartition"
	"hyapp/pkg/rocketmqx"
	servicegrpc "hyapp/pkg/servicekit/grpcserver"
	servicehealth "hyapp/pkg/servicekit/health"
	"hyapp/services/wallet-service/internal/client"
	"hyapp/services/wallet-service/internal/client/binance"
	"hyapp/services/wallet-service/internal/client/bscusdt"
	"hyapp/services/wallet-service/internal/client/googleplay"
	"hyapp/services/wallet-service/internal/client/mifapay"
	"hyapp/services/wallet-service/internal/client/trongrid"
	"hyapp/services/wallet-service/internal/client/v5pay"
	"hyapp/services/wallet-service/internal/config"
	walletservice "hyapp/services/wallet-service/internal/service/wallet"
	mysqlstorage "hyapp/services/wallet-service/internal/storage/mysql"
	grpcserver "hyapp/services/wallet-service/internal/transport/grpc"
)

// App 装配 wallet-service gRPC 入口。
type App struct {
	server                 *grpc.Server
	listener               net.Listener
	health                 *grpchealth.ServingChecker
	healthHTTP             *healthhttp.Server
	mysqlRepo              *mysqlstorage.Repository
	walletSvc              *walletservice.Service
	activityConn           *grpc.ClientConn
	outboxProducer         *rocketmqx.Producer
	realtimeOutboxProducer *rocketmqx.Producer
	projectionConsumer     *rocketmqx.Consumer
	outboxWorkerCfg        config.OutboxWorkerConfig
	outboxArchiveCfg       config.OutboxArchiveConfig
	outboxArchiveStore     *walletOutboxArchiveCOSStore
	// dry-run 游标只存内存，不会被误认为已上传回执；单归档 goroutine 顺序访问无需额外锁。
	outboxArchiveDryRunCursors map[string]mysqlstorage.WalletOutboxArchiveCursor
	realtimeOutboxWorkerCfg    config.OutboxWorkerConfig
	// 普通与实时 lane 共享同一张表但可独立配置事件类型；各自维护 App 轮转游标，避免一个 lane 的吞吐改变另一个 lane 的公平性。
	outboxPartitions                   *outboxpartition.Partitions
	realtimeOutboxPartitions           *outboxpartition.Partitions
	projectionWorkerCfg                config.ProjectionWorkerConfig
	googlePaidSyncWorkerCfg            config.GooglePaidSyncWorkerConfig
	externalRechargeReconcileWorkerCfg config.ExternalRechargeReconcileWorkerConfig
	resourceEquipmentCleanupWorkerCfg  config.ResourceEquipmentCleanupWorkerConfig
	walletOutboxTopic                  string
	realtimeWalletOutboxTopic          string
	walletEventTagMode                 config.WalletEventTagMode
	nodeID                             string
	redPacketExpiryWorkerCfg           config.RedPacketExpiryWorkerConfig
	stopWorker                         context.CancelFunc
	workers                            sync.WaitGroup
	closeOnce                          sync.Once
}

// New 初始化 wallet-service。
func New(cfg config.Config) (*App, error) {
	startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN)
	if err != nil {
		return nil, err
	}
	if err := requireWalletOutboxPurgeAuthorizations(startupCtx, repository, cfg.OutboxArchive); err != nil {
		_ = repository.Close()
		return nil, err
	}
	var outboxArchiveStore *walletOutboxArchiveCOSStore
	if cfg.OutboxArchive.Enabled && cfg.OutboxArchive.Mode == config.OutboxArchiveModeArchiveOnly {
		outboxArchiveStore, err = newWalletOutboxArchiveCOSStore(cfg.OutboxArchive.COS, nil)
		if err != nil {
			_ = repository.Close()
			return nil, err
		}
	}

	listener, err := net.Listen("tcp", cfg.GRPCAddr)
	if err != nil {
		_ = repository.Close()
		return nil, err
	}
	activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}
	var outboxProducer *rocketmqx.Producer
	if cfg.RocketMQ.WalletOutbox.Enabled {
		outboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.ProducerGroup))
		if err != nil {
			_ = activityConn.Close()
			_ = listener.Close()
			_ = repository.Close()
			return nil, err
		}
	}
	var realtimeOutboxProducer *rocketmqx.Producer
	if cfg.RocketMQ.RealtimeOutbox.Enabled {
		realtimeOutboxProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.RealtimeOutbox.ProducerGroup))
		if err != nil {
			shutdownProducers(outboxProducer)
			_ = activityConn.Close()
			_ = listener.Close()
			_ = repository.Close()
			return nil, err
		}
	}

	server := servicegrpc.New("wallet-service")
	svc := walletservice.New(repository, client.NewActivityAchievementClient(activityConn))
	svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
		USDTTRC20Enabled:   cfg.ExternalRecharge.USDTTRC20Enabled,
		USDTTRC20Address:   cfg.ExternalRecharge.USDTTRC20Address,
		USDTTRC20Addresses: cfg.ExternalRecharge.USDTTRC20Addresses,
		USDTBEP20Enabled:   cfg.ExternalRecharge.USDTBEP20Enabled,
		USDTBEP20Address:   cfg.ExternalRecharge.USDTBEP20Address,
		USDTBEP20Addresses: cfg.ExternalRecharge.USDTBEP20Addresses,
		MifaPayNotifyURL:   cfg.ExternalRecharge.MifaPayNotifyURL,
		MifaPayReturnURL:   cfg.ExternalRecharge.MifaPayReturnURL,
		V5PayNotifyURL:     cfg.ExternalRecharge.V5PayNotifyURL,
		V5PayReturnURL:     cfg.ExternalRecharge.V5PayReturnURL,
	})
	if cfg.GooglePlay.Enabled {
		googleClient, err := googleplay.New(cfg.GooglePlay)
		if err != nil {
			shutdownProducers(outboxProducer, realtimeOutboxProducer)
			_ = activityConn.Close()
			_ = listener.Close()
			_ = repository.Close()
			return nil, err
		}
		svc.SetGooglePlayClient(googleClient)
	}
	if cfg.MifaPay.Enabled {
		if mifapayConfigReady(cfg.MifaPay) {
			mifaPayClient, err := mifapay.New(cfg.MifaPay)
			if err != nil {
				shutdownProducers(outboxProducer, realtimeOutboxProducer)
				_ = activityConn.Close()
				_ = listener.Close()
				_ = repository.Close()
				return nil, err
			}
			svc.SetMifaPayClient(mifaPayClient)
		} else {

			logx.Warn(context.Background(), "mifapay_enabled_without_credentials")
		}
	}
	if cfg.V5Pay.Enabled {
		if v5payConfigReady(cfg.V5Pay) {
			svc.SetV5PayClient(v5pay.New(cfg.V5Pay))
		} else {

			logx.Warn(context.Background(), "v5pay_enabled_without_credentials")
		}
	}
	if cfg.TronGrid.Enabled {
		svc.SetTronUSDTClient(trongrid.New(cfg.TronGrid))
	}
	if cfg.BSCUSDT.Enabled {
		svc.SetBSCUSDTClient(bscusdt.New(cfg.BSCUSDT))
	}
	if cfg.Binance.Enabled {
		if cfg.Binance.HasAnyAccount() {
			svc.SetBinanceClient(binance.New(cfg.Binance))
		} else {
			logx.Warn(context.Background(), "binance_enabled_without_credentials")
		}
	}
	walletv1.RegisterWalletServiceServer(server, grpcserver.NewServer(svc))
	walletv1.RegisterWalletCronServiceServer(server, grpcserver.NewCronServer(svc))
	health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{
		ServiceName: "wallet-service",
		HTTPAddr:    cfg.HealthHTTPAddr,
		NodeID:      cfg.NodeID,
		Dependencies: []grpchealth.Dependency{{
			Name:  "mysql",
			Check: repository.Ping,
		}},
	})
	if err != nil {
		shutdownProducers(outboxProducer, realtimeOutboxProducer)
		_ = activityConn.Close()
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}
	projectionConsumer, err := newWalletProjectionConsumer(cfg, svc)
	if err != nil {
		shutdownProducers(outboxProducer, realtimeOutboxProducer)
		_ = activityConn.Close()
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}
	googlePaidSyncWorkerCfg := cfg.GooglePaidSyncWorker
	// A disabled Google client cannot make progress; suppress the worker instead of emitting a permanent error loop in local environments.
	googlePaidSyncWorkerCfg.Enabled = googlePaidSyncWorkerCfg.Enabled && cfg.GooglePlay.Enabled

	return &App{
		server:                             server,
		listener:                           listener,
		health:                             health,
		healthHTTP:                         healthHTTP,
		mysqlRepo:                          repository,
		walletSvc:                          svc,
		activityConn:                       activityConn,
		outboxProducer:                     outboxProducer,
		realtimeOutboxProducer:             realtimeOutboxProducer,
		projectionConsumer:                 projectionConsumer,
		outboxWorkerCfg:                    cfg.OutboxWorker,
		outboxArchiveCfg:                   cfg.OutboxArchive,
		outboxArchiveStore:                 outboxArchiveStore,
		outboxArchiveDryRunCursors:         make(map[string]mysqlstorage.WalletOutboxArchiveCursor),
		realtimeOutboxWorkerCfg:            cfg.RealtimeOutboxWorker,
		outboxPartitions:                   outboxpartition.New(cfg.OutboxWorker.AppCodes, outboxpartition.DefaultDiscoveryRefreshInterval),
		realtimeOutboxPartitions:           outboxpartition.New(cfg.RealtimeOutboxWorker.AppCodes, outboxpartition.DefaultDiscoveryRefreshInterval),
		projectionWorkerCfg:                cfg.ProjectionWorker,
		googlePaidSyncWorkerCfg:            googlePaidSyncWorkerCfg,
		externalRechargeReconcileWorkerCfg: cfg.ExternalRechargeReconcileWorker,
		resourceEquipmentCleanupWorkerCfg:  cfg.ResourceEquipmentCleanupWorker,
		walletOutboxTopic:                  cfg.RocketMQ.WalletOutbox.Topic,
		realtimeWalletOutboxTopic:          cfg.RocketMQ.RealtimeOutbox.Topic,
		walletEventTagMode:                 cfg.RocketMQ.WalletEventTagMode,
		nodeID:                             cfg.NodeID,
		redPacketExpiryWorkerCfg:           cfg.RedPacketExpiryWorker,
	}, nil
}

// Run 启动 gRPC 服务。
func (a *App) Run() error {
	a.runHealthHTTP()
	if err := a.startMQ(); err != nil {
		a.health.MarkStopped()
		return err
	}
	a.runBackgroundWorkers()
	a.health.MarkServing()
	err := a.server.Serve(a.listener)
	a.health.MarkStopped()
	a.closeBackgroundWorkers()
	a.shutdownMQ()
	if errors.Is(err, grpc.ErrServerStopped) {
		return nil
	}

	return err
}

// Close 关闭 gRPC 服务。
func (a *App) Close() {
	a.closeOnce.Do(func() {
		a.health.MarkDraining()
		a.closeBackgroundWorkers()
		servicegrpc.GracefulStop(a.server, 15*time.Second)
		a.closeHealthHTTP()
		if a.activityConn != nil {
			_ = a.activityConn.Close()
		}
		a.shutdownMQ()
		if a.mysqlRepo != nil {

			_ = a.mysqlRepo.Close()
		}
	})
}
