<script lang="ts">
import { untrack } from 'svelte';
interface Props {
    /** @type {import('./$types').PageData} */
    data: any;
}
let { data }: Props = $props();

let log_lvl: number = $state(0);

// *** Import Svelte specific
// import { page } from '$app/state';
import { browser } from '$app/environment';

// *** Import other supporting libraries
import { db_events } from '$lib/ae_events/db_events';
import {
    idaa_loc,
    idaa_sess,
    idaa_slct,
    idaa_trig
} from '$lib/stores/ae_idaa_stores';
import { ae_loc, ae_api, slct } from '$lib/stores/ae_stores';
import { events_func } from '$lib/ae_events/ae_events_functions';

import Element_data_store from '$lib/elements/element_data_store.svelte';
import Comp__event_obj_qry from './ae_idaa_comp__event_obj_qry.svelte';
import Comp__event_obj_li_wrapper from './ae_idaa_comp__event_obj_li_wrapper.svelte';

if (browser) {
    $idaa_slct.event_id = null;
    window.parent.postMessage({ event_id: null }, '*');

    // Use a session-scoped trigger so the persisted IDAA profile is not rewritten
    // on every page mount. Recovery Meetings only needs this to kick the initial search.
    $idaa_sess.recovery_meetings.search_version++;
}

let event_id_li: Array<string> = $state([]);
let search_debounce_timer: any = null;
let last_search_id = 0;
let last_executed_key = '';
// Auto-retry state: one silent background retry is attempted before showing the error
// state to the user. Manual "Try Again" button resets this so users can always retry.
// WHY: Mobile connections drop briefly. A single retry resolves most transient failures
// without the user ever seeing an error. If both attempts fail, surface the error clearly
// with a visible retry button — NOT the same "No meetings found" message a real empty
// result would show. (Conflating the two was a key reason the bug went unsolved so long.)
let auto_retry_count = 0;
// Tracks the category of the last search error for a more informative user message.
// ERR_NETWORK_CHANGED and other fetch failures surface as TypeError in JS.
let qry_error_detail: 'network' | 'server' | null = $state(null);

// ── Escape-hatch: cache-reset button after suspicious zero-result delay ──────────────
//
// WHY: The auto-retry + error state covers API failures (caught exceptions). But there
// is a separate failure mode where the API succeeds and returns 0 results — due to
// stale IDB data, a missed IDB_CONTENT_VERSIONS bump, or an undetected filtering issue.
// In that path qry__status lands on 'done', not 'error', so no retry or error UI fires.
// With ~140 active meetings in the database, zero results with no active filters is
// ALWAYS a symptom of something wrong. After 8 seconds in that state we surface a
// visible escape hatch. Using no-filters as the trigger prevents false positives when
// a member genuinely filters to a category that has no results (e.g. "Type = Workshop").
//
// The reset clears db_events.event directly (bypasses the version check, which would be
// a no-op here since the version already matches after the initial layout-mount clear).
// The SWR search then re-fetches from the API and repopulates the table.

// True only when: search is done, result list is empty, AND no filter dimensions are set.
let no_results_no_filters = $derived(
    $idaa_sess.recovery_meetings.qry__status === 'done' &&
    event_id_li.length === 0 &&
    !$idaa_loc.recovery_meetings.qry__physical &&
    !$idaa_loc.recovery_meetings.qry__virtual &&
    !$idaa_loc.recovery_meetings.qry__type &&
    !($idaa_loc.recovery_meetings.qry__fulltext_str?.trim())
);

// True when any filter dimension is active — drives the guided empty state.
let has_active_filters = $derived(
    !!$idaa_loc.recovery_meetings.qry__physical ||
    !!$idaa_loc.recovery_meetings.qry__virtual ||
    !!$idaa_loc.recovery_meetings.qry__type ||
    !!($idaa_loc.recovery_meetings.qry__fulltext_str?.trim())
);

