<script lang="ts">
/**
 * element_websocket.svelte — Aether WebSocket V3 Client
 *
 * PURPOSE:
 *   Drop-in replacement for element_websocket_v2.svelte using the V3 protocol:
 *   - URL: /v3/ws/group/{group_id}/client/{client_id}?api_key=...&jwt=...
 *   - Strict WS_Message_V3 schema: { msg_type, target, cmd?, msg?, payload? }
 *   - Redis-granular routing (group / direct / broadcast / echo)
 *   - Automatic 45s heartbeat to maintain Redis presence TTL
 *   - Auth via query params (browsers cannot set WS headers)
 *
 * EXTERNAL INTERFACE:
 *   Intentionally prop-compatible with V2 so callers need minimal changes.
 *   New V3-only props: api_key, jwt, x_account_id.
 *
 * SCHEMA CHANGES FROM V2:
 *   - `type` field renamed to `msg_type` ('cmd'|'msg'|'heartbeat'|'presence')
 *   - `target` values: 'group' (was 'all'), 'direct' (was 'dm'), 'echo', 'broadcast'
 *   - `from_id` (server-set) replaces self-echo check via `client_id` in payload
 *   - Do NOT send `client_id`, `group_id`, or `from_id` in message body; server fills them.
 */

interface Props {
    log_lvl?: number;

    // Connection control
    ws_connect?: boolean;
    ws_connect_status?: null | string;
    ws_server?: string;
    ws_retry_delay?: number;
    ws_retry_count?: number;

    // V3 Auth (passed as query params — browsers cannot set WS headers)
    api_key?: string | null;
    jwt?: string | null;
    x_account_id?: string | null;

    // Identity
    group_id?: string;
    client_id?: any;

    // Outbound message content
    cmd?: null | string;
    msg?: null | string;
    trigger_send?: any;
    trigger_connect?: boolean;
    trigger_disconnect?: boolean;

    // Visibility flags (for debug panel)
    hide__ws_element?: boolean;
    hide__ws_form?: boolean;
    hide__ws_messages?: boolean;
    hide__ws_commands?: boolean;

    // Output status bindings
    ws_conn_status?: any;
    ws_recv_status?: any;
    ws_sent_status?: any;
}

let {
    log_lvl = 0,

    ws_connect = $bindable(false),
    ws_connect_status = $bindable(null),
    ws_server = 'dev-api.oneskyit.com',
    ws_retry_delay = 3500,
    ws_retry_count = 0,

    api_key = null,
    jwt = null,
    x_account_id = null,

    group_id = $bindable('ae-grp-99'),
    client_id = $bindable(Date.now()),

    cmd = $bindable(null),
    msg = $bindable(null),
    trigger_send = $bindable(null),
    trigger_connect = $bindable(false),
    trigger_disconnect = $bindable(false),

    hide__ws_element = $bindable(false),
    hide__ws_form = $bindable(true),
    hide__ws_messages = $bindable(false),
    hide__ws_commands = $bindable(false),

    ws_conn_status = $bindable(null),
    ws_recv_status = $bindable(null),
    ws_sent_status = $bindable(null)
}: Props = $props();

// *** Internal state

let ws_received_list_cmd: any[] = $state([]);
let ws_received_list_other: any[] = $state([]);

let ws_group: WebSocket | null = $state(null);
let heartbeat_interval: ReturnType<typeof setInterval> | null = null;

// *** Build V3 URL with auth query params

function build_ws_url(group: string, client: any): string {
    // Use wss:// in production (HTTPS), ws:// for local dev (HTTP)
    const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
    const params = new URLSearchParams();
    if (api_key) params.set('api_key', api_key);
    if (jwt) params.set('jwt', jwt);
    if (x_account_id) params.set('x_account_id', x_account_id);
    const qs = params.toString();
    return `${protocol}://${ws_server}/v3/ws/group/${group}/client/${client}${qs ? '?' + qs : ''}`;
}

// *** Heartbeat — keeps Redis presence TTL alive (V3 requirement)

function start_heartbeat(conn: WebSocket) {
    stop_heartbeat();
    heartbeat_interval = setInterval(() => {
        if (conn.readyState === WebSocket.OPEN) {
            conn.send(
                JSON.stringify({ msg_type: 'heartbeat', target: 'echo' })
            );
            if (log_lvl) console.log('WS V3: heartbeat sent');
        }
    }, 45_000);
}

