import { router } from '@inertiajs/react';
import { useCallback, useEffect, useRef, useState } from 'react';

/**
 * Tier-1 pessimistic project-edit lock client (Team Workspaces P4 / Δ21).
 *
 * Seeds from the server `editLock` baseline prop so the editor renders the
 * right mode on first paint, then:
 *   - On mount (if `can_edit`), pings to acquire the lock.
 *   - The holder heartbeats every HEARTBEAT_INTERVAL while the tab is visible
 *     (Page Visibility API). A read-only viewer polls at the same cadence to
 *     learn when the holder leaves.
 *   - A ping (or a save 403 surfaced via `markLockLost`) returning
 *     `held_by_me:false` flips read-only + toasts "{name} took over editing."
 *   - `takeOver()` POSTs takeover, then `router.reload({ only: ['screen'] })`
 *     so the new holder starts from current bytes, not a stale read.
 *
 * Viewers (`can_edit:false`, P3c) never ping / heartbeat / take over — they
 * just see the holder's name. The lock applies in BOTH modes; a solo buyer
 * always holds it (one cheap heartbeat).
 */

// Mirror the server constants (ProjectEditLockController). 3 missed beats =
// stale; 45s TTL on the server.
const HEARTBEAT_INTERVAL_MS = 15_000;

export type LockHolder = { id: number; name: string };

export type EditLockProp = {
    can_edit: boolean;
    held_by_me: boolean;
    holder: LockHolder | null;
};

export type EditLockState = {
    /** True when the caller may edit (holds the lock) AND is allowed to edit
     *  (can_edit). The canvas + toolbar gate writes off this. */
    canEdit: boolean;
    /** Server-side edit permission (role gate, P3c). A viewer is always
     *  read-only regardless of who holds the lock. */
    canEditWorkspace: boolean;
    /** The OTHER editor's identity when someone else holds the lock. */
    holder: LockHolder | null;
    /** Whether we currently hold the lock. */
    heldByMe: boolean;
};

function readCookie(name: string): string | undefined {
    const match = document.cookie.match(
        new RegExp(`(?:^|; )${name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1')}=([^;]*)`),
    );
    return match ? decodeURIComponent(match[1]!) : undefined;
}

async function lockFetch(
    url: string,
    method: 'POST' | 'DELETE',
): Promise<{ held_by_me: boolean; holder: LockHolder | null } | null> {
    const xsrf = readCookie('XSRF-TOKEN');
    const res = await fetch(url, {
        method,
        credentials: 'same-origin',
        headers: {
            Accept: 'application/json',
            ...(xsrf ? { 'X-XSRF-TOKEN': xsrf } : {}),
        },
        keepalive: method === 'DELETE',
    });
    if (!res.ok) return null;
    try {
        return (await res.json()) as { held_by_me: boolean; holder: LockHolder | null };
    } catch {
        return null;
    }
}

export type UseEditLockResult = EditLockState & {
    /** Force-acquire the lock, then re-hydrate from current bytes. */
    takeOver: () => Promise<void>;
    /** Called by useAutoSave's onLockLost when a save 403s (taken over). */
    markLockLost: (holder: LockHolder | null) => void;
};

