import { CircleX, ImagePlus, LoaderCircle, SendHorizontal } from 'lucide-solid';
import { createEffect, createSignal, For, onCleanup, onMount, Show, type Component } from 'solid-js';
import type { JSX } from 'solid-js';
import {
  createFolder,
  fetchWebimStatus,
  sendWebimMessage,
  uploadFile,
  webimWebSocketUrl,
  type WebImMessage,
} from '../api/client';
import { TabShell } from '../components/layout/TabShell';
import { PageHeader } from '../components/ui/PageHeader';
import { Button } from '../components/ui/Button';
import { Banner } from '../components/ui/Banner';
import { Switch } from '../components/ui/Switch';
import { t } from '../i18n';
import { pushToast } from '../state/toast';

const LS_CHAT_ID = 'esp-claw-webim-chat-id';
const MARKED_CDN_URL = 'https://esp-claw.com/clientjs/marked@18.0.4/marked.umd.min.js';
const DOMPURIFY_CDN_URL = 'https://esp-claw.com/clientjs/dompurify@3.4.5/purify.min.js';
const MARKED_CDN_INTEGRITY =
  'sha384-QIom/Ao3tGhg4C4VY5VTDrHMTPzgsih5cGuY30rd/xp6hWQ+xIGIZ4kxhaQQY+PB';
const DOMPURIFY_CDN_INTEGRITY =
  'sha384-7FXQySTrDscwsLx1i8RIqZM/JHoUVstx4CuL2b7tziI4Glhp3/3dm/j3qUTheVXE';

type MarkedRuntime = {
  parse: (text: string, options?: { async?: false }) => string | Promise<string>;
};

type DomPurifyRuntime = {
  sanitize: (html: string, config?: Record<string, unknown>) => string;
};

type LocalWebImMessage = WebImMessage & {
  localId?: string;
  sendStatus?: 'pending' | 'sent' | 'failed';
  files?: string[];
};

const [webImMessages, setWebImMessages] = createSignal<LocalWebImMessage[]>([]);
let webImUserLocalSeq = 0;
let webImUserLocalId = 0;

declare global {
  interface Window {
    marked?: MarkedRuntime;
    DOMPurify?: DomPurifyRuntime;
  }
}

