package app

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

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/grpchealth"
	"hyapp/pkg/healthhttp"
	"hyapp/pkg/logx"
	"hyapp/pkg/rocketmqx"
	serviceapp "hyapp/pkg/servicekit/app"
	servicegrpc "hyapp/pkg/servicekit/grpcserver"
	servicehealth "hyapp/pkg/servicekit/health"
	servicemq "hyapp/pkg/servicekit/mq"
	"hyapp/services/lucky-gift-service/internal/config"
	"hyapp/services/lucky-gift-service/internal/integration"
	luckygiftservice "hyapp/services/lucky-gift-service/internal/service/luckygift"
	mysqlstorage "hyapp/services/lucky-gift-service/internal/storage/mysql"
	grpcserver "hyapp/services/lucky-gift-service/internal/transport/grpc"
)

// App 装配 lucky-gift-service 的 owner 进程。
// 该进程是幸运礼物规则、抽奖、外部 App 接入和 outbox 的唯一写入者；其他服务只持有 gRPC client。
type App struct {
	cfg           config.Config
	server        *grpc.Server
	listener      net.Listener
	health        *grpchealth.ServingChecker
	healthHTTP    *healthhttp.Server
	repository    *mysqlstorage.Repository
	walletConn    *grpc.ClientConn
	eventProducer *rocketmqx.Producer
	service       *luckygiftservice.Service
	workers       *serviceapp.BackgroundGroup
	closeOnce     sync.Once
}

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 cfg.MySQLAutoMigrate {
		if err := repository.Migrate(startupCtx); err != nil {
			_ = repository.Close()
			return nil, err
		}
	}

	walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		_ = repository.Close()
		return nil, err
	}
	var eventProducer *rocketmqx.Producer
	var eventPublisher luckygiftservice.EventPublisher
	if cfg.RocketMQ.LuckyGiftOutbox.Enabled {
		eventProducer, err = rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ))
		if err != nil {
			_ = walletConn.Close()
			_ = repository.Close()
			return nil, err
		}
		eventPublisher = integration.NewRocketMQEventPublisher(eventProducer, cfg.RocketMQ.LuckyGiftOutbox.Topic)
	}
	listener, err := net.Listen("tcp", cfg.GRPCAddr)
	if err != nil {
		servicemq.ShutdownProducers([]*rocketmqx.Producer{eventProducer})
		_ = walletConn.Close()
		_ = repository.Close()
		return nil, err
	}

	server := servicegrpc.New("lucky-gift-service")
	svc := luckygiftservice.New(repository,
		luckygiftservice.WithWallet(walletv1.NewWalletServiceClient(walletConn)),
		luckygiftservice.WithEventPublisher(eventPublisher),
	)
	luckygiftv1.RegisterLuckyGiftServiceServer(server, grpcserver.NewLuckyGiftServer(svc))
	luckygiftv1.RegisterAdminLuckyGiftServiceServer(server, grpcserver.NewAdminLuckyGiftServer(svc))
	health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{
		ServiceName: "lucky-gift-service",
		HTTPAddr:    cfg.HealthHTTPAddr,
		NodeID:      cfg.NodeID,
		Dependencies: []grpchealth.Dependency{{
			Name:  "mysql",
			Check: repository.Ping,
		}},
	})
	if err != nil {
		_ = listener.Close()
		servicemq.ShutdownProducers([]*rocketmqx.Producer{eventProducer})
		_ = walletConn.Close()
		_ = repository.Close()
		return nil, err
	}

	return &App{
		cfg:           cfg,
		server:        server,
		listener:      listener,
		health:        health,
		healthHTTP:    healthHTTP,
		repository:    repository,
		walletConn:    walletConn,
		eventProducer: eventProducer,
		service:       svc,
		workers:       serviceapp.NewBackground(context.Background()),
	}, nil
}

func (a *App) Run() error {
	if a.healthHTTP != nil {
		go func() {
			if err := a.healthHTTP.Run(); err != nil {
				logx.Error(context.Background(), "lucky_gift_health_http_run_failed", err)
			}
		}()
	}
	if err := servicemq.StartProducers([]*rocketmqx.Producer{a.eventProducer}); err != nil {
		a.health.MarkStopped()
		return err
	}
	a.health.MarkServing()
	if a.cfg.LuckyGiftWorker.Enabled && a.service != nil {
		options := luckyGiftWorkerOptions(a.cfg.NodeID, a.cfg.LuckyGiftWorker)
		a.workers.Go(func(ctx context.Context) {
			a.service.RunWorker(ctx, options)
		})
	}
	if a.cfg.LuckyGiftWorker.Retention.Enabled && a.service != nil {
		retention := a.cfg.LuckyGiftWorker.Retention
		a.workers.Go(func(ctx context.Context) {
			a.service.RunOutboxRetentionWorker(ctx, luckygiftservice.OutboxRetentionOptions{
				ScanInterval: retention.ScanInterval,
				MaxAge:       retention.MaxAge,
				BatchSize:    retention.BatchSize,
			})
		})
	}
	err := a.server.Serve(a.listener)
	if a.workers != nil {
		a.workers.StopAndWait()
	}
	a.health.MarkStopped()
	servicemq.ShutdownProducers([]*rocketmqx.Producer{a.eventProducer})
	if errors.Is(err, grpc.ErrServerStopped) {
		return nil
	}
	return err
}

func (a *App) Close() {
	a.closeOnce.Do(func() {
		if a.health != nil {
			a.health.MarkDraining()
		}
		if a.workers != nil {
			a.workers.StopAndWait()
		}
		servicegrpc.GracefulStop(a.server, 15*time.Second)
		if a.healthHTTP != nil {
			ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
			defer cancel()
			_ = a.healthHTTP.Close(ctx)
		}
		if a.walletConn != nil {
			_ = a.walletConn.Close()
		}
		servicemq.ShutdownProducers([]*rocketmqx.Producer{a.eventProducer})
		if a.repository != nil {
			_ = a.repository.Close()
		}
	})
}

func rocketMQProducerConfig(cfg config.RocketMQConfig) rocketmqx.ProducerConfig {
	return rocketmqx.ProducerConfig{
		EndpointConfig: rocketmqx.EndpointConfig{
			NameServers:      cfg.NameServers,
			NameServerDomain: cfg.NameServerDomain,
			AccessKey:        cfg.AccessKey,
			SecretKey:        cfg.SecretKey,
			SecurityToken:    cfg.SecurityToken,
			Namespace:        cfg.Namespace,
			VIPChannel:       cfg.VIPChannel,
		},
		GroupName:   cfg.LuckyGiftOutbox.ProducerGroup,
		SendTimeout: cfg.LuckyGiftOutbox.SendTimeout,
		Retry:       cfg.LuckyGiftOutbox.Retry,
	}
}

func luckyGiftWorkerOptions(nodeID string, cfg config.LuckyGiftWorkerConfig) luckygiftservice.WorkerOptions {
	return luckygiftservice.WorkerOptions{
		WorkerID:       nodeID,
		PollInterval:   cfg.WorkerPollInterval,
		BatchSize:      cfg.WorkerBatchSize,
		Concurrency:    cfg.WorkerConcurrency,
		LockTTL:        cfg.WorkerLockTTL,
		MaxRetry:       cfg.WorkerMaxRetry,
		PublishTimeout: cfg.PublishTimeout,
		StatsInterval:  cfg.StatsRefreshInterval,
		StatsBatchSize: cfg.StatsBatchSize,
	}
}
