<?php
/**
 * 1MZ Music Player — Contas do leitor (e-mail)
 *
 * O app não pede login para tocar música. Mas para ACTIVAR funções
 * (add-ons marcados como "exige conta"), o user cria/entra numa conta por
 * e-mail. Estas contas são SEPARADAS das contas de artista.
 *
 * Endpoints (namespace 1mzmusic/v1):
 *   POST /account/register   { email, password, name? }
 *   POST /account/login      { email, password }
 *   GET  /account/me         (Authorization: Bearer <token>)
 *   POST /account/logout     (Authorization: Bearer <token>)
 *
 * @package 1MZ_Music_Player
 */

if ( ! defined( 'ABSPATH' ) ) exit;

add_action( 'rest_api_init', 'mp_register_account_routes' );

function mp_register_account_routes() {
    register_rest_route( MP_API_NAMESPACE, '/account/register', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_register',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/login', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_login',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/password-reset', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_password_reset_request',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/me', [
        'methods'             => 'GET',
        'callback'            => 'mp_handle_me',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/logout', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_logout',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/devices', [
        'methods'             => 'GET',
        'callback'            => 'mp_handle_account_devices',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/devices/revoke', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_account_device_revoke',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/devices/replace', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_account_device_replace',
        'permission_callback' => '__return_true',
    ] );
    register_rest_route( MP_API_NAMESPACE, '/account/resend-verification', [
        'methods'             => 'POST',
        'callback'            => 'mp_handle_resend_verification',
        'permission_callback' => '__return_true',
    ] );
    // Sincronização na nuvem dos dados personalizados (qualquer dispositivo).
    register_rest_route( MP_API_NAMESPACE, '/account/sync', [
        [
            'methods'             => 'GET',
            'callback'            => 'mp_handle_sync_get',
            'permission_callback' => '__return_true',
        ],
        [
            'methods'             => 'POST',
            'callback'            => 'mp_handle_sync_put',
            'permission_callback' => '__return_true',
        ],
    ] );
}

add_action( 'template_redirect', 'mp_handle_password_reset_page' );

/** URL que o user abre para verificar o e-mail. */
function mp_verify_url( $token ) {
    return add_query_arg( 'mp_verify', $token, home_url( '/' ) );
}

/** Envia o e-mail de verificação da conta do leitor. */
function mp_send_verification_email( $email, $name, $token ) {
    $url  = mp_verify_url( $token );
    $hi   = $name ? esc_html( $name ) : 'Olá';
    $subj = 'Confirma a tua conta — 1MZ Music';
    $body = '<div style="font-family:Arial,sans-serif;max-width:480px;margin:auto">'
          . '<h2 style="color:#6750A4">Bem-vindo ao 1MZ Music 🎵</h2>'
          . "<p>$hi, confirma o teu e-mail para continuares a usar o app.</p>"
          . '<p style="margin:24px 0"><a href="' . esc_url( $url ) . '" '
          . 'style="background:#6750A4;color:#fff;padding:12px 22px;border-radius:10px;text-decoration:none;font-weight:bold">Confirmar e-mail</a></p>'
          . '<p style="color:#666;font-size:13px">Se não confirmares em 30 dias, a conta será apagada. '
          . 'Se não foste tu, ignora este e-mail.</p>'
          . '<p style="color:#999;font-size:12px">' . esc_url( $url ) . '</p></div>';
    $headers = [ 'Content-Type: text/html; charset=UTF-8' ];
    return wp_mail( $email, $subj, $body, $headers );
}

function mp_handle_resend_verification( WP_REST_Request $request ) {
    global $wpdb;
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Sessão inválida.', 401, 'unauthorized' );
    }
    if ( (int) $row->verified === 1 ) {
        return rmbz_api_ok( [ 'verified' => true ] );
    }
    $token = $row->verify_token ?: bin2hex( random_bytes( 24 ) );
    $wpdb->update( $wpdb->prefix . 'mp_accounts', [ 'verify_token' => $token ], [ 'id' => $row->id ] );
    mp_send_verification_email( $row->email, $row->name, $token );
    return rmbz_api_ok( [ 'sent' => true ] );
}

