<?php
// =============================================================================
// login.php - 대시보드 세션 인증 로그인 페이지
// =============================================================================

// ── 프로덕션: 에러 표시 비활성화 ─────────────────────────
ini_set('display_errors', 0);
error_reporting(E_ALL);
ini_set('log_errors', 1);

require_once __DIR__ . '/config.php';
require_once __DIR__ . '/admin_security.php';
require_once __DIR__ . '/sms_send.php';
require_once __DIR__ . '/cron_heartbeat.php';

// ── 세션 보안 설정 ───────────────────────────────────────────
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
session_start();

// 이미 로그인된 경우 대시보드로 리다이렉트
if (!empty($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
    header('Location: dashboard.php');
    exit;
}

$error = '';
$expired = isset($_GET['expired']);
$configuration_error = (ADMIN_PASSWORD_HASH === '' || !admin_mfa_configured());

// ── 브루트포스 방어: 파일 기반 IP별 실패 횟수 추적 ──────────
$lockout_file = __DIR__ . '/var/login_attempts.json';
$max_attempts = 5;        // 최대 연속 실패 허용
$lockout_sec  = 900;      // 잠금 시간 (15분)
$client_ip    = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';

function load_attempts(string $file): array {
    if (!file_exists($file)) return [];
    $data = @json_decode(file_get_contents($file), true);
    return is_array($data) ? $data : [];
}

// 원자적 읽기-수정-쓰기. 배타 락(flock LOCK_EX) 아래에서 최신 상태를 다시 읽고
// $mutator(array &$data) 로 변경한 뒤 기록한다. 동시 실패 로그인이 카운트를
// 덮어써(lost update) 잠금이 무력화되는 경합을 막는다. 갱신된 배열을 반환한다.
function update_attempts(string $file, callable $mutator): array {
    $fh = @fopen($file, 'c+');
    if ($fh !== false && flock($fh, LOCK_EX)) {
        $raw = stream_get_contents($fh);
        $decoded = ($raw !== false && $raw !== '') ? json_decode($raw, true) : [];
        $data = is_array($decoded) ? $decoded : [];
        $mutator($data);
        ftruncate($fh, 0);
        rewind($fh);
        fwrite($fh, json_encode($data));
        fflush($fh);
        flock($fh, LOCK_UN);
        fclose($fh);
        return $data;
    }

    // 파일을 못 열었거나(예: 권한) 락을 못 얻었으면(예: flock 미지원 FS / NFS)
    // 가용성 우선으로 잠금 없이 best-effort 갱신한다. 락 실패 시에도 변경(증가)이
    // 반드시 반영·반환되도록 하여, 카운터가 멈춰 무제한 시도가 허용되지 않게 한다.
    if ($fh !== false) {
        fclose($fh);
    }
    $data = load_attempts($file);
    $mutator($data);
    @file_put_contents($file, json_encode($data), LOCK_EX);
    return $data;
}

$attempts = load_attempts($lockout_file);
$is_locked = false;

if (isset($attempts[$client_ip])) {
    $rec = $attempts[$client_ip];
    // 잠금 시간 경과 시 초기화
    if ($rec['count'] >= $max_attempts && (time() - $rec['last']) < $lockout_sec) {
        $is_locked = true;
        $remaining = $lockout_sec - (time() - $rec['last']);
        $error = "로그인 시도가 너무 많습니다. {$remaining}초 후 다시 시도하세요.";
    } elseif ($rec['count'] >= $max_attempts && (time() - $rec['last']) >= $lockout_sec) {
        $attempts = update_attempts($lockout_file, function (array &$a) use ($client_ip) {
            unset($a[$client_ip]);
        });
    }
}

// ── CSRF 토큰 생성 ──────────────────────────────────────────
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// POST 로그인 처리
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$is_locked && !$configuration_error) {
    // CSRF 검증
    $submitted_token = $_POST['csrf_token'] ?? '';
    if (!hash_equals($_SESSION['csrf_token'], $submitted_token)) {
        $error = '잘못된 요청입니다. 페이지를 새로고침하세요.';
    } else {
        $username = $_POST['username'] ?? '';
        $password = $_POST['password'] ?? '';
        $otp_code = $_POST['otp_code'] ?? '';

        // 비밀번호 검증: bcrypt 해시
        $pw_ok = password_verify($password, ADMIN_PASSWORD_HASH);
        $mfa_ok = $pw_ok && hash_equals(ADMIN_USER, $username) && admin_totp_verify(ADMIN_TOTP_SECRET, $otp_code);

        if (hash_equals(ADMIN_USER, $username) && $pw_ok && $mfa_ok) {
            // 성공: 실패 기록 초기화
            update_attempts($lockout_file, function (array &$a) use ($client_ip) {
                unset($a[$client_ip]);
            });

            session_regenerate_id(true);
            $_SESSION['logged_in']  = true;
            $_SESSION['login_time'] = time();
            $_SESSION['username']   = $username;
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
            admin_audit_log('login_success', $username, true);

            // 로그인 시 센서 오프라인 체크 즉시 실행 (쓰로틀 없이)
            check_offline_sensors();

            header('Location: dashboard.php');
            exit;
        } else {
            // 실패: 카운트 증가(원자적 읽기-수정-쓰기로 lost update 방지)
            $attempts = update_attempts($lockout_file, function (array &$a) use ($client_ip) {
                if (!isset($a[$client_ip])) {
                    $a[$client_ip] = ['count' => 0, 'last' => 0];
                }
                $a[$client_ip]['count']++;
                $a[$client_ip]['last'] = time();
            });

            $attempt_count = $attempts[$client_ip]['count'] ?? $max_attempts;
            $left = $max_attempts - $attempt_count;
            if ($left > 0) {
                $error = "아이디 또는 비밀번호가 올바르지 않습니다. (남은 시도: {$left}회)";
            } else {
                $error = "로그인 시도가 너무 많습니다. {$lockout_sec}초 후 다시 시도하세요.";
            }

            error_log("[LOGIN] 로그인 실패 - IP: {$client_ip}, User: {$username}");
            admin_audit_log('login_failure', $username, false, [
                'password_ok' => $pw_ok,
                'mfa_present' => preg_replace('/\D+/', '', $otp_code) !== '',
            ]);
        }
    }
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>로그인 - 누수감지 대시보드 <?= htmlspecialchars(APP_VERSION) ?></title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: 'Noto Sans KR', sans-serif; background: #f0f4f8; color: #333;
               display: flex; align-items: center; justify-content: center; min-height: 100vh; }
        .login-box { background: #fff; border-radius: 12px; padding: 40px 36px;
                     box-shadow: 0 4px 24px rgba(0,0,0,.1); width: 100%; max-width: 400px; }
        .login-box h1 { font-size: 1.4rem; color: #1a56db; margin-bottom: 8px; text-align: center; }
        .login-box p.sub { font-size: .85rem; color: #64748b; text-align: center; margin-bottom: 28px; }
        .form-group { margin-bottom: 18px; }
        .form-group label { display: block; font-size: .85rem; color: #475569; margin-bottom: 6px; font-weight: 600; }
        .form-group input { width: 100%; padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 8px;
                            font-size: .95rem; outline: none; transition: border-color .2s; }
        .form-group input:focus { border-color: #1a56db; box-shadow: 0 0 0 3px rgba(26,86,219,.1); }
        .btn { width: 100%; padding: 12px; background: #1a56db; color: #fff; border: none;
               border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer;
               transition: background .2s; }
        .btn:hover { background: #1347b8; }
        .btn:disabled { background: #94a3b8; cursor: not-allowed; }
        .error { background: #fef2f2; color: #991b1b; padding: 10px 14px; border-radius: 8px;
                 font-size: .85rem; margin-bottom: 18px; border: 1px solid #fecaca; }
        .expired { background: #fefce8; color: #854d0e; padding: 10px 14px; border-radius: 8px;
                   font-size: .85rem; margin-bottom: 18px; border: 1px solid #fde68a; }

        /* --- 폰 (≤480px) --- */
        @media (max-width: 480px) {
            .login-box { padding: 28px 20px; margin: 0 12px; }
            .login-box h1 { font-size: 1.2rem; }
            .login-box p.sub { font-size: .8rem; margin-bottom: 20px; }
            .form-group input { font-size: .9rem; padding: 9px 12px; }
            .btn { font-size: .95rem; padding: 11px; }
        }

        /* --- 초소형 (≤360px) --- */
        @media (max-width: 360px) {
            .login-box { padding: 24px 16px; margin: 0 8px; }
            .login-box h1 { font-size: 1.1rem; }
            .form-group input { font-size: .85rem; padding: 8px 10px; }
            .btn { font-size: .9rem; padding: 10px; }
        }
    </style>
</head>
<body>
<div class="login-box">
    <h1>누수감지 모니터링 <?= htmlspecialchars(APP_VERSION) ?></h1>
    <p class="sub">대시보드에 접근하려면 로그인하세요.</p>

    <?php if ($expired): ?>
    <div class="expired">세션이 만료되었습니다. 다시 로그인해주세요.</div>
    <?php endif; ?>

    <?php if ($error): ?>
    <div class="error"><?= htmlspecialchars($error) ?></div>
    <?php endif; ?>

    <?php if ($configuration_error): ?>
    <div class="error">관리자 비밀번호 해시 또는 MFA 인증앱 비밀키가 설정되지 않았습니다. config.local.php의 ADMIN_PASSWORD_HASH와 ADMIN_TOTP_SECRET을 확인하세요.</div>
    <?php endif; ?>

    <form method="POST" action="">
        <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
        <div class="form-group">
            <label for="username">아이디</label>
            <input type="text" id="username" name="username" required autofocus
                   autocomplete="username"
                   value="<?= htmlspecialchars($_POST['username'] ?? '') ?>">
        </div>
        <div class="form-group">
            <label for="password">비밀번호</label>
            <input type="password" id="password" name="password" required autocomplete="current-password">
        </div>
        <div class="form-group">
            <label for="otp_code">인증 앱 6자리 코드</label>
            <input type="text" id="otp_code" name="otp_code" required
                   inputmode="numeric" pattern="[0-9]{6}" maxlength="6"
                   autocomplete="one-time-code">
        </div>
        <button type="submit" class="btn" <?= ($is_locked || $configuration_error) ? 'disabled' : '' ?>>로그인</button>
    </form>
</div>
</body>
</html>
