// 本包承载 user-service 内 Host/Agency/BD 的 MySQL 持久化。
// 父级 mysql.Repository 负责连接池；本包负责领域行锁、命令幂等记录和事件箱写入。
package host

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

	mysqldriver "github.com/go-sql-driver/mysql"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	hostdomain "hyapp/services/user-service/internal/domain/host"
	userdomain "hyapp/services/user-service/internal/domain/user"
)

// 存储对象是 Host 领域在 user-service MySQL 上的持久化实现。
type Repository struct {
	// 数据库连接由父级 user-service MySQL 存储对象持有，本包只复用连接池。
	db *sql.DB
	// 角色范围是低频后台配置、高频业务读取；按 App+scene 短缓存并为每个键收敛并发 miss。
	roleScopeMu    sync.Mutex
	roleScopeCache map[string]roleScopePolicyCacheEntry
	roleScopeLocks map[string]*sync.Mutex
	roleScopeTTL   time.Duration
}

// 基于 user-service 共享连接池创建 Host 领域存储对象。
func New(db *sql.DB) *Repository {
	return &Repository{
		db:             db,
		roleScopeCache: make(map[string]roleScopePolicyCacheEntry),
		roleScopeLocks: make(map[string]*sync.Mutex),
		roleScopeTTL:   30 * time.Second,
	}
}

const (
	coinSellerProfileColumns = `
		user_id, status, merchant_asset_type, can_manage_sub_coin_sellers,
		created_by_user_id, created_at_ms, updated_at_ms`
)

var (
	// 列清单集中维护，保证查询函数和结果回放使用同一组投影字段。
	// 可空外键在存储边界统一转成 0，让领域结构体保持普通值类型。
	// 角色表不再保存区域；领域读取统一从对应 users 行投影当前区域。
	hostProfileColumns = fmt.Sprintf(`
		user_id, status, %s, COALESCE(current_agency_id, 0),
		COALESCE(current_membership_id, 0),
		COALESCE((
			SELECT owner_user_id
			FROM agencies
			WHERE agencies.app_code = host_profiles.app_code
			  AND agencies.agency_id = host_profiles.current_agency_id
			  AND agencies.status = 'active'
			LIMIT 1
		), 0),
		source, first_became_host_at_ms, created_at_ms, updated_at_ms`, userRegionProjection("host_profiles", "user_id"))
	agencyColumns = fmt.Sprintf(`
		agency_id, owner_user_id, %s, parent_bd_user_id, name, avatar, status,
		join_enabled, max_hosts, created_by_user_id, created_at_ms, updated_at_ms,
		COALESCE((
			SELECT COUNT(*)
			FROM agency_memberships active_member
			WHERE active_member.app_code = agencies.app_code
			  AND active_member.agency_id = agencies.agency_id
			  AND active_member.status = '%s'
		), 0)`, userRegionProjection("agencies", "owner_user_id"), hostdomain.MembershipStatusActive)
	bdProfileColumns = fmt.Sprintf(`
		user_id, role, %s, COALESCE(parent_leader_user_id, 0),
		COALESCE(parent_owner_user_id, 0), status,
		created_by_user_id, created_at_ms, updated_at_ms`, userRegionProjection("bd_profiles", "user_id"))
	bdLeaderProfileColumns = fmt.Sprintf(`
		user_id, 'bd_leader', %s, 0, 0, status,
		created_by_user_id, created_at_ms, updated_at_ms`, userRegionProjection("bd_leader_profiles", "user_id"))
	managerProfileColumns = fmt.Sprintf(`
		user_id, %s, status, contact_info, created_by_admin_id, created_at_ms, updated_at_ms`, userRegionProjection("manager_profiles", "user_id"))
	agencyMembershipColumns = fmt.Sprintf(`
		membership_id, agency_id, host_user_id, %s, membership_type, status,
		joined_at_ms, COALESCE(ended_at_ms, 0), COALESCE(ended_by_user_id, 0),
		ended_reason, created_at_ms, updated_at_ms`, userRegionProjection("agency_memberships", "host_user_id"))
	agencyApplicationColumns = fmt.Sprintf(`
		application_id, command_id, applicant_user_id, agency_id, %s, status,
		COALESCE(reviewed_by_user_id, 0), review_reason, COALESCE(reviewed_at_ms, 0),
		created_at_ms, updated_at_ms`, userRegionProjection("agency_applications", "applicant_user_id"))
	roleInvitationColumns = fmt.Sprintf(`
		invitation_id, command_id, invitation_type, status, inviter_user_id,
		inviter_bd_user_id, target_user_id, COALESCE(external_operator_user_id, 0),
		%s, agency_name, parent_bd_user_id,
		COALESCE(parent_leader_user_id, 0), COALESCE(parent_owner_user_id, 0),
		COALESCE(parent_agency_id, 0), COALESCE(processed_by_user_id, 0),
		process_reason, COALESCE(processed_at_ms, 0), created_at_ms, updated_at_ms`, userRegionProjection("role_invitations", "target_user_id"))
)

type sqlQueryer interface {
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}

type rowScanner interface {
	Scan(dest ...any) error
}

// nullableRegionID 把领域层“0 表示缺失”的约定转换为 SQL NULL。
// 这个函数也用于可选用户 ID、成员关系 ID，不只用于区域 ID。
func nullableRegionID(value int64) any {
	if value <= 0 {
		return nil
	}
	return value
}

func userRegionProjection(tableName string, userIDColumn string) string {
	return fmt.Sprintf(`COALESCE((
			SELECT u.region_id
			FROM users u
			WHERE u.app_code = %s.app_code
			  AND u.user_id = %s.%s
			LIMIT 1
		), 0)`, tableName, tableName, userIDColumn)
}