/** Requests a short-lived reset link without revealing whether an email exists. */
function mp_handle_password_reset_request( WP_REST_Request $request ) {
    global $wpdb;
    $email = sanitize_email( (string) $request->get_param( 'email' ) );
    $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : 'unknown';
    $rate_key = 'mp_reset_rate_' . substr( hash( 'sha256', strtolower( $email ) . '|' . $ip ), 0, 40 );
    if ( get_transient( $rate_key ) ) {
        return rmbz_api_error( 'Aguarda alguns minutos antes de pedir outro link.', 429, 'rate_limited' );
    }
    set_transient( $rate_key, 1, 5 * MINUTE_IN_SECONDS );

    if ( is_email( $email ) ) {
        $account = $wpdb->get_row( $wpdb->prepare(
            "SELECT id, email, name FROM {$wpdb->prefix}mp_accounts WHERE email = %s AND status = 'active' LIMIT 1",
            $email
        ) );
        if ( $account ) {
            $token = bin2hex( random_bytes( 32 ) );
            set_transient(
                'mp_password_reset_' . hash( 'sha256', $token ),
                [ 'account_id' => (int) $account->id ],
                30 * MINUTE_IN_SECONDS
            );
            mp_send_password_reset_email( $account, $token );
        }
    }

    return rmbz_api_ok( [
        'sent'    => true,
        'message' => 'Se a conta existir, enviaremos um link válido por 30 minutos.',
    ] );
}

function mp_send_password_reset_email( $account, $token ) {
    $url = add_query_arg( 'mp_reset', $token, home_url( '/' ) );
    $name = $account->name ? esc_html( $account->name ) : 'Olá';
    $subject = 'Redefinir a tua senha - 1MZ Music';
    $body = '<div style="font-family:Arial,sans-serif;max-width:480px;margin:auto">'
        . '<h2 style="color:#6750A4">Proteção da conta 1MZ Music</h2>'
        . '<p>' . $name . ', recebemos um pedido para redefinir a tua senha.</p>'
        . '<p style="margin:24px 0"><a href="' . esc_url( $url ) . '" '
        . 'style="background:#6750A4;color:#fff;padding:12px 22px;border-radius:8px;text-decoration:none;font-weight:bold">Criar nova senha</a></p>'
        . '<p style="color:#666;font-size:13px">O link expira em 30 minutos e só pode ser usado uma vez. Se não foste tu, ignora este e-mail.</p>'
        . '</div>';
    return wp_mail( $account->email, $subject, $body, [ 'Content-Type: text/html; charset=UTF-8' ] );
}

