/* global React, ReactDOM */
const { useState, useEffect, useRef } = React;

// ===================== TWEAKS =====================
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dark": false,
  "accent": "terracotta",
  "density": "comfy",
  "sidebarVisible": true
}/*EDITMODE-END*/;

const ACCENTS_MAP = {
  terracotta: { h: 40, c: 0.12 },
  forest:     { h: 150, c: 0.08 },
  ink:        { h: 260, c: 0.08 },
  ochre:      { h: 80, c: 0.13 },
};

// ===================== STORAGE =====================
const STORAGE = {
  clientId: "shannon.clientId.v1",
  model: "shannon.model.v1",
  thinkingLevel: "shannon.thinkingLevel.v1",
  systemPrompt: "shannon.systemPrompt.v1",
  temperature: "shannon.temperature.v1",
};

const DEFAULT_SYSTEM_PROMPT =
  "You are Shannon, a clear, rigorous, and practical AI assistant. Answer in the user's language unless they ask otherwise. Use the current conversation history as context, but do not assume access to memories or information from other conversations. Explain concepts in layers when useful: intuition first, then the formal version, then a worked example. Ask concise follow-up questions only when the user's goal is genuinely ambiguous.";
const LEGACY_SYSTEM_PROMPT =
  "You are Shannon, a patient learning tutor. Explain concepts in layers: intuition first, then the formal version, then a worked example. Ask concise follow-up questions when the learner's goal is ambiguous.";

function loadText(key, fallback) {
  try { return localStorage.getItem(key) || fallback; } catch (e) { return fallback; }
}

function saveText(key, value) {
  try { localStorage.setItem(key, value); } catch (e) {}
}

function loadSystemPrompt() {
  const stored = loadText(STORAGE.systemPrompt, DEFAULT_SYSTEM_PROMPT);
  return stored === LEGACY_SYSTEM_PROMPT ? DEFAULT_SYSTEM_PROMPT : stored;
}

function getClientId() {
  try {
    const existing = localStorage.getItem(STORAGE.clientId);
    if (existing) return existing;
    const generated = crypto.randomUUID().replace(/-/g, "");
    localStorage.setItem(STORAGE.clientId, generated);
    return generated;
  } catch (e) {
    return "anonymous";
  }
}

function formatDate(ts = Date.now()) {
  const date = new Date(ts);
  const now = new Date();
  const start = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
  const diff = start(now) - start(date);
  if (diff === 0) return "Today";
  if (diff === 86400000) return "Yesterday";
  return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}

function titleFrom(text) {
  const clean = text.replace(/\s+/g, " ").trim();
  return clean.length > 56 ? clean.slice(0, 53) + "..." : clean || "Untitled conversation";
}

function normalizeConversations(items) {
  if (!Array.isArray(items)) return [];
  return items
    .filter((c) => c && typeof c.id === "string")
    .map((c) => ({
      id: c.id,
      title: c.title || "Untitled conversation",
      date: c.date || formatDate(c.updatedAt),
      preview: c.preview || "",
      model: c.model || "smart",
      updatedAt: Number(c.updatedAt || Date.now()),
      messages: Array.isArray(c.messages) ? c.messages.filter((m) => m && m.role && typeof m.text === "string") : [],
    }))
    .sort((a, b) => b.updatedAt - a.updatedAt);
}

function loadTemperature() {
  const n = Number(loadText(STORAGE.temperature, "0.4"));
  return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 0.4;
}

