<?php
// =============================================================
//  POST /api/v1/auth/reset_password
//
//  Verifies the OTP against the reset_token from auth/forgot_password.php
//  and sets a new password — the mobile equivalent of
//  ajax/forgot_password_handler.php's verify_otp + reset_password actions
//  combined into one call (no separate "OTP verified" session flag to
//  carry between them; the token is stateless, see forgot_password.php's
//  header comment).
//
//  Guess-capping: since the token can't mutate itself between calls, the
//  "5 wrong OTPs and you're locked out" property from the web flow is
//  enforced via the existing file-based rate limiter instead, keyed to
//  this specific reset_token so it can't be sidestepped by requesting a
//  fresh one from the same IP.
//
//  Body: { "reset_token": "...", "otp": "123456", "new_password": "...", "confirm_password": "..." }
//  Always responds with the same success message on a well-formed request
//  that fails validation for account-existence reasons — see
//  forgot_password.php's anti-enumeration note.
// =============================================================

require_once __DIR__ . '/../_bootstrap.php';
require_once __DIR__ . '/../../../include/rate_limiter.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    Api::error(405, 'MethodNotAllowed', 'Use POST');
}

$body            = Api::body();
$resetToken      = (string)($body['reset_token'] ?? '');
$otp             = trim((string)($body['otp'] ?? ''));
$newPassword     = (string)($body['new_password'] ?? '');
$confirmPassword = (string)($body['confirm_password'] ?? '');

if ($resetToken === '' || $otp === '') {
    Api::error(422, 'ValidationError', 'reset_token and otp are required');
}
if ($newPassword === '' || $confirmPassword === '') {
    Api::error(422, 'ValidationError', 'New password and confirmation are required');
}
if ($newPassword !== $confirmPassword) {
    Api::error(422, 'ValidationError', 'New password and confirmation do not match');
}
if (strlen($newPassword) < 6) {
    Api::error(422, 'ValidationError', 'Password must be at least 6 characters');
}

$rl = rateLimitAttempt('api_otp_verify:' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . ':' . hash('sha256', $resetToken), 5, 900);
if ($rl['blocked']) {
    Api::error(429, 'TooManyAttempts', 'Too many incorrect attempts. Please request a new code.', [
        'retry_after' => $rl['retry_after'],
    ]);
}

$pending = PendingAuthToken::verify($resetToken, 'password_reset');
$invalidMessage = 'Invalid or expired code. Please request a new one.';

if (!$pending || !password_verify($otp, (string)($pending['otp_hash'] ?? ''))) {
    Api::error(401, 'InvalidCode', $invalidMessage);
}

$table = $pending['table'];
$id    = (int)$pending['id'];
$email = (string)$pending['email'];
$hashed = password_hash($newPassword, PASSWORD_DEFAULT);

$stmt = $conn->prepare("UPDATE `{$table}` SET password = ?, password_change_required = 0 WHERE id = ? AND email = ?");
$stmt->bind_param('sis', $hashed, $id, $email);
$stmt->execute();
$updated = $stmt->affected_rows > 0;
$stmt->close();

if ($updated) {
    $token_manager->revokeAllForAccount($table, $id);
}

// Same response whether or not a real row was updated — see forgot_password.php.
Api::success(['status' => 'password_reset', 'message' => 'Password reset successfully. You can now log in.']);
