package host

import (
	"context"
	"database/sql"
	"fmt"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	hostservice "hyapp/services/user-service/internal/service/host"
)

// CreateSubCoinSeller 是历史直开入口；保留方法只为接口兼容，新关系只能由 ReviewSubCoinSellerApplication 审批通过后创建。
func (r *Repository) CreateSubCoinSeller(ctx context.Context, command hostservice.CreateSubCoinSellerCommand) (hostdomain.CreateSubCoinSellerResult, error) {
	return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.PermissionDenied, "sub coin seller invitation requires admin review")
}

// SubmitSubCoinSellerApplication 把父币商邀请写成待审申请；这里不创建 relation，避免 H5 绕过后台审核。
func (r *Repository) SubmitSubCoinSellerApplication(ctx context.Context, command hostservice.SubmitSubCoinSellerApplicationCommand) (hostdomain.SubmitSubCoinSellerApplicationResult, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	}
	defer tx.Rollback()

	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeSubmitSubCoinSellerApplication, hostdomain.ResultTypeCoinSellerSubApplication); err != nil {
			return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
		}
		application, err := queryCoinSellerSubApplication(ctx, tx, "WHERE application_id = ?", existing.ResultID)
		if err != nil {
			return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
		}
		return hostdomain.SubmitSubCoinSellerApplicationResult{Application: application}, nil
	}

	parentRegionID, _, err := r.requireActiveCoinSellerSubManager(ctx, tx, command.ParentUserID, true)
	if err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	}
	targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	}
	if parentRegionID != targetRegionID {
		// 申请阶段就拦截跨区域目标，避免后台队列里出现必然无法转移库存的待办。
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.RegionMismatch, "target region does not match parent region")
	}
	if err := r.ensureCoinSellerRelationHasNoCycle(ctx, tx, command.ParentUserID, command.TargetUserID); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	}
	if existingParent, ok, err := queryActiveCoinSellerParent(ctx, tx, command.TargetUserID, true); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	} else if ok {
		if existingParent.ParentUserID == command.ParentUserID {
			return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.Conflict, "child coin seller relation already exists")
		}
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.Conflict, "target coin seller already has active parent")
	}
	if pending, ok, err := queryPendingCoinSellerSubApplicationByTarget(ctx, tx, command.TargetUserID, true); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	} else if ok {
		if pending.ParentUserID != command.ParentUserID {
			return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.Conflict, "target coin seller already has pending application")
		}
		// 同一父子目标的重复提交归并到已有 pending，并为新的 command_id 写结果，移动端重试可稳定拿到同一申请。
		if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeSubmitSubCoinSellerApplication, ResultType: hostdomain.ResultTypeCoinSellerSubApplication, ResultID: pending.ApplicationID, CreatedAtMs: command.NowMs}); err != nil {
			return hostdomain.SubmitSubCoinSellerApplicationResult{}, mapHostDuplicateError(err)
		}
		if err := tx.Commit(); err != nil {
			return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
		}
		return hostdomain.SubmitSubCoinSellerApplicationResult{Application: pending}, nil
	}

	application := hostdomain.CoinSellerSubApplication{
		ApplicationID: command.ApplicationID,
		CommandID:     command.CommandID,
		ParentUserID:  command.ParentUserID,
		TargetUserID:  command.TargetUserID,
		Status:        hostdomain.CoinSellerSubApplicationStatusPending,
		CreatedAtMs:   command.NowMs,
		UpdatedAtMs:   command.NowMs,
	}
	if err := insertCoinSellerSubApplication(ctx, tx, application); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, mapHostDuplicateError(err)
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeSubmitSubCoinSellerApplication, ResultType: hostdomain.ResultTypeCoinSellerSubApplication, ResultID: application.ApplicationID, CreatedAtMs: command.NowMs}); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, mapHostDuplicateError(err)
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	}
	return hostdomain.SubmitSubCoinSellerApplicationResult{Application: application}, nil
}

