const {
  plugin: { store, scoped },
  ui: {
    openModal,
    Button,
    ButtonColors,
    ButtonLooks,
    TextBox,
    TextArea,
    ModalRoot,
    ModalHeader,
    ModalBody,
    ModalFooter,
    Header,
    HeaderTags,
    Text,
    TextTags,
    Divider,
    showToast,
    ToastColors,
    injectCss,
  },
  flux: { stores },
} = shelter;

const UNSLOTH_DEFAULT_URL = "http://localhost:8000";
const UNSLOTH_MODEL = "default";

// ── Default settings ────────────────────────────────────────────
store.apiUrl ??= UNSLOTH_DEFAULT_URL;
store.apiKey ??= "";
store.model ??= UNSLOTH_MODEL;
store.maxTokens ??= 1024;
store.temperature ??= 0.7;

// ── Injected CSS for the menu items ─────────────────────────────
const removeCss = injectCss(`
  [data-unsloth-menu-item] {
    display: flex !important;
    align-items: center !important;
    gap: 8px !important;
    padding: 6px 10px !important;
    margin: 2px 6px !important;
    cursor: pointer !important;
    border-radius: 3px !important;
    color: var(--interactive-normal) !important;
    font-size: 14px !important;
    line-height: 18px !important;
    font-weight: 500 !important;
  }
  [data-unsloth-menu-item]:hover {
    background: var(--background-modifier-hover) !important;
    color: var(--interactive-hover) !important;
  }
  [data-unsloth-menu-item] svg {
    width: 18px !important;
    height: 18px !important;
    flex-shrink: 0 !important;
  }
`);
scoped.onDispose(() => removeCss());

// ── SVG icons ───────────────────────────────────────────────────
const SPARKLE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5z"/><path d="M6 14l.5 2L8 16.5 6.5 16 6 18l-.5-2L4 16.5 5.5 16z"/><path d="M16 16l.5 2 1.5-.5-1.5-.5.5-2-.5 2-1.5.5z"/></svg>`;

const REPLY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M4 12h14a3 3 0 0 1 3 3v4"/></svg>`;

// ── Helper: find and fill the message input ─────────────────────
function setMessageInput(text) {
  // Broad selectors for Discord's chat input (textarea or contenteditable div)
  const selectors = [
    // Modern Discord: Slate-based contenteditable div
    '[class*="channelTextArea"] div[role="textbox"][contenteditable="true"]',
    '[class*="channelTextArea"] [data-slate-editor="true"]',
    // Classic Discord: textarea inside channelTextArea
    '[class*="channelTextArea"] textarea',
    // Fallback: role="textbox" anywhere inside channelTextArea
    '[class*="channelTextArea"] [role="textbox"]',
    // Even broader fallbacks (no main/chat prefix)
    'main [class*="channelTextArea"] textarea',
    'main [class*="channelTextArea"] [role="textbox"]',
    '[class*="chat"] [class*="channelTextArea"] textarea',
    '[class*="chat"] [class*="channelTextArea"] [role="textbox"]',
  ];

  let editorEl = null;
  for (const sel of selectors) {
    editorEl = document.querySelector(sel);
    if (editorEl) break;
  }

  if (!editorEl) return false;

  // Determine the native input element
  let nativeInput;
  const isContentEditable =
    editorEl.tagName !== "TEXTAREA" &&
    (editorEl.isContentEditable || editorEl.getAttribute("contenteditable") === "true");

  if (editorEl.tagName === "TEXTAREA") {
    // Classic Discord: direct textarea element
    nativeInput = editorEl;
  } else if (isContentEditable) {
    // Modern Discord: Slate/contenteditable div - use it directly
    nativeInput = editorEl;
  } else {
    // Look for a nested textarea (older layout)
    nativeInput = editorEl.querySelector("textarea");
  }

  if (!nativeInput) return false;

  // ── Set the value ───────────────────────────────────────────
  nativeInput.focus();

  if (nativeInput.tagName === "TEXTAREA") {
    // Classic textarea input
    nativeInput.value = text;
    nativeInput.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
  } else {
    // ContentEditable div — modern Discord Slate editor
    // Slate listens for "beforeinput" events, not plain DOM mutations.
    // We select all existing content, then dispatch beforeinput so
    // Slate handles the replacement through its own React pipeline.

    // 1. Select all existing content so the insert replaces everything
    const sel = window.getSelection();
    if (sel) {
      try {
        const range = document.createRange();
        range.selectNodeContents(nativeInput);
        sel.removeAllRanges();
        sel.addRange(range);
      } catch (_) { /* selection API may fail on some elements */ }
    }

    // 2. Dispatch beforeinput — this is what Slate's withReact
    //    plugin hooks into to update the editor's React state.
    // dispatchEvent returns FALSE when the event was cancelled
    // (Slate calls preventDefault when it handles the event).
    // TRUE means nobody cancelled -> we need the fallback.
    let needsFallback = true;
    try {
      const notCancelled = nativeInput.dispatchEvent(new InputEvent("beforeinput", {
        inputType: "insertText",
        data: text,
        bubbles: true,
        cancelable: true,
      }));
      needsFallback = notCancelled;
    } catch (_) {
      // InputEvent constructor may not be available (old Electron)
    }

    // 3. Fallback: if beforeinput wasn't cancelled by any handler
    //    use execCommand which works on older Discord layouts.
    if (needsFallback) {
      try {
        document.execCommand("selectAll", false, null);
        document.execCommand("insertText", false, text);
      } catch (_) {
        // Last resort: set textContent directly
        nativeInput.textContent = text;
      }
    }

    // 4. Always dispatch input — React-controlled components often
    //    listen for this in addition to beforeinput.
    nativeInput.dispatchEvent(new Event("input", { bubbles: true }));
  }

  return true;
}