function stop_heartbeat() {
    if (heartbeat_interval !== null) {
        clearInterval(heartbeat_interval);
        heartbeat_interval = null;
    }
}

// *** Core connection

function ws_connect_group_id({
    group,
    client
}: {
    group: string;
    client: any;
}): WebSocket {
    if (!group) {
        group = 'ae-grp-99';
        console.warn('WS V3: No group_id — using default:', group);
    }
    if (!client) {
        client = Date.now();
        console.warn('WS V3: No client_id — using timestamp:', client);
    }

    const url = build_ws_url(group, client);
    if (log_lvl) console.log('WS V3 connect URL:', url);

    const conn = new WebSocket(url);

    conn.onopen = () => {
        if (log_lvl) console.log('WS V3: connected');
        ws_connect_status = 'connected';
        ws_conn_status = 'connected';
        ws_retry_count = 0;

        // Announce presence to the group
        conn.send(
            JSON.stringify({
                msg_type: 'msg',
                target: 'group',
                msg: `Client ${String(client).slice(-5)} connected`
            })
        );

        start_heartbeat(conn);
    };

    conn.onmessage = (event) => {
        let data: any;
        try {
            data = JSON.parse(event.data);
        } catch {
            console.warn('WS V3: non-JSON message ignored', event.data);
            return;
        }

        if (log_lvl) console.log('WS V3: received', data);

        // Ignore messages we sent ourselves (server stamps from_id)
        if (data.from_id && String(data.from_id) === String(client)) {
            if (log_lvl) console.log('WS V3: self-echo ignored');
            return;
        }

        // Ignore heartbeat echoes silently
        if (data.msg_type === 'heartbeat') return;

        if (data.msg_type === 'cmd') {
            ws_received_list_cmd = [data, ...ws_received_list_cmd];
        } else {
            ws_received_list_other = [data, ...ws_received_list_other];
        }

        ws_recv_status = data;
    };

    conn.onclose = () => {
        if (log_lvl) console.log('WS V3: connection closed');
        stop_heartbeat();
        ws_connect_status = 'disconnected';
        ws_conn_status = 'disconnected';

        ws_received_list_other = [
            {
                msg_type: 'msg',
                target: 'local',
                msg: `LOCAL: client ${client} disconnected`
            },
            ...ws_received_list_other
        ];

        // Auto-reconnect while ws_connect is still true
        if (ws_connect) {
            if (ws_retry_count >= 10) {
                ws_retry_delay = Math.min(ws_retry_delay + 4999, 120_000);
                console.warn(
                    `WS V3: retry limit reached, delay=${ws_retry_delay}ms`
                );
            }
            setTimeout(() => {
                ws_retry_count += 1;
                ws_group = ws_connect_group_id({ group, client });
            }, ws_retry_delay);
        }
    };

    conn.onerror = () => {
        console.warn('WS V3: connection error');
    };

    return conn;
}

// *** Send helper

function handle_send() {
    if (!ws_group || ws_group.readyState !== WebSocket.OPEN) {
        console.warn('WS V3: send attempted with no open connection');
        return;
    }
    const payload: Record<string, any> = {
        msg_type: 'cmd',
        target: 'group'
    };
    if (cmd) payload.cmd = cmd;
    if (msg) payload.msg = msg;

    if (log_lvl) console.log('WS V3: sending', payload);
    ws_group.send(JSON.stringify(payload));

    ws_sent_status = { ...payload, src: client_id, group_id: group_id };
    cmd = '';
    msg = '';
}

// *** Reactive effects

// Connect / disconnect based on ws_connect flag
$effect(() => {
    if (ws_connect && group_id) {
        ws_group = ws_connect_group_id({ group: group_id, client: client_id });
    } else {
        stop_heartbeat();
        ws_group?.close();
        ws_group = null;
        ws_connect_status = 'disconnected';
    }
});

// Trigger: send command
$effect(() => {
    if (trigger_send && cmd) {
        trigger_send = null;
        handle_send();
    }
});

// Trigger: connect
$effect(() => {
    if (trigger_connect) {
        trigger_connect = false;
        if (!ws_connect) ws_connect = true;
    }
});