// ReviewSubCoinSellerApplication 审核 pending 申请；通过时在同一事务创建真实父子关系。
func (r *Repository) ReviewSubCoinSellerApplication(ctx context.Context, command hostservice.ReviewSubCoinSellerApplicationCommand) (hostdomain.ReviewSubCoinSellerApplicationResult, error) {
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	defer tx.Rollback()

	if existing, ok, err := commandResult(ctx, tx, command.CommandID); err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	} else if ok {
		if err := requireCommandResult(existing, hostdomain.CommandTypeReviewSubCoinSellerApplication, hostdomain.ResultTypeCoinSellerSubApplication); err != nil {
			return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
		}
		return r.coinSellerSubApplicationReviewResult(ctx, tx, existing.ResultID)
	}

	application, err := queryCoinSellerSubApplication(ctx, tx, "WHERE application_id = ? FOR UPDATE", command.ApplicationID)
	if err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	if application.Status != hostdomain.CoinSellerSubApplicationStatusPending {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, xerr.New(xerr.Conflict, "coin seller sub application is already reviewed")
	}

	var relation hostdomain.CoinSellerSubRelation
	var child hostdomain.CoinSellerListItem
	var childProfile hostdomain.CoinSellerProfile
	profileCreated := false
	if command.Decision == hostdomain.CoinSellerSubApplicationStatusApproved {
		relation, childProfile, profileCreated, err = r.createActiveSubCoinSellerRelation(ctx, tx, createActiveSubCoinSellerRelationCommand{
			RelationID:   command.RelationID,
			ParentUserID: application.ParentUserID,
			ChildUserID:  application.TargetUserID,
			NowMs:        command.NowMs,
		})
		if err != nil {
			return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
		}
		application.RelationID = relation.RelationID
	}
	application.Status = command.Decision
	application.ReviewedByAdminID = command.AdminUserID
	application.ReviewReason = command.Reason
	application.ReviewedAtMs = command.NowMs
	application.UpdatedAtMs = command.NowMs
	if err := updateCoinSellerSubApplicationReview(ctx, tx, application); err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeReviewSubCoinSellerApplication, ResultType: hostdomain.ResultTypeCoinSellerSubApplication, ResultID: application.ApplicationID, CreatedAtMs: command.NowMs}); err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, mapHostDuplicateError(err)
	}
	if command.Decision == hostdomain.CoinSellerSubApplicationStatusApproved {
		if profileCreated {
			if err := insertHostOutbox(ctx, tx, command.EventID, "CoinSellerCreated", "coin_seller_profile", childProfile.UserID, command.NowMs); err != nil {
				return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
			}
		}
		if err := insertHostOutbox(ctx, tx, command.EventID+1, "CoinSellerSubRelationCreated", "coin_seller_relation", relation.RelationID, command.NowMs); err != nil {
			return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
		}
	}
	if err := tx.Commit(); err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	if relation.RelationID > 0 {
		child, err = coinSellerListItemByUser(ctx, r.db, relation.ChildUserID)
		if err != nil {
			return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
		}
	}
	return hostdomain.ReviewSubCoinSellerApplicationResult{Application: application, Relation: relation, Child: child}, nil
}