let show_cache_reset_btn = $state(false);
let cache_reset_timer: any = null;

// Starts/cancels the 7-second countdown based on no_results_no_filters.
// $effect only re-runs when the derived VALUE changes (true↔false), not on every
// store write — so the timer counts down cleanly without spurious resets.
$effect(() => {
    if (cache_reset_timer) {
        clearTimeout(cache_reset_timer);
        cache_reset_timer = null;
    }

    if (no_results_no_filters) {
        show_cache_reset_btn = false;
        cache_reset_timer = setTimeout(() => {
            show_cache_reset_btn = true;
        }, 5000);
    } else {
        show_cache_reset_btn = false;
    }

    return () => {
        if (cache_reset_timer) clearTimeout(cache_reset_timer);
    };
});

async function handle_cache_reset() {
    show_cache_reset_btn = false;
    if (cache_reset_timer) { clearTimeout(cache_reset_timer); cache_reset_timer = null; }
    auto_retry_count = 0;
    try {
        await db_events.event.clear();
        console.log('[recovery_meetings] Escape-hatch reset: db_events.event cleared');
    } catch (e) {
        console.warn('[recovery_meetings] Escape-hatch reset: failed to clear IDB', e);
    }
    $idaa_sess.recovery_meetings.search_version++;
}
function clear_filters() {
    $idaa_loc.recovery_meetings.qry__physical = null;
    $idaa_loc.recovery_meetings.qry__virtual = null;
    $idaa_loc.recovery_meetings.qry__type = null;
    $idaa_loc.recovery_meetings.qry__fulltext_str = null;
    $idaa_sess.recovery_meetings.search_version++;
}
// ─────────────────────────────────────────────────────────────────────────────────────

// Standardized Reactive Search Pattern (Aether UI V3)
// This effect manages the orchestration between UI state and data fetching.
$effect(() => {
    // 1. Reactive Dependencies
    // Use $slct (non-persisted, set only by bootstrap Sync Effect) not $ae_loc
    // (persisted, may hold stale account_id from a previous session in localStorage).
    const account_id = $slct.account_id;
    if (!account_id) return; // Wait for bootstrap to set account context

    // Auth gate: do not fetch IDAA events for unauthenticated users.
    // WHY $effect and not +layout.ts: layout load functions fire on SvelteKit link prefetch,
    // causing private data to be written to IDB before Novi auth runs.
    if (!$idaa_loc.novi_verified && !$ae_loc.trusted_access) return;

    // Track filters and the search version (trigger)
    const qry_params = {
        v: $idaa_sess.recovery_meetings.search_version,
        str: $idaa_loc.recovery_meetings.qry__fulltext_str,
        phys: $idaa_loc.recovery_meetings.qry__physical,
        virt: $idaa_loc.recovery_meetings.qry__virtual,
        type: $idaa_loc.recovery_meetings.qry__type,
        limit: $idaa_loc.recovery_meetings.qry__limit,
        order: $idaa_loc.recovery_meetings.qry__order_by,
        remote: $idaa_loc.recovery_meetings.qry__remote_first
    };
    const qry_key = JSON.stringify(qry_params);

    // 2. Debounce Logic
    if (search_debounce_timer) clearTimeout(search_debounce_timer);
    search_debounce_timer = setTimeout(() => {
        // 3. Execution (Untracked to prevent loops)
        untrack(() => {
            handle_search_refresh(qry_key);
        });
    }, 250);

    return () => {
        if (search_debounce_timer) clearTimeout(search_debounce_timer);
    };
});

/**
 * SWR (Stale-While-Revalidate) Search Orchestrator
 *
 * GOAL: Render matching meetings in < 50ms, then update with perfect server data.
 */
