<?php

namespace App\Console\Commands;

use App\Http\Middleware\RestrictConsoleNetwork;
use App\Support\Settings;
use Illuminate\Console\Command;

/**
 * The way back in when the console has locked its owner out.
 *
 * The allowlist is managed in the console — which is fine until the address you
 * manage it from changes, and then the page that would fix the problem is the
 * page the problem blocks. Every gate needs a door that does not depend on
 * itself, and on a server that door is a shell.
 */
class ConsoleAccess extends Command
{
    protected $signature = 'clupilot:console-access
                            {action=show : show|allow|deny|open|close|caddy}
                            {value? : an address or CIDR, for allow and deny}';

    protected $description = 'Show or change who may reach the operator console';

    public function handle(): int
    {
        $action = (string) $this->argument('action');
        $value = trim((string) $this->argument('value'));

        return match ($action) {
            'show' => $this->show(),
            'allow' => $this->allow($value),
            'deny' => $this->deny($value),
            'open' => $this->setRestricted(false),
            'close' => $this->setRestricted(true),
            'caddy' => $this->caddy(),
            default => $this->refuse("Unknown action: {$action}"),
        };
    }

    /**
     * The allowlist as a Caddy matcher, for the reverse proxy to import.
     *
     * The proxy has its own allowlist, hard-coded, and it runs FIRST — so
     * everything the owner adds in the console has no effect whatsoever, and
     * the console's own access page is a decoration. Worse, when the owner's
     * address changes they are turned away by the proxy before the application
     * they could have fixed it in is ever reached.
     *
     * Emitting the matcher from the same list the console manages makes the
     * console the single authority. The agent on the host writes this out and
     * reloads the proxy.
     */
    private function caddy(): int
    {
        // Always the allowlist — never 0.0.0.0/0, not even when the console's
        // own restriction is switched off.
        //
        // The owner's rule is absolute: the console is reachable over the
        // management VPN, or from an address they have listed, and from nowhere
        // else. Letting the console's "open" switch also open the PROXY would
        // put the console on the public internet with one click, which is
        // exactly the state that rule exists to prevent. Switching it off
        // relaxes the application's own check; the proxy keeps its list.
        $ranges = RestrictConsoleNetwork::allowedRanges();

        // Never empty: an empty remote_ip matcher matches NOTHING in Caddy, and
        // the console would become unreachable from anywhere at all — including
        // from the place someone would fix it. Loopback always survives, so a
        // shell on the box is always a way back.
        if ($ranges === []) {
            $ranges = ['127.0.0.1', '::1'];
        }

        $this->line('# Generated from the console allowlist — do not edit by hand.');
        $this->line('# Regenerated by deploy/update-agent.sh; edit it in the console.');
        $this->line('@allowed remote_ip '.implode(' ', $ranges));

        return self::SUCCESS;
    }

    private function show(): int
    {
        $this->line('  restricted : '.(RestrictConsoleNetwork::isRestricted() ? 'yes' : 'no — anyone reaching the hostname gets in'));
        $this->line('  always     : '.implode(', ', (array) config('admin_access.trusted_ranges', [])).'  (VPN, not removable)');

        $own = (array) Settings::get('console.allowed_ips', []);
        $this->line('  additional : '.($own === [] ? '(none)' : implode(', ', $own)));

        return self::SUCCESS;
    }

    private function allow(string $value): int
    {
        if ($value === '') {
            return $this->refuse('Give an address or range: clupilot:console-access allow 203.0.113.7');
        }

        // An entry that matches nothing would be stored, reported as success,
        // and leave whoever is recovering still locked out.
        if (! RestrictConsoleNetwork::isNetwork($value)) {
            return $this->refuse("Not an address or a range: {$value}");
        }

        $list = (array) Settings::get('console.allowed_ips', []);

        if (! in_array($value, $list, true)) {
            $list[] = $value;
            Settings::set('console.allowed_ips', array_values($list));
        }

        $this->info("{$value} may now reach the console.");

        return $this->show();
    }

    private function deny(string $value): int
    {
        Settings::set('console.allowed_ips', array_values(array_filter(
            (array) Settings::get('console.allowed_ips', []),
            fn ($entry) => $entry !== $value,
        )));

        $this->info("{$value} removed.");

        return $this->show();
    }

    private function setRestricted(bool $on): int
    {
        // No lock-out check here on purpose: this command IS the recovery path,
        // and it is only reachable by someone who already has the server.
        Settings::set('console.network_restricted', $on);
        $this->info($on ? 'Console restricted to the VPN and the listed addresses.' : 'Restriction lifted.');

        return $this->show();
    }

    private function refuse(string $message): int
    {
        $this->error($message);

        return self::FAILURE;
    }
}
