package agencyopening

import (
	"context"
	"log/slog"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/agencyopening"
)

const rewardReason = "agency opening reward"

type Repository interface {
	ListAgencyOpeningCycles(ctx context.Context, query domain.ListQuery) ([]domain.Cycle, int64, error)
	GetAgencyOpeningCycle(ctx context.Context, cycleID string) (domain.Cycle, error)
	UpsertAgencyOpeningCycle(ctx context.Context, command domain.CycleCommand, nowMS int64) (domain.Cycle, bool, error)
	SetAgencyOpeningCycleStatus(ctx context.Context, cycleID string, status string, operatorAdminID int64, nowMS int64) (domain.Cycle, error)
	FindCurrentAgencyOpeningCycle(ctx context.Context, regionID int64, nowMS int64) (domain.Cycle, bool, error)
	GetAgencyOpeningApplicationByOwner(ctx context.Context, ownerUserID int64) (domain.Application, bool, error)
	CreateAgencyOpeningApplication(ctx context.Context, cycle domain.Cycle, agency domain.AgencySnapshot, nowMS int64) (domain.Application, bool, error)
	ApproveAgencyOpeningApplication(ctx context.Context, applicationID string, operatorAdminID int64, nowMS int64) (domain.Application, error)
	ListAgencyOpeningApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error)
	ConsumeAgencyOpeningGiftEvent(ctx context.Context, event domain.GiftEvent, nowMS int64) (domain.EventResult, error)
	ClaimDueAgencyOpeningCycles(ctx context.Context, workerID string, nowMS int64, lockTTL time.Duration, batchSize int32) ([]domain.Cycle, error)
	PrepareAgencyOpeningSettlements(ctx context.Context, cycle domain.Cycle, nowMS int64) ([]domain.Application, error)
	MarkAgencyOpeningApplicationGranted(ctx context.Context, applicationID string, walletTransactionID string, grantedAtMS int64) (domain.Application, error)
	MarkAgencyOpeningApplicationFailed(ctx context.Context, applicationID string, reason string, nowMS int64) error
	FinishAgencyOpeningCycleIfComplete(ctx context.Context, cycleID string, nowMS int64) (bool, error)
}

type AgencySource interface {
	ResolveOwnerAgency(ctx context.Context, userID int64) (domain.AgencySnapshot, bool, error)
}

type WalletClient interface {
	CreditAgencyOpeningReward(ctx context.Context, req *walletv1.CreditAgencyOpeningRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditAgencyOpeningRewardResponse, error)
}

type RoomQueryClient interface {
	AdminGetRoom(ctx context.Context, req *roomv1.AdminGetRoomRequest, opts ...grpc.CallOption) (*roomv1.AdminGetRoomResponse, error)
}

type Service struct {
	repository Repository
	agency     AgencySource
	room       RoomQueryClient
	wallet     WalletClient
	now        func() time.Time
}

func New(repository Repository, wallet WalletClient, agency AgencySource, room RoomQueryClient) *Service {
	return &Service{repository: repository, wallet: wallet, agency: agency, room: room, now: time.Now}
}

func (s *Service) ListCycles(ctx context.Context, query domain.ListQuery) ([]domain.Cycle, int64, error) {
	if err := s.requireRepository(); err != nil {
		return nil, 0, err
	}
	query.Status = normalizeStatus(query.Status)
	return s.repository.ListAgencyOpeningCycles(ctx, query)
}

func (s *Service) GetCycle(ctx context.Context, cycleID string) (domain.Cycle, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Cycle{}, err
	}
	cycleID = strings.TrimSpace(cycleID)
	if cycleID == "" {
		return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "cycle_id is required")
	}
	return s.repository.GetAgencyOpeningCycle(ctx, cycleID)
}

func (s *Service) CreateCycle(ctx context.Context, command domain.CycleCommand) (domain.Cycle, bool, error) {
	command.RequireNewRecord = true
	return s.upsertCycle(ctx, command)
}