func userRegionValue(ctx context.Context, q sqlQueryer, userID int64, lockClause string) (int64, error) {
	// 只读取 users 当前区域，不校验用户状态；用于历史邀请的发起人区域对齐，
	// 避免旧库残留的邀请区域快照继续参与判断，又不额外改变“邀请已发出后可处理”的状态语义。
	query := "SELECT COALESCE(region_id, 0) FROM users WHERE app_code = ? AND user_id = ?"
	if strings.TrimSpace(lockClause) != "" {
		query += " " + lockClause
	}
	var regionID int64
	err := q.QueryRowContext(ctx, query, appcode.FromContext(ctx), userID).Scan(&regionID)
	if err == sql.ErrNoRows {
		return 0, xerr.New(xerr.NotFound, "user not found")
	}
	return regionID, err
}

func defaultAgencyProfileFromOwner(ctx context.Context, tx *sql.Tx, ownerUserID int64, explicitName string) (string, string, error) {
	snapshot, err := roleInvitationUserSnapshotByID(ctx, tx, ownerUserID)
	if err != nil {
		return "", "", err
	}
	name := strings.TrimSpace(explicitName)
	if name == "" {
		// Agency 的默认资料只在创建身份这一刻从 owner 用户资料复制一次；
		// 后续用户资料和 Agency 资料完全分离，不能再在查询层动态兜底。
		name = firstNonBlank(snapshot.Username, snapshot.DisplayUserID, fmt.Sprintf("%d", ownerUserID))
	}
	return name, strings.TrimSpace(snapshot.Avatar), nil
}

func firstNonBlank(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func boolValueDefaultTrue(value *bool) bool {
	return value == nil || *value
}

func currentUserRegion(ctx context.Context, q sqlQueryer, userID int64, lockClause string) (int64, error) {
	// Host 领域不拥有 users 表，但所有角色流转都依赖用户当前有效区域。
	// 调用方需要串行化用户身份变化时传入 FOR UPDATE 来锁住 users 行。
	query := "SELECT COALESCE(region_id, 0), status FROM users WHERE app_code = ? AND user_id = ?"
	if strings.TrimSpace(lockClause) != "" {
		query += " " + lockClause
	}
	var regionID int64
	var status string
	err := q.QueryRowContext(ctx, query, appcode.FromContext(ctx), userID).Scan(&regionID, &status)
	if err == sql.ErrNoRows {
		return 0, xerr.New(xerr.NotFound, "user not found")
	}
	if err != nil {
		return 0, err
	}
	if status != "active" {
		return 0, xerr.New(xerr.UserDisabled, "user is not active")
	}
	return regionID, nil
}

func (r *Repository) userRegion(ctx context.Context, q sqlQueryer, userID int64, lockClause string) (int64, error) {
	return currentUserRegion(ctx, q, userID, lockClause)
}

func (r *Repository) userCountry(ctx context.Context, q sqlQueryer, userID int64, lockClause string) (string, error) {
	// App 侧 Agency 推荐按用户当前国家收敛；国家来自用户资料事实，不能由客户端传入。
	query := "SELECT COALESCE(country, ''), status FROM users WHERE app_code = ? AND user_id = ?"
	if strings.TrimSpace(lockClause) != "" {
		query += " " + lockClause
	}
	var country string
	var status string
	err := q.QueryRowContext(ctx, query, appcode.FromContext(ctx), userID).Scan(&country, &status)
	if err == sql.ErrNoRows {
		return "", xerr.New(xerr.NotFound, "user not found")
	}
	if err != nil {
		return "", err
	}
	if status != "active" {
		return "", xerr.New(xerr.UserDisabled, "user is not active")
	}
	return strings.TrimSpace(country), nil
}

func requireActiveRegion(ctx context.Context, q sqlQueryer, regionID int64) error {
	if regionID <= 0 {
		return xerr.New(xerr.InvalidArgument, "region_id is required")
	}
	var status string
	err := q.QueryRowContext(ctx, `
		SELECT status
		FROM regions
		WHERE app_code = ? AND region_id = ?
	`, appcode.FromContext(ctx), regionID).Scan(&status)
	if err == sql.ErrNoRows {
		return xerr.New(xerr.RegionNotFound, "region not found")
	}
	if err != nil {
		return err
	}
	if status != userdomain.RegionStatusActive {
		return xerr.New(xerr.RegionDisabled, "region is disabled")
	}
	return nil
}

func appScopedClause(ctx context.Context, clause string, args ...any) (string, []any) {
	trimmed := strings.TrimSpace(clause)
	if strings.HasPrefix(strings.ToUpper(trimmed), "WHERE ") {
		return "WHERE app_code = ? AND " + strings.TrimSpace(trimmed[len("WHERE "):]), append([]any{appcode.FromContext(ctx)}, args...)
	}
	if trimmed == "" {
		return "WHERE app_code = ?", []any{appcode.FromContext(ctx)}
	}
	return clause, args
}

func mapHostDuplicateError(err error) error {
	// 唯一键是显式行锁之后的最终并发兜底。
	// 这里统一转成领域冲突，避免业务层/传输层感知 MySQL 错误码。
	var mysqlErr *mysqldriver.MySQLError
	if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
		return xerr.New(xerr.Conflict, "host command or relationship already exists")
	}
	return err
}

func requireCommandResult(result hostdomain.CommandResult, commandType string, resultType string) error {
	if result.CommandType != commandType || result.ResultType != resultType {
		// 同一个 command_id 不能跨动作复用，否则调用方无法判断重试语义。
		return xerr.New(xerr.Conflict, "command_id was used by another host command")
	}
	return nil
}