/** Minimal secure web form opened from the reset email. */
function mp_handle_password_reset_page() {
    if ( empty( $_GET['mp_reset'] ) && empty( $_POST['mp_reset_token'] ) ) {
        return;
    }
    global $wpdb;
    $token = sanitize_text_field( wp_unslash( $_POST['mp_reset_token'] ?? $_GET['mp_reset'] ?? '' ) );
    $key = preg_match( '/^[a-f0-9]{64}$/', $token )
        ? 'mp_password_reset_' . hash( 'sha256', $token )
        : '';
    $claim = $key !== '' ? get_transient( $key ) : false;
    $error = '';
    $success = false;

    if ( ! is_array( $claim ) || empty( $claim['account_id'] ) ) {
        $error = 'Este link é inválido ou expirou. Pede um novo link no app.';
    } elseif ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {
        $password = (string) wp_unslash( $_POST['password'] ?? '' );
        $confirm = (string) wp_unslash( $_POST['password_confirm'] ?? '' );
        if ( strlen( $password ) < 8 ) {
            $error = 'A senha precisa de pelo menos 8 caracteres.';
        } elseif ( ! hash_equals( $password, $confirm ) ) {
            $error = 'As senhas não são iguais.';
        } else {
            $account_id = (int) $claim['account_id'];
            $wpdb->query( 'START TRANSACTION' );
            $updated = $wpdb->update(
                $wpdb->prefix . 'mp_accounts',
                [ 'pass_hash' => wp_hash_password( $password ), 'token_hash' => null ],
                [ 'id' => $account_id ]
            );
            if ( false !== $updated ) {
                $wpdb->delete( $wpdb->prefix . 'mp_account_sessions', [ 'account_id' => $account_id ] );
                $web_hash = (string) get_option( 'mp_webtok_acc_' . $account_id, '' );
                if ( $web_hash !== '' ) {
                    delete_option( 'mp_webtok_' . $web_hash );
                }
                delete_option( 'mp_webtok_acc_' . $account_id );
                $wpdb->query( 'COMMIT' );
                delete_transient( $key );
                $success = true;
            } else {
                $wpdb->query( 'ROLLBACK' );
                $error = 'Não foi possível atualizar a senha. Tenta novamente.';
            }
        }
    }

    status_header( $success ? 200 : ( $error !== '' ? 400 : 200 ) );
    nocache_headers();
    ?><!doctype html>
    <html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Segurança da conta - 1MZ Music</title>
    <style>body{margin:0;background:#f7f5fa;color:#1d1b20;font:16px system-ui,sans-serif;display:grid;min-height:100vh;place-items:center}.panel{width:min(420px,calc(100% - 40px));background:#fff;border:1px solid #e6e0e9;border-radius:12px;padding:28px;box-sizing:border-box}h1{font-size:25px;margin:0 0 8px}p{color:#49454f;line-height:1.5}label{display:block;font-weight:650;margin:18px 0 6px}input{width:100%;padding:13px;border:1px solid #79747e;border-radius:8px;box-sizing:border-box;font:inherit}button{width:100%;margin-top:22px;padding:14px;border:0;border-radius:8px;background:#6750a4;color:#fff;font:700 16px system-ui;cursor:pointer}.error{color:#b3261e}.ok{color:#146c2e}</style></head><body><main class="panel">
    <h1><?php echo $success ? 'Senha atualizada' : 'Criar nova senha'; ?></h1>
    <?php if ( $success ) : ?>
        <p class="ok">A senha foi alterada e todas as sessões antigas foram terminadas. Já podes entrar novamente no app.</p>
    <?php elseif ( $error !== '' && ! $claim ) : ?>
        <p class="error"><?php echo esc_html( $error ); ?></p>
    <?php else : ?>
        <?php if ( $error !== '' ) : ?><p class="error"><?php echo esc_html( $error ); ?></p><?php endif; ?>
        <form method="post">
            <input type="hidden" name="mp_reset_token" value="<?php echo esc_attr( $token ); ?>">
            <label for="password">Nova senha</label><input id="password" name="password" type="password" minlength="8" autocomplete="new-password" required>
            <label for="password_confirm">Confirmar senha</label><input id="password_confirm" name="password_confirm" type="password" minlength="8" autocomplete="new-password" required>
            <button type="submit">Atualizar senha</button>
        </form>
    <?php endif; ?>
    </main></body></html><?php
    exit;
}

/** Conta tem VIP activo? (flag manual ou subscrição dentro da validade) */
function mp_is_vip( $row ) {
    if ( ! $row || (int) $row->is_vip !== 1 ) {
        return false;
    }
    if ( ! empty( $row->vip_until ) ) {
        return strtotime( $row->vip_until ) > time();
    }
    return true;
}

/** Entitlements de uma conta: funções que ela pode usar. */
function mp_account_entitlements( $row ) {
    $stored = $row && $row->entitlements ? json_decode( $row->entitlements, true ) : null;
    $base = is_array( $stored )
        ? array_values( array_intersect( $stored, mp_account_gated_features() ) )
        : mp_account_gated_features(); // ter conta activa desbloqueia as funções que exigem conta
    if ( mp_is_vip( $row ) ) {
        $base[] = 'vip';
        // VIP desbloqueia também todas as funções marcadas como "só VIP".
        $base = array_merge( $base, mp_vip_only_features() );
    }
    return array_values( array_unique( $base ) );
}

/** Serializa a conta para o app (sem dados sensíveis). */
function mp_account_payload( $row, $token = null ) {
    $vip = mp_is_vip( $row );
    global $wpdb;
    mp_prune_stale_mobile_sessions( (int) $row->id );
    $device_limit = mp_mobile_session_limit( $row );
    $device_usage = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->prefix}mp_account_sessions WHERE account_id = %d",
        (int) $row->id
    ) );
    $out = [
        'id'           => (int) $row->id,
        'email'        => $row->email,
        'name'         => $row->name,
        'status'       => $row->status,
        'verified'     => ( (int) $row->verified === 1 ),
        'is_vip'       => $vip,
        'vip_until'    => $row->vip_until,
        'vip_source'   => $vip ? mp_vip_source( $row ) : null,
        'cloud_audio_allowed' => mp_account_cloud_audio_allowed( $row ),
        'platform'     => $row->platform,
        'entitlements' => mp_account_entitlements( $row ),
        'device_usage' => $device_usage,
        'device_limit' => $device_limit,
        'device_warning' => $device_limit > 1
            ? ( $device_usage >= $device_limit
                ? 'Atingiste o limite de dispositivos. Remove um aparelho antes de adicionar outro.'
                : ( $device_usage === $device_limit - 1
                    ? 'Resta apenas uma vaga de dispositivo.'
                    : null ) )
            : null,
    ];
    if ( $token !== null ) {
        $out['token'] = $token;
    }
    return $out;
}

