<?php

namespace App\Http\Controllers;

use App\Exceptions\IdentityCollisionException;
use App\Models\Customer;
use App\Models\Operator;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL;

/**
 * Looking at a customer's portal as that customer.
 *
 * Not a shared cookie. Once the console has a hostname of its own, a session
 * cookie set on the console host never reaches the portal host — SESSION_DOMAIN
 * is null and cookies are host-bound. So the handover is a signed, single-use
 * link: it works regardless of cookie scope, it expires, and it leaves a record
 * of who looked at whom.
 */
class ImpersonationController extends Controller
{
    /** Sixty seconds is a handover, not a credential. */
    private const WINDOW = 60;

    public function start(Customer $customer): RedirectResponse
    {
        $this->authorize('customers.impersonate');

        $operator = Auth::guard('operator')->user();

        // impersonate.enter carries no domain of its own — a portal route,
        // reached wherever the portal already answers. Its SIGNATURE is
        // computed over the full absolute URL: UrlGenerator::signedRoute()
        // hashes the generated URL string, host included, and
        // hasCorrectSignature() re-hashes $request->url() — the ACTUAL request
        // URL — on the way back. Left alone, "the generated URL" means
        // whichever host issued THIS request: the console's, once it has an
        // exclusive hostname of its own. RestrictAdminHost then 404s every
        // non-console route reached through the console's exclusive host,
        // including this one — so the link would die before the customer ever
        // saw it, the exact cross-host failure this controller exists to fix.
        //
        // Forced to the portal's own configured root instead, derived from
        // config('app.url') rather than hardcoded, and restored immediately
        // after: nothing else generated later in this request should inherit
        // the override.
        $portalRoot = rtrim((string) config('app.url'), '/');

        URL::forceRootUrl($portalRoot);

        try {
            $signed = URL::temporarySignedRoute(
                'impersonate.enter',
                now()->addSeconds(self::WINDOW),
                // Addressed by CUSTOMER uuid, not user: `users` has no uuid
                // column, and naming the integer key in a URL is what R11
                // forbids. The portal end resolves the account the same way
                // this end would have.
                ['customer' => $customer->uuid, 'operator' => $operator->uuid],
            );
        } finally {
            URL::forceRootUrl(null);
        }

        return redirect()->away($signed);
    }

    /** The portal end of the handover. Signed, and good exactly once. */
    public function enter(Request $request, string $customer, string $operator): RedirectResponse
    {
        abort_unless($request->hasValidSignature(), 403);

        // Single use: the signature's own hash is the key, and it lives exactly
        // as long as the link could. Redis, not the database — the marker is as
        // short-lived as the link and should not need tidying up.
        $marker = 'impersonate:'.hash('sha256', (string) $request->query('signature'));

        abort_unless(Cache::add($marker, true, self::WINDOW), 403);

        // Stale data, not a normal outcome: a customer row sharing its email
        // with an operator should never have been possible to reach this far
        // (see Customer::ensureUser() and CreateNewUser, R21) — refuse rather
        // than hand the operator's own address a portal session.
        try {
            $target = Customer::where('uuid', $customer)->firstOrFail()->ensureUser();
        } catch (IdentityCollisionException) {
            abort(409);
        }
        $who = Operator::where('uuid', $operator)->firstOrFail();

        Auth::guard('web')->login($target);
        session(['impersonator_id' => $who->id]);

        return redirect()->route('dashboard');
    }

    /** Back out of the customer's portal. The operator session was never touched. */
    public function leave(): RedirectResponse
    {
        session()->forget('impersonator_id');
        Auth::guard('web')->logout();

        // By NAME, not AdminArea::home(): this route is reached on the
        // PORTAL's host, and home() is a bare path ('/' in exclusive mode) —
        // resolved against whatever host the request is already on, which is
        // the portal's, not the console's. admin.overview is domain-bound to
        // the console in exclusive mode, so route() generates the correct
        // cross-host, absolute URL back to it; in shared mode it resolves
        // against the current host same as home() would, so nothing changes
        // there.
        return redirect()->route('admin.overview');
    }
}