async function handle_search_refresh(qry_key: string) {
    if (qry_key === last_executed_key) return;
    last_executed_key = qry_key;

    const current_search_id = ++last_search_id;
    const account_id = $slct.account_id;
    const remote_first = $idaa_loc.recovery_meetings.qry__remote_first;

    if (!account_id) return;

    if (log_lvl)
        console.log(
            `[Search #${current_search_id}] Refreshing recovery meetings (remote_first=${remote_first}) for account: ${account_id}...`
        );

    $idaa_sess.recovery_meetings.qry__status = 'loading';
    qry_error_detail = null;

    // If 'Remote First' is toggled (Admin only), we clear results immediately to show fresh state.
    if (remote_first) {
        event_id_li = [];
    }

    // Snapshot parameters to ensure the Fast Path and Revalidation are using the same criteria.
    const qry_str = ($idaa_loc.recovery_meetings.qry__fulltext_str ?? '')
        .toLowerCase()
        .trim();
    const qry_physical = $idaa_loc.recovery_meetings.qry__physical;
    const qry_virtual = $idaa_loc.recovery_meetings.qry__virtual;
    const qry_type = $idaa_loc.recovery_meetings.qry__type;

    let local_ids: string[] = [];

    // 1. FAST PATH: Local IDB Search
    // We query Dexie first to show results from the 499-item cache pool instantly.
    if (!remote_first) {
        try {
            let local_results = await db_events.event
                .filter((ev) => {
                    // Resilient account check
                    const acct_match = ev.account_id === account_id;
                    if (!acct_match) return false;

                    if (qry_type && ev.type !== qry_type) return false;
                    if (qry_physical || qry_virtual) {
                        let match = false;
                        // Loose equality to handle 1, '1', true from DB
                        if (qry_physical && ev.physical == true) match = true;
                        if (qry_virtual && ev.virtual == true) match = true;
                        if (!match) return false;
                    }
                    if (qry_str) {
                        const name = (ev.name ?? '').toLowerCase();
                        const desc = (ev.description ?? '').toLowerCase();
                        const loc = (ev.location_text ?? '').toLowerCase();
                        const dqs = (ev.default_qry_str ?? '').toLowerCase();
                        return (
                            name.includes(qry_str) ||
                            desc.includes(qry_str) ||
                            loc.includes(qry_str) ||
                            dqs.includes(qry_str)
                        );
                    }
                    return true;
                })
                .toArray();

            // Sort local results matching UI selection (Refactored 2026-02-16)
            const sort_mode = $idaa_loc.recovery_meetings.qry__order_by;
            if (sort_mode === 'name_asc' || sort_mode === 'name') {
                local_results.sort((a, b) =>
                    (a.name ?? '').localeCompare(b.name ?? '')
                );
            } else if (sort_mode === 'name_desc') {
                local_results.sort((a, b) =>
                    (b.name ?? '').localeCompare(a.name ?? '')
                );
            } else {
                // Events use legacy tmp_sort_1 encoding (ae_events__event.ts: priority ? 1 : 0),
                // NOT build_tmp_sort's inverted encoding. priority=true→'1' > priority=false→'0',
                // so DESCENDING sort puts priority items first.
                local_results.sort((a, b) =>
                    (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? '')
                );
            }

            local_ids = local_results.map((e) => String(e.id)).filter(Boolean);

            // Update UI immediately. This eliminates the "white page" during searching.
            if (current_search_id === last_search_id) {
                if (log_lvl)
                    console.log(
                        `[Search #${current_search_id}] Fast Path complete. Found ${local_ids.length} items locally.`
                    );
                event_id_li = local_ids;
                if (local_ids.length > 0) {
                    $idaa_sess.recovery_meetings.qry__status = 'done';
                }
            }
        } catch (e) {
            if (log_lvl)
                console.warn('Fast Path failed, waiting for API...', e);
        }
    }

    // 2. REVALIDATE: Server API Search
    // This hits the full database, bypassing the "cache pool" limitation.
    try {
        const results = await events_func.search__event({
            api_cfg: $ae_api,
            for_obj_type: 'account',
            for_obj_id: account_id,
            qry_conference: false,
            qry_physical: qry_physical,
            qry_virtual: qry_virtual,
            qry_type: qry_type,
            qry_str: qry_str || null,
            enabled: $idaa_loc.recovery_meetings.qry__enabled,
            hidden: $idaa_loc.recovery_meetings.qry__hidden,
            limit: $idaa_loc.recovery_meetings.qry__limit,
            order_by_li: $idaa_loc.recovery_meetings.qry__order_by_li,
            log_lvl: 0
        });

        if (current_search_id === last_search_id) {
            let api_results = results || [];

            // SECONDARY FILTER: Re-apply structured filters the API may handle loosely.
            // WHY type/physical/virtual: the backend uses AND logic for body filters, so
            // physical+virtual together would incorrectly exclude either-only meetings —
            // we pass only one at a time and handle OR logic here.
            // WHY NOT qry_str: the API already applied exact LIKE search on default_qry_str
            // (a backend-generated combined index that includes contact name/email). Re-running
            // text filtering client-side against the response fields silently drops meetings
            // that matched only via default_qry_str (e.g., by contact name) because that
            // field may not be present in the response body or may not duplicate the match.
            api_results = api_results.filter((ev) => {
                if (qry_type && ev.type !== qry_type) return false;
                if (qry_physical || qry_virtual) {
                    let match = false;
                    if (qry_physical && ev.physical == true) match = true;
                    if (qry_virtual && ev.virtual == true) match = true;
                    if (!match) return false;
                }
                return true;
            });

            // RE-SORT: Match the fast-path IDB sort so stale/NULL API ordering is corrected.
            // WHY: The RE-SORT previously only checked 'name' (a legacy mode that no longer
            // exists in sort_modes). 'name_asc' and 'name_desc' fell through to the else
            // branch and were silently re-sorted chronologically, ignoring the user's selection.
            const sort_order = $idaa_loc.recovery_meetings.qry__order_by;
            if (sort_order === 'name_asc' || sort_order === 'name') {
                api_results.sort((a, b) =>
                    (a.name ?? '').localeCompare(b.name ?? '')
                );
            } else if (sort_order === 'name_desc') {
                api_results.sort((a, b) =>
                    (b.name ?? '').localeCompare(a.name ?? '')
                );
            } else {
                // Events use legacy tmp_sort_1 encoding (ae_events__event.ts: priority ? 1 : 0).
                // DESCENDING sort puts priority=true ('1') items first.
                api_results.sort((a, b) =>
                    (b.tmp_sort_1 ?? '').localeCompare(a.tmp_sort_1 ?? '')
                );
            }

            const api_ids = api_results
                .map((e: any) => String(e.id))
                .filter(Boolean);

            // UI PROTECTION: Preserve results if API returns nothing (0 results)
            // BUT only if it was a successful empty response, not a failure.
            // ALSO: Only protect if qry_str was the primary change.
            // If Type or Location changed, we want to see the 0.
            const filter_changed =
                qry_type !==
                    $idaa_sess.recovery_meetings.status_qry__last_type ||
                qry_physical !==
                    $idaa_sess.recovery_meetings.status_qry__last_phys ||
                qry_virtual !==
                    $idaa_sess.recovery_meetings.status_qry__last_virt;

            if (
                api_ids.length === 0 &&
                local_ids.length > 0 &&
                !remote_first &&
                !filter_changed
            ) {
                if (log_lvl)
                    console.warn(
                        `[Search #${current_search_id}] API returned 0 matching results. Preserving local cache view.`
                    );
                $idaa_sess.recovery_meetings.qry__status = 'done';
                return;
            }

            $idaa_slct.event_obj_li = api_results;
            event_id_li = api_ids;

            // Snapshot last used filters for future protection checks
            $idaa_sess.recovery_meetings.status_qry__last_request_str = qry_str;
            $idaa_sess.recovery_meetings.status_qry__last_type = qry_type;
            $idaa_sess.recovery_meetings.status_qry__last_phys = qry_physical;
            $idaa_sess.recovery_meetings.status_qry__last_virt = qry_virtual;

            $idaa_sess.recovery_meetings.qry__status = 'done';
            if (log_lvl)
                console.log(
                    `[Search #${current_search_id}] Revalidation Complete. Found ${api_ids.length} items.`
                );
        }
    } catch (error) {
        if (current_search_id === last_search_id) {
            // TypeError = network-level failure (ERR_NETWORK_CHANGED, offline, DNS failure).
            // These are transient by nature; show a different message than a server error.
            const is_network_err = error instanceof TypeError;
            console.error('Revalidation failed:', is_network_err ? `Network error: ${error}` : error);

            if (auto_retry_count < 1) {
                // First failure — schedule one silent retry before surfacing the error.
                // Incrementing search_version triggers the $effect, which rebuilds qry_key
                // with the new version and calls handle_search_refresh again.
                auto_retry_count++;
                console.log(`[Search] API failed — auto-retry ${auto_retry_count}/1 in 3s...`);
                $idaa_sess.recovery_meetings.qry__status = 'loading';
                setTimeout(() => {
                    $idaa_sess.recovery_meetings.search_version++;
                }, 3000);
            } else {
                // Both attempts failed — surface the error distinctly.
                // IMPORTANT: do NOT show "No meetings found" here. A network/API failure
                // is not the same as a genuinely empty result. Conflating the two was why
                // the "no meetings found" bug went undiagnosed for ~1 year — staff and users
                // saw what looked like an empty list and assumed no data, not a failure.
                qry_error_detail = is_network_err ? 'network' : 'server';
                $idaa_sess.recovery_meetings.qry__status = 'error';
                event_id_li = [];
            }
        }
    }
}