// ListSubCoinSellers 返回直属子币商列表；读取前仍校验父币商当前权限，避免权限关闭后继续泄漏团队列表。
func (r *Repository) ListSubCoinSellers(ctx context.Context, command hostservice.ListSubCoinSellersCommand) (hostdomain.ListSubCoinSellersResult, error) {
	if _, _, err := r.requireActiveCoinSellerSubManager(ctx, r.db, command.ParentUserID, false); err != nil {
		return hostdomain.ListSubCoinSellersResult{}, err
	}
	page := command.Page
	pageSize := command.PageSize
	var total int64
	if err := r.db.QueryRowContext(ctx, `
		SELECT COUNT(*)
		FROM coin_seller_relations relation
		INNER JOIN coin_seller_profiles seller
		  ON seller.app_code = relation.app_code
		 AND seller.user_id = relation.child_user_id
		 AND seller.status = ?
		 AND seller.merchant_asset_type = ?
		INNER JOIN users u
		  ON u.app_code = relation.app_code
		 AND u.user_id = relation.child_user_id
		 AND u.status = 'active'
		WHERE relation.app_code = ?
		  AND relation.parent_user_id = ?
		  AND relation.status = ?
	`, hostdomain.CoinSellerStatusActive, hostdomain.CoinSellerMerchantAssetType, appcode.FromContext(ctx), command.ParentUserID, hostdomain.CoinSellerRelationStatusActive).Scan(&total); err != nil {
		return hostdomain.ListSubCoinSellersResult{}, err
	}

	rows, err := r.db.QueryContext(ctx, `
		SELECT
		  u.user_id,
		  u.current_display_user_id,
		  COALESCE(u.username, ''),
		  COALESCE(u.avatar, ''),
		  COALESCE(country.country_id, 0),
		  COALESCE(u.country, ''),
		  COALESCE(country.country_name, ''),
		  COALESCE(country.country_display_name, ''),
		  u.region_id,
		  COALESCE(region.region_code, ''),
		  COALESCE(region.name, ''),
		  seller.status,
		  seller.merchant_asset_type,
		  COALESCE(NULLIF(u.contact_info, ''), seller.contact_info, ''),
		  seller.can_manage_sub_coin_sellers,
		  seller.updated_at_ms
		FROM coin_seller_relations relation
		INNER JOIN users u
		  ON u.app_code = relation.app_code
		 AND u.user_id = relation.child_user_id
		 AND u.status = 'active'
		INNER JOIN coin_seller_profiles seller
		  ON seller.app_code = relation.app_code
		 AND seller.user_id = relation.child_user_id
		 AND seller.status = ?
		 AND seller.merchant_asset_type = ?
		LEFT JOIN countries country
		  ON country.app_code = u.app_code AND country.country_code = u.country
		LEFT JOIN regions region
		  ON region.app_code = u.app_code AND region.region_id = u.region_id
		WHERE relation.app_code = ?
		  AND relation.parent_user_id = ?
		  AND relation.status = ?
		ORDER BY relation.created_at_ms DESC, relation.relation_id DESC
		LIMIT ? OFFSET ?
	`, hostdomain.CoinSellerStatusActive, hostdomain.CoinSellerMerchantAssetType, appcode.FromContext(ctx), command.ParentUserID, hostdomain.CoinSellerRelationStatusActive, pageSize, coinSellerListOffset(page, pageSize))
	if err != nil {
		return hostdomain.ListSubCoinSellersResult{}, err
	}
	defer rows.Close()

	items := make([]hostdomain.CoinSellerListItem, 0, pageSize)
	for rows.Next() {
		item, err := scanCoinSellerListItem(rows)
		if err != nil {
			return hostdomain.ListSubCoinSellersResult{}, err
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		return hostdomain.ListSubCoinSellersResult{}, err
	}
	return hostdomain.ListSubCoinSellersResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}

// CheckCoinSellerSubRelation 校验父子关系、父级权限、子币商状态和区域一致性，供 gateway 转账前使用。
func (r *Repository) CheckCoinSellerSubRelation(ctx context.Context, command hostservice.CheckCoinSellerSubRelationCommand) (hostdomain.CoinSellerListItem, error) {
	parentRegionID, _, err := r.requireActiveCoinSellerSubManager(ctx, r.db, command.ParentUserID, false)
	if err != nil {
		return hostdomain.CoinSellerListItem{}, err
	}
	if _, ok, err := queryActiveCoinSellerRelation(ctx, r.db, command.ParentUserID, command.ChildUserID, false); err != nil {
		return hostdomain.CoinSellerListItem{}, err
	} else if !ok {
		return hostdomain.CoinSellerListItem{}, xerr.New(xerr.PermissionDenied, "child coin seller relation is required")
	}
	child, err := coinSellerListItemByUser(ctx, r.db, command.ChildUserID)
	if err != nil {
		return hostdomain.CoinSellerListItem{}, err
	}
	if child.RegionID != parentRegionID {
		// 用户区域变更后，即使历史关系仍在，也不能继续转移库存。
		return hostdomain.CoinSellerListItem{}, xerr.New(xerr.RegionMismatch, "child region does not match parent region")
	}
	return child, nil
}

type createActiveSubCoinSellerRelationCommand struct {
	RelationID   int64
	ParentUserID int64
	ChildUserID  int64
	NowMs        int64
}

func (r *Repository) createActiveSubCoinSellerRelation(ctx context.Context, tx *sql.Tx, command createActiveSubCoinSellerRelationCommand) (hostdomain.CoinSellerSubRelation, hostdomain.CoinSellerProfile, bool, error) {
	parentRegionID, parentProfile, err := r.requireActiveCoinSellerSubManager(ctx, tx, command.ParentUserID, true)
	if err != nil {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, err
	}
	childRegionID, err := r.userRegion(ctx, tx, command.ChildUserID, "FOR UPDATE")
	if err != nil {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, err
	}
	if parentRegionID != childRegionID {
		// 父子币商库存只允许在同区域内流转，审批时也必须按最新用户区域重新校验。
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, xerr.New(xerr.RegionMismatch, "child region does not match parent region")
	}
	if err := r.ensureCoinSellerRelationHasNoCycle(ctx, tx, command.ParentUserID, command.ChildUserID); err != nil {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, err
	}
	if existingParent, ok, err := queryActiveCoinSellerParent(ctx, tx, command.ChildUserID, true); err != nil {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, err
	} else if ok {
		if existingParent.ParentUserID == command.ParentUserID {
			return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, xerr.New(xerr.Conflict, "child coin seller relation already exists")
		}
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, xerr.New(xerr.Conflict, "child coin seller already has active parent")
	}

	childProfile, ok, err := queryCoinSellerProfileByUserMaybe(ctx, tx, command.ChildUserID, true)
	if err != nil {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, err
	}
	profileCreated := false
	if !ok {
		// 审批通过只补齐“从未是币商”的目标；被后台停用的币商不能被申请流程重新启用。
		childProfile = hostdomain.CoinSellerProfile{
			UserID:                  command.ChildUserID,
			Status:                  hostdomain.CoinSellerStatusActive,
			MerchantAssetType:       hostdomain.CoinSellerMerchantAssetType,
			CanManageSubCoinSellers: true,
			CreatedByUserID:         command.ParentUserID,
			CreatedAtMs:             command.NowMs,
			UpdatedAtMs:             command.NowMs,
		}
		if err := insertCoinSellerProfile(ctx, tx, childProfile); err != nil {
			return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, mapHostDuplicateError(err)
		}
		profileCreated = true
	} else if childProfile.Status != hostdomain.CoinSellerStatusActive || childProfile.MerchantAssetType != hostdomain.CoinSellerMerchantAssetType {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, xerr.New(xerr.PermissionDenied, "child coin seller is not active")
	}

	relation := hostdomain.CoinSellerSubRelation{
		RelationID:      command.RelationID,
		ParentUserID:    command.ParentUserID,
		ChildUserID:     command.ChildUserID,
		Status:          hostdomain.CoinSellerRelationStatusActive,
		CreatedByUserID: parentProfile.UserID,
		CreatedAtMs:     command.NowMs,
		UpdatedAtMs:     command.NowMs,
	}
	if err := insertCoinSellerRelation(ctx, tx, relation); err != nil {
		return hostdomain.CoinSellerSubRelation{}, hostdomain.CoinSellerProfile{}, false, mapHostDuplicateError(err)
	}
	return relation, childProfile, profileCreated, nil
}

func (r *Repository) requireActiveCoinSellerSubManager(ctx context.Context, q sqlQueryer, userID int64, lock bool) (int64, hostdomain.CoinSellerProfile, error) {
	lockClause := ""
	if lock {
		lockClause = "FOR UPDATE"
	}
	regionID, err := r.userRegion(ctx, q, userID, lockClause)
	if err != nil {
		return 0, hostdomain.CoinSellerProfile{}, err
	}
	if err := requireActiveRegion(ctx, q, regionID); err != nil {
		return 0, hostdomain.CoinSellerProfile{}, err
	}
	profileClause := "WHERE user_id = ?"
	if lock {
		profileClause += " FOR UPDATE"
	}
	profile, err := queryCoinSellerProfile(ctx, q, profileClause, userID)
	if err != nil {
		return 0, hostdomain.CoinSellerProfile{}, err
	}
	if profile.Status != hostdomain.CoinSellerStatusActive || profile.MerchantAssetType != hostdomain.CoinSellerMerchantAssetType {
		return 0, hostdomain.CoinSellerProfile{}, xerr.New(xerr.PermissionDenied, "active coin seller is required")
	}
	if !profile.CanManageSubCoinSellers {
		return 0, hostdomain.CoinSellerProfile{}, xerr.New(xerr.PermissionDenied, "coin seller sub permission is required")
	}
	return regionID, profile, nil
}

func (r *Repository) ensureCoinSellerRelationHasNoCycle(ctx context.Context, q sqlQueryer, parentUserID int64, childUserID int64) error {
	// 每个 active 子币商最多一个父级，所以从 parent 向上爬即可发现 child 是否已在祖先链路中。
	seen := map[int64]bool{childUserID: true}
	current := parentUserID
	for current > 0 {
		if seen[current] {
			return xerr.New(xerr.Conflict, "coin seller relation cycle is not allowed")
		}
		seen[current] = true
		relation, ok, err := queryActiveCoinSellerParent(ctx, q, current, false)
		if err != nil {
			return err
		}
		if !ok {
			return nil
		}
		current = relation.ParentUserID
	}
	return nil
}

func coinSellerListItemByUser(ctx context.Context, q sqlQueryer, userID int64) (hostdomain.CoinSellerListItem, error) {
	row := q.QueryRowContext(ctx, `
		SELECT
		  u.user_id,
		  u.current_display_user_id,
		  COALESCE(u.username, ''),
		  COALESCE(u.avatar, ''),
		  COALESCE(country.country_id, 0),
		  COALESCE(u.country, ''),
		  COALESCE(country.country_name, ''),
		  COALESCE(country.country_display_name, ''),
		  u.region_id,
		  COALESCE(region.region_code, ''),
		  COALESCE(region.name, ''),
		  seller.status,
		  seller.merchant_asset_type,
		  COALESCE(NULLIF(u.contact_info, ''), seller.contact_info, ''),
		  seller.can_manage_sub_coin_sellers,
		  seller.updated_at_ms
		FROM users u
		INNER JOIN coin_seller_profiles seller
		  ON seller.app_code = u.app_code
		 AND seller.user_id = u.user_id
		LEFT JOIN countries country
		  ON country.app_code = u.app_code AND country.country_code = u.country
		LEFT JOIN regions region
		  ON region.app_code = u.app_code AND region.region_id = u.region_id
		WHERE u.app_code = ?
		  AND u.user_id = ?
		  AND u.status = 'active'
		  AND seller.status = ?
		  AND seller.merchant_asset_type = ?
	`, appcode.FromContext(ctx), userID, hostdomain.CoinSellerStatusActive, hostdomain.CoinSellerMerchantAssetType)
	item, err := scanCoinSellerListItem(row)
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerListItem{}, xerr.New(xerr.NotFound, "coin seller not found")
	}
	return item, err
}

