const { useEffect, useMemo, useRef, useState } = React;

marked.setOptions({ gfm: true, breaks: true });

function cn(...parts) {
  return parts.filter(Boolean).join(" ");
}

function uid() {
  if (window.crypto && typeof window.crypto.randomUUID === "function") {
    return window.crypto.randomUUID();
  }
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function renderMd(text, sources = []) {
  const html = DOMPurify.sanitize(marked.parse(text || ""));
  if (!sources.length) return html;

  const sourceByIndex = new Map(
    sources.map((source) => [String(source.index), source]),
  );
  const doc = new DOMParser().parseFromString(
    `<div>${html}</div>`,
    "text/html",
  );
  const root = doc.body.firstElementChild;
  const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
    acceptNode(node) {
      if (!/\[(\d+)\]/.test(node.nodeValue || ""))
        return NodeFilter.FILTER_REJECT;
      if (node.parentElement?.closest("a, code, pre"))
        return NodeFilter.FILTER_REJECT;
      return NodeFilter.FILTER_ACCEPT;
    },
  });
  const nodes = [];
  while (walker.nextNode()) nodes.push(walker.currentNode);

  for (const node of nodes) {
    const textValue = node.nodeValue || "";
    const fragment = doc.createDocumentFragment();
    let lastIndex = 0;
    for (const match of textValue.matchAll(/\[(\d+)\]/g)) {
      const source = sourceByIndex.get(match[1]);
      if (!source) continue;

      if (match.index > lastIndex) {
        fragment.append(
          doc.createTextNode(textValue.slice(lastIndex, match.index)),
        );
      }

      const link = doc.createElement("a");
      link.className = "citation-link";
      link.textContent = match[0];
      link.href = sourceUrl(source) || `#source-${source.index}`;
      if (sourceUrl(source)) {
        link.target = "_blank";
        link.rel = "noopener noreferrer";
      }
      fragment.append(link);
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < textValue.length) {
      fragment.append(doc.createTextNode(textValue.slice(lastIndex)));
    }
    node.parentNode.replaceChild(fragment, node);
  }

  return root.innerHTML;
}

function Button({ className, children, ...props }) {
  return (
    <button className={cn("button", className)} {...props}>
      {children}
    </button>
  );
}

const Input = React.forwardRef(function Input({ className, ...props }, ref) {
  return <input ref={ref} className={cn("input", className)} {...props} />;
});

const Textarea = React.forwardRef(function Textarea(
  { className, ...props },
  ref,
) {
  return (
    <textarea ref={ref} className={cn("textarea", className)} {...props} />
  );
});

function Badge({ variant = "secondary", className, children }) {
  return (
    <span className={cn("badge", `badge-${variant}`, className)}>
      {children}
    </span>
  );
}

function Spinner() {
  return <span className="spinner" aria-hidden="true" />;
}

function sourceUrl(source) {
  const cid = String(source.entity_cid || "").trim();
  if (!cid) return null;

  const encCid = encodeURIComponent(cid);
  switch (source.entity_type) {
    case "rule": {
      const bookCid = String(source.book_cid || "").trim();
      const filter = bookCid ? `filter=${encodeURIComponent(bookCid)}&` : "";
      return `https://sirael.dracihlidka.cz/rules?${filter}cid=${encCid}`;
    }
    case "ability":
      return `https://sirael.dracihlidka.cz/abilities/${encCid}`;
    case "spell":
      return `https://sirael.dracihlidka.cz/spells/${encCid}`;
    case "skill":
      return `https://sirael.dracihlidka.cz/skills/${encCid}`;
    case "npc":
    case "monster":
      return `https://sirael.dracihlidka.cz/npcs/${encCid}`;
    case "quest":
      return `https://sirael.dracihlidka.cz/quests/${encCid}`;
    default:
      return null;
  }
}

function usagePercent(used, limit) {
  if (!limit) return 0;
  return Math.min(100, Math.max(0, Math.round((used / limit) * 100)));
}

