import { collection, query, where, getDocs, addDoc, getCountFromServer, onSnapshot } from 'firebase/firestore'; // tools for building and running db queries
import { db, auth } from './config'; // database connection + anonymous auth
import { getNearbyGeohashCells } from '$lib/utils/geohash'; // convert coordinates into a 3x3 grid of geohash cell prefixes (handles cell-boundary edge cases - see geohash.js)
import { doc, getDoc, setDoc, updateDoc, increment, serverTimestamp } from 'firebase/firestore';
import ngeohash from 'ngeohash';
import { checkAndAwardStamp } from './stamps.js'; // "passport stamps" - see stamps.js

// --- global "memory counter" -----------------------------------------------
// A single document (meta/stats) holds `totalMessagesEverPosted`: a running
// total of every message ever created in the whole app, across all areas -
// including ones that have since expired/decayed and dropped out of
// getNearbyMessages's "active" results. It's deliberately a single shared
// doc, separate from individual message documents, because it represents a
// different thing: not "what's currently visible/active" (which shrinks as
// messages decay) but "everything that has ever been said here" (which only
// grows). See addMessage() and getTotalMessageCount() below.
const statsRef = doc(db, 'meta', 'stats');

export async function getNearbyMessages(lat, lng) {
    // query the user's geohash cell AND its 8 neighbors (3x3 grid) rather
    // than a single prefix range - see getNearbyGeohashCells in geohash.js
    // for why a single cell isn't reliable enough across devices.
    const cells = getNearbyGeohashCells(lat, lng);

    const snapshots = await Promise.all(
        cells.map(prefix => getDocs(query(
            collection(db, 'messages'),
            where('geohash', '>=', prefix),
            where('geohash', '<', prefix + '')
        )))
    );

    // merge the 9 result sets into one list. Neighboring geohash cells never
    // overlap, so each message can only appear in one snapshot - but we
    // dedupe by doc id anyway as a defensive measure, in case that ever changes.
    const seen = new Map();
    for (const snapshot of snapshots) {
        for (const docSnap of snapshot.docs) {
            if (!seen.has(docSnap.id)) {
                seen.set(docSnap.id, { id: docSnap.id, ...docSnap.data() });
            }
        }
    }
    const all = [...seen.values()];

    // we filter out messages which have already expired (past their echo date)
    const now = Date.now();
    const active = all.filter(message => { 
        const echoTime = message.lastEchoAt?.toMillis() ?? message.createdAt.toMillis();
        const daysSinceEcho = (now - echoTime) / (1000 * 60 * 60 * 24);

        return daysSinceEcho < 30; // less than 30 means it lives / is active
    });

    return active;
}

// --- live "pins pop into existence" listener --------------------------
// Real-time counterpart to getNearbyMessages() above. Instead of one-time
// getDocs() calls, this opens 9 onSnapshot listeners (the same 3x3 geohash
// cell grid) that keep firing for as long as the subscription is open -
// whenever a message in this area is created, echoed, or ages past the
// 30-day "active" window, onChange() is called again with the freshly
// merged + re-filtered list. +page.svelte uses this to detect messages
// that arrive WHILE the user is actively looking at the map (as opposed
// to the one-time initial load from getNearbyMessages above), so it can
// play the "pop" sound and bounce-animate just those new pins.
//
// The active/expired filter below intentionally mirrors getNearbyMessages
// - kept as its own copy (rather than a shared helper) so this listener's
// merge-on-every-snapshot logic stays self-contained.
//
// Returns an unsubscribe function that tears down all 9 listeners - call
// it when the map/page unmounts.
export function subscribeToNearbyMessages(lat, lng, onChange) {
    const cells = getNearbyGeohashCells(lat, lng);

    // each cell's listener writes its latest batch of docs into its own
    // slot here; every listener re-merges all 9 slots whenever ANY of them
    // fires, so onChange always receives the full up-to-date "nearby" set.
    const cellDocs = cells.map(() => []);

    const unsubscribes = cells.map((prefix, i) =>
        onSnapshot(
            query(
                collection(db, 'messages'),
                where('geohash', '>=', prefix),
                where('geohash', '<', prefix + '\uF8FF')
            ),
            (snapshot) => {
                cellDocs[i] = snapshot.docs.map(d => ({ id: d.id, ...d.data() }));

                // merge the 9 cells the same way getNearbyMessages does -
                // neighboring cells never overlap, dedupe by id defensively
                const seen = new Map();
                for (const docs of cellDocs) {
                    for (const message of docs) {
                        seen.set(message.id, message);
                    }
                }

                const now = Date.now();
                const active = [...seen.values()].filter(message => {
                    const echoTime = message.lastEchoAt?.toMillis() ?? message.createdAt.toMillis();
                    const daysSinceEcho = (now - echoTime) / (1000 * 60 * 60 * 24);
                    return daysSinceEcho < 30;
                });

                onChange(active);
            }
        )
    );

    return () => unsubscribes.forEach(unsub => unsub());
}