func scanCoinSellerListItem(scanner rowScanner) (hostdomain.CoinSellerListItem, error) {
	var item hostdomain.CoinSellerListItem
	err := scanner.Scan(
		&item.UserID,
		&item.DisplayUserID,
		&item.Username,
		&item.Avatar,
		&item.CountryID,
		&item.CountryCode,
		&item.CountryName,
		&item.CountryDisplayName,
		&item.RegionID,
		&item.RegionCode,
		&item.RegionName,
		&item.Status,
		&item.MerchantAssetType,
		&item.ContactInfo,
		&item.CanManageSubCoinSellers,
		&item.UpdatedAtMs,
	)
	return item, err
}

func insertCoinSellerSubApplication(ctx context.Context, tx *sql.Tx, application hostdomain.CoinSellerSubApplication) error {
	_, err := tx.ExecContext(ctx, `
		INSERT INTO coin_seller_sub_applications (
			app_code, application_id, command_id, parent_user_id, target_user_id,
			status, relation_id, reviewed_by_admin_id, review_reason, reviewed_at_ms,
			created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	`, appcode.FromContext(ctx), application.ApplicationID, application.CommandID, application.ParentUserID, application.TargetUserID,
		application.Status, application.RelationID, application.ReviewedByAdminID, application.ReviewReason, application.ReviewedAtMs,
		application.CreatedAtMs, application.UpdatedAtMs)
	return err
}