export function useEditLock(
    projectId: number,
    initial: EditLockProp,
    onToast?: (message: string) => void,
    /** When false, the lock is inert: no heartbeat/ping, no take-over network,
     *  no release. Reverb+concurrent passes false — the lock guard is bypassed
     *  server-side there, so pinging protects nothing and same-account tabs
     *  would otherwise ping-pong the single-holder lock (→ "took over" toast
     *  spam). `canEditWorkspace` still flows from the prop, so editing stays
     *  gated correctly. Defaults true (lock + reverb-locked path). */
    lockEnabled: boolean = true,
): UseEditLockResult {
    const canEditWorkspace = initial.can_edit;

    const [heldByMe, setHeldByMe] = useState<boolean>(
        canEditWorkspace ? initial.held_by_me : false,
    );
    const [holder, setHolder] = useState<LockHolder | null>(initial.holder);

    // Δ10: always-mounted across partial reloads — reconcile from the prop
    // rather than seeding state once. A server refresh (e.g. takeOver's
    // router.reload) re-resolves `editLock`; mirror it here.
    useEffect(() => {
        setHeldByMe(canEditWorkspace ? initial.held_by_me : false);
        setHolder(initial.holder);
    }, [canEditWorkspace, initial.held_by_me, initial.holder]);

    const heldByMeRef = useRef(heldByMe);
    heldByMeRef.current = heldByMe;

    // Stash `onToast` in a ref. Callers pass an inline closure (it reads a
    // toast ref that's populated below the ToastProvider), so its identity
    // changes every render — keeping it OUT of the callback deps stops `ping`
    // from re-creating each render, which would tear down + restart the
    // heartbeat effect and fire an immediate beat every render (a ping/render
    // storm instead of one per HEARTBEAT_INTERVAL).
    const onToastRef = useRef(onToast);
    onToastRef.current = onToast;

    const applyPing = useCallback(
        (result: { held_by_me: boolean; holder: LockHolder | null } | null) => {
            if (!result) return;
            const wasHeld = heldByMeRef.current;
            setHeldByMe(result.held_by_me);
            setHolder(result.holder);
            // Transition from holder → not-holder = we got taken over.
            if (wasHeld && !result.held_by_me) {
                const name = result.holder?.name ?? 'Someone';
                onToastRef.current?.(`${name} took over editing.`);
            }
        },
        [],
    );

    const ping = useCallback(async () => {
        const result = await lockFetch(
            `/projects/${projectId}/edit-lock/ping`,
            'POST',
        );
        applyPing(result);
    }, [projectId, applyPing]);

    const markLockLost = useCallback(
        (lostHolder: LockHolder | null) => {
            const name = lostHolder?.name ?? 'Someone';
            setHeldByMe(false);
            setHolder(lostHolder);
            onToastRef.current?.(`${name} took over editing.`);
        },
        [],
    );

    const takeOver = useCallback(async () => {
        const result = await lockFetch(
            `/projects/${projectId}/edit-lock/takeover`,
            'POST',
        );
        if (result?.held_by_me) {
            setHeldByMe(true);
            setHolder(null);
        }
        // Re-hydrate from current bytes before enabling edits — start from the
        // displaced holder's last save, not a stale read.
        router.reload({ only: ['screen'] });
    }, [projectId]);

    // Heartbeat / poll loop. Viewers (can't edit) never run it; nor does the
    // reverb+concurrent path (lockEnabled=false) — the lock is bypassed there.
    useEffect(() => {
        if (!canEditWorkspace || !lockEnabled) return;

        let interval: ReturnType<typeof setInterval> | null = null;

        const start = () => {
            if (interval) return;
            void ping(); // immediate beat on (re)gaining visibility
            interval = setInterval(() => {
                if (document.visibilityState === 'visible') void ping();
            }, HEARTBEAT_INTERVAL_MS);
        };
        const stop = () => {
            if (interval) {
                clearInterval(interval);
                interval = null;
            }
        };

        const onVisibility = () => {
            if (document.visibilityState === 'visible') start();
            else stop();
        };

        if (document.visibilityState === 'visible') start();
        document.addEventListener('visibilitychange', onVisibility);

        return () => {
            stop();
            document.removeEventListener('visibilitychange', onVisibility);
        };
    }, [canEditWorkspace, lockEnabled, ping]);

    // Best-effort release on unmount / tab close. TTL covers a missed beacon.
    useEffect(() => {
        if (!canEditWorkspace || !lockEnabled) return;
        const release = () => {
            if (!heldByMeRef.current) return;
            void lockFetch(`/projects/${projectId}/edit-lock`, 'DELETE');
        };
        window.addEventListener('beforeunload', release);
        return () => {
            window.removeEventListener('beforeunload', release);
            release();
        };
    }, [canEditWorkspace, lockEnabled, projectId]);

    return {
        canEdit: canEditWorkspace && heldByMe,
        canEditWorkspace,
        holder,
        heldByMe,
        takeOver,
        markLockLost,
    };
}