// ===================== ROOT =====================
function App() {
  const [tweaks, setTweaks] = window.useTweaks(TWEAK_DEFAULTS);
  const clientId = useRef(getClientId()).current;

  const [screen, setScreen] = useState("chat");
  const [activeId, setActiveId] = useState(null);
  const [convos, setConvos] = useState([]);
  const [thread, setThread] = useState([]);
  const [streaming, setStreaming] = useState(false);
  const [models, setModels] = useState(window.MODELS);
  const [thinkingLevels, setThinkingLevels] = useState(window.THINKING_LEVELS || []);
  const [model, setModelState] = useState(() => loadText(STORAGE.model, "smart"));
  const [thinkingLevel, setThinkingLevelState] = useState(() => loadText(STORAGE.thinkingLevel, "auto"));
  const [systemPrompt, setSystemPromptState] = useState(loadSystemPrompt);
  const [temperature, setTemperatureState] = useState(loadTemperature);
  const [editingId, setEditingId] = useState(null);
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
  const streamTimer = useRef();
  const requestController = useRef();

  const apiFetch = (url, options = {}) => {
    const headers = {
      "x-shannon-client-id": clientId,
      ...(options.body ? { "content-type": "application/json" } : {}),
      ...(options.headers || {}),
    };
    return fetch(url, { ...options, headers });
  };

  // root data attrs for theme
  useEffect(() => {
    const root = document.documentElement;
    root.dataset.theme = tweaks.dark ? "dark" : "light";
    root.dataset.accent = tweaks.accent;
    root.dataset.density = tweaks.density;
    const a = ACCENTS_MAP[tweaks.accent] || ACCENTS_MAP.terracotta;
    root.style.setProperty("--accent-h", a.h);
    root.style.setProperty("--accent-c", a.c);
  }, [tweaks]);

  // global shortcuts
  useEffect(() => {
    const onKey = (e) => {
      const inField = e.target.matches && e.target.matches("input,textarea");
      if (e.key === "?" && !inField) {
        e.preventDefault();
        setShortcutsOpen((v) => !v);
      }
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        newChat();
      }
      if (e.key === "Escape") {
        setShortcutsOpen(false);
        setMobileNavOpen(false);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  useEffect(() => {
    let cancelled = false;
    apiFetch("/api/models")
      .then((res) => res.ok ? res.json() : Promise.reject(new Error("Unable to load models")))
      .then((data) => {
        if (cancelled || !Array.isArray(data.models) || data.models.length === 0) return;
        setModels(data.models);
        setModelState((current) => data.models.some((m) => m.id === current) ? current : (data.defaultModel || data.models[0].id));
        if (Array.isArray(data.thinkingLevels) && data.thinkingLevels.length > 0) {
          setThinkingLevels(data.thinkingLevels);
          setThinkingLevelState((current) => data.thinkingLevels.some((l) => l.id === current) ? current : (data.defaultThinkingLevel || "auto"));
        }
      })
      .catch(() => {});
    return () => { cancelled = true; };
  }, []);

  useEffect(() => {
    let cancelled = false;
    apiFetch("/api/conversations")
      .then((res) => res.ok ? res.json() : Promise.reject(new Error("Unable to load conversations")))
      .then((data) => {
        if (!cancelled) setConvos(normalizeConversations(data.conversations || []));
      })
      .catch(() => {});
    return () => { cancelled = true; };
  }, []);

  const setModel = (nextModel) => {
    setModelState(nextModel);
    saveText(STORAGE.model, nextModel);
  };

  const setThinkingLevel = (nextThinkingLevel) => {
    setThinkingLevelState(nextThinkingLevel);
    saveText(STORAGE.thinkingLevel, nextThinkingLevel);
  };

  const setSystemPrompt = (nextPrompt) => {
    setSystemPromptState(nextPrompt);
    saveText(STORAGE.systemPrompt, nextPrompt);
  };

  const setTemperature = (nextTemperature) => {
    const n = Math.max(0, Math.min(1, Number(nextTemperature)));
    setTemperatureState(n);
    saveText(STORAGE.temperature, String(n));
  };

  const openChat = async (id) => {
    const existing = convos.find((c) => c.id === id);
    if (!existing) return;
    setActiveId(id);
    setScreen("chat");
    setThread(existing.messages || []);
    if (existing.model) setModel(existing.model);
    setMobileNavOpen(false);

    try {
      const res = await apiFetch(`/api/conversations/${encodeURIComponent(id)}`);
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.detail || data.error || "Unable to load conversation");
      const convo = data.conversation;
      setThread(convo.messages || []);
      setConvos((items) => normalizeConversations(items.map((c) => c.id === id ? convo : c)));
      if (convo.model) setModel(convo.model);
    } catch (e) {
      setThread([{ role: "assistant", id: "load-error", text: `Unable to load this conversation.\n\n${e.message}` }]);
    }
  };

  const newChat = () => {
    setActiveId(null);
    setThread([]);
    setScreen("chat");
    setMobileNavOpen(false);
  };

  const streamAssistantReply = (convoId, replyId, reply) => {
    let i = 0;
    const tick = () => {
      i += Math.max(2, Math.floor(reply.length / 80));
      const partial = reply.slice(0, i);
      setThread((t) => t.map((m) => m.id === replyId ? { ...m, text: partial } : m));
      updateConversationMessage(convoId, replyId, partial);
      if (i < reply.length) {
        streamTimer.current = setTimeout(tick, 24);
      } else {
        setStreaming(false);
      }
    };
    clearTimeout(streamTimer.current);
    streamTimer.current = setTimeout(tick, 120);
  };

  const activeIdRef = useRef(activeId);
  useEffect(() => { activeIdRef.current = activeId; }, [activeId]);

  const updateConversationMessage = (convoId, messageId, text) => {
    if (!convoId) return;
    setConvos((items) => items.map((c) => c.id === convoId
      ? {
          ...c,
          messages: c.messages.map((m) => m.id === messageId ? { ...m, text } : m),
          updatedAt: Date.now(),
          date: formatDate(),
        }
      : c
    ));
  };

  const upsertConversationMessages = (convoId, messages, selectedModel = model) => {
    setConvos((items) => items.map((c) => c.id === convoId
      ? {
          ...c,
          model: selectedModel,
          preview: messages.filter((m) => m.role === "user").slice(-1)[0]?.text || c.preview,
          messages,
          updatedAt: Date.now(),
          date: formatDate(),
        }
      : c
    ));
  };

  const sendMessage = async (text) => {
    if (!text.trim()) return;
    const userMsg = { role: "user", id: "u" + Date.now(), text };
    const nextThread = [...thread, userMsg];
    let convoId = activeId;
    if (!convoId) {
      const id = "local-" + Date.now();
      convoId = id;
      const newConvo = {
        id,
        title: titleFrom(text),
        date: formatDate(),
        preview: text,
        model,
        updatedAt: Date.now(),
        messages: nextThread,
      };
      setConvos((cs) => [newConvo, ...cs]);
      setActiveId(id);
      activeIdRef.current = id;
    } else {
      upsertConversationMessages(convoId, nextThread);
    }
    setThread(nextThread);
    setStreaming(true);

    const replyId = "a" + Date.now();
    const assistantMsg = { role: "assistant", id: replyId, text: "" };
    setThread((t) => [...t, assistantMsg]);
    upsertConversationMessages(convoId, [...nextThread, assistantMsg]);

    requestController.current?.abort();
    const controller = new AbortController();
    requestController.current = controller;

    try {
      const res = await apiFetch("/api/chat", {
        method: "POST",
        body: JSON.stringify({
          conversationId: activeId && !activeId.startsWith("local-") ? activeId : undefined,
          message: text,
          model,
          history: nextThread.map(({ role, text }) => ({ role, text })),
          systemPrompt,
          temperature,
          thinkingLevel,
        }),
        signal: controller.signal,
      });

      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        throw new Error(data.detail || data.error || `Request failed with ${res.status}`);
      }

      if (controller.signal.aborted) return;
      const serverConversation = data.conversation;
      const serverAssistant = data.assistantMessage;
      if (serverConversation?.id && serverAssistant?.id) {
        const blankMessages = (serverConversation.messages || []).map((m) =>
          m.id === serverAssistant.id ? { ...m, text: "" } : m
        );
        const visibleConversation = { ...serverConversation, messages: blankMessages };
        setActiveId(serverConversation.id);
        activeIdRef.current = serverConversation.id;
        setConvos((items) => {
          const withoutLocal = items.filter((c) => c.id !== convoId && c.id !== serverConversation.id);
          return normalizeConversations([{ ...visibleConversation }, ...withoutLocal]);
        });
        setThread(blankMessages);
        streamAssistantReply(serverConversation.id, serverAssistant.id, serverAssistant.text || data.text || "");
      } else {
        streamAssistantReply(convoId, replyId, data.text || "");
      }
    } catch (error) {
      if (error.name === "AbortError") {
        setThread((t) => t.filter((m) => m.id !== replyId));
        setConvos((items) => items.map((c) => c.id === convoId ? { ...c, messages: (c.messages || []).filter((m) => m.id !== replyId) } : c));
        return;
      }
      streamAssistantReply(
        convoId,
        replyId,
        `I couldn't reach the model backend. Start the local server with Google credentials, then try again.\n\n${error.message}`
      );
    } finally {
      if (requestController.current === controller) requestController.current = null;
    }
  };

  const stopStream = () => {
    requestController.current?.abort();
    clearTimeout(streamTimer.current);
    setStreaming(false);
  };

  const editMessage = async (id, newText) => {
    const next = thread.map((m) => m.id === id ? { ...m, text: newText } : m);
    setThread(next);
    if (activeId) upsertConversationMessages(activeId, next);
    setEditingId(null);
    try {
      await apiFetch(`/api/messages/${encodeURIComponent(id)}`, {
        method: "PATCH",
        body: JSON.stringify({ text: newText }),
      });
    } catch (e) {}
  };

  const clearConversations = () => {
    requestController.current?.abort();
    clearTimeout(streamTimer.current);
    setStreaming(false);
    apiFetch("/api/conversations", { method: "DELETE" }).catch(() => {});
    setConvos([]);
    setThread([]);
    setActiveId(null);
    setScreen("chat");
  };

  const exportConversations = async () => {
    const res = await apiFetch("/api/export");
    const data = res.ok ? await res.json() : { conversations: convos };
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `shannon-learning-${new Date().toISOString().slice(0, 10)}.json`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };

  const activeConvo = convos.find((c) => c.id === activeId);

  const T = window;
  return (
    <div className="app" data-mobile-nav={mobileNavOpen ? "open" : "closed"}>
      <T.Sidebar
        convos={convos}
        activeId={activeId}
        onOpen={openChat}
        onNew={newChat}
        onSettings={() => { setScreen("settings"); setMobileNavOpen(false); }}
        onHistory={() => { setScreen("history"); setMobileNavOpen(false); }}
        onCloseMobile={() => setMobileNavOpen(false)}
        visible={tweaks.sidebarVisible}
      />
      <main className="main">
        <T.TopBar
          screen={screen}
          activeConvo={activeConvo}
          model={model}
          setModel={setModel}
          models={models}
          onMobileMenu={() => setMobileNavOpen(true)}
          onShortcuts={() => setShortcutsOpen(true)}
          onToggleSidebar={() => setTweaks("sidebarVisible", !tweaks.sidebarVisible)}
          tweaks={tweaks}
          setTweaks={setTweaks}
        />
        {screen === "chat" && (activeId
          ? <T.ChatThread thread={thread} streaming={streaming} editingId={editingId} setEditingId={setEditingId} editMessage={editMessage} convo={activeConvo} />
          : <T.EmptyState />)}
        {screen === "settings" && (
          <T.SettingsScreen
            tweaks={tweaks}
            setTweaks={setTweaks}
            models={models}
            model={model}
            setModel={setModel}
            thinkingLevels={thinkingLevels}
            thinkingLevel={thinkingLevel}
            setThinkingLevel={setThinkingLevel}
            systemPrompt={systemPrompt}
            setSystemPrompt={setSystemPrompt}
            temperature={temperature}
            setTemperature={setTemperature}
            onClearConversations={clearConversations}
            onExportConversations={exportConversations}
            conversationCount={convos.length}
          />
        )}
        {screen === "history"  && <T.HistoryScreen convos={convos} onOpen={openChat} />}
        {screen === "chat" && (
          <T.Composer onSend={sendMessage} streaming={streaming} onStop={stopStream} model={model} setModel={setModel} models={models} />
        )}
      </main>
      {shortcutsOpen && <T.ShortcutsOverlay onClose={() => setShortcutsOpen(false)} />}

      <T.TweaksPanel title="Tweaks">
        <T.TweakSection label="Appearance" />
        <T.TweakToggle label="Dark mode" value={tweaks.dark} onChange={(v) => setTweaks("dark", v)} />
        <T.TweakRadio  label="Density" value={tweaks.density} options={["compact", "comfy"]} onChange={(v) => setTweaks("density", v)} />
        <T.TweakSelect label="Accent" value={tweaks.accent}
          options={[
            { value: "terracotta", label: "Terracotta" },
            { value: "forest", label: "Forest" },
            { value: "ink", label: "Ink" },
            { value: "ochre", label: "Ochre" },
          ]}
          onChange={(v) => setTweaks("accent", v)} />
        <T.TweakSection label="Layout" />
        <T.TweakToggle label="Show sidebar" value={tweaks.sidebarVisible} onChange={(v) => setTweaks("sidebarVisible", v)} />
      </T.TweaksPanel>
    </div>
  );
}

window.App = App;
