<?php

namespace App\Livewire\Concerns;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;

/**
 * A password re-entry gate in front of the dangerous parts of a page.
 *
 * Used by anything where holding a signed-in session should not be enough on
 * its own: setting up two-factor authentication, and reading or changing stored
 * credentials. The threat is not a stranger — it is a colleague, or anyone, at
 * an unlocked machine.
 *
 * The marker is keyed on the confirming GUARD and that guard's own ACCOUNT id
 * (passwordConfirmationSessionKey()) — it used to be one flat key shared by
 * every guard, the same one Laravel's own `password.confirm` middleware
 * uses. That worked only as long as one session ever held one identity. In
 * shared-host mode (this VM's own mode) the portal and console guards
 * coexist in the SAME session, so a customer confirming their portal
 * password and an operator confirming an operator-only action both wrote and
 * read that identical flat key — confirming as one identity silently
 * satisfied the gate for the other (Codex R15, P1a). Scoping to guard AND
 * identity closes both halves at once: a marker cannot cross a guard
 * boundary, and cannot survive switching to a different account on the same
 * guard either. No framework route in this app runs Laravel's own
 * password.confirm middleware (checked) — if that changes, it needs the same
 * scoping or this gap reopens.
 *
 * Rate-limited, because a confirmation form is a password oracle: without a
 * limit it is an unauthenticated-feeling place to guess at a known account.
 */
trait ConfirmsPassword
{
    public string $confirmablePassword = '';

    /** How long one confirmation lasts. Laravel's own default. */
    protected function confirmationWindow(): int
    {
        return (int) config('auth.password_timeout', 10800);
    }

    public function passwordRecentlyConfirmed(): bool
    {
        $at = (int) session($this->passwordConfirmationSessionKey(), 0);

        return $at > 0 && (time() - $at) < $this->confirmationWindow();
    }

    /**
     * Which guard's record this component confirms against.
     *
     * Overridden to 'operator' by the console components. A default of 'web'
     * keeps the portal working unchanged — and an override is explicit, where
     * sniffing the request would be one more thing to get wrong on a page that
     * gates access to credentials.
     */
    protected function confirmationGuard(): string
    {
        return 'web';
    }

    public function confirmPassword(): void
    {
        $account = auth()->guard($this->confirmationGuard())->user();
        $key = 'confirm-password:'.$this->confirmationGuard().'|'.$account?->getAuthIdentifier().'|'.request()->ip();

        if (RateLimiter::tooManyAttempts($key, 5)) {
            $this->addError('confirmablePassword', __('auth.throttle', [
                'seconds' => RateLimiter::availableIn($key),
            ]));

            return;
        }

        if ($account === null || ! Hash::check($this->confirmablePassword, $account->password)) {
            RateLimiter::hit($key, 60);
            $this->addError('confirmablePassword', __('admin_settings.password_wrong'));
            $this->reset('confirmablePassword');

            return;
        }

        RateLimiter::clear($key);
        $this->reset('confirmablePassword');

        // Regenerated first: a confirmation raises what this session can do, and
        // a session id that was already known to someone else must not be the
        // one that gets the extra authority.
        session()->regenerate();
        session([$this->passwordConfirmationSessionKey() => time()]);
    }

    /** Give up the confirmation early — leaving a page should not keep it open. */
    public function forgetPasswordConfirmation(): void
    {
        session()->forget($this->passwordConfirmationSessionKey());
    }

    /**
     * The one place this marker's session key is computed.
     *
     * passwordRecentlyConfirmed(), confirmPassword() and
     * forgetPasswordConfirmation() all go through this rather than each
     * building the string themselves, so they cannot drift apart from one
     * another — which is exactly how the guard-agnostic key this replaces
     * came to be read by one guard and written by another.
     */
    private function passwordConfirmationSessionKey(): string
    {
        $guard = $this->confirmationGuard();
        $identity = auth()->guard($guard)->user()?->getAuthIdentifier() ?? 'guest';

        return "auth.password_confirmed_at.{$guard}.{$identity}";
    }

    /**
     * A value shown only in outline: the last four characters, and nothing else.
     *
     * Enough to tell two keys apart, useless to anyone reading over a shoulder
     * or scrolling back through a screen recording.
     */
    protected function outline(?string $value, int $keep = 4): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        return Str::length($value) <= $keep
            ? str_repeat('•', Str::length($value))
            : str_repeat('•', 8).Str::substr($value, -$keep);
    }
}
