// Package app wires cron-service configuration, scheduler, storage and gRPC health.
package app

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

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	activityv1 "hyapp.local/api/proto/activity/v1"
	gamev1 "hyapp.local/api/proto/game/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/grpchealth"
	"hyapp/pkg/healthhttp"
	"hyapp/pkg/logx"
	serviceapp "hyapp/pkg/servicekit/app"
	servicegrpc "hyapp/pkg/servicekit/grpcserver"
	servicehealth "hyapp/pkg/servicekit/health"
	"hyapp/services/cron-service/internal/config"
	"hyapp/services/cron-service/internal/integration"
	"hyapp/services/cron-service/internal/scheduler"
	mysqlstorage "hyapp/services/cron-service/internal/storage/mysql"
)

// App owns the cron-service process lifecycle.
type App struct {
	server       *grpc.Server
	listener     net.Listener
	health       *grpchealth.ServingChecker
	healthHTTP   *healthhttp.Server
	repository   *mysqlstorage.Repository
	scheduler    *scheduler.Scheduler
	userConn     *grpc.ClientConn
	activityConn *grpc.ClientConn
	gameConn     *grpc.ClientConn
	walletConn   *grpc.ClientConn
	workers      *serviceapp.BackgroundGroup
	closeOnce    sync.Once
}

// New initializes cron metadata storage, health service and scheduler.
func New(cfg config.Config) (*App, error) {
	startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// cron-service owns only scheduler metadata. Business data stays in owner service databases.
	repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN)
	if err != nil {
		return nil, err
	}

	listener, err := net.Listen("tcp", cfg.GRPCAddr)
	if err != nil {
		_ = repository.Close()
		return nil, err
	}
	userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}
	activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		_ = userConn.Close()
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}
	gameConn, err := grpc.Dial(cfg.GameServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		_ = activityConn.Close()
		_ = userConn.Close()
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}
	walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		_ = gameConn.Close()
		_ = activityConn.Close()
		_ = userConn.Close()
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}

	server := servicegrpc.New("cron-service")
	health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{
		ServiceName: "cron-service",
		HTTPAddr:    cfg.HealthHTTPAddr,
		NodeID:      cfg.NodeID,
		Dependencies: []grpchealth.Dependency{{
			Name:  "mysql",
			Check: repository.Ping,
		}},
	})
	if err != nil {
		_ = walletConn.Close()
		_ = gameConn.Close()
		_ = activityConn.Close()
		_ = userConn.Close()
		_ = listener.Close()
		_ = repository.Close()
		return nil, err
	}

	userCron := integration.NewUserCronClient(userv1.NewUserCronServiceClient(userConn))
	appRegistry := integration.NewAppRegistryClient(userv1.NewAppRegistryServiceClient(userConn))
	activityCron := integration.NewActivityCronClient(activityv1.NewActivityCronServiceClient(activityConn))
	gameCron := integration.NewGameCronClient(gamev1.NewGameCronServiceClient(gameConn))
	walletCron := integration.NewWalletCronClient(walletv1.NewWalletCronServiceClient(walletConn))
	taskScheduler := scheduler.New(cfg.NodeID, cfg.Tasks, repository, map[string]scheduler.Handler{
		"login_ip_risk":                  userCron.ProcessLoginIPRiskBatch,
		"user_region_rebuild":            userCron.ProcessRegionRebuildBatch,
		"mic_open_session_compensation":  userCron.CompensateMicOpenSessions,
		"room_open_session_compensation": userCron.CompensateRoomOpenSessions,
		"manager_user_block_expiry":      userCron.ExpireManagerUserBlocks,
		"admin_user_ban_expiry":          userCron.ExpireAdminUserBans,
		// CP 榜单任务只占 cron 租约并调用 user-service；真实 MySQL 读取、wallet 头像框补齐和 Redis 替换都在 user-service 内完成。
		"cp_intimacy_leaderboard":           userCron.RefreshCPIntimacyLeaderboard,
		"private_message_history":           userCron.ProcessPrivateMessageHistoryBatch,
		"message_fanout":                    activityCron.ProcessMessageFanoutBatch,
		"growth_level_reward":               activityCron.ProcessLevelRewardBatch,
		"temporary_growth_level":            activityCron.ProcessTemporaryLevelBatch,
		"achievement_reward":                activityCron.ProcessAchievementRewardBatch,
		"weekly_star_settlement":            activityCron.ProcessWeeklyStarSettlementBatch,
		"activity_template_settlement":      activityCron.ProcessActivityTemplateSettlementBatch,
		"agency_opening_settlement":         activityCron.ProcessAgencyOpeningSettlementBatch,
		"cp_weekly_rank_settlement":         activityCron.ProcessCPWeeklyRankSettlementBatch,
		"room_turnover_reward_settlement":   activityCron.ProcessRoomTurnoverRewardSettlementBatch,
		"game_level_event_relay":            gameCron.ProcessLevelEventOutboxBatch,
		"host_salary_daily_settlement":      walletCron.ProcessHostSalaryDailySettlementBatch,
		"vip_daily_coin_rebate":             walletCron.ProcessVIPDailyCoinRebateBatch,
		"host_salary_half_month_settlement": walletCron.ProcessHostSalaryHalfMonthSettlementBatch,
		"host_salary_month_end":             walletCron.ProcessHostSalaryMonthEndBatch,
	}, appRegistry)

	return &App{
		server:       server,
		listener:     listener,
		health:       health,
		healthHTTP:   healthHTTP,
		repository:   repository,
		scheduler:    taskScheduler,
		userConn:     userConn,
		activityConn: activityConn,
		gameConn:     gameConn,
		walletConn:   walletConn,
		workers:      serviceapp.NewBackground(context.Background()),
	}, nil
}

// Run starts scheduler loops and gRPC health serving.
func (a *App) Run() error {
	a.runHealthHTTP()
	a.health.MarkServing()
	defer func() {
		a.workers.StopAndWait()
	}()
	if a.scheduler != nil {
		a.workers.Go(func(ctx context.Context) {
			a.scheduler.Run(ctx)
		})
	}

	err := a.server.Serve(a.listener)
	a.health.MarkStopped()
	if errors.Is(err, grpc.ErrServerStopped) {
		return nil
	}
	return err
}

// Close drains scheduler loops before closing gRPC and MySQL resources.
func (a *App) Close() {
	a.closeOnce.Do(func() {
		a.health.MarkDraining()
		a.workers.StopAndWait()
		servicegrpc.GracefulStop(a.server, 15*time.Second)
		a.closeHealthHTTP()
		if a.repository != nil {
			_ = a.repository.Close()
		}
		if a.userConn != nil {
			_ = a.userConn.Close()
		}
		if a.activityConn != nil {
			_ = a.activityConn.Close()
		}
		if a.gameConn != nil {
			_ = a.gameConn.Close()
		}
		if a.walletConn != nil {
			_ = a.walletConn.Close()
		}
	})
}

func (a *App) runHealthHTTP() {
	if a.healthHTTP == nil {
		return
	}
	go func() {
		if err := a.healthHTTP.Run(); err != nil {
			logx.Error(context.Background(), "health_http_run_failed", err)
		}
	}()
}

func (a *App) closeHealthHTTP() {
	if a.healthHTTP == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	_ = a.healthHTTP.Close(ctx)
}
