<?php
namespace App\Core;

/** Autentikasi pengguna, pembatasan laju login, dan pengelolaan kata sandi. */
class Auth
{
    private static ?array $user = null;

    public static function attempt(string $username, string $password): array
    {
        $db = Database::i();
        $ip = Request::ip();
        $maxFail = Settings::int('keamanan.maks_gagal_login', 5);
        $window  = Settings::int('keamanan.jendela_gagal_menit', 15);
        $lockMin = Settings::int('keamanan.durasi_kunci_menit', 15);

        // Pembatasan laju berbasis (username + IP)
        $recent = (int)$db->value(
            'SELECT COUNT(*) FROM login_attempts
              WHERE success = 0 AND created_at >= (NOW() - INTERVAL :w MINUTE)
                AND (username = :u OR ip_address = :ip)',
            ['w' => $window, 'u' => $username, 'ip' => $ip]
        );
        if ($recent >= $maxFail) {
            self::logAttempt($username, false);
            return ['sukses' => false, 'pesan' => "Terlalu banyak percobaan gagal. Coba lagi dalam {$lockMin} menit."];
        }

        $u = $db->one('SELECT * FROM users WHERE username = :u AND deleted_at IS NULL', ['u' => $username]);

        if (!$u) {
            self::logAttempt($username, false);
            return ['sukses' => false, 'pesan' => 'Nama pengguna atau kata sandi salah.'];
        }
        if ((int)$u['is_active'] !== 1) {
            self::logAttempt($username, false);
            return ['sukses' => false, 'pesan' => 'Akun Anda tidak aktif. Hubungi administrator.'];
        }
        if (!empty($u['locked_until']) && strtotime($u['locked_until']) > time()) {
            self::logAttempt($username, false);
            return ['sukses' => false, 'pesan' => 'Akun terkunci sementara sampai ' . Dates::fmtDateTime($u['locked_until']) . '.'];
        }
        if (!password_verify($password, $u['password_hash'])) {
            $fail = (int)$u['failed_attempts'] + 1;
            $lock = $fail >= $maxFail ? date('Y-m-d H:i:s', time() + $lockMin * 60) : null;
            $db->exec('UPDATE users SET failed_attempts = :f, locked_until = :l WHERE id = :id',
                      ['f' => $fail, 'l' => $lock, 'id' => $u['id']]);
            self::logAttempt($username, false);
            $sisa = max(0, $maxFail - $fail);
            return ['sukses' => false, 'pesan' => 'Nama pengguna atau kata sandi salah.'
                    . ($sisa > 0 ? " Sisa percobaan: {$sisa}." : ' Akun dikunci sementara.')];
        }

        // Sukses
        if (password_needs_rehash($u['password_hash'], PASSWORD_BCRYPT, ['cost' => 12])) {
            $db->exec('UPDATE users SET password_hash = :h WHERE id = :id',
                      ['h' => password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]), 'id' => $u['id']]);
        }
        $db->exec('UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login_at = NOW(), last_login_ip = :ip WHERE id = :id',
                  ['ip' => $ip, 'id' => $u['id']]);

        Session::regenerate();
        Csrf::rotate();
        Session::set('user_id', (int)$u['id']);
        Session::set('login_time', time());
        self::$user = null;
        self::logAttempt($username, true);
        Audit::log('auth', 'login', 'user', (int)$u['id'], 'Pengguna berhasil masuk');

        return ['sukses' => true, 'pesan' => 'Selamat datang, ' . $u['name'] . '.', 'user' => $u];
    }

    private static function logAttempt(string $username, bool $ok): void
    {
        try {
            Database::i()->insert('login_attempts', [
                'username'   => mb_substr($username, 0, 60),
                'ip_address' => Request::ip(),
                'success'    => $ok ? 1 : 0,
                'user_agent' => Request::userAgent(),
            ]);
        } catch (\Throwable $e) { Logger::warning('Gagal mencatat percobaan login: ' . $e->getMessage()); }
    }

    public static function logout(): void
    {
        if (self::check()) {
            Audit::log('auth', 'logout', 'user', self::id(), 'Pengguna keluar');
        }
        Session::destroy();
        self::$user = null;
    }

    public static function check(): bool { return (int)Session::get('user_id', 0) > 0; }
    public static function id(): int     { return (int)Session::get('user_id', 0); }

    public static function user(): ?array
    {
        if (self::$user !== null) return self::$user;
        if (!self::check()) return null;
        $u = Database::i()->one(
            'SELECT u.*, r.code AS role_code, r.name AS role_name, r.data_scope
               FROM users u JOIN roles r ON r.id = u.role_id
              WHERE u.id = :id AND u.deleted_at IS NULL AND u.is_active = 1',
            ['id' => self::id()]
        );
        if (!$u) { Session::destroy(); return null; }
        self::$user = $u;
        return $u;
    }

    /**
     * Mengosongkan cache data pengguna agar dibaca ulang dari basis data.
     * Dipakai setelah profil/peran/penautan master pengguna berubah, dan pada pengujian.
     */
    public static function forget(): void { self::$user = null; Acl::reset(); }

    public static function name(): string   { return self::user()['name'] ?? 'Tamu'; }
    public static function roleCode(): string { return self::user()['role_code'] ?? ''; }
    public static function isSuper(): bool  { return self::roleCode() === 'SUPERADMIN'; }

    public static function mustChangePassword(): bool
    {
        $u = self::user();
        return $u && (int)$u['must_change_password'] === 1;
    }

    public static function changePassword(int $userId, string $new, bool $clearFlag = true, ?string $current = null): array
    {
        $db = Database::i();
        $u = $db->one('SELECT * FROM users WHERE id = :id', ['id' => $userId]);
        if (!$u) return ['sukses' => false, 'pesan' => 'Pengguna tidak ditemukan.'];
        if ($current !== null && !password_verify($current, $u['password_hash'])) {
            return ['sukses' => false, 'pesan' => 'Kata sandi saat ini salah.'];
        }
        $min = Settings::int('keamanan.panjang_sandi_minimal', 8);
        $err = self::validatePassword($new, $min);
        if ($err) return ['sukses' => false, 'pesan' => $err];
        if (password_verify($new, $u['password_hash'])) {
            return ['sukses' => false, 'pesan' => 'Kata sandi baru tidak boleh sama dengan kata sandi lama.'];
        }
        $db->exec('UPDATE users SET password_hash = :h, must_change_password = :m, password_changed_at = NOW() WHERE id = :id', [
            'h' => password_hash($new, PASSWORD_BCRYPT, ['cost' => 12]),
            'm' => $clearFlag ? 0 : 1,
            'id' => $userId,
        ]);
        Audit::log('pengguna', 'ganti_sandi', 'user', $userId, 'Kata sandi diubah');
        self::$user = null;
        return ['sukses' => true, 'pesan' => 'Kata sandi berhasil diperbarui.'];
    }

    public static function validatePassword(string $p, int $min = 8): ?string
    {
        if (mb_strlen($p) < $min) return "Kata sandi minimal {$min} karakter.";
        if (!preg_match('/[A-Za-z]/', $p)) return 'Kata sandi harus memuat minimal satu huruf.';
        if (!preg_match('/\d/', $p))       return 'Kata sandi harus memuat minimal satu angka.';
        return null;
    }

    public static function hash(string $p): string
    {
        return password_hash($p, PASSWORD_BCRYPT, ['cost' => 12]);
    }
}
