<?php

namespace App\Http\Controllers;

use App\Models\Project;
use App\Models\ProjectEditLock;
use App\Services\AuditLogger;
use App\Support\AppMode;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

/**
 * Pessimistic project-edit lock (Team Workspaces P4 / Δ21).
 *
 * One user edits a project at a time. The lock is project-level (covers all
 * screens/factors — sidesteps cross-screen/cross-factor false conflicts). A
 * second user lands read-only and can take over, which flips the previous
 * holder to read-only. No websockets — a lightweight heartbeat poll carries
 * lock state.
 *
 * JSON-returning, desktop-portable (mirrors ScreenController::update). The
 * lock applies in BOTH modes: a solo self-hosted buyer simply always holds
 * the lock (one cheap heartbeat). Role gating (viewers never ping/take over)
 * arrives in P3c — see ProjectController::show's `editLock.can_edit`.
 */
class ProjectEditLockController extends Controller
{
    /**
     * Seconds a lock survives without a heartbeat. 3 missed beats
     * (HEARTBEAT_INTERVAL) → stale → free on the next acquire (expiry-on-read).
     */
    public const LOCK_TTL = 45;

    /** Seconds the client waits between heartbeats while the tab is visible. */
    public const HEARTBEAT_INTERVAL = 15;

    /**
     * The ONE combined call. Upserts the caller's heartbeat iff they hold (or
     * can acquire) a free/expired lock. Acquire + heartbeat + state-read in a
     * single transactional round-trip.
     */
    public function ping(Request $request, Project $project): JsonResponse
    {
        $this->authorizeProject($request, $project);
        $userId = $request->user()->id;

        $state = DB::transaction(function () use ($project, $userId) {
            $lock = ProjectEditLock::query()
                ->where('project_id', $project->id)
                ->lockForUpdate()
                ->first();

            $now = now();

            if ($lock === null) {
                $lock = ProjectEditLock::query()->create([
                    'project_id' => $project->id,
                    'user_id' => $userId,
                    'acquired_at' => $now,
                    'last_heartbeat_at' => $now,
                ]);

                return ['held_by_me' => true, 'lock' => $lock];
            }

            $isStale = $lock->last_heartbeat_at === null
                || $lock->last_heartbeat_at->lt($now->copy()->subSeconds(self::LOCK_TTL));

            if ($lock->user_id === $userId) {
                $lock->last_heartbeat_at = $now;
                $lock->save();

                return ['held_by_me' => true, 'lock' => $lock];
            }

            if ($isStale) {
                // Expired holder — acquire it for the caller.
                $lock->user_id = $userId;
                $lock->acquired_at = $now;
                $lock->last_heartbeat_at = $now;
                $lock->save();

                return ['held_by_me' => true, 'lock' => $lock];
            }

            // Held by someone else who's still alive — caller stays read-only.
            return ['held_by_me' => false, 'lock' => $lock];
        });

        return response()->json($this->lockPayload($state['lock'], $state['held_by_me']));
    }

    /**
     * Force-reassign the lock to the caller (transactional). Audited with the
     * displaced holder. The displaced holder's next/in-flight write 403s on
     * the lock-holder guard, closing the handoff race.
     */
    public function takeover(Request $request, Project $project): JsonResponse
    {
        $this->authorizeProject($request, $project);
        $userId = $request->user()->id;

        $result = DB::transaction(function () use ($project, $userId) {
            $lock = ProjectEditLock::query()
                ->where('project_id', $project->id)
                ->lockForUpdate()
                ->first();

            $now = now();
            $displacedUserId = $lock?->user_id;

            if ($lock === null) {
                $lock = ProjectEditLock::query()->create([
                    'project_id' => $project->id,
                    'user_id' => $userId,
                    'acquired_at' => $now,
                    'last_heartbeat_at' => $now,
                ]);
            } else {
                $lock->user_id = $userId;
                $lock->acquired_at = $now;
                $lock->last_heartbeat_at = $now;
                $lock->save();
            }

            return ['lock' => $lock, 'displaced_user_id' => $displacedUserId];
        });

        AuditLogger::record(
            'project.editlock.takeover',
            $project,
            "Took over the edit lock for project #{$project->id}",
            [
                'project_id' => $project->id,
                'new_holder_id' => $userId,
                'displaced_holder_id' => $result['displaced_user_id'],
            ],
        );

        return response()->json($this->lockPayload($result['lock'], true));
    }

    /**
     * Release the lock (best-effort, on editor unmount / beforeunload). Only
     * the holder releases — a stale-but-not-yet-overtaken row should not be
     * yanked by a non-holder. TTL covers a missed release.
     */
    public function release(Request $request, Project $project): JsonResponse
    {
        $this->authorizeProject($request, $project);
        $userId = $request->user()->id;

        ProjectEditLock::query()
            ->where('project_id', $project->id)
            ->where('user_id', $userId)
            ->delete();

        return response()->json(['released' => true]);
    }

    /**
     * Shared response shape. `holder` is null when the caller holds the lock
     * (the client only needs the *other* editor's identity).
     *
     * @return array<string, mixed>
     */
    private function lockPayload(ProjectEditLock $lock, bool $heldByMe): array
    {
        $holder = null;
        if (! $heldByMe) {
            $holderUser = $lock->user()->first();
            $holder = [
                'id' => $lock->user_id,
                'name' => $holderUser?->name ?? 'Someone',
            ];
        }

        return [
            'held_by_me' => $heldByMe,
            'holder' => $holder,
            'since' => $lock->acquired_at?->toIso8601String(),
        ];
    }

    private function authorizeProject(Request $request, Project $project): void
    {
        if (AppMode::isSelfHosted()) {
            abort_unless($project->tenant_id === null, 404);

            return;
        }

        abort_unless($project->tenant_id === $request->user()->tenant_id, 404);
    }
}
