<?php

namespace App\Http\Controllers;

use App\Models\AuditEvent;
use App\Services\BruteforceGuard;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

/**
 * Honeypot deception layer. Handles requests to decoy paths (routes/web.php, above the guest
 * group) that no legitimate client ever visits — /.env, /wp-login.php, /phpmyadmin, … A hit is
 * proof of a hostile probe, so we: audit it, instant-ban the source IP (BruteforceGuard::banNow,
 * exempt/loopback are skipped), and return a small, self-contained DECEPTIVE body so the attacker
 * keeps chasing the bait instead of noticing a real panel behind it. Attacker-facing strings are
 * intentionally NOT localized (they impersonate generic third-party software).
 */
class HoneypotController extends Controller
{
    /**
     * Per-IP cap on decoy audit rows written in a rolling hour. The decoy routes run WITHOUT
     * BlockBannedIp (an already-banned prober still gets the bait), so a single source can loop
     * over wildcard paths indefinitely — this bounds how far it can grow audit_events. The ban +
     * the deception still happen past the cap; only the (redundant) extra audit rows are dropped.
     */
    private const AUDIT_CAP_PER_HOUR = 60;

    public function trap(Request $request): Response
    {
        // Feature off → behave exactly like an ordinary 404 for these paths (no ban, no audit).
        if (! config('clusev.honeypot.enabled')) {
            abort(404);
        }

        // An AUTHENTICATED user hitting a decoy is a stray request, NOT an attacker: behave like an
        // ordinary 404 (no ban, no deception, no audit) so a logged-in operator/admin never self-bans.
        if (auth()->check()) {
            abort(404);
        }

        $ip = (string) $request->ip();

        // An active (non-safe) method is a real attack attempt; a safe read (GET/HEAD/OPTIONS) is merely
        // viewing a decoy. Only an attempt bans, so simply opening a fake page never locks anyone out of
        // the real panel (an operator testing the honeypot, or a mistaken click, stays free).
        //
        // Use getRealMethod() — the RAW REQUEST_METHOD — NOT isMethod()/getMethod(): those honor the
        // `_method` body/query field and the X-HTTP-Method-Override header, so an attacker could send a
        // real POST spoofed as `_method=PUT` to make isMethod('post') false and dodge the instant-ban
        // entirely (the decoys are Route::any, so the override still routes here). Ban any non-safe verb.
        $isAttempt = ! in_array($request->getRealMethod(), ['GET', 'HEAD', 'OPTIONS'], true);

        // BAN FIRST, and independently of the audit. The instant-ban IS the protection; it must never be
        // skipped because a later step throws on hostile input. Guarded on its own so that even a ban
        // failure (e.g. a momentary DB outage) still serves the deception below.
        //
        // We deliberately do NOT try to spare a "cross-site forged" POST by inspecting the Origin header:
        // the header is attacker-controlled, so any Origin-based skip is a trivial UNIVERSAL ban-evasion
        // (a scanner just sends a foreign Origin) — strictly worse than the narrow reflected-griefing it
        // would prevent. Honeypot bans are time-limited and unban-able from the Threats page.
        if ($isAttempt) {
            try {
                app(BruteforceGuard::class)->banNow($ip, 'honeypot');
            } catch (\Throwable $e) {
                report($e);
            }
        }

        // Audit the probe (best-effort, rate-capped). Fully isolated: a logging failure must not affect
        // the ban above or the response — an unhandled throw here would otherwise surface a Laravel 500
        // that UNMASKS the fake.
        try {
            $this->auditProbe($request, $ip, $isAttempt);
        } catch (\Throwable $e) {
            report($e);
        }

        return $this->deceive($request);
    }

