"use client";

import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import {
  fetchCoursewareRuntimeConfig,
  normalizeCoursewareRuntimeConfig,
} from "~/lib/courseware-runtime";
import {
  getActiveModel,
  getDefaultObjective5ModelOptions,
  getDefaultObjective5Settings,
  LAB2_CHAT_STORAGE_KEY,
  LAB2_CUSTOM_MODEL_VALUE,
  LAB2_DEFAULT_ENDPOINT,
  LAB2_LEGACY_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 runtimeDefaults = useMemo(() => normalizeCoursewareRuntimeConfig(), []);
  const defaults = useMemo(
    () => ({
      ...getDefaultObjective5Settings(),
      endpoint: runtimeDefaults.lab2OllamaUrl,
      selectedModel:
        runtimeDefaults.lab2OllamaModels[0]?.value ??
        getDefaultObjective5Settings().selectedModel,
    }),
    [runtimeDefaults.lab2OllamaModels, runtimeDefaults.lab2OllamaUrl],
  );
  const defaultModelOptions = useMemo(
    () => ensureCustomOption(runtimeDefaults.lab2OllamaModels),
    [runtimeDefaults.lab2OllamaModels],
  );
  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(() => {
    let isCancelled = false;

    async function loadInitialSettings() {
      const resolvedRuntime = await fetchCoursewareRuntimeConfig().catch(() =>
        normalizeCoursewareRuntimeConfig(),
      );
      if (isCancelled) {
        return;
      }

      const resolvedModelOptions = ensureCustomOption(
        resolvedRuntime.lab2OllamaModels,
      );
      const resolvedSelectedModel =
        resolvedRuntime.lab2OllamaModels[0]?.value ?? defaults.selectedModel;

      setModelOptions(resolvedModelOptions);

      try {
        const savedSettings = window.localStorage.getItem(LAB2_CHAT_STORAGE_KEY);
        if (!savedSettings) {
          setEndpoint(resolvedRuntime.lab2OllamaUrl);
          setSelectedModel(resolvedSelectedModel);
          setCustomModel("");
          setApiKey("");
          setHasLoadedSettings(true);
          return;
        }

        const parsed = JSON.parse(savedSettings) as Partial<typeof defaults>;
        const savedEndpoint = parsed.endpoint?.trim();
        const nextEndpoint =
          !savedEndpoint || savedEndpoint === LAB2_LEGACY_DEFAULT_ENDPOINT
            ? resolvedRuntime.lab2OllamaUrl
            : savedEndpoint;
        const savedSelectedModel = parsed.selectedModel?.trim();
        const nextSelectedModel =
          savedSelectedModel === LAB2_CUSTOM_MODEL_VALUE ||
          resolvedModelOptions.some(
            (option) => option.value === savedSelectedModel,
          )
            ? savedSelectedModel || resolvedSelectedModel
            : resolvedSelectedModel;

        setEndpoint(nextEndpoint);
        setApiKey(parsed.apiKey ?? "");
        setSelectedModel(nextSelectedModel);
        setCustomModel(parsed.customModel?.trim() || "");
      } catch {
        window.localStorage.removeItem(LAB2_CHAT_STORAGE_KEY);
        setEndpoint(resolvedRuntime.lab2OllamaUrl);
        setSelectedModel(resolvedSelectedModel);
        setCustomModel("");
        setApiKey("");
      } finally {
        setHasLoadedSettings(true);
      }
    }

    void loadInitialSettings();

    return () => {
      isCancelled = 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;
    }

    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;

    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 (!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 the managed Ollama 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">
        This widget starts on the courseware Ollama service by default. You can
        still swap the endpoint, add an API key, refresh the available models,
        and change models without clearing 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,
    },
  ];
}