/** Guarda o dispositivo activo (single-session: 1 dispositivo por conta). */
function mp_capture_device( WP_REST_Request $request, $account_id ) {
    $platform = sanitize_key( (string) $request->get_param( 'platform' ) );
    if ( ! in_array( $platform, [ 'android', 'ios' ], true ) ) {
        $platform = null;
    }
    $update = [];
    if ( $platform ) {
        $update['platform'] = $platform;
    }
    $model = sanitize_text_field( (string) $request->get_param( 'device_model' ) );
    if ( $model !== '' ) {
        $update['device_model'] = $model;
    }
    $device_id = sanitize_text_field( (string) $request->get_param( 'device_id' ) );
    if ( $device_id !== '' ) {
        $update['device_id'] = $device_id;
    }
    if ( $update ) {
        global $wpdb;
        $wpdb->update( $wpdb->prefix . 'mp_accounts', $update, [ 'id' => $account_id ] );
    }
}

/** Emite um novo token (invalida o anterior → 1 dispositivo activo) e devolve o raw. */
function mp_request_device_id( ?WP_REST_Request $request ) {
    if ( ! $request ) {
        return 'legacy-' . substr( hash( 'sha256', wp_generate_uuid4() ), 0, 32 );
    }
    $device_id = substr( sanitize_text_field( (string) $request->get_param( 'device_id' ) ), 0, 64 );
    if ( $device_id !== '' ) {
        return $device_id;
    }
    $fingerprint = (string) $request->get_param( 'platform' ) . '|'
        . (string) $request->get_param( 'device_model' ) . '|'
        . (string) $request->get_header( 'user-agent' );
    return 'legacy-' . substr( hash( 'sha256', $fingerprint ), 0, 32 );
}