func (s *Service) UpdateCycle(ctx context.Context, command domain.CycleCommand) (domain.Cycle, bool, error) {
	command.RequireNewRecord = false
	return s.upsertCycle(ctx, command)
}

func (s *Service) SetCycleStatus(ctx context.Context, cycleID string, status string, operatorAdminID int64) (domain.Cycle, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Cycle{}, err
	}
	status = normalizeStatus(status)
	if !validStatus(status) || status == domain.StatusSettling {
		return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "status is invalid")
	}
	if operatorAdminID <= 0 {
		return domain.Cycle{}, xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
	}
	return s.repository.SetAgencyOpeningCycleStatus(ctx, strings.TrimSpace(cycleID), status, operatorAdminID, s.now().UTC().UnixMilli())
}

func (s *Service) GetStatus(ctx context.Context, userID int64) (domain.Qualification, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Qualification{}, err
	}
	nowMS := s.now().UTC().UnixMilli()
	result := domain.Qualification{ServerTimeMS: nowMS}
	// 代理区域来自 user-service 的 Agency owner 事实；先解析区域，再按“区域覆盖、全区域默认”顺序选择周期，禁止信任请求参数推断区域。
	agency, ok, err := s.resolveOwnerAgency(ctx, userID)
	if err != nil {
		return domain.Qualification{}, err
	}
	result.Agency = agency
	if !ok || agency.RegionID <= 0 {
		result.IneligibleReason = "active_agency_owner_required"
		return result, nil
	}
	cycle, exists, err := s.repository.FindCurrentAgencyOpeningCycle(ctx, agency.RegionID, nowMS)
	if err != nil {
		return domain.Qualification{}, err
	}
	result.Cycle = cycle
	result.Enabled = exists
	if !exists {
		result.IneligibleReason = "activity_disabled"
		return result, nil
	}
	application, joined, err := s.repository.GetAgencyOpeningApplicationByOwner(ctx, agency.OwnerUserID)
	if err != nil {
		return domain.Qualification{}, err
	}
	result.Application = application
	result.Joined = joined
	result.Eligible, result.IneligibleReason = eligible(cycle, agency, nowMS)
	if joined {
		result.Eligible = false
		result.IneligibleReason = "already_applied"
	}
	return result, nil
}

func (s *Service) Apply(ctx context.Context, userID int64, commandID string) (domain.Application, bool, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Application{}, false, err
	}
	if strings.TrimSpace(commandID) == "" {
		return domain.Application{}, false, xerr.New(xerr.InvalidArgument, "command_id is required")
	}
	qualification, err := s.GetStatus(ctx, userID)
	if err != nil {
		return domain.Application{}, false, err
	}
	if !qualification.Enabled {
		return domain.Application{}, false, xerr.New(xerr.Conflict, "activity is not active")
	}
	if qualification.Joined {
		return qualification.Application, false, nil
	}
	if !qualification.Eligible {
		return domain.Application{}, false, xerr.New(xerr.Conflict, qualification.IneligibleReason)
	}
	return s.repository.CreateAgencyOpeningApplication(ctx, qualification.Cycle, qualification.Agency, s.now().UTC().UnixMilli())
}

func (s *Service) ApproveApplication(ctx context.Context, applicationID string, operatorAdminID int64) (domain.Application, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Application{}, err
	}
	applicationID = strings.TrimSpace(applicationID)
	if applicationID == "" {
		return domain.Application{}, xerr.New(xerr.InvalidArgument, "application_id is required")
	}
	if operatorAdminID <= 0 {
		return domain.Application{}, xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
	}
	// 开业积分必须从后台同意这一刻重新开始计算，不能把用户申请后、审核前的房间礼物补进活动分。
	return s.repository.ApproveAgencyOpeningApplication(ctx, applicationID, operatorAdminID, s.now().UTC().UnixMilli())
}

func (s *Service) ListApplications(ctx context.Context, query domain.ApplicationQuery) ([]domain.Application, int64, error) {
	if err := s.requireRepository(); err != nil {
		return nil, 0, err
	}
	query.Status = normalizeApplicationStatus(query.Status)
	return s.repository.ListAgencyOpeningApplications(ctx, query)
}