// ── API call ────────────────────────────────────────────────────
async function callUnsloth(prompt) {
  const url = `${store.apiUrl.replace(/\/+$/, "")}/v1/chat/completions`;

  const headers = { "Content-Type": "application/json" };
  if (store.apiKey) {
    headers["Authorization"] = `Bearer ${store.apiKey}`;
  }

  const body = {
    model: store.model,
    messages: [{ role: "user", content: prompt }],
    stream: false,
    max_tokens: store.maxTokens,
    temperature: store.temperature,
  };

  let res;
  let fetchError = null;

  try {
    res = await fetch(url, {
      method: "POST",
      headers,
      body: JSON.stringify(body),
    });
  } catch (e) {
    fetchError = e;
  }

  if (fetchError) {
    // Network-level error (e.g. Failed to fetch, CORS, DNS, etc.)
    throw new DetailedApiError({
      message: fetchError.message || "Failed to fetch",
      url,
      prompt,
      model: store.model,
      apiUrl: store.apiUrl,
      status: null,
      responseBody: null,
      timestamp: new Date().toISOString(),
      cause: fetchError,
    });
  }

  if (!res.ok) {
    let responseBody = null;
    let detail = `API returned ${res.status}`;
    try {
      responseBody = await res.text();
      try {
        const err = JSON.parse(responseBody);
        detail = err.error?.message || err.detail || detail;
      } catch {}
    } catch {}

    throw new DetailedApiError({
      message: detail,
      url,
      prompt,
      model: store.model,
      apiUrl: store.apiUrl,
      status: res.status,
      responseBody,
      timestamp: new Date().toISOString(),
      cause: null,
    });
  }

  const data = await res.json();
  const text = data?.choices?.[0]?.message?.content || "";
  return text;
}

// ── Detailed error class ─────────────────────────────────────────
class DetailedApiError extends Error {
  constructor(details) {
    super(details.message);
    this.name = "DetailedApiError";
    this.url = details.url;
    this.prompt = details.prompt;
    this.model = details.model;
    this.apiUrl = details.apiUrl;
    this.status = details.status;
    this.responseBody = details.responseBody;
    this.timestamp = details.timestamp;
    this.cause = details.cause;
  }
}