/** Emits a token for one mobile device without exposing other sessions. */
function mp_issue_token( $account_id, ?WP_REST_Request $request = null ) {
    global $wpdb;
    $token = bin2hex( random_bytes( 32 ) );
    $hash  = mp_hash_token( $token );
    $row   = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}mp_accounts WHERE id = %d LIMIT 1",
        $account_id
    ) );
    $device_id = mp_request_device_id( $request );
    $platform = $request ? sanitize_key( (string) $request->get_param( 'platform' ) ) : '';
    $model = $request ? substr( sanitize_text_field( (string) $request->get_param( 'device_model' ) ), 0, 120 ) : '';
    $now = current_time( 'mysql', true );
    $sessions = $wpdb->prefix . 'mp_account_sessions';
    $existing_id = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT id FROM {$sessions} WHERE account_id = %d AND device_id = %s LIMIT 1",
        $account_id,
        $device_id
    ) );
    $session_data = [
        'account_id'  => (int) $account_id,
        'device_id'   => $device_id,
        'token_hash'  => $hash,
        'platform'    => $platform ?: null,
        'device_model'=> $model ?: null,
        'last_seen'   => $now,
    ];
    if ( $existing_id > 0 ) {
        $wpdb->update( $sessions, $session_data, [ 'id' => $existing_id ] );
    } else {
        $session_data['created_at'] = $now;
        $wpdb->insert( $sessions, $session_data );
    }

    // Never retire another phone implicitly. Login checks capacity first; this
    // final guard handles two devices racing to claim the last slot.
    $limit = mp_mobile_session_limit( $row );
    $session_count = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT COUNT(*) FROM {$sessions} WHERE account_id = %d",
        $account_id
    ) );
    if ( $session_count > $limit ) {
        $wpdb->delete( $sessions, [ 'token_hash' => $hash ] );
        return null;
    }

    // Keep the legacy column current for rolling deployments and old plugin code.
    $wpdb->update(
        $wpdb->prefix . 'mp_accounts',
        [ 'token_hash' => $hash, 'last_login' => $now ],
        [ 'id' => $account_id ]
    );
    return $token;
}

function mp_login_rate_key( $email ) {
    $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : 'unknown';
    return 'mp_login_fail_' . substr( hash( 'sha256', strtolower( (string) $email ) . '|' . $ip ), 0, 40 );
}

function mp_create_device_challenge( $account_id, WP_REST_Request $request ) {
    $raw = bin2hex( random_bytes( 32 ) );
    $key = 'mp_device_challenge_' . hash( 'sha256', $raw );
    set_transient( $key, [
        'account_id' => (int) $account_id,
        'device_id'  => mp_request_device_id( $request ),
        'platform'   => sanitize_key( (string) $request->get_param( 'platform' ) ),
        'model'      => substr( sanitize_text_field( (string) $request->get_param( 'device_model' ) ), 0, 120 ),
    ], 5 * MINUTE_IN_SECONDS );
    return $raw;
}

function mp_device_limit_error( $row, WP_REST_Request $request ) {
    $limit = mp_mobile_session_limit( $row );
    $sessions = mp_mobile_sessions_payload( (int) $row->id );
    return rmbz_api_error(
        'Atingiste o limite de dispositivos. Escolhe um aparelho para substituir.',
        409,
        'device_limit',
        [
            'challenge'  => mp_create_device_challenge( (int) $row->id, $request ),
            'expires_in' => 300,
            'used'       => count( $sessions ),
            'limit'      => $limit,
            'devices'    => $sessions,
        ]
    );
}

function mp_handle_register( WP_REST_Request $request ) {
    global $wpdb;
    $table    = $wpdb->prefix . 'mp_accounts';
    $email    = sanitize_email( (string) $request->get_param( 'email' ) );
    $password = (string) $request->get_param( 'password' );
    $name     = sanitize_text_field( (string) $request->get_param( 'name' ) );

    if ( ! is_email( $email ) ) {
        return rmbz_api_error( 'E-mail inválido.', 422, 'invalid_email' );
    }
    if ( strlen( $password ) < 8 ) {
        return rmbz_api_error( 'A senha precisa de pelo menos 8 caracteres.', 422, 'weak_password' );
    }
    $exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE email = %s", $email ) );
    if ( $exists ) {
        return rmbz_api_error( 'Já existe uma conta com este e-mail.', 409, 'email_taken' );
    }

    $verify_token = bin2hex( random_bytes( 24 ) );
    $wpdb->insert( $table, [
        'email'        => $email,
        'pass_hash'    => wp_hash_password( $password ),
        'name'         => $name ?: null,
        'status'       => 'active',
        'verified'     => 0,
        'verify_token' => $verify_token,
        'created_at'   => current_time( 'mysql', true ),
    ] );
    $id  = (int) $wpdb->insert_id;
    if ( ! $id ) {
        return rmbz_api_error( 'Não foi possível criar a conta.', 500, 'create_failed' );
    }
    mp_send_verification_email( $email, $name, $verify_token );
    $token = mp_issue_token( $id, $request );
    if ( ! $token ) {
        return rmbz_api_error( 'Conta criada, mas não foi possível abrir a sessão.', 503, 'session_failed' );
    }
    mp_capture_device( $request, $id );
    $row   = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) );
    return rmbz_api_ok( mp_account_payload( $row, $token ) );
}

