<?php

use App\Http\Controllers\ImpersonationController;
use App\Livewire\Admin;
use App\Models\DpaVersion;
use App\Models\Invoice;
use App\Services\Billing\InvoiceRenderer;
use App\Services\Deployment\UpdateChannel;
use App\Services\Mail\MailPreviews;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;

/*
| The operator console.
|
| Its own file, and registered BEFORE routes/web.php, because once the console
| has a hostname to itself it lives at the ROOT of that host — where `/` and
| `/settings` also exist in the customer portal, and the first matching route
| wins.
|
| Where it is mounted is AdminArea's decision, not this file's: `/` on the
| console hostname where the console has one to itself, `/admin` on any host
| otherwise. Route NAMES are `admin.*` in both cases, so every route('admin.…')
| in the views is unaffected by which mode an installation is in.
|
| RestrictAdminHost is listed here as well as prepended to the `web` group:
| Livewire records it on the component snapshot and re-applies it to
| /livewire/update, so a console action cannot be driven through another host.
*/
Route::get('/', Admin\Overview::class)->name('overview');
Route::get('/customers', Admin\Customers::class)->name('customers');
Route::get('/instances', Admin\Instances::class)->name('instances');
Route::get('/hosts', Admin\Hosts::class)->name('hosts');
Route::get('/hosts/create', Admin\HostCreate::class)->name('hosts.create');
Route::get('/hosts/{host}', Admin\HostDetail::class)->name('hosts.show');
// Das Terminal öffnet sich in einem eigenen Fenster und hat deshalb keinen
// Navigationseintrag — es wird nur aus der Liste und der Detailseite heraus
// aufgerufen. `hosts.manage` prüft das Bauteil selbst in mount().
Route::get('/hosts/{host}/terminal', Admin\HostTerminal::class)->name('hosts.terminal');
Route::get('/datacenters', Admin\Datacenters::class)->name('datacenters');

// Hostnamen und Zertifikate. Eigene Seite und nicht unter Einstellungen: was
// hier steht, entscheidet, ob eine Adresse überhaupt antwortet — das ist keine
// Voreinstellung, das ist Betrieb.
Route::get('/proxy-hosts', Admin\ProxyHosts::class)->name('proxy-hosts');
Route::get('/plans', Admin\Plans::class)->name('plans');
Route::get('/plans/{uuid}', Admin\PlanVersions::class)->name('plans.versions');
// POST so the identity change is CSRF-protected (a GET could be forced cross-site).
Route::post('/impersonate/{customer}', [ImpersonationController::class, 'start'])->name('impersonate');
Route::get('/provisioning', Admin\Provisioning::class)->name('provisioning');
Route::get('/maintenance', Admin\Maintenance::class)->name('maintenance');
Route::get('/incidents', Admin\Incidents::class)->name('incidents');
Route::get('/capacity', Admin\Capacity::class)->name('capacity');
Route::get('/vpn', Admin\Vpn::class)->name('vpn');
Route::get('/revenue', Admin\Revenue::class)->name('revenue');
Route::get('/finance', Admin\Finance::class)->name('finance');
// One customer, everything about them, and a box to write to them.
Route::get('/customers/{uuid}', Admin\CustomerDetail::class)->name('customer');
// What customers wrote to us — see Admin\Inbox.
Route::get('/inbox', Admin\Inbox::class)->name('inbox');
// Looking at a mail before a customer does. The list is a page; the render is a
// plain response, because a mail is a whole document and belongs in its own tab.
Route::get('/mail/preview', Admin\MailPreview::class)->name('mail.preview');
Route::get('/mail/preview/{key}', function (string $key) {
    abort_unless(auth('operator')->user()?->can('mail.manage') ?? false, 403);

    $mailable = app(MailPreviews::class)->make($key);

    abort_if($mailable === null, 404);

    // The rendered mail, as the recipient's client would get it. Not escaped
    // into a console page: this is the document itself.
    return response($mailable->render());
})->name('mail.preview.show');
// The processing agreement (Art. 28 DSGVO): the document, the version in force,
// and who has accepted it. The TEXT is uploaded, never written here.
Route::get('/processing-agreement', Admin\ProcessingAgreements::class)->name('dpa');
Route::get('/processing-agreement/{uuid}/{which}', function (string $uuid, string $which) {
    abort_unless(auth('operator')->user()?->can('dpa.manage') ?? false, 403);
    abort_unless(in_array($which, ['agreement', 'measures'], true), 404);

    $version = DpaVersion::query()->where('uuid', $uuid)->firstOrFail();
    $path = $which === 'agreement' ? $version->agreement_path : $version->measures_path;

    abort_if($path === null || ! Storage::disk('local')->exists($path), 404);

    // Inline: a contract is read before it is filed. With ?download=1 it is
    // kept instead, under a name carrying the version.
    $name = ($which === 'agreement' ? 'CluPilot-AV-Vertrag-' : 'CluPilot-TOM-').$version->version.'.pdf';

    return request()->boolean('download')
        ? Storage::disk('local')->download($path, $name)
        : Storage::disk('local')->response($path, $name, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'inline; filename="'.$name.'"',
        ]);
})->name('dpa.file');