function UsageMeter({ usage }) {
  const minute = usagePercent(
    usage?.used_minute || 0,
    usage?.limit_minute || 0,
  );
  const daily = usagePercent(usage?.used_day || 0, usage?.limit_day || 0);

  return (
    <div className="usage" aria-label="Token usage">
      <div className="usage-row">
        <span className="usage-label">minute</span>
        <span className="usage-track" aria-hidden="true">
          <span className="usage-fill" style={{ width: `${minute}%` }} />
        </span>
        <span className="usage-percent">{minute}%</span>
      </div>
      <div className="usage-row">
        <span className="usage-label">daily</span>
        <span className="usage-track" aria-hidden="true">
          <span className="usage-fill" style={{ width: `${daily}%` }} />
        </span>
        <span className="usage-percent">{daily}%</span>
      </div>
    </div>
  );
}

function tokenRoles(tokenParsed, clientId) {
  const realmRoles = tokenParsed?.realm_access?.roles || [];
  const clientRoles = tokenParsed?.resource_access?.[clientId]?.roles || [];
  return [...new Set([...realmRoles, ...clientRoles])].sort();
}

function AuthUser({ auth }) {
  if (!auth?.enabled || !auth.email) return null;
  return (
    <div className="auth-user">
      <span className="auth-email" title={auth.email}>
        {auth.email}
      </span>
    </div>
  );
}

function BookAccess({ books }) {
  if (!books?.length) return null;
  return (
    <div className="book-access" aria-label="Dostupné knihy">
      {books.map((book) => (
        <span className="book-chip" key={book}>
          {book}
        </span>
      ))}
    </div>
  );
}

function ViewSwitch({ view, onChange }) {
  return (
    <div className="view-switch" role="tablist" aria-label="Admin view">
      {["chat", "sessions"].map((item) => (
        <button
          key={item}
          type="button"
          role="tab"
          aria-selected={view === item}
          className={cn("view-tab", view === item && "view-tab-active")}
          onClick={() => onChange(item)}
        >
          {item}
        </button>
      ))}
    </div>
  );
}

function SourceLink({ source }) {
  const label = `[${source.index}] ${source.entity_type}/${source.name || source.entity_cid}`;
  const meta = source.book_cid
    ? ` (${source.book_cid}${source.page ? ` p.${source.page}` : ""})`
    : "";
  const href = sourceUrl(source);

  if (!href)
    return (
      <span id={`source-${source.index}`}>
        {label}
        {meta}
      </span>
    );

  return (
    <a
      id={`source-${source.index}`}
      href={href}
      target="_blank"
      rel="noopener noreferrer"
    >
      {label}
      {meta}
    </a>
  );
}

function AssistantMessage({ text, sources }) {
  const html = useMemo(() => renderMd(text, sources || []), [text, sources]);

  return (
    <article className="message message-assistant">
      <div className="md" dangerouslySetInnerHTML={{ __html: html }} />
      {sources && sources.length > 0 ? (
        <div className="sources">
          <span className="sources-label">sources:</span>
          {sources.map((source, index) => (
            <span
              className="source-item"
              key={`${source.index}-${source.entity_cid || index}`}
            >
              <SourceLink source={source} />
            </span>
          ))}
        </div>
      ) : null}
    </article>
  );
}

function UserMessage({ text }) {
  return <article className="message message-user">{text}</article>;
}

function ErrorMessage({ text }) {
  return <article className="message message-error">{text}</article>;
}

function EventRow({ event }) {
  return (
    <div className="event-row">
      <Badge variant={event.error ? "destructive" : "secondary"}>
        {event.label}
      </Badge>
      <span className="event-text">
        {event.code ? <code>{event.code}</code> : event.text}
      </span>
    </div>
  );
}

function EventsPanel({ events, status }) {
  return (
    <section className="events-card" aria-live="polite">
      {events.map((event) => (
        <EventRow key={event.id} event={event} />
      ))}
      {status ? (
        <div className="event-row">
          <Badge>status</Badge>
          <Spinner />
          <span className="event-text">{status}</span>
        </div>
      ) : null}
    </section>
  );
}

