<?php

namespace App\Livewire\Threats;

use App\Models\AuditEvent;
use App\Models\BannedIp;
use App\Services\BruteforceGuard;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;

/**
 * THREATS dashboard — the operator-facing frontend for the honeypot deception layer. Surfaces the
 * hostile-probe feed (security.honeypot_hit / security.honeytoken_used / auth.ip_banned audit
 * events), KPI counters, and the active IP bans with an unban control. Read-only intelligence plus
 * one mutation (unban); the honeypot itself is env-driven and has no toggle here.
 */
#[Layout('layouts.app')]
class Index extends Component
{
    use WithPagination;

    /** Threat entries per page — page is deep-linked via ?page= (Livewire WithPagination). */
    private const PER_PAGE = 25;

    /** The honeypot / ban action codes that make up the threat feed. */
    private const FEED_ACTIONS = [
        'security.honeypot_hit',
        'security.honeypot_login',
        'security.honeytoken_used',
        'auth.ip_banned',
    ];

    /** Freitext-Filter über IP / Ziel des Bedrohungs-Ereignisses. */
    #[Url]
    public string $q = '';

    public function mount(): void
    {
        // Defense-in-depth: the route already carries can:manage-panel, but a persisted Livewire
        // component could be re-hydrated after a role change — re-check on mount.
        abort_unless(Auth::user()?->can('manage-panel'), 403);
    }

    /** A changed search must restart at page 1 — never land on an out-of-range page. */
    public function updatedQ(): void
    {
        $this->resetPage();
    }

    /**
     * Honeypot / ban events, newest first, optionally filtered. The filter runs in SQL (so it spans
     * the whole history, not just one page) and matches the source IP and the probed target path.
     * Mirrors Audit\Index::eventsQuery() but scoped to the honeypot action set.
     */
    protected function eventsQuery(): Builder
    {
        $query = AuditEvent::query()->whereIn('action', self::FEED_ACTIONS)->latest();

        $needle = trim($this->q);
        if ($needle === '') {
            return $query;
        }

        $like = '%'.mb_strtolower($needle).'%';

        return $query->where(function (Builder $w) use ($like): void {
            $w->whereRaw('LOWER(ip) LIKE ?', [$like])
                ->orWhereRaw('LOWER(target) LIKE ?', [$like]);
        });
    }

    /** Currently active bans (not yet expired), newest first — a short list for the ban panel. */
    protected function bannedIps(): Collection
    {
        return BannedIp::active()->latest('banned_until')->limit(50)->get();
    }

    /**
     * Page numbers to render, with "…" gaps for long histories (1 … 4 5 6 … 20).
     *
     * @return array<int, int|string>
     */
    protected function pageWindow(int $current, int $total): array
    {
        if ($total <= 7) {
            return range(1, max(1, $total));
        }

        $pages = [1];
        $from = max(2, $current - 1);
        $to = min($total - 1, $current + 1);
        if ($from > 2) {
            $pages[] = '…';
        }
        for ($i = $from; $i <= $to; $i++) {
            $pages[] = $i;
        }
        if ($to < $total - 1) {
            $pages[] = '…';
        }
        $pages[] = $total;

        return $pages;
    }

    /**
     * Lift an active ban. Reuses the exact unban path + audit code + toast convention from
     * Settings\LoginProtection so the two management surfaces stay consistent.
     */
    public function unban(string $ip, BruteforceGuard $guard): void
    {
        abort_unless(Auth::user()?->can('manage-panel'), 403);

        $guard->unban($ip);

        AuditEvent::create([
            'user_id' => Auth::id(),
            'actor' => Auth::user()?->name ?? 'system',
            'action' => 'auth.ip_unbanned',
            'target' => $ip,
            'ip' => request()->ip(),
        ]);

        $this->dispatch('notify', message: __('threats.unbanned_toast', ['ip' => $ip]));
    }

    public function render()
    {
        /** @var LengthAwarePaginator $events */
        $events = $this->eventsQuery()->paginate(self::PER_PAGE);

        // Most active source across all honeypot/token events (null when there is no traffic yet).
        $topIp = AuditEvent::query()
            ->whereIn('action', ['security.honeypot_hit', 'security.honeypot_login', 'security.honeytoken_used'])
            ->whereNotNull('ip')
            ->selectRaw('ip, COUNT(*) as hits')
            ->groupBy('ip')
            ->orderByDesc('hits')
            ->limit(1)
            ->value('ip');

        return view('livewire.threats.index', [
            'events' => $events,
            'pageWindow' => $this->pageWindow($events->currentPage(), $events->lastPage()),
            'bannedIps' => $this->bannedIps(),
            'probes_24h' => AuditEvent::query()
                ->where('action', 'security.honeypot_hit')
                ->where('created_at', '>=', now()->subDay())
                ->count(),
            // POST fake-login attempts in the last 24h — the "someone tried to log in" signal.
            'login_attempts_24h' => AuditEvent::query()
                ->where('action', 'security.honeypot_login')
                ->where('created_at', '>=', now()->subDay())
                ->count(),
            'banned_total' => BannedIp::active()->count(),
            'honeytoken_trips' => AuditEvent::query()->where('action', 'security.honeytoken_used')->count(),
            'top_ip' => $topIp,
        ])->title(__('threats.title'));
    }
}
