/**
 * Shared cross-device cloud sync, via free Firebase Firestore.
 *
 * One sync code covers everything that uses this file - the planning page's
 * saved plans (assets/js/planning/04-app.jsx) and the site-wide favourites
 * list (assets/js/shared/favourites.jsx, surfaced in navbar.jsx) both call
 * useCloudSync() below with their own collection/field, but share the same
 * code (and the same localStorage key for it) so setting sync up once, from
 * either feature, covers both - no accounts/login, just a code.
 *
 * Firebase itself is NOT loaded by default on every page. Most visitors
 * never touch sync, so the SDK (two external scripts) is only fetched when
 * it's actually needed: on a page where this device already has a sync code
 * saved, or the moment someone clicks "Set Up Sync" / "Join" for the first
 * time. See loadFirebaseSdk() below.
 *
 * Setup (one-time, in the Firebase console):
 * 1. Create a free project at https://console.firebase.google.com
 * 2. Build > Firestore Database > Create database (any region, doesn't matter)
 * 3. Paste your project's config below (Project settings > General > Your apps > Web app)
 * 4. Firestore > Rules tab, replace the default rules with:
 *      rules_version = '2';
 *      service cloud.firestore {
 *        match /databases/{database}/documents {
 *          match /planLibraries/{syncCode} {
 *            allow read, write: if true;
 *          }
 *          match /favouriteLibraries/{syncCode} {
 *            allow read, write: if true;
 *          }
 *        }
 *      }
 *
 * Anyone who somehow guessed your exact code could read/write that one
 * collection's document, but Firestore doesn't allow listing/enumerating
 * documents under the rules above, and the code has ~62 bits of entropy -
 * an acceptable tradeoff for a free, login-free feature holding non-sensitive
 * shoot plans and bookmarks.
 */

const FIREBASE_CONFIG = {
    apiKey: "AIzaSyD2TRA17vR18v_1lz_3oF3nCvHQpDLhUpk",
    authDomain: "ephemeris-planner.firebaseapp.com",
    projectId: "ephemeris-planner",
    storageBucket: "ephemeris-planner.firebasestorage.app",
    messagingSenderId: "1019337925337",
    appId: "1:1019337925337:web:0bbdc63bff64d2068754c8"
};

const FIREBASE_SDK_URLS = [
    'https://www.gstatic.com/firebasejs/12.11.0/firebase-app-compat.js',
    'https://www.gstatic.com/firebasejs/12.11.0/firebase-firestore-compat.js'
];

// Loads the two SDK scripts in order, exactly once, regardless of how many
// components ask for it at once - every caller shares the same promise.
let firebaseSdkPromise = null;
const loadFirebaseSdk = () => {
    if (window.firebase) return Promise.resolve();
    if (firebaseSdkPromise) return firebaseSdkPromise;
    firebaseSdkPromise = FIREBASE_SDK_URLS.reduce((chain, url) => chain.then(() => new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = url;
        script.onload = () => resolve();
        script.onerror = () => reject(new Error(`Failed to load ${url}`));
        document.head.appendChild(script);
    })), Promise.resolve()).catch((err) => {
        firebaseSdkPromise = null; // let a later attempt retry instead of staying permanently rejected
        throw err;
    });
    return firebaseSdkPromise;
};

let firestoreDbInstance = null;
const getFirestoreDb = () => {
    if (firestoreDbInstance) return firestoreDbInstance;
    if (!FIREBASE_CONFIG.apiKey || FIREBASE_CONFIG.apiKey.includes('PASTE_YOUR')) return null;
    if (!window.firebase) return null;
    try {
        if (!window.firebase.apps || !window.firebase.apps.length) {
            window.firebase.initializeApp(FIREBASE_CONFIG);
        }
        firestoreDbInstance = window.firebase.firestore();
        return firestoreDbInstance;
    } catch (e) {
        console.warn("Firebase init failed:", e.message);
        return null;
    }
};

// 3 groups of 4 characters from a safe alphabet (no ambiguous 0/O/1/I/L),
// e.g. "XK4P-7QRT-9MWL" - short enough to type by hand, long enough
// (~62 bits) that guessing someone else's code isn't realistic.
const SYNC_CODE_ALPHABET = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
const generateSyncCode = () => {
    const randomGroup = () => Array.from({ length: 4 }, () => SYNC_CODE_ALPHABET[Math.floor(Math.random() * SYNC_CODE_ALPHABET.length)]).join('');
    return `${randomGroup()}-${randomGroup()}-${randomGroup()}`;
};