function ChatItem({ item }) {
  if (item.type === "user") return <UserMessage text={item.text} />;
  if (item.type === "assistant")
    return <AssistantMessage text={item.text} sources={item.sources} />;
  if (item.type === "events")
    return <EventsPanel events={item.events} status={item.status} />;
  if (item.type === "error") return <ErrorMessage text={item.text} />;
  return null;
}

function EmptyState() {
  return (
    <div className="empty-state">
      <section className="empty-card">
        <h2>Sirael CHAT</h2>
        <p>
          Zeptej se na pravidla, postavy, kouzla nebo lore. Odpovědi budou
          doplněné zdroji.
        </p>
      </section>
    </div>
  );
}

function formatDateTime(value) {
  if (!value) return "";
  return new Date(value).toLocaleString("cs-CZ", {
    dateStyle: "short",
    timeStyle: "medium",
  });
}

function formatCost(value) {
  const cost = Number(value || 0);
  return `$${cost.toFixed(6)}`;
}

function AdminSessions({ sessions, status, onRefresh }) {
  return (
    <section className="admin-panel">
      <div className="admin-head">
        <h2 className="admin-title">sessions</h2>
        <Button type="button" onClick={onRefresh}>
          refresh
        </Button>
      </div>
      {status ? <p className="admin-answer">{status}</p> : null}
      <div className="admin-list">
        {sessions.map((session) => (
          <article className="admin-session" key={session.session_id}>
            <div className="admin-session-meta">
              <span>{formatDateTime(session.finished_at)}</span>
              <span>
                {session.user_email ||
                  session.user_subject ||
                  session.usage_key ||
                  "anonymous"}
              </span>
              <span>{session.status}</span>
              <span>{session.total_tokens} tok</span>
              <span>{formatCost(session.estimated_cost_usd)}</span>
              <span>
                books:{" "}
                {(session.source_books || []).length
                  ? session.source_books.join(", ")
                  : "all"}
              </span>
            </div>
            <p className="admin-question">{session.question}</p>
            {session.answer ? (
              <p className="admin-answer">{session.answer}</p>
            ) : null}
          </article>
        ))}
      </div>
    </section>
  );
}

async function* sseLines(resp) {
  const reader = resp.body.getReader();
  const dec = new TextDecoder();
  let buf = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buf += dec.decode(value, { stream: true });

    let idx;
    while ((idx = buf.indexOf("\n\n")) >= 0) {
      const block = buf.slice(0, idx);
      buf = buf.slice(idx + 2);
      let ev = "message";
      let data = "";

      for (const line of block.split("\n")) {
        if (line.startsWith("event:")) ev = line.slice(6).trim();
        else if (line.startsWith("data:")) data += line.slice(5).trim();
      }

      yield { ev, data: data ? JSON.parse(data) : {} };
    }
  }
}