    /** Record the probe as an audit event, rate-capped per source IP (see AUDIT_CAP_PER_HOUR). */
    private function auditProbe(Request $request, string $ip, bool $isAttempt): void
    {
        // Bound audit growth per source IP WITHOUT a check-then-act race: `add` seeds the counter with a
        // 1-hour TTL on the first hit only, then the atomic `increment` both counts and returns the new
        // total (Redis INCR keeps the TTL). Concurrent hits can at most overshoot by the concurrency
        // count — fine for a DoS bound. Past the cap we drop the redundant row (the ban already happened).
        $capKey = 'honeypot:audit:'.md5($ip);
        Cache::add($capKey, 0, now()->addHour());
        if (Cache::increment($capKey) > self::AUDIT_CAP_PER_HOUR) {
            return;
        }

        // Attacker-controlled strings are scrubbed to valid UTF-8 before they reach the array-cast
        // `meta` column: a raw invalid byte (e.g. a lone 0x80 in the password field or User-Agent)
        // would make json_encode throw JsonEncodingException on the cast. mb_scrub replaces ill-formed
        // sequences with the Unicode substitute char.
        $clean = static fn (?string $v, int $n = 190): string => Str::limit(mb_scrub((string) $v), $n);

        $meta = [
            'ua' => $clean($request->userAgent()),
            'method' => $request->method(),
            'query' => $clean($request->getQueryString()),
        ];

        // Capture the credentials a prober typed into the fake login — their guesses, which are threat
        // intel (a legitimate user never reaches a decoy). The field names span the impersonated apps:
        // WordPress (log/pwd), phpMyAdmin (pma_username/pma_password), and the generic form.
        $user = $request->input('log') ?? $request->input('pma_username') ?? $request->input('username');
        $pass = $request->input('pwd') ?? $request->input('pma_password') ?? $request->input('password');
        if (is_string($user) && $user !== '') {
            $meta['tried_user'] = $clean($user, 120);
        }
        if (is_string($pass) && $pass !== '') {
            $meta['tried_pass'] = $clean($pass, 120);
        }

        AuditEvent::create([
            'user_id' => null,
            'actor' => 'system',
            'action' => $isAttempt ? 'security.honeypot_login' : 'security.honeypot_hit',
            'target' => $clean($request->path()),
            'ip' => $ip,
            'meta' => $meta,
        ]);
    }

    /** Pick a convincing fake response for the probed path. */
    private function deceive(Request $request): Response
    {
        $path = strtolower($request->path());

        if ($path === '.env') {
            return response($this->fakeDotenv(), 200, ['Content-Type' => 'text/plain; charset=UTF-8']);
        }

        if ($path === '.git/config') {
            return response($this->fakeGitConfig(), 200, ['Content-Type' => 'text/plain; charset=UTF-8']);
        }

        if (str_starts_with($path, 'wp-login.php') || str_starts_with($path, 'wp-admin')) {
            return response($this->fakeWordPress(), 200, ['Content-Type' => 'text/html; charset=UTF-8']);
        }

        if (str_starts_with($path, 'phpmyadmin')) {
            return response($this->fakePhpMyAdmin(), 200, ['Content-Type' => 'text/html; charset=UTF-8']);
        }

        return response($this->fakeGenericLogin(), 200, ['Content-Type' => 'text/html; charset=UTF-8']);
    }

    /**
     * Plausible-looking dotenv seeded with the per-install canaries (the bait) when configured. When a
     * canary is empty (unconfigured/dev), a plausible static value is served instead so the decoy still
     * looks authentic — it is simply not registered as a detectable honeytoken.
     */
    private function fakeDotenv(): string
    {
        $c = (array) config('clusev.honeypot.canaries');

        $appKey = ($c['app_key'] ?? '') !== '' ? $c['app_key'] : 'base64:H0n3yP0tC4n4ryK3yD0N0tUs3AAAAAAAAAAAAAAAAA0=';
        $dbPassword = ($c['db_password'] ?? '') !== '' ? $c['db_password'] : 'Sup3rS3cr3t-Pr0d-DB-9f2a7c';
        $apiKey = ($c['api_key'] ?? '') !== '' ? $c['api_key'] : 'a7f3c9e1d5b04826bf1a3c7e9d2f6b84';

        return implode("\n", [
            'APP_NAME=Application',
            'APP_ENV=production',
            'APP_KEY='.$appKey,
            'APP_DEBUG=false',
            'APP_URL=https://app.internal.example.com',
            '',
            'DB_CONNECTION=mysql',
            'DB_HOST=10.20.0.14',
            'DB_PORT=3306',
            'DB_DATABASE=app_production',
            'DB_USERNAME=app_prod',
            'DB_PASSWORD='.$dbPassword,
            '',
            'API_KEY='.$apiKey,
            'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE',
            'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
            'AWS_DEFAULT_REGION=eu-central-1',
            '',
        ]);
    }

    /** Plausible fake git config (an [core]/[remote "origin"] INI) so the /.git/config decoy looks real. */
    private function fakeGitConfig(): string
    {
        return implode("\n", [
            '[core]',
            "\trepositoryformatversion = 0",
            "\tfilemode = true",
            "\tbare = false",
            "\tlogallrefupdates = true",
            '[remote "origin"]',
            "\turl = https://git.internal.example.com/app/backend.git",
            "\tfetch = +refs/heads/*:refs/remotes/origin/*",
            '[branch "main"]',
            "\tremote = origin",
            "\tmerge = refs/heads/main",
            '',
        ]);
    }

