<script>
    import { onMount } from 'svelte';
    import MapView from '$lib/components/MapView.svelte';

    import { getNearbyMessages, hasAnyMessagesEverNearby, getMessageById, subscribeToNearbyMessages } from '$lib/firebase/messages.js';
    import { getDecayInfo } from '$lib/utils/time.js'; // checks whether a shared message has expired
    import { messagesStore, setMessages, livePinIdsStore } from '$lib/stores/messagesStore.js';
    import { playPinPopSound } from '$lib/utils/sound.js'; // "pin pop into existence" live-arrival sound
    import { mapStore } from '$lib/stores/mapStore.js';
    import { initAuth, userStore } from '$lib/stores/userStore.js';
    import { getStamps } from '$lib/firebase/stamps.js'; // "passport stamps" - see stamps.js
    import { stampsStore } from '$lib/stores/stampsStore.js'; // drives the empty/filled stamp-book icon below
    import { page } from '$app/stores'; // current route, used to highlight the active bottom-nav tab
    import { replaceState } from '$app/navigation'; // clears the ?message= param after handling a shared link

    import BottomSheet from '$lib/components/BottomSheet.svelte';
    import SidePanel from '$lib/components/SidePanel.svelte';

    import ComposeSheet from '$lib/components/ComposeSheet.svelte';

    let lat = $state();
    let lng = $state();
    let error = $state();

    // handle for the real-time "pins pop into existence" listener
    // (subscribeToNearbyMessages, messages.js) - assigned once the initial
    // load finishes (see onMount below) and called on unmount to tear down
    // its 9 onSnapshot listeners. Plain variable, not a rune: it's only ever
    // read in the onMount cleanup function, never rendered.
    let unsubscribeLive;

    let windowWidth = $state(0);

    let isMobile = $derived(windowWidth < 768);

    // --- "you're the first here" ambient message --------------------------
    // `firstHereMounted` controls whether the message exists in the DOM at
    // all - it's set true at most once, right after the initial
    // getNearbyMessages() call resolves (see onMount below), and never
    // re-checked again for the rest of this page load ("once per page load").
    // `firstHereVisible` drives the CSS opacity fade transition, the same
    // mount/visible split used by the opening ritual overlay in +layout.svelte.
    let firstHereMounted = $state(false);
    let firstHereVisible = $state(false);

    onMount(() => {
        // sign this device in anonymously so messages can be linked to a persistent UID
        initAuth();

        // The map fills 100vh and all its controls (FAB, bottom nav, sheets)
        // are position:fixed, so this page itself never needs to scroll.
        // This used to be an `overflow: hidden` rule on :global(body) in the
        // stylesheet below, but that leaked across client-side navigation and
        // broke scrolling on /archive too - applying/removing it here scopes
        // it to exactly this page's lifetime.
        document.body.style.overflow = 'hidden';

        if (!navigator.geolocation) {
            error = "Your browser doesn't support geolocation :(";
            return; // do nothing
        }
        navigator.geolocation.getCurrentPosition(
            (position) => {
                lat = position.coords.latitude;
                lng = position.coords.longitude;
            },
            () => {
                error = "Location access denied. Please enable location to use Overheard.";
            }
        );
        // populate the messages store
        navigator.geolocation.getCurrentPosition(
            async (position) => {
                const messages = await getNearbyMessages(position.coords.latitude, position.coords.longitude);
                // setMessages (instead of messagesStore.set) compares against the
                // previous list and plays a soft chime for any newly-appeared pins
                setMessages(messages);

                // --- "you're the first here" zero-results check --------
                // getNearbyMessages() already filters out anything past its
                // 30-day decay, so messages.length === 0 alone just means
                // "nothing ACTIVE nearby right now" - it doesn't distinguish
                // "no one has ever posted here" from "people posted here
                // before, but it's all since faded". To make that
                // distinction (per the feature spec, "truly never posted"
                // interpretation), we only fall back to
                // hasAnyMessagesEverNearby() - a cheap count-only query over
                // the same geohash area, including expired messages - when
                // the active list comes back empty. The ambient message only
                // appears if THAT also comes back empty, i.e. literally
                // nothing has ever been left in this ~1.2km area.
                if (messages.length === 0) {
                    const everPosted = await hasAnyMessagesEverNearby(position.coords.latitude, position.coords.longitude);
                    if (!everPosted) {
                        firstHereMounted = true;
                    }
                }

                // --- "pins pop into existence": live listener -----------
                // Now that the initial set has loaded, open a real-time
                // listener for the same area. `knownIds` is every message id
                // already on the map from the load above - it's the
                // boundary between "already here" and "arrived while I was
                // watching". Only ids NOT in `knownIds` get the pop sound +
                // bounce animation (see livePinIdsStore, MapView.svelte's
                // renderPins); `knownIds` then grows to include them, so
                // they don't "arrive" a second time on the next snapshot.
                const knownIds = new Set(messages.map((m) => m.id));

                unsubscribeLive = subscribeToNearbyMessages(
                    position.coords.latitude,
                    position.coords.longitude,
                    (liveMessages) => {
                        const newIds = liveMessages
                            .filter((m) => !knownIds.has(m.id))
                            .map((m) => m.id);

                        if (newIds.length > 0) {
                            newIds.forEach((id) => knownIds.add(id));
                            playPinPopSound();
                            livePinIdsStore.set(new Set(newIds));
                        }

                        // replace wholesale (not setMessages) - this also
                        // picks up live updates to existing messages (e.g.
                        // echoCount from someone else echoing) without
                        // re-triggering the ambient "new pin" chime, which
                        // setMessages would do for the same ids
                        messagesStore.set(liveMessages);
                    }
                );
            }
        );

        // cleanup: undo the body overflow lock when this page unmounts
        // (e.g. navigating to /archive), so other routes can scroll normally,
        // and tear down the live listener above if it was ever set up.
        return () => {
            document.body.style.overflow = '';
            unsubscribeLive?.();
        };
    });

    // --- "passport stamps": load this user's earned stamps once signed in --
    // initAuth() (called above) signs this device in anonymously and updates
    // userStore asynchronously - this effect waits for userStore.ready and
    // then fetches this user's stamps, populating stampsStore so the
    // bottom-nav/desktop stamp-book icons below know whether to show their
    // empty or filled state.
    $effect(() => {
        if ($userStore.ready) {
            getStamps();
        }
    });

    // Fade-in + auto-dismiss timing for the "first here" message - mirrors
    // the opening ritual overlay's pattern in +layout.svelte: wait a frame so
    // the element first paints at opacity 0, then fade in; after a short
    // hold, fade out; then unmount entirely so it can't linger over the map.
    $effect(() => {
        if (!firstHereMounted) return;

        const fadeInFrame = requestAnimationFrame(() => {
            firstHereVisible = true;
        });

        const fadeInMs = 1200;
        const holdMs = 4000; // "a few seconds" before it fades on its own
        const fadeOutMs = 1200;

        const fadeOutTimer = setTimeout(() => {
            firstHereVisible = false;
        }, fadeInMs + holdMs);

        const removeTimer = setTimeout(() => {
            firstHereMounted = false;
        }, fadeInMs + holdMs + fadeOutMs);

        return () => {
            cancelAnimationFrame(fadeInFrame);
            clearTimeout(fadeOutTimer);
            clearTimeout(removeTimer);
        };
    });

    // Early dismissal: as soon as the user starts interacting (opening the
    // compose sheet), fade the message out immediately rather than waiting
    // for the timers above - "disappear once the user starts interacting".
    $effect(() => {
        if (firstHereMounted && $mapStore.composing) {
            firstHereVisible = false;
            setTimeout(() => { firstHereMounted = false; }, 1200);
        }
    });

    // --- auto-open a message shared via link/QR code (SharePopover.svelte) --
    // `sharedLinkHandled` makes this run at most once per page load: the
    // first time lat/lng are set (map has loaded) AND the URL has a
    // `?message=<id>` param, it's flipped to true immediately, so later
    // re-runs of this effect (e.g. lat/lng updating again) are no-ops.
    let sharedLinkHandled = $state(false);

    // "this one has already faded" ambient notice - same mount/visible split
    // and fade timing as the "first here" message above, shown when a shared
    // link points at a message that no longer exists or has expired.
    let fadedNoticeMounted = $state(false);
    let fadedNoticeVisible = $state(false);

    $effect(() => {
        if (sharedLinkHandled) return;
        if (!(lat && lng)) return; // wait until the map has a location to center on

        const sharedId = $page.url.searchParams.get('message');
        sharedLinkHandled = true; // one-time entry behavior, regardless of outcome

        if (!sharedId) return; // normal load, nothing to auto-open

        (async () => {
            // Direct Firestore lookup by document ID - bypasses
            // getNearbyMessages's geohash filter entirely, since a shared
            // message could have been left anywhere in the world, not just
            // this device's ~1.2km area.
            const shared = await getMessageById(sharedId);
            const decay = shared ? getDecayInfo(shared.createdAt, shared.lastEchoAt) : null;

            if (shared && decay && !decay.isExpired) {
                mapStore.set({ selectedMessage: shared, composing: false });
            } else {
                // doesn't exist, or past its 30-day decay - graceful
                // ambient notice instead of an error
                fadedNoticeMounted = true;
            }

            // Clear the `message` param so this is a one-time entry
            // behavior, not persistent state - reloading or sharing the
            // current URL afterward won't re-trigger the auto-open.
            // replaceState (rather than goto) swaps the URL without a
            // navigation/history entry.
            const url = new URL(window.location.href);
            url.searchParams.delete('message');
            replaceState(url, {});
        })();
    });

    // fade-in + auto-dismiss timing for the "already faded" notice - mirrors
    // the "first here" message's $effect above
    $effect(() => {
        if (!fadedNoticeMounted) return;

        const fadeInFrame = requestAnimationFrame(() => {
            fadedNoticeVisible = true;
        });

        const fadeInMs = 1200;
        const holdMs = 4000;
        const fadeOutMs = 1200;

        const fadeOutTimer = setTimeout(() => {
            fadedNoticeVisible = false;
        }, fadeInMs + holdMs);

        const removeTimer = setTimeout(() => {
            fadedNoticeMounted = false;
        }, fadeInMs + holdMs + fadeOutMs);

        return () => {
            cancelAnimationFrame(fadeInFrame);
            clearTimeout(fadeOutTimer);
            clearTimeout(removeTimer);
        };
    });