// The answers an operator gives over and over, written once — see
// Admin\MailTemplates.
Route::get('/mail-templates', Admin\MailTemplates::class)->name('templates');
// The register of what left the building — see Admin\MailLog.
Route::get('/mail-log', Admin\MailLog::class)->name('mail-log');
Route::get('/invoices', Admin\Invoices::class)->name('invoices');
// Writing one by hand, for work that came off no price list. Same series, same
// numbering — see Admin\NewInvoice.
Route::get('/invoices/new', Admin\NewInvoice::class)->name('invoices.new');

// The PDF, rendered on demand from the frozen document. A plain route rather
// than a Livewire action: a download is a download, and routing one through a
// component round-trip only adds a way for it to fail.
Route::get('/invoices/{uuid}/pdf', function (string $uuid) {
    Gate::authorize('site.manage');

    $invoice = Invoice::query()->where('uuid', $uuid)->firstOrFail();

    return response()->streamDownload(
        fn () => print (app(InvoiceRenderer::class)->forInvoice($invoice)),
        $invoice->number.'.pdf',
        ['Content-Type' => 'application/pdf'],
    );
})->name('invoices.pdf');
Route::get('/mail', Admin\Mail::class)->name('mail');
// Der Notschalter und die Sicht auf die drei Versandspuren — eigene Route statt
// eines weiteren Abschnitts auf admin/mail (siehe deren Kommentar oben), weil
// diese Seite den laufenden Betrieb beobachtet statt etwas einzurichten.
Route::get('/mail-pace', Admin\MailPace::class)->name('mail-pace');
// Die Tuer zu allem, was man einmal einrichtet — neun Seiten, die bis hierher
// zwischen dem Tagesgeschaeft in der Seitenleiste standen. Ohne eigene
// Berechtigung: die Seite zeigt nur Kacheln, und jede prueft ihre eigene.
Route::get('/setup', Admin\Setup::class)->name('setup');
Route::get('/integrations', Admin\Integrations::class)->name('integrations');
// The former admin.secrets and admin.infrastructure pages, merged into the
// one above — grouped by what each value configures, not by which of the two
// mechanisms (SecretVault vs App\Support\Settings) happens to hold it. Kept
// as redirects, permanently, so a bookmark or a link out in the wild does not
// 404 — the page has genuinely moved, the same reasoning legal.status already
// uses in routes/web.php.
Route::get('/secrets', fn () => redirect()->route('admin.integrations', status: 301))->name('secrets');
Route::get('/infrastructure', fn () => redirect()->route('admin.integrations', status: 301))->name('infrastructure');
// Everything App\Support\Readiness knows how to check, in one place — gated
// the same way as Integrations just above, since it is the page where both
// halves that page configures (Settings and the vault) come back together as
// one list of what is still missing.
Route::get('/readiness', Admin\Readiness::class)->name('readiness');

