<?php
declare(strict_types=1);

require_once __DIR__ . '/bootstrap.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/ip.php';
require_once __DIR__ . '/user_auth_lib.php';
require_once __DIR__ . '/admin_2fa_lib.php';

header('Content-Type: application/json; charset=utf-8');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['ok' => false, 'error' => 'method'], JSON_UNESCAPED_UNICODE);
    exit;
}

mocker_require_csrf_post();

const SITE_USER_LOGIN_MAX_ATTEMPTS = 5;
const SITE_USER_LOGIN_LOCK_SECONDS = 900;

$ip = trim((string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
$key = 'mocker_user_login_' . hash('sha256', $ip);
$now = time();
$attempts = $_SESSION[$key] ?? ['count' => 0, 'first' => $now, 'locked_until' => 0];
if (!is_array($attempts)) {
    $attempts = ['count' => 0, 'first' => $now, 'locked_until' => 0];
}
$lockedUntil = (int) ($attempts['locked_until'] ?? 0);
if ($lockedUntil > $now) {
    http_response_code(429);
    echo json_encode(['ok' => false, 'error' => 'too_many_attempts', 'retryAfter' => $lockedUntil - $now], JSON_UNESCAPED_UNICODE);
    exit;
}
if ($lockedUntil > 0 && $lockedUntil <= $now) {
    $attempts = ['count' => 0, 'first' => $now, 'locked_until' => 0];
}

$input = json_decode((string) file_get_contents('php://input'), true) ?: [];
$identity = trim((string) ($input['identity'] ?? ''));
$password = (string) ($input['password'] ?? '');
$twofaCode = trim((string) ($input['twofaCode'] ?? ''));
$twofaChallenge = trim((string) ($input['twofaChallenge'] ?? ''));

$completeLogin = static function (PDO $pdo, array $row, string $key, int $now): void {
    $id = (int) $row['id'];
    unset($_SESSION[$key], $_SESSION['mocker_user_2fa_pending']);
    session_regenerate_id(true);
    $_SESSION['mocker_user_id'] = $id;

    $lip = mocker_client_ip();
    $upd = $pdo->prepare(
        'UPDATE users SET last_login_at = :t, last_seen_at = :t2, last_login_ip = :lip WHERE id = :id'
    );
    $upd->execute([
        ':t' => $now,
        ':t2' => $now,
        ':lip' => $lip !== '' ? $lip : null,
        ':id' => $id,
    ]);

    $stId = $pdo->prepare('SELECT ' . mocker_user_select_auth_columns() . ' FROM users WHERE id = :id LIMIT 1');
    $stId->execute([':id' => $id]);
    $fresh = $stId->fetch(PDO::FETCH_ASSOC);
    $userPayload = mocker_user_auth_payload($pdo, $fresh ?: $row);

    echo json_encode(
        [
            'ok' => true,
            'needsPasswordChange' => !empty($userPayload['needsPasswordChange']),
            'needsProfileCompletion' => !empty($userPayload['needsProfileCompletion']),
            'user' => $userPayload,
        ],
        JSON_UNESCAPED_UNICODE
    );
    exit;
};

if ($twofaChallenge !== '') {
    $pending = $_SESSION['mocker_user_2fa_pending'] ?? null;
    if (!is_array($pending)) {
        http_response_code(400);
        echo json_encode(['ok' => false, 'error' => 'twofa_missing_challenge'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $expiresAt = (int) ($pending['expiresAt'] ?? 0);
    if ($expiresAt <= $now) {
        unset($_SESSION['mocker_user_2fa_pending']);
        http_response_code(400);
        echo json_encode(['ok' => false, 'error' => 'twofa_expired'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $expectedChallenge = (string) ($pending['challenge'] ?? '');
    if ($expectedChallenge === '' || !hash_equals($expectedChallenge, $twofaChallenge)) {
        http_response_code(401);
        echo json_encode(['ok' => false, 'error' => 'twofa_invalid_challenge'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $attemptCount = (int) ($pending['attempts'] ?? 0);
    if ($attemptCount >= 5) {
        unset($_SESSION['mocker_user_2fa_pending']);
        http_response_code(429);
        echo json_encode(['ok' => false, 'error' => 'too_many_attempts'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $pending['attempts'] = $attemptCount + 1;
    $_SESSION['mocker_user_2fa_pending'] = $pending;

    $secret = (string) ($pending['secret'] ?? '');
    $ok = $twofaCode !== '' && $secret !== '' && mocker_admin_twofa_verify_code($secret, $twofaCode, 1);
    if (!$ok) {
        http_response_code(401);
        echo json_encode(
            ['ok' => false, 'error' => 'twofa_invalid', 'remaining' => max(0, 5 - (int) $pending['attempts'])],
            JSON_UNESCAPED_UNICODE
        );
        exit;
    }
    $row = is_array($pending['row'] ?? null) ? $pending['row'] : null;
    if (!$row) {
        http_response_code(400);
        echo json_encode(['ok' => false, 'error' => 'twofa_missing_challenge'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    try {
        $pdo = mocker_db();
    } catch (Throwable $e) {
        http_response_code(503);
        echo json_encode(['ok' => false, 'error' => 'db'], JSON_UNESCAPED_UNICODE);
        exit;
    }
    $completeLogin($pdo, $row, $key, $now);
}

$fail = static function () use ($key, &$attempts, $now): void {
    $first = (int) ($attempts['first'] ?? $now);
    if (($now - $first) > SITE_USER_LOGIN_LOCK_SECONDS) {
        $attempts = ['count' => 1, 'first' => $now, 'locked_until' => 0];
    } else {
        $count = (int) ($attempts['count'] ?? 0) + 1;
        $attempts['count'] = $count;
        $attempts['first'] = $first;
        if ($count >= SITE_USER_LOGIN_MAX_ATTEMPTS) {
            $attempts['locked_until'] = $now + SITE_USER_LOGIN_LOCK_SECONDS;
        }
    }
    $_SESSION[$key] = $attempts;
    http_response_code(401);
    echo json_encode(
        [
            'ok' => false,
            'error' => 'invalid',
            'remaining' => max(0, SITE_USER_LOGIN_MAX_ATTEMPTS - (int) ($attempts['count'] ?? 0)),
        ],
        JSON_UNESCAPED_UNICODE
    );
    exit;
};

if ($identity === '' || $password === '') {
    http_response_code(400);
    echo json_encode(['ok' => false, 'error' => 'fields'], JSON_UNESCAPED_UNICODE);
    exit;
}

try {
    $pdo = mocker_db();
} catch (Throwable $e) {
    http_response_code(503);
    echo json_encode(['ok' => false, 'error' => 'db'], JSON_UNESCAPED_UNICODE);
    exit;
}

mocker_user_auth_migrate($pdo);

$st = $pdo->prepare(
    'SELECT ' . mocker_user_select_auth_columns() . ' FROM users WHERE username = :i COLLATE NOCASE OR email = :i2 COLLATE NOCASE LIMIT 1'
);
$st->execute([':i' => $identity, ':i2' => $identity]);
$row = $st->fetch(PDO::FETCH_ASSOC);

if (!$row || !mocker_user_verify_credentials($pdo, $row, $identity, $password)) {
    $fail();
}

if ((int) ($row['is_active'] ?? 1) !== 1) {
    http_response_code(403);
    echo json_encode(['ok' => false, 'error' => 'inactive'], JSON_UNESCAPED_UNICODE);
    exit;
}

$twofaEnabled = (int) ($row['twofa_enabled'] ?? 0) === 1;
$twofaSecret = trim((string) ($row['twofa_secret'] ?? ''));
if ($twofaEnabled && $twofaSecret !== '') {
    $challenge = rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
    $_SESSION['mocker_user_2fa_pending'] = [
        'challenge' => $challenge,
        'expiresAt' => $now + 300,
        'attempts' => 0,
        'secret' => $twofaSecret,
        'userId' => (int) $row['id'],
        'row' => $row,
    ];
    echo json_encode(['ok' => true, 'requires2fa' => true, 'twofaChallenge' => $challenge], JSON_UNESCAPED_UNICODE);
    exit;
}

$completeLogin($pdo, $row, $key, $now);