function mp_handle_login( WP_REST_Request $request ) {
    global $wpdb;
    $table    = $wpdb->prefix . 'mp_accounts';
    $email    = sanitize_email( (string) $request->get_param( 'email' ) );
    $password = (string) $request->get_param( 'password' );
    $rate_key = mp_login_rate_key( $email );
    $failures = (int) get_transient( $rate_key );
    if ( $failures >= 5 ) {
        return rmbz_api_error(
            'Muitas tentativas. Aguarda 15 minutos antes de tentar novamente.',
            429,
            'rate_limited',
            [ 'retry_after' => 900 ]
        );
    }

    $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE email = %s", $email ) );
    if ( ! $row || ! wp_check_password( $password, $row->pass_hash ) ) {
        set_transient( $rate_key, $failures + 1, 15 * MINUTE_IN_SECONDS );
        return rmbz_api_error( 'E-mail ou senha incorretos.', 401, 'invalid_credentials' );
    }
    delete_transient( $rate_key );
    if ( $row->status !== 'active' ) {
        return rmbz_api_error( 'Esta conta está bloqueada.', 403, 'account_blocked' );
    }
    if ( 'web' === sanitize_key( (string) $request->get_param( 'platform' ) ) ) {
        // Leitor web (play.1musicmoz.com): sessão própria — NÃO invalida o token do telemóvel.
        $token = mp_issue_web_token( (int) $row->id );
    } else {
        $sessions = $wpdb->prefix . 'mp_account_sessions';
        mp_prune_stale_mobile_sessions( (int) $row->id );
        $device_id = mp_request_device_id( $request );
        $known_device = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$sessions} WHERE account_id = %d AND device_id = %s",
            (int) $row->id,
            $device_id
        ) );
        $used = (int) $wpdb->get_var( $wpdb->prepare(
            "SELECT COUNT(*) FROM {$sessions} WHERE account_id = %d",
            (int) $row->id
        ) );
        if ( $known_device === 0 && $used >= mp_mobile_session_limit( $row ) ) {
            return mp_device_limit_error( $row, $request );
        }
        $token = mp_issue_token( (int) $row->id, $request );
        if ( ! $token ) {
            return mp_device_limit_error( $row, $request );
        }
        mp_capture_device( $request, (int) $row->id );
    }
    $row   = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $row->id ) );
    return rmbz_api_ok( mp_account_payload( $row, $token ) );
}

/**
 * Token de sessão WEB (30 dias), guardado em options para não tocar na coluna
 * token_hash (a sessão única do app). Uma sessão web por conta — o login novo
 * substitui o anterior.
 */
function mp_issue_web_token( $account_id ) {
    $token = bin2hex( random_bytes( 32 ) );
    $hash  = mp_hash_token( $token );
    $prev  = (string) get_option( 'mp_webtok_acc_' . (int) $account_id, '' );
    if ( $prev !== '' ) {
        delete_option( 'mp_webtok_' . $prev );
    }
    update_option( 'mp_webtok_acc_' . (int) $account_id, $hash, 'no' );
    update_option( 'mp_webtok_' . $hash, (int) $account_id . '|' . ( time() + 30 * DAY_IN_SECONDS ), 'no' );
    return $token;
}