func updateCoinSellerSubApplicationReview(ctx context.Context, tx *sql.Tx, application hostdomain.CoinSellerSubApplication) error {
	result, err := tx.ExecContext(ctx, `
		UPDATE coin_seller_sub_applications
		SET status = ?, relation_id = ?, reviewed_by_admin_id = ?, review_reason = ?,
		    reviewed_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND application_id = ? AND status = ?
	`, application.Status, application.RelationID, application.ReviewedByAdminID, application.ReviewReason,
		application.ReviewedAtMs, application.UpdatedAtMs, appcode.FromContext(ctx), application.ApplicationID,
		hostdomain.CoinSellerSubApplicationStatusPending)
	if err != nil {
		return err
	}
	if affected, _ := result.RowsAffected(); affected == 0 {
		return xerr.New(xerr.Conflict, "coin seller sub application is already reviewed")
	}
	return nil
}

func queryCoinSellerSubApplication(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.CoinSellerSubApplication, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	application, err := scanCoinSellerSubApplication(q.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT application_id, command_id, parent_user_id, target_user_id, status,
		       relation_id, reviewed_by_admin_id, review_reason, reviewed_at_ms,
		       created_at_ms, updated_at_ms
		FROM coin_seller_sub_applications %s`, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerSubApplication{}, xerr.New(xerr.NotFound, "coin seller sub application not found")
	}
	return application, err
}

func queryPendingCoinSellerSubApplicationByTarget(ctx context.Context, q sqlQueryer, targetUserID int64, lock bool) (hostdomain.CoinSellerSubApplication, bool, error) {
	clause := "WHERE target_user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, targetUserID, hostdomain.CoinSellerSubApplicationStatusPending)
	application, err := scanCoinSellerSubApplication(q.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT application_id, command_id, parent_user_id, target_user_id, status,
		       relation_id, reviewed_by_admin_id, review_reason, reviewed_at_ms,
		       created_at_ms, updated_at_ms
		FROM coin_seller_sub_applications %s`, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerSubApplication{}, false, nil
	}
	if err != nil {
		return hostdomain.CoinSellerSubApplication{}, false, err
	}
	return application, true, nil
}