</script>

<svelte:window bind:innerWidth={windowWidth} /> <!--this sends the windowWidth to our mobile checker -->

<!-- "this one has already faded" notice for a shared link/QR code pointing
     at a message that no longer exists or has expired (see the
     sharedLinkHandled $effect above) - ambient and auto-dismissing, same
     fade pattern as the "first here" bubble, rather than an error page -->
{#if fadedNoticeMounted}
    <p class="faded-notice" class:visible={fadedNoticeVisible} aria-hidden="true">
        this one has already faded.
    </p>
{/if}

{#if error}
    <p class="error">{error}</p>
{:else if !(lat && lng)}
    <p class="loading">Looking for you...</p>
{/if}

<!-- map must fill the whole screen; wrapped in .map-container so desktop
     can push it right of the side panel via the media query below.
     (this also removes a duplicate <MapView> that was rendering two map instances)
     on mobile, bottom padding reserves space so pins aren't hidden behind the bottom nav -->
{#if lat && lng}
    <div class="map-container" style="padding-bottom: {windowWidth < 768 ? '64px' : '0'}">
        <!-- firstHereMounted/firstHereVisible (state + fade timing computed
             in onMount/$effects above) are passed down so MapView can render
             the "you're the first here" bubble positioned near the user's
             location marker, rather than floating independently of the map. -->
        <MapView {lat} {lng} {firstHereMounted} {firstHereVisible} />
    </div>
{/if}

<!-- show the right panel based on mobile or desktop-->
{#if windowWidth < 768}
    <BottomSheet message={$mapStore.selectedMessage} />
{:else}
    <!-- SidePanel itself swaps between the list view and a message detail
         view depending on whether a message is selected -->
    <SidePanel message={$mapStore.selectedMessage} />
{/if}

<!-- desktop-only stamp book entry point, sits just above the FAB. mobile gets
     a bottom-nav tab instead (see below). Outline icon when no stamps have
     been earned yet, filled/decorated once at least one has - same icon pair
     as the mobile nav-item below. -->
{#if windowWidth >= 768}
    <a href="/stampbook" class="stamp-book-btn" class:has-stamps={$stampsStore.length > 0} aria-label="Your stamp book" title="Your stamp book">
        <!-- "smiley face sticker" icon: circle with two eyes + a smile -
             filled (decorated sticker) once at least one stamp is earned,
             plain outline until then -->
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="12" cy="12" r="9" fill="currentColor" fill-opacity={$stampsStore.length > 0 ? 0.15 : 0}/>
            <circle cx="9" cy="10" r="0.9" fill="currentColor" stroke="none"/>
            <circle cx="15" cy="10" r="0.9" fill="currentColor" stroke="none"/>
            <path d="M8 14.5c1 1.5 2.5 2 4 2s3-0.5 4-2"/>
        </svg>
    </a>
{/if}

<!-- floating action button for adding a message; hidden while composing, and
     hidden while a message is open on mobile (BottomSheet covers this area)
     - but kept visible on desktop even with a message open, since SidePanel
     is a fixed sidebar that doesn't cover the map/FAB -->
{#if !$mapStore.composing && (!$mapStore.selectedMessage || !isMobile)}
    <button
    class="fab"
    onclick={() => mapStore.set(
        {selectedMessage: null, composing: true })}>
    +
    </button>
{/if}

<!-- bottom nav, mobile only -->
{#if windowWidth < 768}
    <nav class="bottom-nav">
        <a href="/" class="nav-item" class:active={$page.url.pathname === '/'}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                <rect x="3" y="3" width="7" height="7" rx="1"/>
                <rect x="14" y="3" width="7" height="7" rx="1"/>
                <rect x="3" y="14" width="7" height="7" rx="1"/>
                <rect x="14" y="14" width="7" height="7" rx="1"/>
            </svg>
            <span>Map</span>
        </a>
        <a href="/archive" class="nav-item" class:active={$page.url.pathname === '/archive'}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
                <rect x="2" y="4" width="20" height="16" rx="2"/>
                <path d="M2 9h20"/>
                <path d="M9 4v5"/>
                <path d="M15 4v5"/>
            </svg>
            <span>Archive</span>
        </a>
        <!-- "passport stamps" tab - "smiley face sticker" icon (circle with
             two eyes + a smile), outline with zero stamps, filled/decorated
             once at least one has been earned (see stampsStore) -->
        <a href="/stampbook" class="nav-item" class:active={$page.url.pathname === '/stampbook'}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="9" fill="currentColor" fill-opacity={$stampsStore.length > 0 ? 0.15 : 0}/>
                <circle cx="9" cy="10" r="0.9" fill="currentColor" stroke="none"/>
                <circle cx="15" cy="10" r="0.9" fill="currentColor" stroke="none"/>
                <path d="M8 14.5c1 1.5 2.5 2 4 2s3-0.5 4-2"/>
            </svg>
            <span>Stamps</span>
        </a>
    </nav>
{/if}

<!--compose sheet (making a message)-->
{#if $mapStore.composing}
    <!-- pass isMobile so ComposeSheet can render as a bottom sheet on mobile
         (unchanged) vs. a centered popup with backdrop on desktop -->
    <ComposeSheet {lat} {lng} {isMobile} />
{/if}


<style>
:global(body) {
    margin: 0;
    padding: 0;
    /* overflow: hidden moved into onMount above, scoped to this page's
       lifetime - removed here so it doesn't leak into /archive. */
}

.error, .loading {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
    font-family: Georgia, 'Times New Roman', Times, serif;
    color: #666;

}

/* "this one has already faded" notice - small pastel pill, top-center,
   fades in/out via .visible (same opacity transition as .ritual-overlay in
   +layout.svelte and .first-here in MapView.svelte) */
.faded-notice {
    position: fixed;
    top: 1.5rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 250;
    margin: 0;
    background: #fdfaf5;
    color: #999;
    font-family: Georgia, 'Times New Roman', Times, serif;
    font-size: 0.85rem;
    padding: 0.6rem 1.2rem;
    border-radius: 50px;
    opacity: 0;
    transition: opacity 1.2s ease;
    pointer-events: none;
}

.faded-notice.visible {
    opacity: 1;
}

/* purple FAB; only rendered when nothing is composing/selected, so no
   "shifted" state is needed anymore (that rule has been removed) */
.fab {
    position: fixed;
    bottom: 5rem;
    right: 1.5rem;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: #c4a8f5;
    color: white;
    font-size: 1.8rem;
    border: none;
    cursor: pointer;
    /* flat 2D style for now - shadow removed, paper-style shadows may be added later */
    z-index: 150;
    display: flex;
    align-items:center;
    justify-content: center;
    line-height: 1;
}

/* desktop has no bottom nav bar, so the 5rem bottom offset above (sized to
   clear the mobile nav) leaves the FAB looking awkwardly high - bring it
   down closer to the corner. mobile keeps the 5rem value from .fab above. */
@media (min-width: 768px) {
    .fab {
        bottom: 1.5rem;
    }
}

/* desktop-only "passport stamps" entry point, stacked directly above the FAB
   (56px tall + the FAB's 1.5rem bottom offset + a small gap) - smaller and
   more muted than the FAB since it's a secondary action */
.stamp-book-btn {
    position: fixed;
    right: 1.5rem;
    bottom: calc(1.5rem + 56px + 0.75rem);
    width: 44px;
    height: 44px;
    border-radius: 50%;
    background: white;
    border: 1px solid rgba(0, 0, 0, 0.08);
    color: #bbb;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    text-decoration: none;
    z-index: 150;
    transition: color 0.15s, border-color 0.15s;
}

/* once at least one stamp is earned, tint the button the same purple used
   for .nav-item.active / .echo-button elsewhere in the app */
.stamp-book-btn.has-stamps {
    color: #c4a8f5;
    border-color: rgba(196, 168, 245, 0.4);
}

/* mobile-only bottom nav bar with Map/Archive links */
.bottom-nav {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 64px;
    background: white;
    border-top: 1px solid #f0f0f0;
    display: flex;
    align-items: center;
    justify-content: space-around;
    z-index: 200;
}

.nav-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    text-decoration: none;
    color: #ccc;
    font-size: 0.7rem;
    font-family: sans-serif;
    transition: color 0.15s;
}

/* highlight the tab matching the current route */
.nav-item.active {
    color: #c4a8f5;
}

/* map fills the screen on mobile; on desktop it's pushed right of the
   fixed-width SidePanel (340px) so the panel doesn't sit on top of it.
   box-sizing: border-box so the inline padding-bottom (reserved for the
   bottom nav on mobile) shrinks the map instead of overflowing 100vh */
.map-container {
    width: 100%;
    height: 100vh;
    box-sizing: border-box;
}

@media (min-width: 768px) {
    .map-container {
        margin-left: 340px;
        width: calc(100% - 340px);
    }
}

</style>