const MARKDOWN_SANITIZE_CONFIG = {
  ALLOWED_TAGS: [
    'a',
    'blockquote',
    'br',
    'code',
    'em',
    'h1',
    'h2',
    'h3',
    'h4',
    'h5',
    'h6',
    'hr',
    'li',
    'ol',
    'p',
    'pre',
    's',
    'strong',
    'table',
    'tbody',
    'td',
    'th',
    'thead',
    'tr',
    'ul',
  ],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOWED_URI_REGEXP: /^(?:(?:(?:https?|mailto|tel):|[#/]))/i,
};

let markdownRuntimePromise: Promise<void> | null = null;

function escapeMarkdownHtml(text: string): string {
  return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function loadExternalScript(src: string, integrity: string, testReady: () => boolean): Promise<void> {
  if (testReady()) {
    return Promise.resolve();
  }

  const existing = document.querySelector<HTMLScriptElement>(`script[data-webim-md="${src}"]`);
  if (existing) {
    if (existing.dataset.loaded === '1') {
      return testReady() ? Promise.resolve() : Promise.reject(new Error(src));
    }
    return new Promise((resolve, reject) => {
      existing.addEventListener('load', () => resolve(), { once: true });
      existing.addEventListener(
        'error',
        () => {
          existing.remove();
          reject(new Error(src));
        },
        { once: true },
      );
    });
  }

  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    script.crossOrigin = 'anonymous';
    script.integrity = integrity;
    script.dataset.webimMd = src;
    script.onload = () => {
      script.dataset.loaded = '1';
      testReady() ? resolve() : reject(new Error(src));
    };
    script.onerror = () => {
      script.remove();
      reject(new Error(src));
    };
    document.head.append(script);
  });
}

async function loadMarkdownRuntime(): Promise<void> {
  if (window.marked?.parse && window.DOMPurify?.sanitize) {
    return;
  }

  markdownRuntimePromise ??= Promise.all([
    loadExternalScript(MARKED_CDN_URL, MARKED_CDN_INTEGRITY, () => !!window.marked?.parse),
    loadExternalScript(DOMPURIFY_CDN_URL, DOMPURIFY_CDN_INTEGRITY, () => !!window.DOMPurify?.sanitize),
  ]).then(() => undefined);

  try {
    await markdownRuntimePromise;
  } catch (error) {
    markdownRuntimePromise = null;
    throw error;
  }
}

function renderMarkdown(text: string): string {
  const html = window.marked?.parse(escapeMarkdownHtml(text), { async: false });
  if (typeof html !== 'string') {
    return '';
  }
  return window.DOMPurify?.sanitize(html, MARKDOWN_SANITIZE_CONFIG) ?? '';
}

const MarkdownMessage: Component<{ preview: boolean; text: string }> = (props) => (
  <Show
    when={props.preview}
    fallback={<p class="m-0 whitespace-pre-wrap break-words">{props.text}</p>}
  >
    <div class="webim-markdown break-words" innerHTML={renderMarkdown(props.text)} />
  </Show>
);

function loadOrCreateChatId(): string {
  try {
    const saved = localStorage.getItem(LS_CHAT_ID);
    if (saved) return saved;
  } catch {
    /* ignore */
  }
  const id =
    typeof crypto !== 'undefined' && crypto.randomUUID
      ? crypto.randomUUID()
      : 'w-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
  try {
    localStorage.setItem(LS_CHAT_ID, id);
  } catch {
    /* ignore */
  }
  return id;
}

export const WebImPage: Component = () => {
  const chatId = loadOrCreateChatId();
  /** In-memory transcript only — lost on refresh. */
  const messages = webImMessages;
  const setMessages = setWebImMessages;
  const [input, setInput] = createSignal('');
  const [pendingPaths, setPendingPaths] = createSignal<string[]>([]);
  const [error, setError] = createSignal<string | null>(null);
  const [bound, setBound] = createSignal<boolean | null>(null);
  const [wsReady, setWsReady] = createSignal(false);
  const [sending, setSending] = createSignal(false);
  const [markdownPreview, setMarkdownPreview] = createSignal(false);
  const [markdownPreviewLoading, setMarkdownPreviewLoading] = createSignal(false);
  let fileRef: HTMLInputElement | undefined;
  let messagesRef: HTMLDivElement | undefined;
  let ws: WebSocket | null = null;
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  let heartbeatTimer: ReturnType<typeof setInterval> | null = null;

  const clearReconnect = () => {
    if (reconnectTimer !== null) {
      clearTimeout(reconnectTimer);
      reconnectTimer = null;
    }
  };

  const clearHeartbeat = () => {
    if (heartbeatTimer !== null) {
      clearInterval(heartbeatTimer);
      heartbeatTimer = null;
    }
  };

  const teardownWs = () => {
    clearReconnect();
    clearHeartbeat();
    if (ws) {
      ws.onopen = null;
      ws.onclose = null;
      ws.onmessage = null;
      ws.onerror = null;
      try {
        ws.close();
      } catch {
        /* ignore */
      }
      ws = null;
    }
    setWsReady(false);
  };

  const scheduleReconnect = () => {
    clearReconnect();
    reconnectTimer = setTimeout(() => {
      reconnectTimer = null;
      connectWs();
    }, 2000);
  };

  const connectWs = () => {
    teardownWs();
    const socket = new WebSocket(webimWebSocketUrl());
    ws = socket;

    socket.onopen = () => {
      const hello = JSON.stringify({ type: 'hello', chat_id: chatId });

      setWsReady(true);
      clearReconnect();
      try {
        socket.send(hello);
      } catch {
        /* ignore */
      }
      clearHeartbeat();
      heartbeatTimer = setInterval(() => {
        if (socket.readyState !== WebSocket.OPEN) {
          return;
        }
        try {
          socket.send('{"type":"ping"}');
        } catch {
          /* ignore */
        }
      }, 15000);
    };

    socket.onclose = () => {
      setWsReady(false);
      clearHeartbeat();
      scheduleReconnect();
    };

    socket.onerror = () => {
      setWsReady(false);
    };

    socket.onmessage = (ev: MessageEvent<string>) => {
      let data: Record<string, unknown>;
      try {
        data = JSON.parse(ev.data) as Record<string, unknown>;
      } catch {
        return;
      }
      const cid = typeof data.chat_id === 'string' ? data.chat_id : '';
      if (!cid || cid !== chatId) {
        return;
      }
      const role = typeof data.role === 'string' ? data.role : '';
      if (role !== 'assistant') {
        return;
      }
      const seq = typeof data.seq === 'number' ? data.seq : 0;
      const text = typeof data.text === 'string' ? data.text : '';
      const ts_ms = typeof data.ts_ms === 'number' ? data.ts_ms : undefined;
      let links: WebImMessage['links'];
      const rawLinks = data.links;
      if (Array.isArray(rawLinks)) {
        links = rawLinks.map((x) => {
          const o = x as Record<string, unknown>;
          return {
            url: typeof o.url === 'string' ? o.url : '',
            label: typeof o.label === 'string' ? o.label : '',
          };
        });
      }
      setMessages((prev) => [...prev, { seq, role: 'assistant', text, ts_ms, links }]);
    };
  };

  onMount(async () => {
    try {
      await createFolder('/inbox/webim', { recursive: true });
    } catch {
      /* may already exist */
    }
    try {
      const st = await fetchWebimStatus();
      setBound(!!st.bound);
    } catch {
      setBound(false);
    }
    connectWs();
  });

  onCleanup(() => {
    teardownWs();
  });

  createEffect(() => {
    messages().length;
    markdownPreview();
    requestAnimationFrame(() => {
      if (messagesRef) {
        messagesRef.scrollTop = messagesRef.scrollHeight;
      }
    });
  });

  const onPickFile = async (ev: Event) => {
    const inputEl = ev.currentTarget as HTMLInputElement;
    const file = inputEl.files?.[0];
    if (!file) return;
    const name = `${Date.now().toString(36)}_${file.name.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
    const path = '/inbox/webim/' + name;
    try {
      await uploadFile(path, file);
      setPendingPaths((p) => [...p, path]);
      pushToast(t('webimUploaded') as string, 'info', 2500);
    } catch (e) {
      pushToast((e as Error).message, 'error', 4000);
    }
    inputEl.value = '';
  };

  const makeLocalMessage = (text: string, files: string[]): LocalWebImMessage => {
    webImUserLocalSeq -= 1;
    webImUserLocalId += 1;
    return {
      seq: webImUserLocalSeq,
      role: 'user',
      text,
      files,
      localId: `user-${Date.now().toString(36)}-${webImUserLocalId.toString(36)}`,
      sendStatus: 'pending',
    };
  };

  const updateLocalMessageStatus = (
    localId: string,
    sendStatus: NonNullable<LocalWebImMessage['sendStatus']>,
  ) => {
    setMessages((prev) => prev.map((m) => (m.localId === localId ? { ...m, sendStatus } : m)));
  };

  const postLocalMessage = async (message: LocalWebImMessage) => {
    if (!message.localId) return;
    setSending(true);
    setError(null);
    try {
      await sendWebimMessage(chatId, message.text, message.files ?? []);
      updateLocalMessageStatus(message.localId, 'sent');
    } catch (e) {
      updateLocalMessageStatus(message.localId, 'failed');
      setError((e as Error).message);
    } finally {
      setSending(false);
    }
  };

  const send = async () => {
    const text = input().trim();
    const files = pendingPaths();
    if (!text && files.length === 0) return;
    if (!wsReady()) return;
    if (bound() === false) {
      pushToast(t('webimNoBind') as string, 'error', 5000);
      return;
    }
    const localMessage = makeLocalMessage(text, files);
    setMessages((prev) => [...prev, localMessage]);
    setInput('');
    setPendingPaths([]);
    await postLocalMessage(localMessage);
  };

  const retryMessage = async (message: LocalWebImMessage) => {
    if (!message.localId || sending() || !wsReady()) return;
    const retry = { ...message, sendStatus: 'pending' as const };
    setMessages((prev) => [...prev.filter((m) => m.localId !== message.localId), retry]);
    await postLocalMessage(retry);
  };

  const onInputKeyDown: JSX.EventHandler<HTMLTextAreaElement, KeyboardEvent> = (e) => {
    if (e.ctrlKey && e.key === 'Enter') {
      e.preventDefault();
      if (!sending() && wsReady()) {
        void send();
      }
    }
  };

  const toggleMarkdownPreview = async (checked: boolean) => {
    if (!checked) {
      setMarkdownPreview(false);
      return;
    }

    setMarkdownPreviewLoading(true);
    try {
      await loadMarkdownRuntime();
      setMarkdownPreview(true);
    } catch {
      setMarkdownPreview(false);
      pushToast(t('webimMarkdownPreviewLoadFailed') as string, 'error', 5000);
    } finally {
      setMarkdownPreviewLoading(false);
    }
  };

  return (
    <TabShell class="flex h-[calc(100dvh-5.5rem)] min-h-[520px] flex-col sm:h-[calc(100dvh-6.5rem)]">
      <PageHeader
        title={t('navWebIm') as string}
        description={t('webimDesc') as string}
        actions={
          <Show when={wsReady()}>
            <span class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-[rgba(104,211,145,0.2)] bg-[var(--color-green-dim)] text-[var(--color-green)] text-[0.78rem] font-medium">
              <span class="w-1.5 h-1.5 rounded-full bg-[var(--color-green)] pulse-dot" />
              {t('webimOnline')}
            </span>
          </Show>
        }
      />

      <Show when={error()}>
        <div class="px-5 pt-2">
          <Banner kind="error" message={error() ?? undefined} />
        </div>
      </Show>
      <Show when={bound() === false}>
        <div class="px-5 pt-2">
          <Banner kind="info" message={t('webimNoBind') as string} />
        </div>
      </Show>

      <div class="flex min-h-0 flex-1 flex-col">
        <div class="flex min-h-0 flex-1 flex-col min-w-0 border border-[var(--color-border-subtle)] rounded-none bg-white/[0.02]">
          <div
            ref={messagesRef}
            class="relative flex min-h-0 flex-1 overflow-auto p-4 flex-col gap-3"
          >
            <For each={messages()}>
              {(m) => (
                <div
                  class={[
                    'flex max-w-[min(100%,36rem)] items-start gap-2',
                    m.role === 'user' ? 'self-end' : 'self-start',
                  ].join(' ')}
                >
                  <Show when={m.role === 'user' && m.sendStatus !== 'sent'}>
                    <span class="mt-2 flex h-5 w-5 shrink-0 items-center justify-center">
                      <Show when={m.sendStatus === 'pending'}>
                        <LoaderCircle class="h-4 w-4 animate-spin text-[var(--color-text-muted)]" />
                      </Show>
                      <Show when={m.sendStatus === 'failed'}>
                        <button
                          type="button"
                          class="inline-flex h-5 w-5 items-center justify-center rounded-full text-[rgb(248,113,113)] transition hover:bg-[rgba(248,113,113,0.12)] hover:text-[rgb(252,165,165)] disabled:opacity-60"
                          title={t('webimRetrySend') as string}
                          aria-label={t('webimRetrySend') as string}
                          disabled={sending() || !wsReady()}
                          onClick={() => void retryMessage(m)}
                        >
                          <CircleX class="h-4 w-4" />
                        </button>
                      </Show>
                    </span>
                  </Show>
                  <div
                    class={[
                      'min-w-0 rounded-[var(--radius-md)] px-3 py-2 text-[0.88rem] leading-relaxed',
                      m.role === 'user'
                        ? 'bg-[var(--color-accent)]/18 text-[var(--color-text-primary)]'
                        : 'bg-white/6 text-[var(--color-text-primary)]',
                    ].join(' ')}
                  >
                    <MarkdownMessage preview={markdownPreview()} text={m.text} />
                    <Show when={(m.links?.length ?? 0) > 0}>
                      <ul class="mt-2 space-y-1 list-none m-0 p-0">
                        <For each={m.links ?? []}>
                          {(lnk) => (
                            <li>
                              <a
                                href={lnk.url}
                                download=""
                                class="text-[var(--color-accent-soft)] hover:underline text-[0.82rem]"
                              >
                                {lnk.label || lnk.url}
                              </a>
                            </li>
                          )}
                        </For>
                      </ul>
                    </Show>
                  </div>
                </div>
              )}
            </For>
            <Show when={!wsReady() || messages().length === 0}>
              <div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
                <p
                  class={[
                    'm-0 text-center inline-flex items-center px-4 py-2 rounded-full border text-[0.82rem] font-medium',
                    !wsReady()
                      ? 'border-[rgba(245,158,11,0.28)] bg-[rgba(245,158,11,0.12)] text-[rgb(245,158,11)]'
                      : 'border-[rgba(104,211,145,0.2)] bg-[var(--color-green-dim)] text-[var(--color-green)]',
                  ].join(' ')}
                >
                  {!wsReady() ? (t('webimWsReconnecting') as string) : (t('webimEmpty') as string)}
                </p>
              </div>
            </Show>
          </div>

          <Show when={pendingPaths().length > 0}>
            <div class="px-4 pb-1 text-[0.76rem] text-[var(--color-text-muted)]">
              {t('webimPendingFiles')}: {pendingPaths().length}
            </div>
          </Show>

          <div class="p-3 border-t border-[var(--color-border-subtle)] flex flex-col gap-2">
            <textarea
              class="w-full min-h-[72px] rounded-[var(--radius-sm)] bg-black/25 border border-[var(--color-border-subtle)] px-3 py-2 text-[0.88rem] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)]"
              placeholder={t('webimPlaceholder') as string}
              value={input()}
              onInput={(e) => setInput(e.currentTarget.value)}
              onKeyDown={onInputKeyDown}
            />
            <div class="flex flex-wrap items-center gap-2">
              <input
                ref={fileRef}
                type="file"
                accept="image/*"
                class="hidden"
                onChange={(e) => void onPickFile(e)}
              />
              <Button
                size="sm"
                variant="secondary"
                type="button"
                onClick={() => fileRef?.click()}
                disabled={sending() || !wsReady()}
              >
                <span class="inline-flex items-center gap-1.5">
                  <ImagePlus class="w-4 h-4" />
                  {t('webimAttach')}
                </span>
              </Button>
              <Switch
                class="ml-1"
                labelClass="text-[var(--color-text-secondary)]"
                checked={markdownPreview()}
                disabled={markdownPreviewLoading()}
                onChange={(checked) => void toggleMarkdownPreview(checked)}
                label={
                  markdownPreviewLoading()
                    ? (t('webimMarkdownPreviewLoading') as string)
                    : (t('webimMarkdownPreview') as string)
                }
              />
              <span class="text-[0.76rem] text-[var(--color-text-muted)] sm:ml-auto">
                {t('webimSendShortcut')}
              </span>
              <Button
                size="sm"
                variant="primary"
                onClick={() => void send()}
                disabled={sending() || !wsReady()}
              >
                <span class="inline-flex items-center gap-1.5">
                  <SendHorizontal class="w-4 h-4" />
                  {t('webimSend')}
                </span>
              </Button>
            </div>
          </div>
        </div>
      </div>
    </TabShell>
  );
};
