"use client";

import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import {
  getActiveModel,
  getDefaultObjective5ModelOptions,
  getDefaultObjective5Settings,
  isLocalEndpoint,
  LAB2_CHAT_STORAGE_KEY,
  LAB2_CUSTOM_MODEL_VALUE,
  LAB2_DEFAULT_ENDPOINT,
  type Objective5ModelOption,
  type Objective5Metrics,
  type Objective5Message,
  type Objective5RenderMode,
  svgToDataUrl,
} from "~/lib/lab2-chat";

type ChatTurn = Objective5Message & {
  error?: string;
  id: string;
  metrics?: Objective5Metrics | null;
  model?: string;
  renderMode: Objective5RenderMode;
  svg?: string;
};

type ChatApiSuccess = {
  content: string;
  error?: string;
  metrics?: Objective5Metrics | null;
  renderMode: Objective5RenderMode;
  role: "assistant";
  svg?: string;
};

const starterPrompts = [
  "Draw a pelican riding a bicycle.",
  "Draw a raccoon conducting an orchestra with a baguette baton.",
  "Draw a capybara skateboarding through a volcano museum.",
] as const;

function formatMetricValue(value: number, suffix = "") {
  if (Number.isInteger(value)) {
    return `${value}${suffix}`;
  }

  return `${value.toFixed(1)}${suffix}`;
}

function renderMetrics(metrics: Objective5Metrics | null | undefined) {
  if (!metrics) return null;

  const metricItems = [
    typeof metrics.tokensPerSecond === "number"
      ? `Tokens/sec ${formatMetricValue(metrics.tokensPerSecond)}`
      : null,
    typeof metrics.completionTokens === "number"
      ? `Output tokens ${formatMetricValue(metrics.completionTokens)}`
      : null,
    typeof metrics.promptTokens === "number"
      ? `Prompt tokens ${formatMetricValue(metrics.promptTokens)}`
      : null,
    typeof metrics.evalDurationMs === "number"
      ? `Eval ${formatMetricValue(metrics.evalDurationMs, " ms")}`
      : null,
    typeof metrics.totalDurationMs === "number"
      ? `Total ${formatMetricValue(metrics.totalDurationMs, " ms")}`
      : null,
  ].filter(Boolean);

  if (metricItems.length === 0) {
    return null;
  }

  return (
    <div className="objective5-chat__metrics" aria-label="Response metrics">
      {metricItems.map((item) => (
        <span className="objective5-chat__metric-pill" key={item}>
          {item}
        </span>
      ))}
    </div>
  );
}