// ── Error detail modal ──────────────────────────────────────────
const ERROR_MODAL_CLASS = "uns-errmodal";

// Injected CSS for the error modal — wider modal so all detail fits
const removeErrCss = injectCss(`
  .${ERROR_MODAL_CLASS} {
    width: 620px !important;
    max-width: 90vw !important;
  }
`);
scoped.onDispose(() => removeErrCss());

function ErrorDetailModal(props) {
  const error = props.error;

  return (
    <ModalRoot class={ERROR_MODAL_CLASS}>
      <ModalHeader close={props.close}>Unsloth Chat - Error Details</ModalHeader>
      <ModalBody>
        <div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
          {/* Error message */}
          <div>
            <Header tag={HeaderTags.HeadingSM} margin={false}>Error</Header>
            <div style={{
              marginTop: "4px",
              padding: "8px 12px",
              background: "var(--info-danger-background)",
              color: "var(--text-danger)",
              borderRadius: "4px",
              fontSize: "14px",
              wordBreak: "break-word",
            }}>
              {error.message}
            </div>
          </div>

          {/* Status code */}
          <div>
            <Header tag={HeaderTags.HeadingSM} margin={false}>HTTP Status</Header>
            <Text tag={TextTags.textSM} style={{ marginTop: "4px", display: "block" }}>
              {error.status !== null ? error.status : "N/A (network error)"}
            </Text>
          </div>

          {/* API URL used */}
          <div>
            <Header tag={HeaderTags.HeadingSM} margin={false}>Request URL</Header>
            <div style={{
              marginTop: "4px",
              padding: "6px 10px",
              background: "var(--background-secondary)",
              borderRadius: "4px",
              fontFamily: "var(--font-code, monospace)",
              fontSize: "12px",
              wordBreak: "break-all",
              whiteSpace: "pre-wrap",
            }}>
              {error.url}
            </div>
          </div>

          {/* Configured API URL */}
          <div>
            <Header tag={HeaderTags.HeadingSM} margin={false}>Configured API URL</Header>
            <div style={{
              marginTop: "4px",
              padding: "6px 10px",
              background: "var(--background-secondary)",
              borderRadius: "4px",
              fontFamily: "var(--font-code, monospace)",
              fontSize: "12px",
              wordBreak: "break-all",
              whiteSpace: "pre-wrap",
            }}>
              {error.apiUrl}
            </div>
          </div>

          {/* Model */}
          <div>
            <Header tag={HeaderTags.HeadingSM} margin={false}>Model</Header>
            <Text tag={TextTags.textSM} style={{ marginTop: "4px", display: "block" }}>
              {error.model}
            </Text>
          </div>

          {/* Prompt */}
          <div>
            <Header tag={HeaderTags.HeadingSM} margin={false}>Prompt</Header>
            <div style={{
              marginTop: "4px",
              padding: "8px 12px",
              background: "var(--background-secondary)",
              borderRadius: "4px",
              fontSize: "13px",
              whiteSpace: "pre-wrap",
              wordBreak: "break-word",
              maxHeight: "120px",
              overflowY: "auto",
            }}>
              {error.prompt}
            </div>
          </div>

          {/* Response body */}
          {error.responseBody && (
            <div>
              <Header tag={HeaderTags.HeadingSM} margin={false}>Response Body</Header>
              <div style={{
                marginTop: "4px",
                padding: "8px 12px",
                background: "var(--background-secondary)",
                borderRadius: "4px",
                fontSize: "12px",
                fontFamily: "var(--font-code, monospace)",
                whiteSpace: "pre-wrap",
                wordBreak: "break-word",
                maxHeight: "200px",
                overflowY: "auto",
              }}>
                {error.responseBody}
              </div>
            </div>
          )}

          {/* Cause (network error details) */}
          {error.cause && (
            <div>
              <Header tag={HeaderTags.HeadingSM} margin={false}>Network Error</Header>
              <div style={{
                marginTop: "4px",
                padding: "8px 12px",
                background: "var(--background-secondary)",
                borderRadius: "4px",
                fontSize: "12px",
                fontFamily: "var(--font-code, monospace)",
                whiteSpace: "pre-wrap",
                wordBreak: "break-word",
              }}>
                {error.cause.toString()}
              </div>
            </div>
          )}

          {/* Timestamp */}
          <div>
            <Text tag={TextTags.textXS} style={{ color: "var(--text-muted)", display: "block" }}>
              Occurred at: {error.timestamp}
            </Text>
          </div>
        </div>
      </ModalBody>
      <ModalFooter>
        <div style={{ display: "flex", gap: "8px", justifyContent: "flex-end", width: "100%" }}>
          <Button
            look={ButtonLooks.FILLED}
            color={ButtonColors.PRIMARY}
            onClick={props.close}
          >
            Close
          </Button>
        </div>
      </ModalFooter>
    </ModalRoot>
  );
}