func (s *Service) HandleGiftEvent(ctx context.Context, event domain.GiftEvent) (int32, error) {
	if err := s.requireRepository(); err != nil {
		return 0, err
	}
	if event.EventID == "" || event.RegionID < 0 || event.RoomOwnerUserID <= 0 || event.CoinSpent <= 0 {
		return 0, nil
	}
	result, err := s.repository.ConsumeAgencyOpeningGiftEvent(ctx, event, s.now().UTC().UnixMilli())
	if err != nil {
		return 0, err
	}
	if result.Status == domain.EventStatusConsumed {
		return 1, nil
	}
	return 0, nil
}

// HandleRoomEvent consumes committed room gift facts and projects revenue to the applicant room owner.
// 机器人礼物、非礼物事件和无扣费金额事件都跳过；房间 owner 从 room-service 当前事实解析，保证“开业流水”只统计申请人自己房间内发生的真实扣费。
func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) (int32, error) {
	if s == nil || envelope == nil || envelope.GetEventType() != "RoomGiftSent" {
		return 0, nil
	}
	var gift roomeventsv1.RoomGiftSent
	if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
		return 0, err
	}
	roomID := strings.TrimSpace(envelope.GetRoomId())
	contributionValue := gift.GetGiftValue()
	regionID := gift.GetVisibleRegionId()
	if gift.GetIsRobotGift() || roomID == "" || contributionValue <= 0 {
		return 0, nil
	}
	eventCtx := appcode.WithContext(ctx, envelope.GetAppCode())
	ownerUserID, err := s.fetchRoomOwner(eventCtx, roomID)
	if xerr.IsCode(err, xerr.NotFound) {
		// RoomGiftSent 已经是 room-service 提交过的历史事实；若消费时房间已被删除，当前 owner 已不可恢复，
		// 该事件对“开业房主流水”只能收敛为 skipped。这里只吞精确 NotFound，网络、超时和内部错误仍返回 MQ 重试，避免把临时依赖故障误判成业务终态。
		return 0, nil
	}
	if err != nil || ownerUserID <= 0 {
		return 0, err
	}
	return s.HandleGiftEvent(eventCtx, domain.GiftEvent{
		EventID:         strings.TrimSpace(envelope.GetEventId()),
		RegionID:        regionID,
		RoomID:          roomID,
		RoomOwnerUserID: ownerUserID,
		TargetUserID:    gift.GetTargetUserId(),
		// 开业流水只消费 wallet 结算后的 HeatValue，也就是 RoomGiftSent.gift_value；原始扣费 coin_spent 不再参与奖励达标。
		CoinSpent:    contributionValue,
		OccurredAtMS: envelope.GetOccurredAtMs(),
	})
}

func (s *Service) ProcessSettlementBatch(ctx context.Context, runID string, workerID string, batchSize int32, lockTTL time.Duration) (claimed int32, processed int32, success int32, failure int32, hasMore bool, err error) {
	if err := s.requireRepository(); err != nil {
		return 0, 0, 0, 0, false, err
	}
	if batchSize <= 0 {
		batchSize = 50
	}
	if lockTTL <= 0 {
		lockTTL = 30 * time.Second
	}
	if strings.TrimSpace(workerID) == "" {
		workerID = "activity-agency-opening"
	}
	cycles, err := s.repository.ClaimDueAgencyOpeningCycles(ctx, workerID, s.now().UTC().UnixMilli(), lockTTL, batchSize)
	if err != nil {
		return 0, 0, 0, 0, false, err
	}
	for _, cycle := range cycles {
		settlements, prepareErr := s.repository.PrepareAgencyOpeningSettlements(ctx, cycle, s.now().UTC().UnixMilli())
		if prepareErr != nil {
			logx.Error(ctx, "agency_opening_prepare_settlements_failed", prepareErr, slog.String("cycle_id", cycle.CycleID), slog.String("run_id", runID))
			failure++
			continue
		}
		if len(settlements) == 0 {
			if _, finishErr := s.repository.FinishAgencyOpeningCycleIfComplete(ctx, cycle.CycleID, s.now().UTC().UnixMilli()); finishErr != nil {
				failure++
				continue
			}
			success++
			continue
		}
		for _, settlement := range settlements {
			processed++
			if _, grantErr := s.grantSettlement(ctx, settlement); grantErr != nil {
				logx.Error(ctx, "agency_opening_grant_failed", grantErr, slog.String("application_id", settlement.ApplicationID), slog.String("cycle_id", settlement.CycleID))
				failure++
				continue
			}
			success++
		}
		if _, finishErr := s.repository.FinishAgencyOpeningCycleIfComplete(ctx, cycle.CycleID, s.now().UTC().UnixMilli()); finishErr != nil {
			failure++
		}
	}
	return int32(len(cycles)), processed, success, failure, len(cycles) >= int(batchSize), nil
}