function App() {
  const [messages, setMessages] = useState([]);
  const [usage, setUsage] = useState(null);
  const [question, setQuestion] = useState("");
  const [isSending, setIsSending] = useState(false);
  const [auth, setAuth] = useState({
    ready: false,
    enabled: false,
    keycloak: null,
    email: "",
    roles: [],
    allowedRole: "rag-user",
    adminRole: "rag-admin",
    books: [],
  });
  const [authError, setAuthError] = useState("");
  const [view, setView] = useState("chat");
  const [adminSessions, setAdminSessions] = useState([]);
  const [adminStatus, setAdminStatus] = useState("");
  const scrollRef = useRef(null);
  const textareaRef = useRef(null);
  const isAdmin = auth.roles.includes(auth.adminRole);

  useEffect(() => {
    let cancelled = false;

    async function initAuth() {
      try {
        const configResp = await fetch("/auth/config");
        if (!configResp.ok)
          throw new Error(`${configResp.status}: auth config failed`);
        const config = await configResp.json();
        if (!config.enabled) {
          if (!cancelled) {
            setAuth({
              ready: true,
              enabled: false,
              keycloak: null,
              email: "",
              roles: config.devRoles || [],
              allowedRole: config.allowedRole || "rag-user",
              adminRole: config.adminRole || "rag-admin",
              books: config.books || [],
            });
          }
          return;
        }

        const keycloak = new Keycloak({
          url: config.url,
          realm: config.realm,
          clientId: config.clientId,
        });
        await keycloak.init({
          onLoad: "login-required",
          pkceMethod: "S256",
          checkLoginIframe: false,
        });

        if (cancelled) return;
        const roles = tokenRoles(keycloak.tokenParsed, config.clientId);
        const allowedRole = config.allowedRole || "rag-user";
        if (!roles.includes(allowedRole)) {
          throw new Error(`missing required role: ${allowedRole}`);
        }

        const authedConfigResp = await fetch("/auth/config", {
          headers: { authorization: `Bearer ${keycloak.token}` },
        });
        const authedConfig = authedConfigResp.ok
          ? await authedConfigResp.json()
          : config;
        setAuth({
          ready: true,
          enabled: true,
          keycloak,
          email:
            keycloak.tokenParsed?.email ||
            keycloak.tokenParsed?.preferred_username ||
            "",
          roles,
          allowedRole,
          adminRole: authedConfig.adminRole || config.adminRole || "rag-admin",
          books: authedConfig.books || [],
        });
      } catch (err) {
        if (!cancelled) {
          setAuth({
            ready: true,
            enabled: false,
            keycloak: null,
            email: "",
            roles: [],
            allowedRole: "rag-user",
            adminRole: "rag-admin",
            books: [],
          });
          setAuthError(String(err));
        }
      }
    }

    initAuth();
    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    const node = scrollRef.current;
    if (node) node.scrollTop = node.scrollHeight;
  }, [messages]);

  async function authHeaders() {
    if (!auth.enabled || !auth.keycloak) return {};
    await auth.keycloak.updateToken(30);
    return { authorization: `Bearer ${auth.keycloak.token}` };
  }

  async function loadAdminSessions() {
    if (!isAdmin) return;
    setAdminStatus("loading");
    try {
      const resp = await fetch("/api/admin/chat-sessions?limit=20", {
        headers: await authHeaders(),
      });
      if (!resp.ok) throw new Error(`${resp.status}: sessions failed`);
      const data = await resp.json();
      setAdminSessions(data.sessions || []);
      setAdminStatus("");
    } catch (err) {
      setAdminStatus(String(err));
    }
  }

  useEffect(() => {
    if (!isAdmin && view !== "chat") setView("chat");
    if (isAdmin && view === "sessions") loadAdminSessions();
  }, [isAdmin, view]);

  function updateEvents(id, patch) {
    setMessages((current) =>
      current.map((item) => {
        if (item.id !== id || item.type !== "events") return item;
        return { ...item, ...patch };
      }),
    );
  }

  function appendEvent(id, event) {
    setMessages((current) =>
      current.map((item) => {
        if (item.id !== id || item.type !== "events") return item;
        return {
          ...item,
          events: [...item.events, { id: uid(), ...event }],
        };
      }),
    );
  }

  function markLastSearchEvent(id) {
    setMessages((current) =>
      current.map((item) => {
        if (item.id !== id || item.type !== "events") return item;
        const events = [...item.events];
        for (let index = events.length - 1; index >= 0; index -= 1) {
          if (events[index].kind === "search") {
            events[index] = { ...events[index], label: "searched" };
            break;
          }
        }
        return { ...item, events };
      }),
    );
  }

  async function send(message) {
    const eventsId = uid();
    setMessages((current) => [
      ...current,
      { id: uid(), type: "user", text: message },
      { id: eventsId, type: "events", events: [], status: "starting" },
    ]);

    let final = null;
    let finalSources = [];

    const resp = await fetch("/api/chat/stream", {
      method: "POST",
      headers: {
        "content-type": "application/json",
        ...(await authHeaders()),
      },
      body: JSON.stringify({ message }),
    });

    if (!resp.ok || !resp.body) {
      updateEvents(eventsId, { status: "" });
      setMessages((current) => [
        ...current,
        {
          id: uid(),
          type: "error",
          text: `${resp.status}: stream failed`,
        },
      ]);
      return;
    }

    for await (const { ev, data } of sseLines(resp)) {
      switch (ev) {
        case "usage":
          setUsage(data);
          break;
        case "status":
          updateEvents(eventsId, { status: data.text || "" });
          break;
        case "tool_call":
          appendEvent(eventsId, {
            kind: "search",
            label: "search",
            code: data.args?.query || "",
          });
          updateEvents(eventsId, { status: "searching" });
          break;
        case "tool_result":
          markLastSearchEvent(eventsId);
          updateEvents(eventsId, { status: "thinking" });
          break;
        case "sources":
          finalSources = data.sources || [];
          break;
        case "answer":
          final = data.text || "";
          break;
        case "error":
          appendEvent(eventsId, {
            label: "error",
            text: data.message || "error",
            error: true,
          });
          updateEvents(eventsId, { status: "" });
          break;
        case "done":
          updateEvents(eventsId, { status: "" });
          if (final !== null) {
            setMessages((current) => [
              ...current,
              {
                id: uid(),
                type: "assistant",
                text: final,
                sources: finalSources,
              },
            ]);
          }
          if (isAdmin) await loadAdminSessions();
          return;
      }
    }

    updateEvents(eventsId, { status: "" });
  }

  async function handleSubmit(event) {
    event.preventDefault();
    const message = question.trim();
    if (!message || isSending) return;

    setQuestion("");
    setIsSending(true);

    try {
      await send(message);
    } catch (err) {
      setMessages((current) => [
        ...current,
        { id: uid(), type: "error", text: String(err) },
      ]);
    } finally {
      setIsSending(false);
      requestAnimationFrame(() => textareaRef.current?.focus());
    }
  }

  function handleQuestionKeyDown(event) {
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      event.currentTarget.form?.requestSubmit();
    }
  }

  if (!auth.ready) {
    return (
      <div className="empty-state">
        <section className="empty-card">
          <h2>Sirael Chat</h2>
          <p>Přihlašuji...</p>
        </section>
      </div>
    );
  }

  if (authError) {
    return (
      <div className="empty-state">
        <section className="empty-card">
          <h2>Sirael Chat</h2>
          <p>{authError}</p>
        </section>
      </div>
    );
  }

  return (
    <div className="app-shell">
      <header className="app-header">
        <div className={cn("header-inner", isAdmin && "header-inner-admin")}>
          <div className="brand">
            <span className="brand-mark" aria-hidden="true">
              S
            </span>
            <h1 className="brand-title">Sirael Chat</h1>
          </div>
          {isAdmin ? <ViewSwitch view={view} onChange={setView} /> : null}
          <BookAccess books={auth.books} />
          <UsageMeter usage={usage} />
          <AuthUser auth={auth} />
        </div>
      </header>

      <main ref={scrollRef} className="chat-scroll">
        {isAdmin && view === "sessions" ? (
          <AdminSessions
            sessions={adminSessions}
            status={adminStatus}
            onRefresh={loadAdminSessions}
          />
        ) : (
          <div className="chat-stack">
            {messages.length === 0 ? (
              <EmptyState />
            ) : (
              messages.map((item) => <ChatItem key={item.id} item={item} />)
            )}
          </div>
        )}
      </main>

      {view === "chat" ? (
        <form className="composer" onSubmit={handleSubmit}>
          <div className="composer-inner">
            <div className="field">
              <Textarea
                ref={textareaRef}
                value={question}
                onChange={(event) => setQuestion(event.target.value)}
                onKeyDown={handleQuestionKeyDown}
                placeholder="Zeptej se na pravidla, postavy, kouzla..."
                required
              />
            </div>
            <div className="composer-tools">
              <Button type="submit" disabled={isSending}>
                {isSending ? <Spinner /> : null}
                {isSending ? "Odesílám" : "Odeslat"}
              </Button>
            </div>
          </div>
        </form>
      ) : null}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