    private function fakeWordPress(): string
    {
        return <<<'HTML'
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Log In &lsaquo; Blog &#8212; WordPress</title>
<style>
html{background:#f0f0f1}
body{background:#f0f0f1;color:#3c434a;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;font-size:13px;line-height:1.4;margin:0}
#login{width:320px;padding:8% 0 0;margin:auto}
#login h1{text-align:center;margin:0 0 25px}
#login h1 a{display:inline-block;line-height:0}
#login h1 svg{width:84px;height:84px;fill:#3c434a}
.screen-reader-text{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(1px,1px,1px,1px)}
#loginform{background:#fff;border:1px solid #c3c4c7;box-shadow:0 1px 3px rgba(0,0,0,.04);padding:26px 24px;margin:0 0 16px;overflow:hidden}
#loginform p{margin:0 0 16px}
#loginform label{color:#3c434a;font-size:14px;display:block;margin-bottom:3px}
#loginform .input{width:100%;box-sizing:border-box;font-size:24px;line-height:1.33;padding:3px 8px;background:#fff;border:1px solid #8c8f94;border-radius:4px;color:#2c3338;min-height:40px}
#loginform .input:focus{border-color:#2271b1;box-shadow:0 0 0 1px #2271b1;outline:2px solid transparent}
.forgetmenot label{display:inline!important;font-size:12px}
.submit{margin:0!important;text-align:right}
.button-primary{background:#2271b1;border:1px solid #2271b1;color:#fff;border-radius:3px;padding:0 14px;height:36px;font-size:14px;line-height:34px;cursor:pointer}
#nav,#backtoblog{margin:24px 0 0;text-align:center;font-size:13px}
#nav a,#backtoblog a{color:#50575e;text-decoration:none}
#nav a:hover{color:#135e96}
</style>
</head>
<body class="login">
<div id="login">
<h1><a href="https://wordpress.org/"><svg viewBox="0 0 122.5 122.5" role="img" aria-label="WordPress"><path d="M8.7,61.3c0,20.8,12.1,38.7,29.6,47.3L13.2,39.9C10.3,46.4,8.7,53.7,8.7,61.3z M96.7,58.6c0-6.5-2.3-11-4.3-14.5c-2.6-4.3-5.1-8-5.1-12.3c0-4.8,3.7-9.3,8.9-9.3c0.2,0,0.5,0,0.7,0c-9.4-8.6-21.9-13.9-35.7-13.9c-18.5,0-34.7,9.5-44.2,23.9c1.2,0,2.4,0.1,3.3,0.1c5.5,0,14-0.7,14-0.7c2.8-0.2,3.2,4,0.3,4.3c0,0-2.9,0.3-6,0.5l19,56.5l11.4-34.2l-8.1-22.3c-2.8-0.2-5.5-0.5-5.5-0.5c-2.8-0.2-2.5-4.5,0.3-4.3c0,0,8.7,0.7,13.9,0.7c5.5,0,14-0.7,14-0.7c2.8-0.2,3.2,4,0.3,4.3c0,0-2.9,0.3-6,0.5l18.9,56.1l5.2-17.4C95.4,68.6,96.7,63.2,96.7,58.6z M62.9,65.8l-15.7,45.6c4.7,1.4,9.6,2.1,14.8,2.1c6.1,0,12-1.1,17.5-3c-0.1-0.2-0.3-0.5-0.4-0.7L62.9,65.8z M107.4,36.3c0.2,1.7,0.3,3.4,0.3,5.4c0,5.3-1,11.2-4,18.7l-16,46.2c15.6-9.1,26.1-26,26.1-45.3C113.8,52.2,111.5,43.6,107.4,36.3z M61.3,0C27.5,0,0,27.5,0,61.3s27.5,61.3,61.3,61.3s61.3-27.5,61.3-61.3S95,0,61.3,0z M61.3,119.7c-32.2,0-58.4-26.2-58.4-58.4s26.2-58.4,58.4-58.4s58.4,26.2,58.4,58.4S93.5,119.7,61.3,119.7z"/></svg><span class="screen-reader-text">Powered by WordPress</span></a></h1>
<form name="loginform" id="loginform" action="wp-login.php" method="post">
<p><label for="user_login">Username or Email Address</label>
<input type="text" name="log" id="user_login" class="input" value="" autocomplete="username"></p>
<p><label for="user_pass">Password</label>
<input type="password" name="pwd" id="user_pass" class="input" value="" autocomplete="current-password"></p>
<p class="forgetmenot"><label for="rememberme"><input name="rememberme" type="checkbox" id="rememberme" value="forever"> Remember Me</label></p>
<p class="submit"><input type="submit" name="wp-submit" id="wp-submit" class="button button-primary" value="Log In"></p>
</form>
<p id="nav"><a href="wp-login.php?action=lostpassword">Lost your password?</a></p>
<p id="backtoblog"><a href="/">&larr; Go to Blog</a></p>
</div>
</body>
</html>
HTML;
    }

    private function fakePhpMyAdmin(): string
    {
        return <<<'HTML'
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>phpMyAdmin</title>
<style>
html,body{background:#f4f4f4;margin:0}
body{color:#464646;font-family:"Segoe UI",Tahoma,Verdana,Arial,sans-serif;font-size:13px}
#page{width:400px;margin:44px auto 0}
.logo{display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:6px}
.logo svg{width:46px;height:46px}
.logo span{font-size:24px;color:#235a81;font-weight:bold;letter-spacing:-.5px}
.logo span b{color:#e8862a;font-weight:bold}
h1{font-size:18px;font-weight:normal;color:#235a81;text-align:center;margin:0 0 20px}
form.login fieldset{background:#fff;border:1px solid #a0a0a0;border-radius:5px;padding:6px 0 16px;margin:0}
form.login legend{background:#e5e5e5;border:1px solid #a0a0a0;border-radius:4px;padding:4px 12px;color:#235a81;font-weight:bold;margin-left:14px}
.item{padding:7px 18px;overflow:hidden}
.item label{display:inline-block;width:104px;color:#333;vertical-align:middle}
.textfield,#select_server{padding:6px;border:1px solid #a0a0a0;border-radius:3px;font-size:14px;width:210px;box-sizing:border-box;background:#fff}
.textfield:focus,#select_server:focus{border-color:#235a81;outline:2px solid #cfe0ef}
.button{margin:8px 0 0 122px;background:#235a81;color:#fff;border:1px solid #1c4a6b;border-radius:4px;padding:8px 26px;font-size:14px;cursor:pointer}
</style>
</head>
<body>
<div id="page">
<div class="logo"><svg viewBox="0 0 64 64" role="img" aria-label="database"><ellipse cx="32" cy="14" rx="22" ry="9" fill="#235a81"/><path d="M10 14v18c0 5 10 9 22 9s22-4 22-9V14c0 5-10 9-22 9s-22-4-22-9z" fill="#2f6d99"/><path d="M10 32v18c0 5 10 9 22 9s22-4 22-9V32c0 5-10 9-22 9s-22-4-22-9z" fill="#235a81"/></svg><span>php<b>My</b>Admin</span></div>
<h1>Welcome to phpMyAdmin</h1>
<form method="post" action="phpmyadmin" name="login_form" class="login">
<fieldset>
<legend>Log in</legend>
<div class="item"><label for="input_username">Username:</label><input type="text" name="pma_username" id="input_username" value="" class="textfield" autocomplete="username"></div>
<div class="item"><label for="input_password">Password:</label><input type="password" name="pma_password" id="input_password" value="" class="textfield" autocomplete="current-password"></div>
<div class="item"><label for="select_server">Server Choice:</label><select name="server" id="select_server"><option value="1">localhost:3306</option></select></div>
<input type="submit" name="submit" value="Go" class="button">
</fieldset>
</form>
</div>
</body>
</html>
HTML;
    }

    private function fakeGenericLogin(): string
    {
        return <<<'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in</title>
<style>
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:#eef1f5;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#1a2332;display:flex;align-items:center;justify-content:center;min-height:100vh}
main{background:#fff;width:340px;padding:36px 32px 32px;border-radius:12px;box-shadow:0 6px 28px rgba(20,30,55,.10)}
.brand{display:flex;align-items:center;justify-content:center;gap:9px;margin:0 0 8px}
.brand svg{width:30px;height:30px}
.brand span{font-size:16px;font-weight:600;color:#334155}
h1{font-size:19px;font-weight:600;margin:0 0 22px;text-align:center}
label{display:block;font-size:13px;color:#5b6472;margin:0 0 6px}
input{width:100%;padding:10px 12px;border:1px solid #d5dbe3;border-radius:7px;font-size:14px;margin:0 0 16px;background:#fff}
input:focus{border-color:#2563eb;outline:2px solid #bfdbfe}
button{width:100%;padding:11px;background:#2563eb;color:#fff;border:0;border-radius:7px;font-size:14px;font-weight:600;cursor:pointer;margin-top:4px}
button:hover{background:#1d4ed8}
.foot{text-align:center;margin:16px 0 0;font-size:12px;color:#94a3b8}
</style>
</head>
<body>
<main>
<div class="brand"><svg viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="lock"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg><span>Admin Console</span></div>
<h1>Sign in to continue</h1>
<form method="post" action="administrator">
<label for="username">Username</label>
<input type="text" name="username" id="username" autocomplete="username">
<label for="password">Password</label>
<input type="password" name="password" id="password" autocomplete="current-password">
<button type="submit">Sign in</button>
</form>
<p class="foot">Authorized personnel only</p>
</main>
</body>
</html>
HTML;
    }
}
