package auth

import (
	"context"
	"database/sql"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	authdomain "hyapp/services/user-service/internal/domain/auth"
)

func (r *Repository) CreateSession(ctx context.Context, session authdomain.Session) error {
	// refresh token 只保存 hash，原文不会写入数据库。
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
	`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.LastHeartbeatAtMs, session.LastHeartbeatRequestID, session.CreatedAtMs, session.UpdatedAtMs)
	if err != nil {
		// session_id 或 refresh hash 冲突统一映射成 auth 领域冲突。
		return mapAuthDuplicateError(err)
	}

	return nil
}

// FindActiveSessionByRefreshHash 查找未过期且未吊销的 session。

func (r *Repository) FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error) {
	// 查到 session 后再判断 revoked/expired，给调用方稳定 reason。
	session, err := r.findSessionByRefreshHash(ctx, refreshTokenHash)
	if err != nil {
		return authdomain.Session{}, err
	}
	if session.RevokedAtMs > 0 {
		// 已吊销 session 不能继续 refresh。
		return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if session.ExpiresAtMs <= nowMs {
		// 过期 session 与吊销 session 使用不同 reason。
		return authdomain.Session{}, xerr.New(xerr.SessionExpired, "session expired")
	}

	return session, nil
}

// FindActiveSessionByID 查找未过期且未吊销的 session。

func (r *Repository) FindActiveSessionByID(ctx context.Context, sessionID string, nowMs int64) (authdomain.Session, error) {
	// onboarding 完成后只需要重签 access token，不能要求客户端再提交 refresh token 原文。
	session, err := r.findSessionByID(ctx, sessionID)
	if err != nil {
		return authdomain.Session{}, err
	}
	if session.RevokedAtMs > 0 {
		// 已吊销 session 不能继续签发任何 access token。
		return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if session.ExpiresAtMs <= nowMs {
		// session 过期后只能重新登录，不能用完成注册接口续命。
		return authdomain.Session{}, xerr.New(xerr.SessionExpired, "session expired")
	}

	return session, nil
}

// ReplaceSession 原子吊销旧 session 并创建新 session。

func (r *Repository) ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error {
	// refresh 轮换必须原子吊销旧 session 并插入新 session。
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	var oldRevokedAt sql.Null[int64]
	err = tx.QueryRowContext(ctx, `
		SELECT revoked_at_ms
		FROM auth_sessions
		WHERE app_code = ? AND session_id = ?
		FOR UPDATE
	`, appcode.FromContext(ctx), oldSessionID).Scan(&oldRevokedAt)
	if err == sql.ErrNoRows || oldRevokedAt.Valid {
		// 旧 session 不存在或已经吊销时，不能再次轮换。
		return xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if err != nil {
		return err
	}

	_, err = tx.ExecContext(ctx, `
		UPDATE auth_sessions
		SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
		WHERE app_code = ? AND session_id = ?
	`, revokedAtMs, "REFRESH_ROTATED", "", "refresh_token", revokedAtMs, appcode.FromContext(ctx), oldSessionID)
	if err != nil {
		return err
	}

	if err := insertSession(ctx, tx, newSession); err != nil {
		// 插入新 session 失败会回滚旧 session 吊销。
		return err
	}

	return tx.Commit()
}

// RevokeSession 按 session_id 或 refresh token hash 吊销 session。

func (r *Repository) RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error) {
	if sessionID == "" && refreshTokenHash != "" {
		// 只传 refresh token 时先反查 session_id。
		session, err := r.findSessionByRefreshHash(ctx, refreshTokenHash)
		if err != nil {
			return false, err
		}
		sessionID = session.SessionID
	}

	return r.revokeSessionByID(ctx, sessionID, revokedAtMs, "USER_LOGOUT", "", "user_logout")
}

func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) {
	return r.revokeSessionByID(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy)
}

func (r *Repository) RevokeUserDeviceSessionsForRisk(ctx context.Context, sourceSessionID string, userID int64, revokedAtMs int64, reason string, requestID string, revokedBy string) ([]string, error) {
	// IP 风控是异步执行；客户端可能已经用源 session 的 refresh token 轮换出了新 session。
	// 这里先锁定源 session，拿到设备和创建时间，再圈定同一用户、同一设备、同一登录链路之后产生的未过期 session，避免误伤同账号其他设备。
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()

	var deviceID string
	var sourceUserID int64
	var sourceCreatedAtMs int64
	err = tx.QueryRowContext(ctx, `
		SELECT user_id, device_id, created_at_ms
		FROM auth_sessions
		WHERE app_code = ? AND session_id = ?
		FOR UPDATE
	`, appcode.FromContext(ctx), sourceSessionID).Scan(&sourceUserID, &deviceID, &sourceCreatedAtMs)
	if err == sql.ErrNoRows {
		// 风控任务来自登录成功后的持久 session；若源 session 已被异常清理，按空结果幂等结束。
		return nil, tx.Commit()
	}
	if err != nil {
		return nil, err
	}
	if sourceUserID != userID {
		// job.user_id 和 session.user_id 不一致说明数据链路被污染，不能扩大吊销范围。
		return nil, xerr.New(xerr.SessionRevoked, "risk session owner mismatch")
	}

	rows, err := tx.QueryContext(ctx, `
		SELECT session_id
		FROM auth_sessions
		WHERE app_code = ?
		  AND user_id = ?
		  AND device_id = ?
		  AND created_at_ms >= ?
		  AND expires_at_ms > ?
		FOR UPDATE
	`, appcode.FromContext(ctx), userID, deviceID, sourceCreatedAtMs, revokedAtMs)
	if err != nil {
		return nil, err
	}
	var sessionIDs []string
	for rows.Next() {
		var sessionID string
		if err := rows.Scan(&sessionID); err != nil {
			_ = rows.Close()
			return nil, err
		}
		sessionIDs = append(sessionIDs, sessionID)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(sessionIDs) == 0 {
		return sessionIDs, tx.Commit()
	}

	for _, sessionID := range sessionIDs {
		// 已经被 refresh 轮换的 session 不覆盖原撤销原因，但仍返回给上层写 denylist，避免旧 access token 继续通过 gateway。
		if _, err := tx.ExecContext(ctx, `
			UPDATE auth_sessions
			SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
			WHERE app_code = ? AND session_id = ? AND revoked_at_ms IS NULL
		`, revokedAtMs, reason, requestID, revokedBy, revokedAtMs, appcode.FromContext(ctx), sessionID); err != nil {
			return nil, err
		}
	}

	return sessionIDs, tx.Commit()
}

func (r *Repository) UpdateSessionHeartbeat(ctx context.Context, sessionID string, userID int64, heartbeatAtMs int64, requestID string) (authdomain.Session, error) {
	// App 心跳绑定服务端 session，不能只按 user_id 宽泛刷新，避免旧 token 或跨设备请求续住错误会话。
	result, err := r.db.ExecContext(ctx, `
		UPDATE auth_sessions
		SET last_heartbeat_at_ms = ?, last_heartbeat_request_id = ?, updated_at_ms = ?
		WHERE app_code = ? AND session_id = ? AND user_id = ? AND revoked_at_ms IS NULL AND expires_at_ms > ?
	`, heartbeatAtMs, requestID, heartbeatAtMs, appcode.FromContext(ctx), sessionID, userID, heartbeatAtMs)
	if err != nil {
		return authdomain.Session{}, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return authdomain.Session{}, err
	}
	if affected == 0 {
		// 未命中只说明 session 不可用或不属于当前用户；对外统一成 session revoked，避免泄漏会话存在性。
		return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}

	return r.findSessionByID(ctx, sessionID)
}

func (r *Repository) revokeSessionByID(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) {
	result, err := r.db.ExecContext(ctx, `
		UPDATE auth_sessions
		SET revoked_at_ms = ?, revoked_reason = ?, revoked_request_id = ?, revoked_by = ?, updated_at_ms = ?
		WHERE app_code = ? AND session_id = ? AND revoked_at_ms IS NULL
	`, revokedAtMs, reason, requestID, revokedBy, revokedAtMs, appcode.FromContext(ctx), sessionID)
	if err != nil {
		return false, err
	}

	affected, err := result.RowsAffected()
	if err != nil {
		return false, err
	}

	// affected=false 表示没有可吊销 session，调用方按幂等结果处理。
	return affected > 0, nil
}

// FindThirdPartyIdentity 查找 provider + subject 绑定。

func (r *Repository) findSessionByRefreshHash(ctx context.Context, refreshTokenHash string) (authdomain.Session, error) {
	// refresh token hash 是唯一索引，最多命中一条 session。
	var session authdomain.Session
	var revokedAt sql.Null[int64]
	err := r.db.QueryRowContext(ctx, `
		SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
		FROM auth_sessions
		WHERE app_code = ? AND refresh_token_hash = ?
	`, appcode.FromContext(ctx), refreshTokenHash).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &session.LastHeartbeatAtMs, &session.LastHeartbeatRequestID, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
	if err == sql.ErrNoRows {
		// 找不到 hash 按 revoked 返回，避免暴露 token 是否存在。
		return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if err != nil {
		return authdomain.Session{}, err
	}
	if revokedAt.Valid {
		// sql.Null[int64] 转换成领域零值语义。
		session.RevokedAtMs = revokedAt.V
	}

	return session, nil
}

func (r *Repository) findSessionByID(ctx context.Context, sessionID string) (authdomain.Session, error) {
	// session_id 来自已校验 access token 的 sid claim，仍必须回查服务端 session 状态。
	var session authdomain.Session
	var revokedAt sql.Null[int64]
	err := r.db.QueryRowContext(ctx, `
		SELECT app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms
		FROM auth_sessions
		WHERE app_code = ? AND session_id = ?
	`, appcode.FromContext(ctx), sessionID).Scan(&session.AppCode, &session.SessionID, &session.UserID, &session.RefreshTokenHash, &session.DeviceID, &session.ExpiresAtMs, &session.LastHeartbeatAtMs, &session.LastHeartbeatRequestID, &revokedAt, &session.RevokedReason, &session.RevokedRequestID, &session.RevokedBy, &session.CreatedAtMs, &session.UpdatedAtMs)
	if err == sql.ErrNoRows {
		// 不暴露 session_id 是否存在，统一按 revoked 处理。
		return authdomain.Session{}, xerr.New(xerr.SessionRevoked, "session revoked")
	}
	if err != nil {
		return authdomain.Session{}, err
	}
	if revokedAt.Valid {
		session.RevokedAtMs = revokedAt.V
	}

	return session, nil
}

func insertSession(ctx context.Context, tx *sql.Tx, session authdomain.Session) error {
	// insertSession 只在已有事务中使用，保证与 session 替换或三方注册同提交。
	_, err := tx.ExecContext(ctx, `
		INSERT INTO auth_sessions (app_code, session_id, user_id, refresh_token_hash, device_id, expires_at_ms, last_heartbeat_at_ms, last_heartbeat_request_id, revoked_at_ms, revoked_reason, revoked_request_id, revoked_by, created_at_ms, updated_at_ms)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, '', '', '', ?, ?)
	`, appcode.Normalize(session.AppCode), session.SessionID, session.UserID, session.RefreshTokenHash, session.DeviceID, session.ExpiresAtMs, session.LastHeartbeatAtMs, session.LastHeartbeatRequestID, session.CreatedAtMs, session.UpdatedAtMs)
	if err != nil {
		return mapAuthDuplicateError(err)
	}

	return nil
}
