<?php

namespace App\Services\Devices;

use App\Models\LoginSession;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * Where an account is signed in, and how to end one of them.
 *
 * Everything is scoped by guard as well as by id, in every method. Operators
 * and customers are two tables and two guards (R21), so operator 1 and customer
 * 1 share an integer and nothing else — an unscoped query here would let either
 * of them sign the other out.
 *
 * A session counts as live only if the framework still has its row AND that row
 * has been touched within the session lifetime. The join alone is not enough:
 * Laravel collects expired sessions by lottery, so on a quiet installation rows
 * for sessions nobody could still use sit there for days, and the list would
 * offer somebody a laptop they closed last month as somewhere they are signed
 * in right now.
 */
final class SessionRegistry
{
    /**
     * The live sessions for one identity, newest activity first.
     *
     * @return Collection<int, object>
     */
    public function for(string $guard, int $id, ?string $currentUuid = null): Collection
    {
        return DB::table('login_sessions as ls')
            ->join('user_devices as d', 'd.id', '=', 'ls.device_id')
            ->join('sessions as s', 's.id', '=', 'ls.session_id')
            ->where('d.guard', $guard)
            ->where('d.authenticatable_id', $id)
            ->where('s.last_activity', '>=', now()->subMinutes((int) config('session.lifetime'))->getTimestamp())
            ->orderByDesc('ls.last_seen_at')
            ->get([
                'ls.uuid as uuid',
                'ls.ip_address as ip_address',
                'ls.last_seen_at as last_seen_at',
                'd.name as device',
                'd.uuid as device_uuid',
            ])
            ->map(function (object $row) use ($currentUuid) {
                $row->is_current = $currentUuid !== null && $row->uuid === $currentUuid;
                $row->last_seen_at = $row->last_seen_at ? \Illuminate\Support\Carbon::parse($row->last_seen_at) : null;

                return $row;
            });
    }

    /** End one session. Returns false when it is not this identity's to end. */
    public function end(string $guard, int $id, string $uuid): bool
    {
        $row = DB::table('login_sessions as ls')
            ->join('user_devices as d', 'd.id', '=', 'ls.device_id')
            ->where('d.guard', $guard)
            ->where('d.authenticatable_id', $id)
            ->where('ls.uuid', $uuid)
            ->first(['ls.id as id', 'ls.session_id as session_id']);

        if ($row === null) {
            return false;
        }

        $this->forget($row->session_id);
        DB::table('login_sessions')->where('id', $row->id)->delete();

        return true;
    }

    /**
     * End everything except the session asking.
     *
     * The current one is excluded rather than ended and re-created: signing
     * somebody out of the page they are using to sign other people out is not
     * what the button says, and it leaves them unable to see whether it worked.
     *
     * @return int how many were ended
     */
    public function endOthers(string $guard, int $id, ?string $currentUuid): int
    {
        $rows = DB::table('login_sessions as ls')
            ->join('user_devices as d', 'd.id', '=', 'ls.device_id')
            ->where('d.guard', $guard)
            ->where('d.authenticatable_id', $id)
            ->when($currentUuid !== null, fn ($q) => $q->where('ls.uuid', '!=', $currentUuid))
            ->get(['ls.id as id', 'ls.session_id as session_id']);

        foreach ($rows as $row) {
            $this->forget($row->session_id);
        }

        DB::table('login_sessions')->whereIn('id', $rows->pluck('id'))->delete();

        return $rows->count();
    }

    /** The uuid of the session making this request, if it has one. */
    public function currentUuid(): ?string
    {
        $uuid = session()->get(LoginSession::KEY);

        return is_string($uuid) && $uuid !== '' ? $uuid : null;
    }

    /**
     * Delete the framework's own row, which is what actually signs the browser
     * out: its next request finds no session and starts an empty one.
     */
    private function forget(?string $sessionId): void
    {
        if ($sessionId !== null && $sessionId !== '') {
            DB::table('sessions')->where('id', $sessionId)->delete();
        }
    }
}