// ── The prompt modal ────────────────────────────────────────────
function PromptModal(props) {
  const [prompt, setPrompt] = shelter.solid.createSignal(props.initialPrompt || "");
  const [loading, setLoading] = shelter.solid.createSignal(false);
  const [error, setError] = shelter.solid.createSignal("");

  let inputRef = undefined;
  let confirmRef = undefined;

  shelter.solid.onMount(() => {
    if (inputRef) inputRef.focus();
  });

  async function handleSubmit() {
    const p = prompt().trim();
    if (!p) return;

    setLoading(true);
    setError("");

    try {
      const result = await callUnsloth(p);

      if (result) {
        const ok = setMessageInput(result);
        if (ok) {
          showToast({
            title: "Unsloth Chat",
            content: "Response inserted into message input!",
            color: ToastColors.SUCCESS,
          });
        } else {
          showToast({
            title: "Unsloth Chat",
            content: "Could not find message input. Response copied to clipboard.",
            color: ToastColors.WARNING,
          });
          try {
            await navigator.clipboard.writeText(result);
          } catch {}
        }
      } else {
        setError("The model returned an empty response.");
      }

      props.close();
    } catch (e) {
      if (e instanceof DetailedApiError) {
        openModal((p) => <ErrorDetailModal close={p.close} error={e} />);
      } else {
        setError(e.message || "An error occurred");
      }
    } finally {
      setLoading(false);
    }
  }

  function handleKeyDown(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      handleSubmit();
    }
    if (e.key === "Escape") {
      props.close();
    }
  }

  return (
    <ModalRoot>
      <ModalHeader close={props.close}>Unsloth Studio</ModalHeader>
      <ModalBody>
        <Header tag={HeaderTags.HeadingSM} margin={false}>
          Prompt
        </Header>
        <div style={{ marginTop: "8px" }}>
          <TextArea
            ref={(el) => { inputRef = el; }}
            value={prompt()}
            placeholder="Ask the local LLM something..."
            onInput={(v) => setPrompt(v)}
            onKeyDown={handleKeyDown}
            resize-y={true}
            mono={false}
          />
        </div>
        {error() && (
          <div style={{
            marginTop: "8px",
            padding: "8px 12px",
            background: "var(--info-danger-background)",
            color: "var(--text-danger)",
            borderRadius: "4px",
            fontSize: "14px",
          }}>
            {error()}
          </div>
        )}
      </ModalBody>
      <ModalFooter>
        <div style={{ display: "flex", gap: "8px", justifyContent: "flex-end", width: "100%" }}>
          <Button
            look={ButtonLooks.FILLED}
            color={ButtonColors.PRIMARY}
            onClick={handleSubmit}
            disabled={loading() || !prompt().trim()}
            style={{ minWidth: "180px" }}
          >
            {loading() ? "Generating..." : "Generate & Insert"}
          </Button>
        </div>
      </ModalFooter>
    </ModalRoot>
  );
}

// ── Flux helpers: get current channel & messages ─────────────────
function getCurrentChannelId() {
  try {
    // Try SelectedChannelStore first (most common)
    const chan = stores.SelectedChannelStore || stores.ChannelStore;
    if (chan && typeof chan.getChannelId === "function") {
      return chan.getChannelId();
    }
  } catch (_) {}
  // Fallback: parse URL
  try {
    const m = window.location.pathname.match(/\/channels\/(\d+|@me)\/(\d+)/);
    if (m) return m[2];
  } catch (_) {}
  return null;
}