if (browser) {
    console.log('Browser environment ready.');
    window.parent.postMessage({ event_id: $idaa_slct?.event_id ?? null }, '*');
}
</script>

<svelte:head>
    <title>
        IDAA Recovery Meetings - Novi - {$ae_loc?.title}
    </title>
</svelte:head>

<Comp__event_obj_qry />

<div class="w-full max-w-xl mx-auto space-y-1">
    <button
        type="button"
        onclick={() => {
            $idaa_loc.recovery_meetings.ds_info_collapsed =
                !($idaa_loc.recovery_meetings.ds_info_collapsed ?? false);
        }}
        class="novi_btn btn btn-sm w-full flex items-center justify-between
               rounded-lg preset-outlined-surface-200-800 hover:preset-tonal-surface
               opacity-60 hover:opacity-100 transition-all px-3 py-1"
        title={$idaa_loc.recovery_meetings.ds_info_collapsed
            ? 'Show meeting info'
            : 'Collapse meeting info'}>
        <span class="text-sm">
            <span class="fas fa-info-circle mr-1 text-xs"></span>Meeting Info
        </span>
        <span class="fas text-xs opacity-60
            {$idaa_loc.recovery_meetings.ds_info_collapsed ? 'fa-chevron-down' : 'fa-chevron-up'}">
        </span>
    </button>
    {#if !($idaa_loc.recovery_meetings.ds_info_collapsed ?? false)}
        <Element_data_store
            ds_code="recovery_meetings_info"
            ds_type="html"
            class_li="rounded-lg preset-outlined-surface-200-800 m-auto p-2 space-y-2 w-full"
            show_edit_btn={true} />
    {/if}
</div>

{#if Array.isArray(event_id_li) && event_id_li.length}
    <Comp__event_obj_li_wrapper
        {event_id_li}
        link_to_type={'account'}
        link_to_id={$slct.account_id}
        limit={$idaa_loc.recovery_meetings.qry__limit}
        {log_lvl} />
{:else}
    <div class="space-y-2">
        {#if $idaa_sess.recovery_meetings.qry__status === 'loading'}
            <div
                class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
                <span class="fas fa-spinner fa-spin m-1"></span>
                Searching...
            </div>
        {:else if $idaa_sess.recovery_meetings.qry__status === 'error'}
            <div
                class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
                <p>
                    {#if qry_error_detail === 'network'}
                        Network connection interrupted — please check your connection and try again.
                    {:else}
                        Unable to load meetings — server error. Please try again.
                    {/if}
                </p>
                <p class="text-xs opacity-60">
                    If "Try Again" keeps failing, use "Clear Cache &amp; Reload" to reset your local data.
                </p>
                <div class="flex flex-row flex-wrap items-center justify-center gap-2">
                    <button
                        type="button"
                        class="btn btn-sm preset-tonal-secondary"
                        onclick={() => {
                            auto_retry_count = 0;
                            $idaa_sess.recovery_meetings.search_version++;
                        }}>
                        <span class="fas fa-redo m-1"></span>
                        Try Again
                    </button>
                    <!-- Escape hatch for persistent server errors caused by stale auth state in
                         localStorage (stale account_id, api_secret_key, or site config). "Try Again"
                         reuses the same bad state and loops indefinitely — this clears it.
                         Mirrors the "Clear Cache & Reload" button in the IDAA layout auth error state. -->
                    <button
                        type="button"
                        class="btn btn-sm preset-tonal-secondary preset-outlined-warning-100-900 hover:preset-filled-warning-200-800 transition-all"
                        onclick={async () => {
                            // Save the iframe reload URL first — it carries the Novi UUID + key
                            // params needed to re-authenticate after the cache wipe.
                            let reload_url: string | null = null;
                            try { reload_url = sessionStorage.getItem('idaa_iframe_reload_url'); } catch { /* ignore */ }

                            // Delete every IDB database on this origin (not just db_events.event).
                            try {
                                const db_list = await indexedDB.databases();
                                for (const db of db_list) {
                                    if (db.name) indexedDB.deleteDatabase(db.name);
                                }
                            } catch { /* ignore */ }

                            // Clear all localStorage and sessionStorage. IDAA auth comes from
                            // the Novi UUID in the URL, not localStorage, so a full clear is safe.
                            localStorage.clear();
                            sessionStorage.clear();

                            if (reload_url) { location.href = reload_url; } else { location.reload(); }
                        }}>
                        <span class="fas fa-sync-alt m-1"></span>
                        Clear Cache &amp; Reload
                    </button>
                </div>
            </div>
        {:else}
            {#if has_active_filters}
                <!-- Guided empty state: filters are active but returned no results.
                     Distinct from the zero-unfiltered-results path (which indicates a data problem).
                     Here the member may simply have narrowed too far — offer a one-click escape. -->
                <div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
                    <p>No meetings found for these filters.</p>
                    <button
                        type="button"
                        class="btn btn-sm preset-tonal-primary m-auto"
                        onclick={clear_filters}>
                        <span class="fas fa-times m-1"></span>
                        Clear all filters
                    </button>
                </div>
            {:else}
                <div
                    class="ae_highlight ae_padding_md ae_row ae_flex_justify_center">
                    No recovery meetings found matching your criteria.
                </div>
                {#if show_cache_reset_btn}
                    <!-- Escape hatch: surfaces after 8s when zero results + no active filters.
                         With ~140 active meetings, zero unfiltered results always indicates
                         stale IDB data. Clears the event cache and triggers a fresh API fetch. -->
                    <div class="ae_highlight ae_padding_md ae_row ae_flex_justify_center flex-col gap-2 text-center">
                        <p class="text-sm opacity-75">Still not seeing meetings? Your local cache may be out of date.</p>
                        <button
                            type="button"
                            class="btn btn-sm preset-tonal-warning m-auto"
                            onclick={handle_cache_reset}>
                            <span class="fas fa-sync-alt m-1"></span>
                            Refresh Meeting List Cache
                        </button>
                    </div>
                {/if}
            {/if}
        {/if}
    </div>
{/if}
