package appuser

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

	"hyapp-admin-server/internal/appctx"
	userv1 "hyapp.local/api/proto/user/v1"

	mysqlDriver "github.com/go-sql-driver/mysql"
)

// AppUserThirdPartyIdentity 暴露三方绑定 openid 事实：openid = third_party_identities.provider_subject。
type AppUserThirdPartyIdentity struct {
	Provider    string `json:"provider"`
	OpenID      string `json:"openid"`
	UnionID     string `json:"unionId"`
	CreatedAtMs int64  `json:"createdAtMs"`
}

// AppUserDevice 是设备 tab 的一行设备档案，来自 user_devices 认证成功聚合。
type AppUserDevice struct {
	DeviceID      string `json:"deviceId"`
	Platform      string `json:"platform"`
	DeviceModel   string `json:"deviceModel"`
	Manufacturer  string `json:"manufacturer"`
	OSVersion     string `json:"osVersion"`
	IMEI          string `json:"imei"`
	AppVersion    string `json:"appVersion"`
	BuildNumber   string `json:"buildNumber"`
	FirstSeenAtMs int64  `json:"firstSeenAtMs"`
	LastSeenAtMs  int64  `json:"lastSeenAtMs"`
}

// AppUserAccessToken 是后台按需重签的 access token 结果；不含 refresh token，取用行为已写操作日志。
type AppUserAccessToken struct {
	AccessToken              string `json:"accessToken"`
	TokenType                string `json:"tokenType"`
	ExpiresInSec             int64  `json:"expiresInSec"`
	SessionID                string `json:"sessionId"`
	DeviceID                 string `json:"deviceId"`
	SessionLastHeartbeatAtMs int64  `json:"sessionLastHeartbeatAtMs"`
}

// accessTokenIssuerClient 只暴露后台重签 token 的 RPC；与 userAdminProjectionClient 相同，
// 通过类型断言探测能力，避免为单一后台功能扩大 userclient.Client 主契约。
type accessTokenIssuerClient interface {
	AdminIssueUserAccessToken(context.Context, *userv1.AdminIssueUserAccessTokenRequest) (*userv1.AdminIssueUserAccessTokenResponse, error)
}

// loginAuditSnapshotSQL 按单个 login_type 等值命中 idx_login_audit_latest_success 后取最新一条。
// 不能对多个 login_type 用 IN + 全局 ORDER BY：在线用户 access_resign 行每天数千条，索引范围必须逐类型收敛。
const loginAuditSnapshotSQL = `
	(SELECT COALESCE(audit.client_ip, '') AS client_ip, audit.ip_country_code, audit.app_version, audit.build_number, audit.created_at_ms, audit.id
	 FROM login_audit AS audit
	 WHERE audit.app_code = ? AND audit.user_id = ? AND audit.result = 'success' AND audit.blocked = 0 AND audit.login_type = ?
	 ORDER BY audit.created_at_ms DESC, audit.id DESC
	 LIMIT 1)`

// realLoginTypes 与 dashboard 用户版本统计口径一致：只有真实登录/续期链路携带客户端版本头。
// access_resign 是 25s 心跳重签，app_version 恒为空，只用于 IP 新鲜度。
var realLoginTypes = []string{"password", "third_party", "refresh"}

// ipLoginTypes 在真实登录之外追加 access_resign：在线用户的最新 IP 由心跳审计行提供（≤25s 旧）。
var ipLoginTypes = []string{"password", "third_party", "refresh", "access_resign"}

// fillLoginSnapshots 填充详情页的最新登录 IP 和当前 App 版本快照，两者口径不同必须分开取：
// IP 含 access_resign（新鲜度优先），版本排除 access_resign（心跳行版本恒为空，会把口径打回 unknown）。
func (s *Service) fillLoginSnapshots(ctx context.Context, item *AppUser, userID int64) error {
	if s.userDB == nil || item == nil {
		return nil
	}
	appCode := appctx.FromContext(ctx)

	ip, ipCountry, _, _, ipAtMs, err := s.queryLatestLoginAudit(ctx, appCode, userID, ipLoginTypes)
	if err != nil {
		return err
	}
	item.LastLoginIP = ip
	item.LastLoginIPCountryCode = ipCountry
	item.LastLoginIPAtMs = ipAtMs

	_, _, appVersion, buildNumber, versionAtMs, err := s.queryLatestLoginAudit(ctx, appCode, userID, realLoginTypes)
	if err != nil {
		return err
	}
	item.AppVersion = appVersion
	item.BuildNumber = buildNumber
	item.AppVersionAtMs = versionAtMs
	return nil
}