function getRecentMessages(count = 10) {
  const channelId = getCurrentChannelId();
  if (!channelId) return [];

  try {
    const MessageStore = stores.MessageStore;
    if (!MessageStore || typeof MessageStore.getMessages !== "function") return [];
    const coll = MessageStore.getMessages(channelId);
    if (!coll) return [];
    const arr = typeof coll.toArray === "function" ? coll.toArray() : Array.from(coll);
    return arr.filter(m => m && m.content != null).slice(-count);
  } catch (_) {
    return [];
  }
}

// ── Suggest reply from recent messages ──────────────────────────
async function suggestReply() {
  const messages = getRecentMessages(10);

  if (messages.length === 0) {
    showToast({
      title: "Unsloth Chat",
      content: "No recent messages found in this channel.",
      color: ToastColors.WARNING,
    });
    return;
  }

  // Build conversation transcript
  const convo = messages.map(m => {
    const author = m.author?.username || m.author?.globalName || "Unknown";
    const content = (m.content || "").trim() || "(attachment)";
    return `${author}: ${content}`;
  }).join("\n");

  const prompt =
    "The following is a recent conversation in a Discord channel. " +
    "Write a short, natural, contextually appropriate reply that " +
    "continues this conversation. Just write the reply message, " +
    "nothing else.\n\n---\n" + convo + "\n---\n\nReply:";

  try {
    const result = await callUnsloth(prompt);
    if (result) {
      const ok = setMessageInput(result);
      if (ok) {
        showToast({
          title: "Unsloth Chat",
          content: "Suggested reply inserted!",
          color: ToastColors.SUCCESS,
        });
      } else {
        showToast({
          title: "Unsloth Chat",
          content: "Could not find message input. Response copied to clipboard.",
          color: ToastColors.WARNING,
        });
        try { await navigator.clipboard.writeText(result); } catch {}
      }
    } else {
      showToast({
        title: "Unsloth Chat",
        content: "The model returned an empty response.",
        color: ToastColors.WARNING,
      });
    }
  } catch (e) {
    if (e instanceof DetailedApiError) {
      openModal((p) => <ErrorDetailModal close={p.close} error={e} />);
    } else {
      showToast({
        title: "Unsloth Chat",
        content: e.message || "An error occurred",
        color: ToastColors.DANGER || ToastColors.WARNING,
      });
    }
  }
}

// ── Menu item factory ───────────────────────────────────────────
function createMenuItem(label, iconSvg, onClick) {
  const el = document.createElement("div");
  el.setAttribute("data-unsloth-menu-item", "");
  el.setAttribute("role", "menuitem");
  el.setAttribute("tabindex", "0");
  el.innerHTML = iconSvg + `<span>${label}</span>`;
  el.addEventListener("click", (e) => {
    e.stopPropagation();
    onClick();
  });
  return el;
}

// ── Inject items into the + menu popout ─────────────────────────
function injectPlusMenuItems() {
  // Look for open menu scrollers — the + popout is a menu layer
  const scrollers = document.querySelectorAll('[class*="menu"] [class*="scroller"]');
  for (const scroller of scrollers) {
    // Already injected?
    if (scroller.querySelector("[data-unsloth-menu-item]")) continue;

    // Identify the + menu: it contains items for upload / create thread etc.
    // We look for any element that hints this is the channel-attach popout.
    const hint =
      scroller.querySelector('[class*="upload"], [class*="attach"]') ||
      scroller.closest('[class*="channelTextArea"]');
    if (!hint) continue;

    // Append a divider then our two items
    const divider = document.createElement("div");
    divider.style.cssText =
      "margin:4px 6px;border-top:1px solid var(--background-modifier-accent);";

    const askAi = createMenuItem("Ask AI", SPARKLE_ICON, () => {
      openModal((p) => <PromptModal close={p.close} />);
    });

    const suggest = createMenuItem("Suggest reply", REPLY_ICON, () => {
      suggestReply();
    });

    scroller.appendChild(divider);
    scroller.appendChild(askAi);
    scroller.appendChild(suggest);
  }
}