function buildTurnId() {
  return `turn-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}

function toApiConversation(messages: ChatTurn[]) {
  return messages.map(({ content, role }) => ({ content, role }));
}

export function Objective5Chat() {
  const defaults = useMemo(() => getDefaultObjective5Settings(), []);
  const defaultModelOptions = useMemo(() => getDefaultObjective5ModelOptions(), []);
  const [endpoint, setEndpoint] = useState(defaults.endpoint);
  const [apiKey, setApiKey] = useState(defaults.apiKey);
  const [selectedModel, setSelectedModel] = useState(defaults.selectedModel);
  const [customModel, setCustomModel] = useState(defaults.customModel);
  const [draft, setDraft] = useState<string>(starterPrompts[1]);
  const [messages, setMessages] = useState<ChatTurn[]>([]);
  const [modelOptions, setModelOptions] =
    useState<Objective5ModelOption[]>(defaultModelOptions);
  const [modelError, setModelError] = useState<string | null>(null);
  const [isRefreshingModels, setIsRefreshingModels] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [hasLoadedSettings, setHasLoadedSettings] = useState(false);

  const activeModel = getActiveModel(selectedModel, customModel);

  useEffect(() => {
    try {
      const savedSettings = window.localStorage.getItem(LAB2_CHAT_STORAGE_KEY);
      if (!savedSettings) {
        setHasLoadedSettings(true);
        return;
      }

      const parsed = JSON.parse(savedSettings) as Partial<typeof defaults>;
      setEndpoint(parsed.endpoint?.trim() || LAB2_DEFAULT_ENDPOINT);
      setApiKey(parsed.apiKey ?? "");
      setSelectedModel(parsed.selectedModel?.trim() || defaults.selectedModel);
      setCustomModel(parsed.customModel?.trim() || "");
    } catch {
      window.localStorage.removeItem(LAB2_CHAT_STORAGE_KEY);
    } finally {
      setHasLoadedSettings(true);
    }
  }, [defaults.selectedModel]);

  useEffect(() => {
    if (!hasLoadedSettings) return;

    window.localStorage.setItem(
      LAB2_CHAT_STORAGE_KEY,
      JSON.stringify({
        apiKey,
        customModel,
        endpoint,
        selectedModel,
      }),
    );
  }, [apiKey, customModel, endpoint, hasLoadedSettings, selectedModel]);

  const refreshModels = useCallback(async () => {
    const trimmedEndpoint = endpoint.trim();
    const trimmedKey = apiKey.trim();

    if (!trimmedEndpoint) {
      setModelError("Enter an endpoint before refreshing models.");
      return;
    }

    if (!trimmedKey && !isLocalEndpoint(trimmedEndpoint)) {
      setModelError("Enter an API key before refreshing remote models.");
      return;
    }

    setIsRefreshingModels(true);
    setModelError(null);

    try {
      const response = await fetch("/api/lab2/models", {
        body: JSON.stringify({
          apiKey: trimmedKey,
          endpoint: trimmedEndpoint,
        }),
        headers: {
          "Content-Type": "application/json",
        },
        method: "POST",
      });

      const payload = (await response.json()) as {
        error?: string;
        models?: Objective5ModelOption[];
      };

      if (!response.ok) {
        throw new Error(payload.error || "Could not load models.");
      }

      const nextOptions = Array.isArray(payload.models) ? payload.models : [];
      const optionsWithCustom = ensureCustomOption(nextOptions);
      setModelOptions(optionsWithCustom);
      setSelectedModel((currentModel) => {
        if (
          currentModel === LAB2_CUSTOM_MODEL_VALUE ||
          optionsWithCustom.some((option) => option.value === currentModel)
        ) {
          return currentModel;
        }

        return optionsWithCustom[0]?.value ?? currentModel;
      });
    } catch (caughtError) {
      setModelError(
        caughtError instanceof Error
          ? caughtError.message
          : "Could not load models.",
      );
    } finally {
      setIsRefreshingModels(false);
    }
  }, [apiKey, endpoint]);

  useEffect(() => {
    if (!hasLoadedSettings) return;
    if (!endpoint.trim()) return;
    if (!apiKey.trim() && !isLocalEndpoint(endpoint.trim())) return;

    void refreshModels();
  }, [apiKey, endpoint, hasLoadedSettings, refreshModels]);

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    const prompt = draft.trim();
    const trimmedEndpoint = endpoint.trim();
    const trimmedKey = apiKey.trim();

    if (!trimmedEndpoint) {
      setError("Enter the model endpoint before sending a prompt.");
      return;
    }

    if (!trimmedKey && !isLocalEndpoint(trimmedEndpoint)) {
      setError("Enter an API key before sending a prompt to a remote endpoint.");
      return;
    }

    if (!activeModel) {
      setError("Choose one of the quantized models or enter a custom model name.");
      return;
    }

    if (!prompt) {
      setError("Enter a prompt to compare qualitative output.");
      return;
    }

    const nextUserTurn: ChatTurn = {
      content: prompt,
      id: buildTurnId(),
      renderMode: "text",
      role: "user",
    };

    const nextConversation = [...messages, nextUserTurn];

    setMessages(nextConversation);
    setDraft("");
    setError(null);
    setIsSubmitting(true);

    try {
      const response = await fetch("/api/lab2/chat", {
        body: JSON.stringify({
          apiKey: trimmedKey,
          endpoint: trimmedEndpoint,
          messages: toApiConversation(nextConversation),
          model: activeModel,
        }),
        headers: {
          "Content-Type": "application/json",
        },
        method: "POST",
      });

      const payload = (await response.json()) as ChatApiSuccess & {
        error?: string;
      };

      if (!response.ok) {
        throw new Error(payload.error || "The chat request failed.");
      }

      const assistantTurn: ChatTurn = {
        content: payload.content,
        error: payload.error,
        id: buildTurnId(),
        metrics: payload.metrics,
        model: activeModel,
        renderMode: payload.renderMode,
        role: "assistant",
        svg: payload.svg,
      };

      setMessages((currentMessages) => [...currentMessages, assistantTurn]);
    } catch (caughtError) {
      setError(
        caughtError instanceof Error
          ? caughtError.message
          : "The chat request failed.",
      );
    } finally {
      setIsSubmitting(false);
    }
  }

  return (
    <section className="objective5-chat" data-widget-enhanced="true">
      <div className="objective5-chat__header">
        <p className="objective5-chat__eyebrow">Objective 5 Lab Widget</p>
        <h3>Compare qualitative output with a hosted chat endpoint</h3>
        <p className="objective5-chat__lede">
          Switch between quantized models, reuse the same prompt, and ask for
          text or simple SVG sketches like{" "}
          <code>Draw a pelican riding a bicycle.</code>
        </p>
      </div>

      <div className="objective5-chat__settings">
        <label className="objective5-chat__field">
          <span>Endpoint</span>
          <input
            autoComplete="off"
            name="endpoint"
            onChange={(event) => setEndpoint(event.target.value)}
            placeholder={LAB2_DEFAULT_ENDPOINT}
            type="url"
            value={endpoint}
          />
        </label>

        <label className="objective5-chat__field">
          <span>API key</span>
          <input
            autoComplete="off"
            name="apiKey"
            onChange={(event) => setApiKey(event.target.value)}
            placeholder="Paste a lab key here"
            type="text"
            value={apiKey}
          />
        </label>

        <label className="objective5-chat__field">
          <span>Model</span>
          <div className="objective5-chat__model-row">
            <select
              name="selectedModel"
              onChange={(event) => setSelectedModel(event.target.value)}
              value={selectedModel}
            >
              {modelOptions.map((option) => (
                <option key={option.value} value={option.value}>
                  {option.label}
                </option>
              ))}
            </select>
            <button
              className="objective5-chat__refresh-button"
              disabled={isRefreshingModels}
              onClick={() => void refreshModels()}
              type="button"
            >
              {isRefreshingModels ? "Refreshing..." : "Refresh Models"}
            </button>
          </div>
        </label>

        {selectedModel === LAB2_CUSTOM_MODEL_VALUE ? (
          <label className="objective5-chat__field">
            <span>Custom model id</span>
            <input
              autoComplete="off"
              name="customModel"
              onChange={(event) => setCustomModel(event.target.value)}
              placeholder="provider/model-name"
              type="text"
              value={customModel}
            />
          </label>
        ) : null}
      </div>

      <p className="objective5-chat__settings-note">
        Settings stay in your browser for this lab only. Available models are
        refreshed from the configured endpoint, and changing the model does not
        clear the transcript.
      </p>

      {modelError ? (
        <p className="objective5-chat__error objective5-chat__error--inline">
          {modelError}
        </p>
      ) : null}

      <div className="objective5-chat__prompt-row">
        {starterPrompts.map((prompt) => (
          <button
            className="objective5-chat__prompt-chip"
            key={prompt}
            onClick={() => setDraft(prompt)}
            type="button"
          >
            {prompt}
          </button>
        ))}
      </div>

      <div className="objective5-chat__transcript" aria-live="polite">
        {messages.length === 0 ? (
          <div className="objective5-chat__empty">
            <strong>Try the same prompt on each model.</strong>
            <p>
              Start with one of the suggested prompts, then switch the model and
              send the same question again to compare coherence and SVG fidelity.
            </p>
          </div>
        ) : (
          messages.map((message) => {
            const svgDataUrl =
              message.renderMode === "svg" && message.svg
                ? svgToDataUrl(message.svg)
                : null;

            return (
              <article
                className={`objective5-chat__message objective5-chat__message--${message.role}`}
                key={message.id}
              >
                <div className="objective5-chat__message-meta">
                  <span>{message.role === "user" ? "You" : "Assistant"}</span>
                  {message.model ? <code>{message.model}</code> : null}
                </div>

                {message.renderMode === "svg" && svgDataUrl ? (
                  <div className="objective5-chat__svg-block">
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img
                      alt={`SVG sketch generated by ${message.model ?? "the selected model"}`}
                      className="objective5-chat__svg-preview"
                      src={svgDataUrl}
                    />
                    <details className="objective5-chat__svg-source">
                      <summary>View SVG source</summary>
                      <pre>
                        <code>{message.svg}</code>
                      </pre>
                    </details>
                  </div>
                ) : (
                  <pre className="objective5-chat__message-body">
                    <code>{message.content}</code>
                  </pre>
                )}

                {message.role === "assistant" ? renderMetrics(message.metrics) : null}

                {message.error ? (
                  <p className="objective5-chat__message-warning">
                    {message.error}
                  </p>
                ) : null}
              </article>
            );
          })
        )}
      </div>

      <form className="objective5-chat__composer" onSubmit={handleSubmit}>
        <label className="objective5-chat__composer-label" htmlFor="objective5-draft">
          Prompt
        </label>
        <textarea
          id="objective5-draft"
          name="draft"
          onChange={(event) => setDraft(event.target.value)}
          placeholder="Ask a text question or request an SVG sketch."
          rows={5}
          value={draft}
        />

        <div className="objective5-chat__composer-actions">
          <div className="objective5-chat__composer-state">
            <span>Current model</span>
            <strong>{activeModel || "Choose a model"}</strong>
          </div>
          <button disabled={isSubmitting} type="submit">
            {isSubmitting ? "Sending..." : "Send Prompt"}
          </button>
        </div>

        {error ? <p className="objective5-chat__error">{error}</p> : null}
      </form>
    </section>
  );
}

function ensureCustomOption(modelOptions: Objective5ModelOption[]) {
  if (
    modelOptions.some((option) => option.value === LAB2_CUSTOM_MODEL_VALUE)
  ) {
    return modelOptions;
  }

  return [
    ...modelOptions,
    {
      label: "Custom model",
      value: LAB2_CUSTOM_MODEL_VALUE,
    },
  ];
}