func scanCoinSellerSubApplication(scanner rowScanner) (hostdomain.CoinSellerSubApplication, error) {
	var application hostdomain.CoinSellerSubApplication
	err := scanner.Scan(
		&application.ApplicationID,
		&application.CommandID,
		&application.ParentUserID,
		&application.TargetUserID,
		&application.Status,
		&application.RelationID,
		&application.ReviewedByAdminID,
		&application.ReviewReason,
		&application.ReviewedAtMs,
		&application.CreatedAtMs,
		&application.UpdatedAtMs,
	)
	return application, err
}

func (r *Repository) coinSellerSubApplicationReviewResult(ctx context.Context, q sqlQueryer, applicationID int64) (hostdomain.ReviewSubCoinSellerApplicationResult, error) {
	application, err := queryCoinSellerSubApplication(ctx, q, "WHERE application_id = ?", applicationID)
	if err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	if application.RelationID <= 0 {
		return hostdomain.ReviewSubCoinSellerApplicationResult{Application: application}, nil
	}
	relation, err := queryCoinSellerRelation(ctx, q, "WHERE relation_id = ?", application.RelationID)
	if err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	child, err := coinSellerListItemByUser(ctx, q, relation.ChildUserID)
	if err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	return hostdomain.ReviewSubCoinSellerApplicationResult{Application: application, Relation: relation, Child: child}, nil
}