// --- "you're the first here" check (+page.svelte) -------------------------
// getNearbyMessages() above already fetches every doc matching the geohash
// prefix into `all`, then filters out anything past its 30-day decay into
// `active`. So `active.length === 0` alone can't tell apart two very
// different situations: "nobody has EVER posted in this ~1.2km area" vs
// "people posted here before, but everything since faded/expired".
// Rather than re-fetching and re-filtering all those documents again, this
// runs the SAME geohash range query through getCountFromServer - a
// Firestore aggregation query that returns just a number without
// transferring any document data, so it stays cheap even though it repeats
// the query. Returns true if at least one message (active or expired) has
// ever existed in this area.
export async function hasAnyMessagesEverNearby(lat, lng) {
    // same 3x3 cell grid as getNearbyMessages, for the same boundary reason -
    // neighboring cells never overlap, so the counts can simply be summed.
    const cells = getNearbyGeohashCells(lat, lng);

    const counts = await Promise.all(
        cells.map(prefix => getCountFromServer(query(
            collection(db, 'messages'),
            where('geohash', '>=', prefix),
            where('geohash', '<', prefix + '')
        )))
    );

    return counts.some(snapshot => snapshot.data().count > 0);
}

// update the echo counter
export async function echoMessage(messageId) {
    const ref = doc(db, 'messages', messageId);
    await updateDoc(ref, {
        echoCount: increment(1),
        lastEchoAt: serverTimestamp()
    });
}

// adding the message location
// moodColor (optional): the author's picked pastel swatch from ComposeSheet's
// mood-color row, as an hsl(...) string - or null if they didn't pick one.
// stored as-is on the message doc; pins.js falls back to a random pastel
// when this is null (see pinColor() in pins.js).
export async function addMessage(lat, lng, text, imageUrl = '', moodColor = null){
    // stored at max precision (9, ~5m cells) so getQueryPrefix() in
    // geohash.js can use any precision up to 9 without breaking the
    // >= / < prefix-range query in getNearbyMessages (a shorter query
    // prefix can match a longer stored geohash, but not vice versa).
    const geohash = ngeohash.encode(lat, lng, 9);

    await addDoc(collection(db, 'messages'), {
        text,
        imageUrl,
        lat,
        lng,
        geohash,
        moodColor,
        createdAt: serverTimestamp(),
        lastEchoAt: serverTimestamp(),
        echoCount: 0,
        // links the message to this device's persistent anonymous Firebase UID
        authorId: auth.currentUser?.uid ?? 'anon'
    });

    // --- global "memory counter": increment-only ----------------------------
    // Same increment(1) pattern as echoCount above, but on the shared
    // meta/stats doc instead of this message's own doc. setDoc with
    // { merge: true } both creates meta/stats on the very first call (if it
    // doesn't exist yet - increment() treats a missing field as starting from
    // 0) and increments the existing field on every call after that. This
    // counter never decrements: when a message later expires/decays out of
    // getNearbyMessages's active results, nothing here is touched, so the
    // total keeps representing everything ever posted, not just what's
    // currently visible.
    // Wrapped in try/catch so a failure here (e.g. security rules not yet
    // deployed for meta/stats) never breaks the actual posting of the
    // message above - the counter is a nice-to-have, not core functionality.
    try {
        await setDoc(statsRef, { totalMessagesEverPosted: increment(1) }, { merge: true });
    } catch (err) {
        console.error('[memory counter] failed to increment meta/stats:', err);
    }

    // --- "passport stamps": award a stamp if this is a new geohash-4 region -
    // checkAndAwardStamp (stamps.js) reverse-geocodes lat/lng and writes a
    // new stamp doc only if this user has never posted from this ~city-sized
    // region before. Wrapped in try/catch like the memory counter above - a
    // failed geocode or permission error here should never prevent the
    // message itself from posting.
    try {
        await checkAndAwardStamp(lat, lng);
    } catch (err) {
        console.error('[stamps] failed to check/award stamp:', err);
    }
}

// fetch the current value of the global "memory counter" (meta/stats), for
// the GlobalCountPill. Returns 0 if no message has ever been posted yet (the
// doc won't exist until the very first addMessage() call above), or if the
// read fails for any reason (e.g. security rules not yet deployed) - the pill
// simply won't render in that case (see GlobalCountPill's $globalCount check).
export async function getTotalMessageCount() {
    try {
        const snap = await getDoc(statsRef);
        return snap.exists() ? (snap.data().totalMessagesEverPosted ?? 0) : 0;
    } catch (err) {
        console.error('[memory counter] failed to read meta/stats:', err);
        return 0;
    }
}

// --- direct by-ID lookup, for shared links/QR codes (SharePopover.svelte,
// +page.svelte's auto-open-on-load logic) ----------------------------------
// getNearbyMessages() above can only ever find messages inside the current
// device's ~1.2km geohash prefix - a message shared as a link/QR code may
// have been left anywhere in the world, so opening it can't go through that
// geohash filter at all. getDoc-by-ID is a direct document lookup that
// works regardless of geohash, at the cost of needing the exact message ID
// (which is exactly what the shared link/QR encodes). Returns null (rather
// than throwing) if the doc doesn't exist or the read fails, so callers can
// show a graceful "this one has already faded" message instead of an error.
export async function getMessageById(id) {
    try {
        const ref = doc(db, 'messages', id);
        const snap = await getDoc(ref);
        return snap.exists() ? { id: snap.id, ...snap.data() } : null;
    } catch (err) {
        console.error('[share link] failed to fetch message by id:', err);
        return null;
    }
}

// fetch every message this device has authored, for the archive page
export async function getMyMessages() {
    const uid = auth.currentUser?.uid;
    if (!uid) return []; // not signed in yet, nothing to show

    const q = query(
        collection(db, 'messages'),
        where('authorId', '==', uid)
    );

    const snapshot = await getDocs(q);
    return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}