/** GET /account/sync — devolve os dados personalizados guardados na nuvem. */
function mp_handle_sync_get( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Sessão inválida.', 401, 'unauthorized' );
    }
    return rmbz_api_ok( [
        'data'       => $row->sync_data ? json_decode( $row->sync_data, true ) : null,
        'updated_at' => $row->sync_updated,
    ] );
}

/** POST /account/sync — guarda os dados personalizados (favoritos, playlists, YouTube, etc.). */
function mp_handle_sync_put( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Sessão inválida.', 401, 'unauthorized' );
    }
    $payload = $request->get_param( 'data' );
    if ( $payload === null ) {
        // Aceita também o corpo JSON inteiro como dados.
        $payload = $request->get_json_params();
    }
    $json = wp_json_encode( $payload );
    if ( strlen( (string) $json ) > 2 * 1024 * 1024 ) { // limite ~2MB
        return rmbz_api_error( 'Dados demasiado grandes.', 413, 'too_large' );
    }
    global $wpdb;
    $wpdb->update(
        $wpdb->prefix . 'mp_accounts',
        [ 'sync_data' => $json, 'sync_updated' => current_time( 'mysql', true ) ],
        [ 'id' => $row->id ]
    );
    return rmbz_api_ok( [ 'saved' => true, 'updated_at' => current_time( 'mysql', true ) ] );
}

function mp_handle_me( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Sessão inválida.', 401, 'unauthorized' );
    }
    return rmbz_api_ok( mp_account_payload( $row ) );
}

function mp_handle_logout( WP_REST_Request $request ) {
    global $wpdb;
    $row = mp_account_from_request( $request );
    if ( $row ) {
        $auth = (string) $request->get_header( 'authorization' );
        $raw = stripos( $auth, 'bearer ' ) === 0 ? trim( substr( $auth, 7 ) ) : '';
        $hash = $raw !== '' ? mp_hash_token( $raw ) : '';
        if ( $hash !== '' ) {
            if ( get_option( 'mp_webtok_' . $hash, '' ) !== '' ) {
                delete_option( 'mp_webtok_' . $hash );
                if ( get_option( 'mp_webtok_acc_' . (int) $row->id, '' ) === $hash ) {
                    delete_option( 'mp_webtok_acc_' . (int) $row->id );
                }
            }
            $wpdb->delete( $wpdb->prefix . 'mp_account_sessions', [ 'token_hash' => $hash ] );
            if ( hash_equals( (string) $row->token_hash, $hash ) ) {
                $wpdb->update(
                    $wpdb->prefix . 'mp_accounts',
                    [ 'token_hash' => null ],
                    [ 'id' => $row->id ]
                );
            }
        }
    }
    return rmbz_api_ok( [ 'logged_out' => true ] );
}

function mp_account_devices_state( $row, $current_hash = '' ) {
    $devices = mp_mobile_sessions_payload( (int) $row->id, $current_hash );
    $limit = mp_mobile_session_limit( $row );
    return [
        'devices' => $devices,
        'used'    => count( $devices ),
        'limit'   => $limit,
        'full'    => count( $devices ) >= $limit,
        'warning' => count( $devices ) >= $limit
            ? 'Todos os lugares estão ocupados.'
            : ( count( $devices ) === $limit - 1 ? 'Resta apenas um lugar.' : null ),
    ];
}

function mp_handle_account_devices( WP_REST_Request $request ) {
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Sessão inválida.', 401, 'unauthorized' );
    }
    return rmbz_api_ok( mp_account_devices_state( $row, mp_request_token_hash( $request ) ) );
}