// Trigger: disconnect
$effect(() => {
    if (trigger_disconnect) {
        trigger_disconnect = false;
        stop_heartbeat();
        if (ws_connect) ws_connect = false;
        ws_group?.close();
        ws_group = null;
        ws_connect_status = 'disconnected';
    }
});

function prevent_default<T extends Event>(fn: (event: T) => void) {
    return function (event: T) {
        event.preventDefault();
        fn(event);
    };
}
</script>

<!-- Debug panel — only visible when ws_connect=true and hide__ws_element=false -->
<section
    class:hidden={!ws_connect || hide__ws_element}
    class="ae_element__websocket relative container mx-auto mt-32 mb-32 bg-pink-100 p-1 pb-16 text-xs">
    <span class="absolute top-0 right-0 flex flex-col gap-1">
        <button
            type="button"
            onclick={() => {
                hide__ws_form = !hide__ws_form;
            }}
            class="btn btn-sm hover:preset-filled-tertiary-500 text-xs"
            class:preset-tonal-tertiary={hide__ws_form}
            class:preset-filled-tertiary-500={!hide__ws_form}>
            {hide__ws_form ? 'Show Form' : 'Hide Form?'}
        </button>
        <button
            type="button"
            onclick={() => {
                hide__ws_messages = !hide__ws_messages;
            }}
            class="btn btn-sm hover:preset-filled-tertiary-500 text-xs"
            class:preset-tonal-tertiary={hide__ws_messages}
            class:preset-filled-tertiary-500={!hide__ws_messages}>
            {hide__ws_messages ? 'Show Messages' : 'Hide Messages?'}
        </button>
        <button
            type="button"
            onclick={() => {
                hide__ws_commands = !hide__ws_commands;
            }}
            class="btn btn-sm hover:preset-filled-tertiary-500 text-xs"
            class:preset-tonal-tertiary={hide__ws_commands}
            class:preset-filled-tertiary-500={!hide__ws_commands}>
            {hide__ws_commands ? 'Show Commands' : 'Hide Commands?'}
        </button>
    </span>

    <header>
        <h1 class="h6 text-center">
            WebSocket V3 — {ws_connect_status ?? 'not connected'}
        </h1>
        <p class="text-center opacity-60">
            Group: {group_id} · Client: {String(client_id).slice(-8)}
        </p>
    </header>

    {#if !hide__ws_form}
        <form
            onsubmit={prevent_default(handle_send)}
            class="mt-2 flex flex-row flex-wrap gap-1">
            <input
                type="text"
                bind:value={group_id}
                placeholder="Group ID"
                class="input w-36 text-sm" />
            <input
                type="text"
                bind:value={cmd}
                placeholder="Command"
                class="input w-36 text-sm" />
            <input
                type="text"
                bind:value={msg}
                placeholder="Message"
                class="input w-36 text-sm" />
            <button
                type="submit"
                class="btn btn-sm preset-tonal-primary"
                disabled={!ws_connect || ws_connect_status !== 'connected'}>
                Send
            </button>
        </form>
    {/if}

    {#if !hide__ws_commands}
        <div class="mt-2">
            <h2 class="font-bold">Commands received:</h2>
            <ul class="max-h-32 overflow-y-auto">
                {#each ws_received_list_cmd as item}
                    <li class="border-b border-pink-200 py-0.5 font-mono">
                        <span class="font-bold text-red-700">{item.cmd}</span>
                        {#if item.from_id}<span class="ml-1 opacity-50"
                                >from {item.from_id}</span
                            >{/if}
                    </li>
                {/each}
            </ul>
        </div>
    {/if}

    {#if !hide__ws_messages}
        <div class="mt-2">
            <h2 class="font-bold">Messages received:</h2>
            <ul class="max-h-32 overflow-y-auto">
                {#each ws_received_list_other as item}
                    <li class="border-b border-pink-200 py-0.5 font-mono">
                        <span class="opacity-50">[{item.msg_type}]</span>
                        {item.msg ?? ''}
                        {#if item.from_id}<span class="ml-1 opacity-50"
                                >from {item.from_id}</span
                            >{/if}
                    </li>
                {/each}
            </ul>
        </div>
    {/if}
</section>