// Was angefangen und nicht fertig ist. Unter System, weil es den Zustand DIESER
// Installation beschreibt und nicht die Arbeit eines Tages — und ohne eigene
// Berechtigung: wer die Konsole öffnen darf, soll wissen, worauf er sich
// verlässt und worauf nicht.
Route::get('/open-work', Admin\OpenWork::class)->name('open-work');
Route::get('/settings', Admin\Settings::class)->name('settings');
// Eine Root-Schale auf DEN CluPilot-Server, nicht auf einen Host. Eigenes
// Fenster, deshalb kein Navigationseintrag; aufgerufen wird sie aus den
// Einstellungen. Die Prüfungen stehen im Bauteil selbst und in dieser
// Reihenfolge: `server.terminal`, dann die Netzliste, dann die Einrichtung.
Route::get('/server/terminal', Admin\ServerTerminal::class)->name('server.terminal');
// Rollen und ihre Rechte. Eigene Seite statt eines Abschnitts unter
// Einstellungen: eine Matrix aus sechs Rollen und einundzwanzig Rechten ist
// keine Karte mehr, und wer sie öffnet, tut genau eine Sache.
Route::get('/roles', Admin\Roles::class)->name('roles');
// Wo Geld ausbleibt, beide Arten an einer Stelle: Verträge im Rückstand und
// Käufe, deren Zahlung Tage später geplatzt ist. Hinter billing.manage, damit
// die Buchhaltung es sieht, ohne Admin zu sein.
Route::get('/payment-problems', Admin\PaymentProblems::class)->name('payment-problems');
// Its own route so RequireOperatorTwoFactor can exempt ENROLMENT without
// exempting the rest of admin.settings — see that middleware for why.
Route::get('/two-factor-setup', Admin\TwoFactorSetup::class)->name('two-factor-setup');

// POST so Laravel's CSRF middleware protects it, like every other state change.
Route::post('/logout', function () {
    Auth::guard('operator')->logout();

    // NOT session()->invalidate(): where the console and the portal share a
    // host (shared/fallback mode — this VM's own mode), both guards keep
    // their login key in the SAME session. invalidate() is flush() +
    // migrate(true) — it discards every attribute in the session, not just
    // the operator's, so it would silently sign a customer out of the portal
    // too if the same browser happened to carry both. regenerate() issues a
    // new session id (no fixation risk) without touching the other
    // attributes, so the 'web' guard's own login key — already removed for
    // 'operator' by the logout() call above — survives untouched.
    session()->regenerate();
    session()->regenerateToken();

    return redirect()->route('admin.login');
})->name('logout');

/*
 * A deployment restarts the very application that is watching it.
 *
 * The settings card polls itself with wire:poll, which is fine until the run
 * reaches the restart: the requests fail, and what comes back afterwards is a
 * new build being asked questions by the old page's JavaScript. So the card
 * never learned that the update had finished and an operator had to reload to
 * find out — reported exactly that way.
 *
 * This is the smallest thing that can answer "is it done, and is it still the
 * same build" across that gap: no Livewire, no component state, no assets. A
 * failed request here is expected (it IS the restart) and the poller keeps
 * trying rather than giving up.
 */
Route::get('/update/state', function (UpdateChannel $channel) {
    $state = $channel->state();
    // Only once a run has actually started, not while merely queued: before
    // that, lastLog() would return the PREVIOUS run's leftovers, which would
    // sit under a "starts in 0:47" countdown looking like the current run
    // already has a history.
    $started = $state['started_at'] !== null;

    return response()->json([
        'running' => $state['running'],
        'commit' => $state['commit'],
        'behind' => $state['behind'],
        'last_finished_at' => $state['last_finished_at']?->toIso8601String(),
        // Already localized ("Schritt: …"), the same sentence the settings
        // card itself renders — so the maintenance overlay (layouts/admin)
        // only ever displays a ready-made string and never assembles German
        // text in JavaScript.
        'step' => $state['phase'] !== null
            ? __('admin_settings.update_step', ['step' => $state['phase']])
            : null,
        // Ready to print — the same sentence update_since renders on the
        // settings card — so the overlay never assembles it either.
        'running_since' => $started
            ? __('admin_settings.update_since', ['when' => $state['started_at']->diffForHumans()])
            : null,
        // The tail of the run log: a run that is genuinely stuck is then
        // visibly stuck AT a step, instead of a spinner nobody can read
        // anything from.
        'log' => $started ? $channel->lastLog() : null,
    ])->header('Cache-Control', 'no-store');
})->name('update.state');