function mp_handle_account_device_revoke( WP_REST_Request $request ) {
    global $wpdb;
    $row = mp_account_from_request( $request );
    if ( ! $row ) {
        return rmbz_api_error( 'Sessão inválida.', 401, 'unauthorized' );
    }
    $session_id = absint( $request->get_param( 'session_id' ) );
    $sessions = $wpdb->prefix . 'mp_account_sessions';
    $session = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$sessions} WHERE id = %d AND account_id = %d LIMIT 1",
        $session_id,
        (int) $row->id
    ) );
    if ( ! $session ) {
        return rmbz_api_error( 'Dispositivo não encontrado.', 404, 'device_not_found' );
    }
    $current_hash = mp_request_token_hash( $request );
    if ( $current_hash !== '' && hash_equals( (string) $session->token_hash, $current_hash ) ) {
        return rmbz_api_error( 'Usa “Terminar sessão” para remover este dispositivo.', 409, 'current_device' );
    }
    $wpdb->delete( $sessions, [ 'id' => $session_id, 'account_id' => (int) $row->id ] );
    if ( hash_equals( (string) $row->token_hash, (string) $session->token_hash ) ) {
        $wpdb->update( $wpdb->prefix . 'mp_accounts', [ 'token_hash' => null ], [ 'id' => $row->id ] );
    }
    return rmbz_api_ok( mp_account_devices_state( $row, $current_hash ) );
}

function mp_handle_account_device_replace( WP_REST_Request $request ) {
    global $wpdb;
    $challenge = sanitize_text_field( (string) $request->get_param( 'challenge' ) );
    if ( ! preg_match( '/^[a-f0-9]{64}$/', $challenge ) ) {
        return rmbz_api_error( 'Pedido de substituição inválido.', 401, 'invalid_challenge' );
    }
    $transient_key = 'mp_device_challenge_' . hash( 'sha256', $challenge );
    $claim = get_transient( $transient_key );
    if ( ! is_array( $claim ) || empty( $claim['account_id'] ) ) {
        return rmbz_api_error( 'Este pedido expirou. Entra novamente.', 401, 'challenge_expired' );
    }
    if ( ! hash_equals( (string) $claim['device_id'], mp_request_device_id( $request ) ) ) {
        return rmbz_api_error( 'O pedido pertence a outro dispositivo.', 403, 'challenge_device_mismatch' );
    }
    $request_platform = sanitize_key( (string) $request->get_param( 'platform' ) );
    $request_model = substr( sanitize_text_field( (string) $request->get_param( 'device_model' ) ), 0, 120 );
    if ( ! hash_equals( (string) $claim['platform'], $request_platform )
        || ! hash_equals( (string) $claim['model'], $request_model ) ) {
        return rmbz_api_error( 'Os dados do dispositivo mudaram. Entra novamente.', 403, 'challenge_device_mismatch' );
    }
    $account_id = (int) $claim['account_id'];
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}mp_accounts WHERE id = %d AND status = 'active' LIMIT 1",
        $account_id
    ) );
    if ( ! $row ) {
        return rmbz_api_error( 'Conta indisponível.', 403, 'account_blocked' );
    }
    $session_id = absint( $request->get_param( 'session_id' ) );
    $sessions = $wpdb->prefix . 'mp_account_sessions';
    $target = $wpdb->get_row( $wpdb->prepare(
        "SELECT id FROM {$sessions} WHERE id = %d AND account_id = %d LIMIT 1",
        $session_id,
        $account_id
    ) );
    if ( ! $target ) {
        return rmbz_api_error( 'O dispositivo escolhido já não está ativo.', 409, 'device_changed' );
    }

    delete_transient( $transient_key ); // one use only
    if ( mp_mobile_session_limit( $row ) === 1 ) {
        $wpdb->delete( $sessions, [ 'account_id' => $account_id ] );
    } else {
        $wpdb->delete( $sessions, [ 'id' => $session_id, 'account_id' => $account_id ] );
    }
    $token = mp_issue_token( $account_id, $request );
    if ( ! $token ) {
        return rmbz_api_error( 'O limite mudou. Entra novamente para atualizar a lista.', 409, 'device_limit_changed' );
    }
    mp_capture_device( $request, $account_id );
    $row = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}mp_accounts WHERE id = %d LIMIT 1",
        $account_id
    ) );
    return rmbz_api_ok( mp_account_payload( $row, $token ) );
}