// One shared code, one shared localStorage key - a change made by ANY
// useCloudSync() instance on the page (there can be more than one mounted
// at once, e.g. the navbar's favourites sync alongside the planning page's
// own) is broadcast here so every instance picks it up immediately, and the
// 'storage' event covers the same happening in another tab.
const SYNC_CODE_KEY = 'np_sync_code';
const SYNC_CODE_EVENT = 'np-sync-code-changed';

/**
 * useCloudSync({ collection, field, data, setData, mergeById })
 *
 *   collection - Firestore collection name, e.g. 'planLibraries'
 *   field      - the array field stored in that collection's document, e.g. 'plans'
 *   data       - the current local array (reactive - a plain useState value)
 *   setData    - applies a remote array locally. For plain useState-backed
 *                data this is just the setter; for anything also mirrored
 *                to its own localStorage key outside this hook (favourites),
 *                pass the function that writes through to that store too,
 *                so other components relying on it see the update as well.
 *   mergeById  - (localArray, remoteArray) => merged array, used only when
 *                joining a code that already has data on it.
 *
 * Returns { syncCode, syncStatus, connect, disconnect }.
 */
function useCloudSync({ collection, field, data, setData, mergeById }) {
    const [syncCode, setSyncCode] = useState(() => localStorage.getItem(SYNC_CODE_KEY) || null);
    const [syncStatus, setSyncStatus] = useState('idle'); // idle | syncing | synced | error
    const [firebaseReady, setFirebaseReady] = useState(() => !!window.firebase);
    const isApplyingRemoteRef = useRef(false); // set true right before a remote update is applied locally, so the push effect below knows to skip re-pushing it (avoids an echo loop between devices)

    // Keep this instance's syncCode in step with any other instance on the
    // same page, and with other tabs.
    useEffect(() => {
        const onCodeChange = () => setSyncCode(localStorage.getItem(SYNC_CODE_KEY) || null);
        window.addEventListener(SYNC_CODE_EVENT, onCodeChange);
        window.addEventListener('storage', onCodeChange);
        return () => {
            window.removeEventListener(SYNC_CODE_EVENT, onCodeChange);
            window.removeEventListener('storage', onCodeChange);
        };
    }, []);

    // A code is already saved on this device (from a previous visit, or set
    // up just now by another instance on this page) - make sure Firebase is
    // actually loaded before anything below tries to use it.
    useEffect(() => {
        if (!syncCode || firebaseReady) return;
        loadFirebaseSdk().then(() => setFirebaseReady(true)).catch((err) => {
            console.warn(`Sync: couldn't load Firebase SDK (${collection}):`, err.message);
            setSyncStatus('error');
        });
    }, [syncCode, firebaseReady]);

    // Push local changes up.
    useEffect(() => {
        if (!syncCode || !firebaseReady) return;
        if (isApplyingRemoteRef.current) {
            isApplyingRemoteRef.current = false; // consume the flag - this change came FROM the cloud, don't push it straight back
            return;
        }
        const db = getFirestoreDb();
        if (!db) { setSyncStatus('error'); return; }

        setSyncStatus('syncing');
        try {
            // Defensive: strips down to plain, guaranteed-serializable data via a
            // JSON round-trip. Firestore's set() rejects anything that isn't a
            // plain object (e.g. a Leaflet LatLng class instance, which has
            // methods attached), and can throw that rejection SYNCHRONOUSLY
            // rather than as a promise rejection, which a .catch() on the
            // returned promise can never catch. This sanitize step plus the
            // surrounding try/catch means a stray non-plain value degrades to
            // a sync error instead of crashing the page.
            const sanitized = JSON.parse(JSON.stringify(data));
            db.collection(collection).doc(syncCode).set({
                [field]: sanitized,
                updatedAt: firebase.firestore.FieldValue.serverTimestamp()
            }).then(() => setSyncStatus('synced'))
              .catch(err => { console.warn(`Sync push failed (${collection}):`, err.message); setSyncStatus('error'); });
        } catch (err) {
            console.warn(`Sync push failed (${collection}, sanitize error):`, err.message);
            setSyncStatus('error');
        }
    }, [data, syncCode, firebaseReady]);

    // Real-time listener: picks up changes made on other devices automatically,
    // no manual refresh needed.
    useEffect(() => {
        if (!syncCode || !firebaseReady) return;
        const db = getFirestoreDb();
        if (!db) { setSyncStatus('error'); return; }

        const unsubscribe = db.collection(collection).doc(syncCode).onSnapshot(
            (doc) => {
                if (doc.exists) {
                    isApplyingRemoteRef.current = true;
                    setData(doc.data()[field] || []);
                }
                setSyncStatus('synced');
            },
            (err) => {
                console.warn(`Sync listen failed (${collection}):`, err.message);
                setSyncStatus('error');
            }
        );
        return () => unsubscribe();
    }, [syncCode, firebaseReady]);

    // Belt-and-braces refetch on returning to the tab. Mobile browsers
    // routinely suspend a backgrounded tab's WebSocket connection - the
    // onSnapshot listener above doesn't always resume cleanly by itself once
    // that happens, which reads as "edited on my phone, laptop doesn't see it
    // until I refresh": the write reaches Firestore fine, but the OTHER
    // device's already-open live connection just isn't delivering it while
    // it's suspended. A plain page reload always "worked" because it opens a
    // brand new connection from scratch; this does the same thing without
    // needing the reload, by explicitly pulling straight from the server
    // (bypassing any locally cached copy, which could be just as stale as the
    // suspended listener) the moment the tab becomes visible or focused again.
    useEffect(() => {
        if (!syncCode || !firebaseReady) return;

        const refetchFromServer = () => {
            const db = getFirestoreDb();
            if (!db) return;
            db.collection(collection).doc(syncCode).get({ source: 'server' })
                .then((doc) => {
                    if (doc.exists) {
                        isApplyingRemoteRef.current = true;
                        setData(doc.data()[field] || []);
                    }
                    setSyncStatus('synced');
                })
                .catch((err) => console.warn(`Sync refetch-on-resume failed (${collection}):`, err.message));
        };

        const onVisible = () => { if (document.visibilityState === 'visible') refetchFromServer(); };
        document.addEventListener('visibilitychange', onVisible);
        window.addEventListener('focus', refetchFromServer);
        return () => {
            document.removeEventListener('visibilitychange', onVisible);
            window.removeEventListener('focus', refetchFromServer);
        };
    }, [syncCode, firebaseReady]);

    // Connects to a sync code. For a brand-new code, pushes the current local
    // data as the cloud's starting copy. For joining an existing code (e.g.
    // entered on a second device), merges remote + local via mergeById rather
    // than overwriting either - so connecting a second device can't
    // accidentally wipe out data that only existed on one side.
    const connect = async (code, { isNew = false } = {}) => {
        try {
            await loadFirebaseSdk();
        } catch (err) {
            setSyncStatus('error');
            return { ok: false, reason: 'sdk-load-failed', error: err };
        }
        setFirebaseReady(true);
        const db = getFirestoreDb();
        if (!db) {
            return { ok: false, reason: 'not-configured' };
        }

        setSyncStatus('syncing');
        try {
            const docRef = db.collection(collection).doc(code);
            if (!isNew) {
                const docSnap = await docRef.get();
                if (docSnap.exists) {
                    const remoteItems = docSnap.data()[field] || [];
                    const mergedList = JSON.parse(JSON.stringify(mergeById(data, remoteItems)));
                    isApplyingRemoteRef.current = true;
                    setData(mergedList);
                    await docRef.set({ [field]: mergedList, updatedAt: firebase.firestore.FieldValue.serverTimestamp() });
                    localStorage.setItem(SYNC_CODE_KEY, code);
                    window.dispatchEvent(new Event(SYNC_CODE_EVENT));
                    setSyncCode(code);
                    setSyncStatus('synced');
                    return { ok: true, merged: true };
                }
                // Joining a code that doesn't exist is almost always a typo - fail
                // loudly here rather than silently creating a brand-new,
                // disconnected document under the mistyped code and falsely
                // reporting success (two devices each "successfully" syncing to
                // two different documents, so nothing ever actually propagates).
                setSyncStatus('idle');
                return { ok: false, reason: 'not-found' };
            }
            // Only reached when isNew is true - genuinely creating a fresh code.
            await docRef.set({ [field]: JSON.parse(JSON.stringify(data)), updatedAt: firebase.firestore.FieldValue.serverTimestamp() });
            localStorage.setItem(SYNC_CODE_KEY, code);
            window.dispatchEvent(new Event(SYNC_CODE_EVENT));
            setSyncCode(code);
            setSyncStatus('synced');
            return { ok: true, merged: false };
        } catch (err) {
            console.warn(`Sync connect failed (${collection}):`, err.message);
            setSyncStatus('error');
            return { ok: false, reason: 'error', error: err };
        }
    };

    const disconnect = () => {
        localStorage.removeItem(SYNC_CODE_KEY);
        window.dispatchEvent(new Event(SYNC_CODE_EVENT));
        setSyncCode(null);
        setSyncStatus('idle');
    };

    return { syncCode, syncStatus, connect, disconnect };
}