// queryLatestLoginAudit 对给定登录类型集合做“每类型 LIMIT 1 + UNION ALL 再取最新”的单用户查询。
// 没有任何成功登录时返回零值而不是错误：老用户/被清审计的用户详情页仍要能打开。
func (s *Service) queryLatestLoginAudit(ctx context.Context, appCode string, userID int64, loginTypes []string) (ip string, ipCountry string, appVersion string, buildNumber string, createdAtMs int64, err error) {
	if len(loginTypes) == 0 {
		return "", "", "", "", 0, nil
	}
	querySQL := ""
	args := make([]any, 0, len(loginTypes)*3)
	for i, loginType := range loginTypes {
		if i > 0 {
			querySQL += "\n\tUNION ALL\n"
		}
		querySQL += loginAuditSnapshotSQL
		args = append(args, appCode, userID, loginType)
	}
	// UNION ALL 的列名取第一个子查询的别名；外层只按 (created_at_ms, id) 在最多 len(loginTypes) 条候选里挑最新。
	querySQL = `
		SELECT candidate.client_ip, candidate.ip_country_code, candidate.app_version, candidate.build_number, candidate.created_at_ms
		FROM (` + querySQL + `
		) AS candidate
		ORDER BY candidate.created_at_ms DESC, candidate.id DESC
		LIMIT 1`

	scanErr := s.userDB.QueryRowContext(ctx, querySQL, args...).Scan(&ip, &ipCountry, &appVersion, &buildNumber, &createdAtMs)
	if errors.Is(scanErr, sql.ErrNoRows) {
		return "", "", "", "", 0, nil
	}
	if scanErr != nil {
		return "", "", "", "", 0, scanErr
	}
	return ip, ipCountry, appVersion, buildNumber, createdAtMs, nil
}

// fillThirdPartyIdentities 读取用户全部三方绑定；idx_third_party_user_id(app_code, user_id) 覆盖查询。
func (s *Service) fillThirdPartyIdentities(ctx context.Context, item *AppUser, userID int64) error {
	if s.userDB == nil || item == nil {
		return nil
	}
	rows, err := s.userDB.QueryContext(ctx, `
		SELECT provider, provider_subject, COALESCE(provider_union_id, ''), created_at_ms
		FROM third_party_identities
		WHERE app_code = ? AND user_id = ?
		ORDER BY provider ASC, created_at_ms ASC
	`, appctx.FromContext(ctx), userID)
	if err != nil {
		return err
	}
	defer rows.Close()

	identities := make([]AppUserThirdPartyIdentity, 0, 2)
	for rows.Next() {
		var identity AppUserThirdPartyIdentity
		if err := rows.Scan(&identity.Provider, &identity.OpenID, &identity.UnionID, &identity.CreatedAtMs); err != nil {
			return err
		}
		identities = append(identities, identity)
	}
	item.ThirdPartyIdentities = identities
	return rows.Err()
}

// fillDevices 读取设备档案。user_devices 由 user-service 迁移 013 引入，线上 DDL 人工执行，
// 表尚未创建（Error 1146）时按空列表处理，不能让整个用户详情 500。
func (s *Service) fillDevices(ctx context.Context, item *AppUser, userID int64) error {
	if s.userDB == nil || item == nil {
		return nil
	}
	rows, err := s.userDB.QueryContext(ctx, `
		SELECT device_id, platform, device_model, manufacturer, os_version, imei, app_version, build_number, first_seen_at_ms, last_seen_at_ms
		FROM user_devices
		WHERE app_code = ? AND user_id = ?
		ORDER BY last_seen_at_ms DESC
		LIMIT 20
	`, appctx.FromContext(ctx), userID)
	if err != nil {
		var mysqlErr *mysqlDriver.MySQLError
		if errors.As(err, &mysqlErr) && mysqlErr.Number == 1146 {
			item.Devices = []AppUserDevice{}
			return nil
		}
		return err
	}
	defer rows.Close()

	devices := make([]AppUserDevice, 0, 4)
	for rows.Next() {
		var device AppUserDevice
		if err := rows.Scan(
			&device.DeviceID,
			&device.Platform,
			&device.DeviceModel,
			&device.Manufacturer,
			&device.OSVersion,
			&device.IMEI,
			&device.AppVersion,
			&device.BuildNumber,
			&device.FirstSeenAtMs,
			&device.LastSeenAtMs,
		); err != nil {
			return err
		}
		devices = append(devices, device)
	}
	item.Devices = devices
	return rows.Err()
}

// IssueAccessToken 调 user-service 按用户最新 active session 重签 access token。
// NotFound（无活跃会话）原样向上抛，由 handler 映射成前端可直接展示的 404 文案。
func (s *Service) IssueAccessToken(ctx context.Context, userID int64) (AppUserAccessToken, error) {
	if s.tokenIssuerClient == nil {
		return AppUserAccessToken{}, fmt.Errorf("user client is not configured")
	}
	nowMs := nowMillis()
	appCode := appctx.FromContext(ctx)
	resp, err := s.tokenIssuerClient.AdminIssueUserAccessToken(ctx, &userv1.AdminIssueUserAccessTokenRequest{
		Meta: &userv1.RequestMeta{
			RequestId: fmt.Sprintf("admin-app-user-token-%d", nowMs),
			Caller:    "hyapp-admin-server",
			SentAtMs:  nowMs,
			AppCode:   appCode,
		},
		AppCode: appCode,
		UserId:  userID,
	})
	if err != nil {
		return AppUserAccessToken{}, err
	}
	return AppUserAccessToken{
		AccessToken:              resp.GetAccessToken(),
		TokenType:                resp.GetTokenType(),
		ExpiresInSec:             resp.GetExpiresInSec(),
		SessionID:                resp.GetSessionId(),
		DeviceID:                 resp.GetDeviceId(),
		SessionLastHeartbeatAtMs: resp.GetSessionLastHeartbeatAtMs(),
	}, nil
}