func (s *Service) grantSettlement(ctx context.Context, settlement domain.Application) (domain.Application, error) {
	if settlement.Status == domain.ApplicationStatusGranted {
		return settlement, nil
	}
	if settlement.RewardCoinAmount <= 0 {
		nowMS := s.now().UTC().UnixMilli()
		return s.repository.MarkAgencyOpeningApplicationGranted(ctx, settlement.ApplicationID, "", nowMS)
	}
	if s.wallet == nil {
		err := xerr.New(xerr.Unavailable, "wallet client is not configured")
		_ = s.repository.MarkAgencyOpeningApplicationFailed(ctx, settlement.ApplicationID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
		return domain.Application{}, err
	}
	resp, err := s.wallet.CreditAgencyOpeningReward(ctx, &walletv1.CreditAgencyOpeningRewardRequest{
		CommandId:     settlement.WalletCommandID,
		AppCode:       appcode.FromContext(ctx),
		TargetUserId:  settlement.AgencyOwnerUserID,
		Amount:        settlement.RewardCoinAmount,
		ApplicationId: settlement.ApplicationID,
		CycleId:       settlement.CycleID,
		AgencyId:      settlement.AgencyID,
		RankNo:        settlement.RewardRankNo,
		ScoreCoins:    settlement.ScoreCoinAmount,
		Reason:        rewardReason,
	})
	if err != nil {
		_ = s.repository.MarkAgencyOpeningApplicationFailed(ctx, settlement.ApplicationID, xerr.MessageOf(err), s.now().UTC().UnixMilli())
		return domain.Application{}, err
	}
	return s.repository.MarkAgencyOpeningApplicationGranted(ctx, settlement.ApplicationID, resp.GetTransactionId(), resp.GetGrantedAtMs())
}

func (s *Service) upsertCycle(ctx context.Context, command domain.CycleCommand) (domain.Cycle, bool, error) {
	if err := s.requireRepository(); err != nil {
		return domain.Cycle{}, false, err
	}
	command.Cycle.Status = normalizeStatus(command.Cycle.Status)
	if err := validateCycle(command.Cycle, command.OperatorAdminID); err != nil {
		return domain.Cycle{}, false, err
	}
	return s.repository.UpsertAgencyOpeningCycle(ctx, command, s.now().UTC().UnixMilli())
}

func (s *Service) resolveOwnerAgency(ctx context.Context, userID int64) (domain.AgencySnapshot, bool, error) {
	if s.agency == nil {
		return domain.AgencySnapshot{}, false, xerr.New(xerr.Unavailable, "agency source is not configured")
	}
	return s.agency.ResolveOwnerAgency(ctx, userID)
}

func (s *Service) fetchRoomOwner(ctx context.Context, roomID string) (int64, error) {
	if s.room == nil {
		return 0, xerr.New(xerr.Unavailable, "room query client is not configured")
	}
	roomID = strings.TrimSpace(roomID)
	if roomID == "" {
		return 0, xerr.New(xerr.InvalidArgument, "room_id is required")
	}
	resp, err := s.room.AdminGetRoom(ctx, &roomv1.AdminGetRoomRequest{
		Meta: &roomv1.RequestMeta{
			// 事件没有携带 owner 快照，按 room_id 生成稳定 request_id，方便追踪某次流水为何命中或跳过申请人。
			RequestId: "agency-opening-room-owner:" + roomID,
			AppCode:   appcode.FromContext(ctx),
			SentAtMs:  s.now().UTC().UnixMilli(),
		},
		RoomId: roomID,
	})
	if err != nil {
		return 0, err
	}
	return resp.GetRoom().GetOwnerUserId(), nil
}

func (s *Service) requireRepository() error {
	if s == nil || s.repository == nil {
		return xerr.New(xerr.Unavailable, "agency opening repository is not configured")
	}
	return nil
}

func validateCycle(cycle domain.Cycle, operatorAdminID int64) error {
	if cycle.RegionID < 0 {
		// region_id=0 是全区域周期，正数周期覆盖对应区域。
		return xerr.New(xerr.InvalidArgument, "region_id must not be negative")
	}
	if strings.TrimSpace(cycle.Title) == "" {
		return xerr.New(xerr.InvalidArgument, "title is required")
	}
	if !validStatus(cycle.Status) || cycle.Status == domain.StatusSettling || cycle.Status == domain.StatusSettled {
		return xerr.New(xerr.InvalidArgument, "status is invalid")
	}
	if cycle.StartMS <= 0 || cycle.EndMS <= cycle.StartMS {
		return xerr.New(xerr.InvalidArgument, "cycle period is invalid")
	}
	if cycle.MinHostCount <= 0 {
		return xerr.New(xerr.InvalidArgument, "min_host_count must be positive")
	}
	if cycle.MaxAgencyAgeDays < 0 {
		return xerr.New(xerr.InvalidArgument, "max_agency_age_days must not be negative")
	}
	if len(cycle.Rewards) == 0 {
		return xerr.New(xerr.InvalidArgument, "reward tiers are required")
	}
	seen := map[int32]bool{}
	previousThreshold := int64(0)
	for _, reward := range cycle.Rewards {
		if reward.RankNo <= 0 || reward.ThresholdCoinSpent <= 0 || reward.RewardCoinAmount < 0 {
			return xerr.New(xerr.InvalidArgument, "reward tier threshold or amount is invalid")
		}
		if seen[reward.RankNo] {
			return xerr.New(xerr.InvalidArgument, "reward tier duplicated")
		}
		if previousThreshold > 0 && reward.ThresholdCoinSpent <= previousThreshold {
			return xerr.New(xerr.InvalidArgument, "reward tier threshold must increase")
		}
		seen[reward.RankNo] = true
		previousThreshold = reward.ThresholdCoinSpent
	}
	if operatorAdminID <= 0 {
		return xerr.New(xerr.InvalidArgument, "operator_admin_id is required")
	}
	return nil
}

func eligible(cycle domain.Cycle, agency domain.AgencySnapshot, nowMS int64) (bool, string) {
	if cycle.RegionID != 0 && agency.RegionID != cycle.RegionID {
		return false, "agency_region_not_eligible"
	}
	if agency.Status != "active" {
		return false, "active_agency_required"
	}
	if agency.HostCount <= cycle.MinHostCount {
		return false, "host_count_not_enough"
	}
	if cycle.MaxAgencyAgeDays > 0 && agency.AgencyCreatedAtMS > 0 {
		maxAgeMS := int64(cycle.MaxAgencyAgeDays) * int64(24*time.Hour/time.Millisecond)
		if nowMS-agency.AgencyCreatedAtMS > maxAgeMS {
			return false, "agency_too_old"
		}
	}
	return true, ""
}

func normalizeStatus(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if value == "" {
		return domain.StatusDraft
	}
	return value
}

func normalizeApplicationStatus(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func validStatus(value string) bool {
	switch value {
	case domain.StatusDraft, domain.StatusActive, domain.StatusDisabled, domain.StatusSettling, domain.StatusSettled:
		return true
	default:
		return false
	}
}