// ── Hook the + button to inject items when popout opens ──────────
function hookAttachButton(btn) {
  if (btn.dataset.unslothHooked) return;
  btn.dataset.unslothHooked = "1";
  btn.addEventListener("click", () => {
    // The popout renders asynchronously, so wait a tick
    setTimeout(injectPlusMenuItems, 50);
  });
}

scoped.observeDom(
  '[class*="channelTextArea"] [class*="attachButton"]',
  hookAttachButton,
);

// ── Export settings component ───────────────────────────────────
export function settings() {
  const [apiUrl, setApiUrl] = shelter.solid.createSignal(store.apiUrl);
  const [apiKey, setApiKey] = shelter.solid.createSignal(store.apiKey);
  const [model, setModel] = shelter.solid.createSignal(store.model);
  const [maxTokens, setMaxTokens] = shelter.solid.createSignal(String(store.maxTokens));
  const [temperature, setTemperature] = shelter.solid.createSignal(String(store.temperature));

  function saveApiUrl(v) {
    setApiUrl(v);
    store.apiUrl = v;
  }
  function saveApiKey(v) {
    setApiKey(v);
    store.apiKey = v;
  }
  function saveModel(v) {
    setModel(v);
    store.model = v;
  }
  function saveMaxTokens(v) {
    setMaxTokens(v);
    const n = Number(v);
    if (!isNaN(n) && n > 0) store.maxTokens = n;
  }
  function saveTemperature(v) {
    setTemperature(v);
    const n = Number(v);
    if (!isNaN(n) && n >= 0 && n <= 2) store.temperature = n;
  }

  return (
    <div style={{ padding: "16px 0" }}>
      <Header tag={HeaderTags.HeadingLG}>Unsloth Studio Connection</Header>
      <Divider mt mb />

      <div style={{ marginBottom: "16px" }}>
        <Text tag={TextTags.textSM} style={{ marginBottom: "4px", display: "block" }}>
          API URL
        </Text>
        <TextBox
          value={apiUrl()}
          placeholder="http://localhost:8000"
          onInput={saveApiUrl}
        />
        <Text tag={TextTags.textXS} style={{ marginTop: "4px", color: "var(--text-muted)", display: "block" }}>
          The URL of your running Unsloth Studio instance.
        </Text>
      </div>

      <div style={{ marginBottom: "16px" }}>
        <Text tag={TextTags.textSM} style={{ marginBottom: "4px", display: "block" }}>
          API Key (optional)
        </Text>
        <TextBox
          value={apiKey()}
          placeholder="Leave blank if no auth required"
          onInput={saveApiKey}
        />
        <Text tag={TextTags.textXS} style={{ marginTop: "4px", color: "var(--text-muted)", display: "block" }}>
          Your Unsloth Studio API key/token for authenticated access.
        </Text>
      </div>

      <div style={{ marginBottom: "16px" }}>
        <Text tag={TextTags.textSM} style={{ marginBottom: "4px", display: "block" }}>
          Model
        </Text>
        <TextBox
          value={model()}
          placeholder="default"
          onInput={saveModel}
        />
      </div>

      <div style={{ marginBottom: "16px" }}>
        <Text tag={TextTags.textSM} style={{ marginBottom: "4px", display: "block" }}>
          Max Tokens
        </Text>
        <TextBox
          value={maxTokens()}
          placeholder="1024"
          onInput={saveMaxTokens}
        />
      </div>

      <div style={{ marginBottom: "16px" }}>
        <Text tag={TextTags.textSM} style={{ marginBottom: "4px", display: "block" }}>
          Temperature (0.0 - 2.0)
        </Text>
        <TextBox
          value={temperature()}
          placeholder="0.7"
          onInput={saveTemperature}
        />
      </div>
    </div>
  );
}