func queryCoinSellerRelation(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.CoinSellerSubRelation, error) {
	clause, args = appScopedClause(ctx, clause, args...)
	relation, err := scanCoinSellerRelation(q.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT relation_id, parent_user_id, child_user_id, status, created_by_user_id, created_at_ms, updated_at_ms
		FROM coin_seller_relations %s`, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerSubRelation{}, xerr.New(xerr.NotFound, "coin seller relation not found")
	}
	return relation, err
}

func queryActiveCoinSellerParent(ctx context.Context, q sqlQueryer, childUserID int64, lock bool) (hostdomain.CoinSellerSubRelation, bool, error) {
	clause := "WHERE child_user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, childUserID, hostdomain.CoinSellerRelationStatusActive)
	relation, err := scanCoinSellerRelation(q.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT relation_id, parent_user_id, child_user_id, status, created_by_user_id, created_at_ms, updated_at_ms
		FROM coin_seller_relations %s`, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerSubRelation{}, false, nil
	}
	if err != nil {
		return hostdomain.CoinSellerSubRelation{}, false, err
	}
	return relation, true, nil
}

func queryActiveCoinSellerRelation(ctx context.Context, q sqlQueryer, parentUserID int64, childUserID int64, lock bool) (hostdomain.CoinSellerSubRelation, bool, error) {
	clause := "WHERE parent_user_id = ? AND child_user_id = ? AND status = ?"
	if lock {
		clause += " FOR UPDATE"
	}
	clause, args := appScopedClause(ctx, clause, parentUserID, childUserID, hostdomain.CoinSellerRelationStatusActive)
	relation, err := scanCoinSellerRelation(q.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT relation_id, parent_user_id, child_user_id, status, created_by_user_id, created_at_ms, updated_at_ms
		FROM coin_seller_relations %s`, clause), args...))
	if err == sql.ErrNoRows {
		return hostdomain.CoinSellerSubRelation{}, false, nil
	}
	if err != nil {
		return hostdomain.CoinSellerSubRelation{}, false, err
	}
	return relation, true, nil
}

func scanCoinSellerRelation(scanner rowScanner) (hostdomain.CoinSellerSubRelation, error) {
	var relation hostdomain.CoinSellerSubRelation
	err := scanner.Scan(
		&relation.RelationID,
		&relation.ParentUserID,
		&relation.ChildUserID,
		&relation.Status,
		&relation.CreatedByUserID,
		&relation.CreatedAtMs,
		&relation.UpdatedAtMs,
	)
	return relation, err
}

func coinSellerListOffset(page int, pageSize int) int {
	if page <= 1 {
		return 0
	}
	return (page - 1) * pageSize
}
