<?php
declare(strict_types=1);

/**
 * /reset_password.php
 * Встановлення нового пароля по одноразовому токену з email.
 *
 * Поведінка:
 * - noindex + no-store
 * - CSRF
 * - IP block (як для login)
 * - rate-limit (email scopes + uid scopes)
 * - токен одноразовий, зберігається в окремій таблиці як sha256
 * - після успішного reset користувач автоматично логіниться
 */

require_once __DIR__ . '/includes/start.php';
require_once __DIR__ . '/includes/security.php';
require_once __DIR__ . '/includes/security_ip_rules.php';
require_once __DIR__ . '/includes/security_rate_limit.php';

if (!isset($pdo) || !($pdo instanceof PDO)) {
    $pdo = function_exists('db') ? db() : null;
}
if (!($pdo instanceof PDO)) {
    http_response_code(500);
    exit('DB error');
}

if (!function_exists('tr_reset')) {
    function tr_reset(string $key, array $fallbacks): string {
        $v = function_exists('t') ? (string)t($key) : $key;
        if ($v !== '' && $v !== $key) return $v;

        $l = function_exists('lang') ? (string)lang() : 'uk';
        if (!in_array($l, ['uk', 'ru', 'en'], true)) $l = 'uk';

        return (string)($fallbacks[$l] ?? $fallbacks['uk'] ?? $key);
    }
}
if (!function_exists('h')) {
    function h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
}
if (!function_exists('db_has_column')) {
    function db_has_column(PDO $pdo, string $table, string $col): bool {
        try {
            $st = $pdo->prepare("SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ? LIMIT 1");
            $st->execute([$table, $col]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}
if (!function_exists('pick_first_existing_col')) {
    function pick_first_existing_col(PDO $pdo, string $table, array $candidates): ?string {
        foreach ($candidates as $c) {
            if (db_has_column($pdo, $table, (string)$c)) return (string)$c;
        }
        return null;
    }
}
if (!function_exists('db_table_exists_local')) {
    function db_table_exists_local(PDO $pdo, string $table): bool {
        if (function_exists('start_db_table_exists')) {
            try {
                return (bool)start_db_table_exists($pdo, $table);
            } catch (Throwable $e) {
                // fallback нижче
            }
        }

        try {
            $st = $pdo->prepare("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ? LIMIT 1");
            $st->execute([$table]);
            return (bool)$st->fetchColumn();
        } catch (Throwable $e) {
            return false;
        }
    }
}
if (!function_exists('redirect_303')) {
    function redirect_303(string $to): void {
        if (!headers_sent()) {
            header('Location: ' . $to, true, 303);
        } else {
            echo '<meta http-equiv="refresh" content="0;url=' . htmlspecialchars($to, ENT_QUOTES, 'UTF-8') . '">';
            echo '<script>location.href=' . json_encode($to) . ';</script>';
        }
        exit;
    }
}

function reset_rate_build_email_scopes(string $mode, string $ip, string $email): array {
    $ip = trim($ip);
    $email = mb_strtolower(trim($email), 'UTF-8');

    $scopes = [];

    if ($mode === 'ip') {
        if ($ip !== '') $scopes[] = ['type' => 'ip', 'key' => $ip];
    } elseif ($mode === 'user') {
        if ($email !== '') $scopes[] = ['type' => 'email', 'key' => $email];
    } else {
        if ($ip !== '' && $email !== '') $scopes[] = ['type' => 'ip_email', 'key' => $ip . '|' . $email];
        if ($ip !== '') $scopes[] = ['type' => 'ip', 'key' => $ip];
        if ($email !== '') $scopes[] = ['type' => 'email', 'key' => $email];
    }

    $uniq = [];
    foreach ($scopes as $s) {
        $k = (string)$s['type'] . '|' . (string)$s['key'];
        $uniq[$k] = $s;
    }
    return array_values($uniq);
}

function reset_rate_lock_message(?string $lockedUntil): string {
    $lockedUntil = trim((string)$lockedUntil);

    $generic = tr_reset('rate_locked_generic', [
        'uk' => 'Забагато спроб. Спробуйте пізніше.',
        'ru' => 'Слишком много попыток. Попробуйте позже.',
        'en' => 'Too many attempts. Try again later.',
    ]);

    if ($lockedUntil === '') return $generic;

    $ts = strtotime($lockedUntil);
    if ($ts <= 0) return $generic;

    $leftSec = $ts - time();
    if ($leftSec < 0) $leftSec = 0;
    $mins = (int)ceil($leftSec / 60);
    if ($mins < 1) $mins = 1;

    return tr_reset('rate_locked_until', [
        'uk' => 'Забагато спроб. Спробуйте знову через ' . $mins . ' хв.',
        'ru' => 'Слишком много попыток. Попробуйте снова через ' . $mins . ' мин.',
        'en' => 'Too many attempts. Try again in ' . $mins . ' min.',
    ]);
}

function reset_password_load_row(PDO $pdo, string $rawToken, string $emailCol, string $passCol, ?string $activeCol): ?array {
    $rawToken = trim($rawToken);
    if ($rawToken === '' || strlen($rawToken) < 20) return null;

    $hash = hash('sha256', $rawToken);

    $cols = [
        'pr.id AS reset_id',
        'pr.user_id',
        'pr.email AS reset_email',
        'pr.token_hash',
        'pr.expires_at',
        'pr.used_at',
        "u.`$emailCol` AS user_email_val",
        "u.`$passCol` AS pass_val",
    ];
    if ($activeCol) {
        $cols[] = "u.`$activeCol` AS active_val";
    }

    $sql = 'SELECT ' . implode(', ', $cols) . '
            FROM password_reset_tokens pr
            INNER JOIN users u ON u.id = pr.user_id
            WHERE pr.token_hash = ?
            LIMIT 1';

    $st = $pdo->prepare($sql);
    $st->execute([$hash]);
    $row = $st->fetch(PDO::FETCH_ASSOC) ?: null;

    return $row ?: null;
}

function reset_password_is_row_usable(?array $row, ?string $activeCol): bool {
    if (!is_array($row)) return false;

    $usedAt = trim((string)($row['used_at'] ?? ''));
    if ($usedAt !== '') return false;

    $expTs = strtotime((string)($row['expires_at'] ?? ''));
    if ($expTs <= 0 || $expTs < time()) return false;

    if ($activeCol && (int)($row['active_val'] ?? 0) !== 1) {
        return false;
    }

    $uid = (int)($row['user_id'] ?? 0);
    $passVal = (string)($row['pass_val'] ?? '');
    $emailVal = trim((string)($row['user_email_val'] ?? ''));

    return $uid > 0 && $passVal !== '' && $emailVal !== '';
}

$title = tr_reset('reset_password_title', [
    'uk' => 'Новий пароль',
    'ru' => 'Новый пароль',
    'en' => 'New password',
]);

$meta_robots = 'noindex, nofollow, noarchive, nosnippet';
$meta_desc   = '';
$canonical   = function_exists('abs_url') ? abs_url('/reset_password.php') : '/reset_password.php';
if (!headers_sent()) {
    header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet', true);
    header('Cache-Control: no-store', true);
}

$emailCol  = pick_first_existing_col($pdo, 'users', ['email']);
$passCol   = pick_first_existing_col($pdo, 'users', ['pass_hash', 'password_hash']);
$activeCol = pick_first_existing_col($pdo, 'users', ['is_active', 'active', 'verified', 'email_verified', 'is_verified']);
$resetTableOk = db_table_exists_local($pdo, 'password_reset_tokens');

$schemaError = null;
if (!$emailCol || !$passCol) {
    $schemaError = 'Table users must contain email and pass_hash/password_hash columns.';
} elseif (!$resetTableOk) {
    $schemaError = 'Table password_reset_tokens not found. Run the SQL migration first.';
}

$token = trim((string)($_GET['token'] ?? ($_POST['token'] ?? '')));
$selfUrl = '/reset_password.php';
if ($token !== '') {
    $selfUrl .= '?token=' . urlencode($token);
}

$row = null;
$tokenOk = false;
if ($schemaError === null && $token !== '') {
    try {
        $pdo->exec("DELETE FROM password_reset_tokens WHERE (used_at IS NOT NULL AND used_at < (NOW() - INTERVAL 7 DAY)) OR expires_at < (NOW() - INTERVAL 7 DAY)");
    } catch (Throwable $e) {}

    try {
        $row = reset_password_load_row($pdo, $token, $emailCol, $passCol, $activeCol);
        $tokenOk = reset_password_is_row_usable($row, $activeCol);
    } catch (Throwable $e) {
        $row = null;
        $tokenOk = false;
    }
}

if (is_post()) {
    $clientIp = (string)($_SERVER['REMOTE_ADDR'] ?? '');
    $emailForScope = $row ? mb_strtolower(trim((string)($row['user_email_val'] ?? '')), 'UTF-8') : '';

    try {
        if (function_exists('security_ip_is_blocked')) {
            $chk = security_ip_is_blocked($pdo, $clientIp, 'login');
            if (!empty($chk['blocked'])) {
                try { security_log_login_attempt($pdo, false, 'password_reset_confirm_ip_blocked', $row ? (int)($row['user_id'] ?? 0) : null, $emailForScope); } catch (Throwable $e) {}

                $reason = trim(strip_tags((string)($chk['reason'] ?? '')));
                if ($reason === '') $reason = 'Blocked';

                flash_set('err', tr_reset('ip_blocked_msg', [
                    'uk' => 'Вас заблоковано: ' . $reason,
                    'ru' => 'Вы заблокированы: ' . $reason,
                    'en' => 'You are blocked: ' . $reason,
                ]));
                redirect_303($selfUrl);
            }
        }
    } catch (Throwable $e) {}

    $rateSettings = null;
    $rateEmailScopes = [];
    $rateUserScopes  = [];
    try {
        $rateSettings = security_rate_get_settings($pdo);
        $mode = (string)($rateSettings['mode'] ?? 'both');
        if (!in_array($mode, ['ip', 'user', 'both'], true)) $mode = 'both';

        $rateEmailScopes = reset_rate_build_email_scopes($mode, $clientIp, $emailForScope);
        $lockedInfo = security_rate_is_locked($pdo, $rateEmailScopes);
        if (!empty($lockedInfo['locked'])) {
            try { security_log_login_attempt($pdo, false, 'password_reset_confirm_rate_locked', $row ? (int)($row['user_id'] ?? 0) : null, $emailForScope); } catch (Throwable $e) {}
            flash_set('err', reset_rate_lock_message((string)($lockedInfo['locked_until'] ?? '')));
            redirect_303($selfUrl);
        }

        $uid = $row ? (int)($row['user_id'] ?? 0) : 0;
        if ($uid > 0 && function_exists('security_rate_scopes')) {
            $rateUserScopes = security_rate_scopes($mode, $clientIp, $uid);
            $lockedInfo2 = security_rate_is_locked($pdo, $rateUserScopes);
            if (!empty($lockedInfo2['locked'])) {
                try { security_log_login_attempt($pdo, false, 'password_reset_confirm_rate_locked_uid', $uid, $emailForScope); } catch (Throwable $e) {}
                flash_set('err', reset_rate_lock_message((string)($lockedInfo2['locked_until'] ?? '')));
                redirect_303($selfUrl);
            }
        }
    } catch (Throwable $e) {
        $rateSettings = null;
        $rateEmailScopes = [];
        $rateUserScopes = [];
    }

    if (!csrf_check(post_str('csrf'))) {
        try { security_log_login_attempt($pdo, false, 'password_reset_confirm_csrf', $row ? (int)($row['user_id'] ?? 0) : null, $emailForScope); } catch (Throwable $e) {}

        try {
            if (is_array($rateSettings)) {
                if ($rateEmailScopes) security_rate_register_failure($pdo, $rateSettings, $rateEmailScopes);
                if ($rateUserScopes)  security_rate_register_failure($pdo, $rateSettings, $rateUserScopes);
            }
        } catch (Throwable $e) {}

        flash_set('err', t('bad_csrf'));
        redirect_303($selfUrl);
    }

    if ($schemaError !== null) {
        flash_set('err', $schemaError);
        redirect_303($selfUrl);
    }

    if (!$tokenOk || !$row) {
        try { security_log_login_attempt($pdo, false, 'password_reset_bad_token', null, $emailForScope); } catch (Throwable $e) {}
        flash_set('err', tr_reset('reset_password_bad_token', [
            'uk' => 'Посилання недійсне або вже використане.',
            'ru' => 'Ссылка недействительна или уже использована.',
            'en' => 'The link is invalid or has already been used.',
        ]));
        redirect_303($selfUrl);
    }

    $pass1 = post_str('password');
    $pass2 = post_str('password2');

    $err = null;
    if (mb_strlen($pass1) < 6) {
        $err = function_exists('t') ? (string)t('password_short') : tr_reset('password_short', [
            'uk' => 'Пароль занадто короткий.',
            'ru' => 'Пароль слишком короткий.',
            'en' => 'Password is too short.',
        ]);
    } elseif ($pass1 !== $pass2) {
        $err = function_exists('t') ? (string)t('password_mismatch') : tr_reset('password_mismatch', [
            'uk' => 'Паролі не співпадають.',
            'ru' => 'Пароли не совпадают.',
            'en' => 'Passwords do not match.',
        ]);
    }

    if ($err !== null) {
        try { security_log_login_attempt($pdo, false, 'password_reset_password_invalid', (int)($row['user_id'] ?? 0), $emailForScope); } catch (Throwable $e) {}

        try {
            if (is_array($rateSettings)) {
                if ($rateEmailScopes) security_rate_register_failure($pdo, $rateSettings, $rateEmailScopes);
                if ($rateUserScopes)  security_rate_register_failure($pdo, $rateSettings, $rateUserScopes);
            }
        } catch (Throwable $e) {}

        flash_set('err', $err);
        redirect_303($selfUrl);
    }

    try {
        $newHash = password_hash($pass1, PASSWORD_DEFAULT);
        $uid = (int)($row['user_id'] ?? 0);
        $userEmail = mb_strtolower(trim((string)($row['user_email_val'] ?? '')), 'UTF-8');

        $pdo->beginTransaction();

        $upUser = $pdo->prepare("UPDATE users SET `$passCol` = ? WHERE id = ? LIMIT 1");
        $upUser->execute([$newHash, $uid]);

        $upTokens = $pdo->prepare("UPDATE password_reset_tokens SET used_at = NOW() WHERE user_id = ? AND used_at IS NULL");
        $upTokens->execute([$uid]);

        $pdo->commit();

        if (function_exists('login_user')) {
            login_user($uid);
        }

        try {
            if (is_array($rateSettings)) {
                if ($rateEmailScopes) security_rate_clear($pdo, $rateEmailScopes);
                if ($rateUserScopes)  security_rate_clear($pdo, $rateUserScopes);
            }
        } catch (Throwable $e) {}

        try { security_log_login_attempt($pdo, true, 'password_reset_ok', $uid, $userEmail); } catch (Throwable $e) {}

        flash_set('ok', tr_reset('reset_password_success', [
            'uk' => 'Пароль успішно змінено.',
            'ru' => 'Пароль успешно изменён.',
            'en' => 'Password has been changed successfully.',
        ]));
        redirect_303('/');
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }

        try { security_log_login_attempt($pdo, false, 'password_reset_db_fail', (int)($row['user_id'] ?? 0), $emailForScope); } catch (Throwable $e2) {}

        try {
            if (is_array($rateSettings)) {
                if ($rateEmailScopes) security_rate_register_failure($pdo, $rateSettings, $rateEmailScopes);
                if ($rateUserScopes)  security_rate_register_failure($pdo, $rateSettings, $rateUserScopes);
            }
        } catch (Throwable $e3) {}

        flash_set('err', 'DB error');
        redirect_303($selfUrl);
    }
}

require __DIR__ . '/includes/header.php';
?>

<div class="row justify-content-center">
  <div class="col-md-6 col-lg-5">
    <div class="card card-app p-4">
      <h1 class="h4 mb-3"><?= h($title) ?></h1>

      <?php if ($schemaError !== null): ?>
        <div class="alert alert-danger mb-3"><?= h($schemaError) ?></div>
        <a class="btn btn-outline-secondary" href="/forgot_password.php">
          <?= h(tr_reset('forgot_password_link', [
              'uk' => 'Запросити нове посилання',
              'ru' => 'Запросить новую ссылку',
              'en' => 'Request a new link',
          ])) ?>
        </a>
      <?php elseif ($token === '' || !$tokenOk): ?>
        <div class="alert alert-danger mb-3">
          <?= h(tr_reset('reset_password_bad_token', [
              'uk' => 'Посилання недійсне або вже використане.',
              'ru' => 'Ссылка недействительна или уже использована.',
              'en' => 'The link is invalid or has already been used.',
          ])) ?>
        </div>
        <a class="btn btn-outline-secondary" href="/forgot_password.php">
          <?= h(tr_reset('forgot_password_link', [
              'uk' => 'Запросити нове посилання',
              'ru' => 'Запросить новую ссылку',
              'en' => 'Request a new link',
          ])) ?>
        </a>
      <?php else: ?>
        <p class="text-muted small mb-3">
          <?= h(tr_reset('reset_password_intro', [
              'uk' => 'Введи новий пароль для свого акаунта.',
              'ru' => 'Введи новый пароль для своего аккаунта.',
              'en' => 'Enter a new password for your account.',
          ])) ?>
        </p>

        <form method="post" class="vstack gap-3" action="<?= h($selfUrl) ?>">
          <?= csrf_field() ?>
          <input type="hidden" name="token" value="<?= h($token) ?>">

          <div>
            <label class="form-label"><?= h(t('password')) ?></label>
            <input class="form-control" type="password" name="password" required autocomplete="new-password">
          </div>

          <div>
            <label class="form-label"><?= h(t('repeat_password')) ?></label>
            <input class="form-control" type="password" name="password2" required autocomplete="new-password">
          </div>

          <button class="btn btn-primary w-100" type="submit">
            <?= h(tr_reset('reset_password_submit', [
                'uk' => 'Зберегти новий пароль',
                'ru' => 'Сохранить новый пароль',
                'en' => 'Save new password',
            ])) ?>
          </button>
        </form>
      <?php endif; ?>
    </div>
  </div>
</div>

<?php require __DIR__ . '/includes/footer.php'; ?>
