package cprelation

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"slices"
	"strings"
	"time"

	"hyapp-admin-server/internal/integration/activityclient"
	activityv1 "hyapp.local/api/proto/activity/v1"
)

const (
	relationTypeCP      = "cp"
	relationTypeBrother = "brother"
	relationTypeSister  = "sister"
	statusActive        = "active"
	levelCount          = 5
	defaultExpireHours  = 24
)

var (
	ErrInvalidArgument = errors.New("invalid argument")
	relationTypes      = []string{relationTypeCP, relationTypeBrother, relationTypeSister}
	relationLabels     = map[string]string{
		relationTypeCP:      "CP",
		relationTypeBrother: "兄弟",
		relationTypeSister:  "姐妹",
	}
	defaultLevelThresholds = []int64{0, 10_000, 30_000, 60_000, 100_000}
)

type DB interface {
	BeginTx(context.Context, *sql.TxOptions) (*sql.Tx, error)
	ExecContext(context.Context, string, ...any) (sql.Result, error)
	QueryContext(context.Context, string, ...any) (*sql.Rows, error)
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

type Service struct {
	db       DB
	activity activityclient.Client
}

type configDTO struct {
	AppCode      string        `json:"appCode"`
	Relations    []relationDTO `json:"relations"`
	WeeklyRank   weeklyRankDTO `json:"weeklyRank"`
	ServerTimeMS int64         `json:"serverTimeMs"`
}

type relationDTO struct {
	RelationType           string     `json:"relationType"`
	DisplayName            string     `json:"displayName"`
	MaxCountPerUser        int32      `json:"maxCountPerUser"`
	ApplicationExpireHours int32      `json:"applicationExpireHours"`
	BreakupCostCoins       int64      `json:"breakupCostCoins"`
	Status                 string     `json:"status"`
	Levels                 []levelDTO `json:"levels"`
	UpdatedAtMS            int64      `json:"updatedAtMs"`
}

type levelDTO struct {
	Level                 int32  `json:"level"`
	IntimacyThreshold     int64  `json:"intimacyThreshold"`
	RewardResourceGroupID int64  `json:"rewardResourceGroupId"`
	LevelIconURL          string `json:"levelIconUrl"`
	Status                string `json:"status"`
	UpdatedAtMS           int64  `json:"updatedAtMs"`
}

type weeklyRankDTO struct {
	Enabled          bool                  `json:"enabled"`
	RelationType     string                `json:"relationType"`
	TopCount         int32                 `json:"topCount"`
	Rewards          []weeklyRankRewardDTO `json:"rewards"`
	UpdatedByAdminID int64                 `json:"updatedByAdminId"`
	CreatedAtMS      int64                 `json:"createdAtMs"`
	UpdatedAtMS      int64                 `json:"updatedAtMs"`
}

type weeklyRankRewardDTO struct {
	RankNo          int32 `json:"rankNo"`
	ResourceGroupID int64 `json:"resourceGroupId"`
}

type updateRequest struct {
	Relations  []relationDTO  `json:"relations"`
	WeeklyRank *weeklyRankDTO `json:"weeklyRank"`
}

func NewService(db DB, activity activityclient.Client) *Service {
	return &Service{db: db, activity: activity}
}

// GetConfig 读取当前 App 的关系容量和等级阈值；缺配置时先落默认值，保证后台首次打开就是可保存状态。
func (s *Service) GetConfig(ctx context.Context, appCode string) (configDTO, error) {
	nowMs := time.Now().UTC().UnixMilli()
	if err := s.ensureSchema(ctx); err != nil {
		return configDTO{}, err
	}
	if err := s.ensureDefaultConfig(ctx, appCode, nowMs); err != nil {
		return configDTO{}, err
	}
	relations, err := s.listRelations(ctx, appCode)
	if err != nil {
		return configDTO{}, err
	}
	weeklyRank, err := s.getWeeklyRankConfig(ctx, appCode)
	if err != nil {
		return configDTO{}, err
	}
	return configDTO{AppCode: appCode, Relations: relations, WeeklyRank: weeklyRank, ServerTimeMS: nowMs}, nil
}

// UpdateConfig 覆盖低频配置；写入时按 relation_type + level_no upsert，运行中消费逻辑每次读取最新阈值。
func (s *Service) UpdateConfig(ctx context.Context, appCode string, req updateRequest) (configDTO, error) {
	nowMs := time.Now().UTC().UnixMilli()
	if err := s.ensureSchema(ctx); err != nil {
		return configDTO{}, err
	}
	relations, err := normalizeRelations(req.Relations)
	if err != nil {
		return configDTO{}, err
	}
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return configDTO{}, err
	}
	defer func() { _ = tx.Rollback() }()

	for _, relation := range relations {
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO user_cp_relation_configs (
				app_code, relation_type, max_count_per_user, application_expire_hours, breakup_cost_coins,
				status, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
				max_count_per_user = VALUES(max_count_per_user),
				application_expire_hours = VALUES(application_expire_hours),
				breakup_cost_coins = VALUES(breakup_cost_coins),
				status = VALUES(status),
				updated_at_ms = VALUES(updated_at_ms)`,
			appCode, relation.RelationType, relation.MaxCountPerUser, relation.ApplicationExpireHours, relation.BreakupCostCoins,
			relation.Status, nowMs, nowMs,
		); err != nil {
			return configDTO{}, err
		}
		for _, level := range relation.Levels {
			if _, err := tx.ExecContext(ctx, `
				INSERT INTO user_cp_level_rules (
					app_code, relation_type, level_no, intimacy_threshold,
					reward_resource_group_id, level_icon_url, status, created_at_ms, updated_at_ms
				) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
				ON DUPLICATE KEY UPDATE
					intimacy_threshold = VALUES(intimacy_threshold),
					reward_resource_group_id = VALUES(reward_resource_group_id),
					level_icon_url = VALUES(level_icon_url),
					status = VALUES(status),
					updated_at_ms = VALUES(updated_at_ms)`,
				appCode, relation.RelationType, level.Level, level.IntimacyThreshold,
				level.RewardResourceGroupID, level.LevelIconURL, level.Status, nowMs, nowMs,
			); err != nil {
				return configDTO{}, err
			}
		}
	}
	if err := tx.Commit(); err != nil {
		return configDTO{}, err
	}
	if req.WeeklyRank != nil {
		// CP 关系配置归 user DB，周榜启停和发奖事实归 activity-service；这里在 user 配置提交后再写 activity，
		// 失败时返回错误让后台明确重试，避免误以为奖励配置已经生效。
		if err := s.updateWeeklyRankConfig(ctx, appCode, *req.WeeklyRank); err != nil {
			return configDTO{}, err
		}
	}
	return s.GetConfig(ctx, appCode)
}

func (s *Service) getWeeklyRankConfig(ctx context.Context, appCode string) (weeklyRankDTO, error) {
	if s.activity == nil {
		return weeklyRankDTO{Enabled: true, RelationType: relationTypeCP, TopCount: 3}, nil
	}
	resp, err := s.activity.GetCPWeeklyRankConfig(ctx, &activityv1.GetCPWeeklyRankConfigRequest{Meta: s.activityMeta(appCode)})
	if err != nil {
		return weeklyRankDTO{}, err
	}
	return weeklyRankFromProto(resp.GetConfig()), nil
}

func (s *Service) updateWeeklyRankConfig(ctx context.Context, appCode string, req weeklyRankDTO) error {
	if s.activity == nil {
		return errors.New("activity client is not configured")
	}
	_, err := s.activity.UpdateCPWeeklyRankConfig(ctx, &activityv1.UpdateCPWeeklyRankConfigRequest{
		Meta:            s.activityMeta(appCode),
		Config:          weeklyRankToProto(req),
		OperatorAdminId: req.UpdatedByAdminID,
	})
	return err
}

func (s *Service) activityMeta(appCode string) *activityv1.RequestMeta {
	return &activityv1.RequestMeta{
		RequestId: "admin-cp-config",
		Caller:    "admin-server",
		AppCode:   appCode,
		SentAtMs:  time.Now().UTC().UnixMilli(),
	}
}

func weeklyRankFromProto(item *activityv1.CPWeeklyRankConfig) weeklyRankDTO {
	if item == nil {
		return weeklyRankDTO{Enabled: true, RelationType: relationTypeCP, TopCount: 3}
	}
	rewards := make([]weeklyRankRewardDTO, 0, len(item.GetRewards()))
	for _, reward := range item.GetRewards() {
		rewards = append(rewards, weeklyRankRewardDTO{RankNo: reward.GetRankNo(), ResourceGroupID: reward.GetResourceGroupId()})
	}
	return weeklyRankDTO{
		Enabled:          item.GetEnabled(),
		RelationType:     item.GetRelationType(),
		TopCount:         item.GetTopCount(),
		Rewards:          rewards,
		UpdatedByAdminID: item.GetUpdatedByAdminId(),
		CreatedAtMS:      item.GetCreatedAtMs(),
		UpdatedAtMS:      item.GetUpdatedAtMs(),
	}
}

func weeklyRankToProto(item weeklyRankDTO) *activityv1.CPWeeklyRankConfig {
	rewards := make([]*activityv1.CPWeeklyRankReward, 0, len(item.Rewards))
	for _, reward := range item.Rewards {
		rewards = append(rewards, &activityv1.CPWeeklyRankReward{RankNo: reward.RankNo, ResourceGroupId: reward.ResourceGroupID})
	}
	return &activityv1.CPWeeklyRankConfig{
		Enabled:      item.Enabled,
		RelationType: relationTypeCP,
		TopCount:     item.TopCount,
		Rewards:      rewards,
	}
}

func (s *Service) listRelations(ctx context.Context, appCode string) ([]relationDTO, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT relation_type, max_count_per_user, application_expire_hours, breakup_cost_coins, status, updated_at_ms
		FROM user_cp_relation_configs
		WHERE app_code = ?`, appCode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	relationsByType := map[string]relationDTO{}
	for rows.Next() {
		var item relationDTO
		if err := rows.Scan(&item.RelationType, &item.MaxCountPerUser, &item.ApplicationExpireHours, &item.BreakupCostCoins, &item.Status, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		item.DisplayName = relationLabels[item.RelationType]
		relationsByType[item.RelationType] = item
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	levelsByType, err := s.levelsByRelationType(ctx, appCode)
	if err != nil {
		return nil, err
	}
	for relationType, levels := range levelsByType {
		item := relationsByType[relationType]
		item.Levels = levels
		relationsByType[relationType] = item
	}
	relations := make([]relationDTO, 0, len(relationTypes))
	for _, relationType := range relationTypes {
		item := relationsByType[relationType]
		item.RelationType = relationType
		item.DisplayName = relationLabels[relationType]
		item.Levels = normalizeLevelDefaults(item.Levels)
		relations = append(relations, item)
	}
	return relations, nil
}

// levelsByRelationType 必须把读取失败直接抛回 handler；后台配置页不能用默认等级掩盖坏 SQL 或坏数据。
func (s *Service) levelsByRelationType(ctx context.Context, appCode string) (map[string][]levelDTO, error) {
	rows, err := s.db.QueryContext(ctx, `
		SELECT relation_type, level_no, intimacy_threshold, reward_resource_group_id, level_icon_url, status, updated_at_ms
		FROM user_cp_level_rules
		WHERE app_code = ?
		ORDER BY relation_type ASC, level_no ASC`, appCode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	result := map[string][]levelDTO{}
	for rows.Next() {
		var relationType string
		var item levelDTO
		if err := rows.Scan(&relationType, &item.Level, &item.IntimacyThreshold, &item.RewardResourceGroupID, &item.LevelIconURL, &item.Status, &item.UpdatedAtMS); err != nil {
			return nil, err
		}
		result[relationType] = append(result[relationType], item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return result, nil
}

func (s *Service) ensureDefaultConfig(ctx context.Context, appCode string, nowMs int64) error {
	for _, relationType := range relationTypes {
		maxCount := int32(1)
		if relationType == relationTypeBrother || relationType == relationTypeSister {
			maxCount = 5
		}
		if _, err := s.db.ExecContext(ctx, `
			INSERT INTO user_cp_relation_configs (
				app_code, relation_type, max_count_per_user, application_expire_hours, breakup_cost_coins,
				status, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, 0, 'active', ?, ?)
			ON DUPLICATE KEY UPDATE relation_type = VALUES(relation_type)`,
			appCode, relationType, maxCount, defaultExpireHours, nowMs, nowMs,
		); err != nil {
			return err
		}
		for level := int32(1); level <= levelCount; level++ {
			if _, err := s.db.ExecContext(ctx, `
				INSERT INTO user_cp_level_rules (
					app_code, relation_type, level_no, intimacy_threshold,
					reward_resource_group_id, level_icon_url, status, created_at_ms, updated_at_ms
				) VALUES (?, ?, ?, ?, 0, '', 'active', ?, ?)
				ON DUPLICATE KEY UPDATE level_no = VALUES(level_no)`,
				appCode, relationType, level, defaultLevelThresholds[level-1], nowMs, nowMs,
			); err != nil {
				return err
			}
		}
	}
	return nil
}

func (s *Service) ensureSchema(ctx context.Context) error {
	if s == nil || s.db == nil {
		return errors.New("user db is not configured")
	}
	if _, err := s.db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS user_cp_relation_configs (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
		  relation_type VARCHAR(32) NOT NULL,
		  max_count_per_user INT NOT NULL,
		  application_expire_hours INT NOT NULL,
		  breakup_cost_coins BIGINT NOT NULL DEFAULT 0,
		  status VARCHAR(32) NOT NULL DEFAULT 'active',
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, relation_type)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`); err != nil {
		return err
	}
	if err := s.ensureColumn(ctx, "user_cp_relation_configs", "breakup_cost_coins", `
		ALTER TABLE user_cp_relation_configs
		ADD COLUMN breakup_cost_coins BIGINT NOT NULL DEFAULT 0 AFTER application_expire_hours`); err != nil {
		return err
	}
	_, err := s.db.ExecContext(ctx, `
		CREATE TABLE IF NOT EXISTS user_cp_level_rules (
		  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
		  relation_type VARCHAR(32) NOT NULL,
		  level_no INT NOT NULL,
		  intimacy_threshold BIGINT NOT NULL,
		  reward_resource_group_id BIGINT NOT NULL DEFAULT 0,
		  level_icon_url VARCHAR(512) NOT NULL DEFAULT '',
		  status VARCHAR(32) NOT NULL DEFAULT 'active',
		  created_at_ms BIGINT NOT NULL,
		  updated_at_ms BIGINT NOT NULL,
		  PRIMARY KEY (app_code, relation_type, level_no),
		  KEY idx_user_cp_level_rules_status (app_code, status, relation_type, level_no)
		) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`)
	return err
}

func (s *Service) ensureColumn(ctx context.Context, tableName string, columnName string, alterSQL string) error {
	var found string
	err := s.db.QueryRowContext(ctx, `
		SELECT COLUMN_NAME
		FROM INFORMATION_SCHEMA.COLUMNS
		WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?
		LIMIT 1`, tableName, columnName).Scan(&found)
	if err == nil {
		return nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return err
	}
	_, err = s.db.ExecContext(ctx, alterSQL)
	return err
}

func normalizeRelations(input []relationDTO) ([]relationDTO, error) {
	if len(input) == 0 {
		return nil, fmt.Errorf("%w: 关系配置不能为空", ErrInvalidArgument)
	}
	seen := map[string]bool{}
	out := make([]relationDTO, 0, len(relationTypes))
	for _, relation := range input {
		relation.RelationType = strings.ToLower(strings.TrimSpace(relation.RelationType))
		if !slices.Contains(relationTypes, relation.RelationType) {
			return nil, fmt.Errorf("%w: 关系类型不正确", ErrInvalidArgument)
		}
		if seen[relation.RelationType] {
			return nil, fmt.Errorf("%w: 关系类型不能重复", ErrInvalidArgument)
		}
		seen[relation.RelationType] = true
		// CP 关系仍是硬性一人一个；后台页面只允许调整兄弟/姐妹数量，接口层也强制收敛防止绕过前端。
		if relation.RelationType == relationTypeCP {
			relation.MaxCountPerUser = 1
		}
		if relation.MaxCountPerUser <= 0 {
			return nil, fmt.Errorf("%w: %s数量必须大于0", ErrInvalidArgument, relationLabels[relation.RelationType])
		}
		if relation.ApplicationExpireHours <= 0 {
			relation.ApplicationExpireHours = defaultExpireHours
		}
		if relation.BreakupCostCoins < 0 {
			return nil, fmt.Errorf("%w: %s解除费用不能小于0", ErrInvalidArgument, relationLabels[relation.RelationType])
		}
		relation.Status = defaultString(strings.TrimSpace(relation.Status), statusActive)
		relation.Levels = normalizeLevelDefaults(relation.Levels)
		if err := validateLevels(relation.Levels); err != nil {
			return nil, err
		}
		out = append(out, relation)
	}
	for _, relationType := range relationTypes {
		if !seen[relationType] {
			return nil, fmt.Errorf("%w: 缺少%s配置", ErrInvalidArgument, relationLabels[relationType])
		}
	}
	slices.SortFunc(out, func(left, right relationDTO) int {
		return slices.Index(relationTypes, left.RelationType) - slices.Index(relationTypes, right.RelationType)
	})
	return out, nil
}

func normalizeLevelDefaults(input []levelDTO) []levelDTO {
	byLevel := map[int32]levelDTO{}
	for _, level := range input {
		byLevel[level.Level] = level
	}
	levels := make([]levelDTO, 0, levelCount)
	for levelNo := int32(1); levelNo <= levelCount; levelNo++ {
		level := byLevel[levelNo]
		level.Level = levelNo
		level.Status = defaultString(strings.TrimSpace(level.Status), statusActive)
		if level.IntimacyThreshold == 0 && levelNo > 1 {
			level.IntimacyThreshold = defaultLevelThresholds[levelNo-1]
		}
		level.LevelIconURL = strings.TrimSpace(level.LevelIconURL)
		levels = append(levels, level)
	}
	return levels
}

func validateLevels(levels []levelDTO) error {
	if len(levels) != levelCount {
		return fmt.Errorf("%w: 等级必须配置1到5级", ErrInvalidArgument)
	}
	previous := int64(-1)
	for _, level := range levels {
		if level.Level < 1 || level.Level > levelCount {
			return fmt.Errorf("%w: 等级只能是1到5", ErrInvalidArgument)
		}
		if level.IntimacyThreshold < 0 {
			return fmt.Errorf("%w: 第%d级亲密值不能小于0", ErrInvalidArgument, level.Level)
		}
		if level.IntimacyThreshold <= previous {
			return fmt.Errorf("%w: 等级亲密值必须递增", ErrInvalidArgument)
		}
		if level.RewardResourceGroupID < 0 {
			return fmt.Errorf("%w: 奖励资源组不能小于0", ErrInvalidArgument)
		}
		previous = level.IntimacyThreshold
	}
	return nil
}

func defaultString(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return strings.TrimSpace(value)
}
