import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import {
  LuArrowRight,
  LuArrowUp,
  LuCheckCircle,
  LuHome,
  LuImagePlus,
  LuLoader2,
  LuMaximize2,
  LuMinimize2,
  LuRocket,
  LuSquare,
  LuTrash2,
  LuTrendingUp,
  LuX,
} from 'react-icons/lu';

import { recomaze_ai_personalization_env } from '../../env';
import { useCopilot } from '../../context/copilot.context';
import {
  fetchCopilotArticleStatus,
  fetchCopilotContentState,
  fetchCopilotHistory,
  fetchCopilotQuests,
  publishCopilotArticleToBlog,
  refreshCopilotQuests,
  saveCopilotConversation,
  streamCopilot,
} from '../../service/copilot/copilot.service';
import type {
  ICopilotAction,
  ICopilotMessage,
  ICopilotQuestOverview,
} from '../../service/copilot/copilot.interface';
import ArticleModal from '../visibility/ArticleModal';

import { cn } from './cn';
import { CopilotMarkdown } from './CopilotMarkdown';
import { CopilotSteps } from './CopilotSteps';
import { CopilotHome } from './CopilotHome';

/** Tool ids whose result is an async generation worth showing as a status card. */
const GENERATION_TOOL_IDS: ReadonlySet<string> = new Set<string>([
  'generate_article',
  'start_visibility_scan',
  'generate_product_content',
  'bulk_generate_product_content',
]);

/** How often an in-chat generation card polls its job status. */
const GENERATION_POLL_INTERVAL_MS = 6_000;

/** Article statuses that mean the in-chat generation card can show "ready". */
const READY_ARTICLE_STATUSES: ReadonlySet<string> = new Set<string>([
  'ready',
  'published',
]);

/** Content-job states that mean the emailed result is on its way / done. */
const DONE_CONTENT_STATES: ReadonlySet<string> = new Set<string>([
  'completed',
  'failed',
]);

/** Tool ids that change server data - after these, affected pages re-fetch. */
const WRITE_TOOL_IDS: ReadonlySet<string> = new Set<string>([
  'call_api',
  'bulk_delete',
  'generate_article',
  'generate_product_content',
  'start_visibility_scan',
  'update_brand_keywords',
  'update_visibility_settings',
  'generate_kb_document',
  'push_kb_document',
  'add_knowledge_text',
]);

/** Logo shown in the launcher / panel header (WordPress-localized plugin icon). */
const COPILOT_LOGO: string = recomaze_ai_personalization_env?.plugin_icon ?? '';

/**
 * Effective width of the open (non-wide) panel, in px. The panel is
 * ``sm:w-[380px] lg:w-[400px]``; the wider 400px is used so the WP admin
 * content is pushed clear of the panel on desktop, where the overlap matters.
 */
const PANEL_OPEN_WIDTH_PX = 400;

/** Body class toggled while the panel is open so WP admin content shifts left. */
const COPILOT_OPEN_BODY_CLASS = 'recomaze-copilot-open';

/**
 * CSS that (a) keeps the fixed panel + launcher below the WP admin bar, and
 * (b) pushes ONLY the plugin's own React root (#recomaze) left while the panel
 * is open, so the panel never overlaps the page. We deliberately do NOT touch
 * #wpcontent/#wpfooter: shrinking those disturbs the WordPress admin layout
 * (left menu) and can trigger horizontal overflow. Injected once, app-wide.
 */
const COPILOT_LAYOUT_CSS: string = `
  .recomaze-copilot-panel { top: 32px; height: calc(100% - 32px); }
  .recomaze-copilot-launcher { bottom: 1.25rem; }
  body.${COPILOT_OPEN_BODY_CLASS} #recomaze {
    margin-right: ${PANEL_OPEN_WIDTH_PX}px;
    transition: margin-right .2s;
  }
  @media screen and (max-width: 782px) {
    .recomaze-copilot-panel { top: 46px; height: calc(100% - 46px); }
    body.${COPILOT_OPEN_BODY_CLASS} #recomaze { margin-right: 0; }
  }
  /* WordPress admin styles bare inputs/textareas with a grey field, border and
     a dark focus box-shadow. Neutralize that inside the panel so the chat input
     stays transparent (no grey box / black focus outline when typing). */
  .recomaze-copilot-panel input,
  .recomaze-copilot-panel textarea {
    background: transparent !important;
    border: 0 !important;
    border-radius: 0 !important;
    box-shadow: none !important;
    outline: none !important;
    min-height: 0 !important;
    color: inherit !important;
  }
`;

/** Whether the app-wide copilot layout CSS has been injected this session. */
let copilotLayoutCssInjected = false;

/**
 * Inject the panel-layout stylesheet once. Self-contained: appends a single
 * <style> to <head>; subsequent calls are no-ops.
 *
 * @return Nothing.
 */
function ensureCopilotLayoutCss(): void {
  if (copilotLayoutCssInjected || typeof document === 'undefined') return;
  copilotLayoutCssInjected = true;
  const style: HTMLStyleElement = document.createElement('style');
  style.id = 'recomaze-copilot-layout';
  style.textContent = COPILOT_LAYOUT_CSS;
  document.head.appendChild(style);
}

/**
 * Reads a merchant-attached image file as a data:image/...;base64 URI so it can
 * be sent to the agent for brand-color detection.
 *
 * @param file - The image file the merchant attached.
 * @return The data URI, or null when the file could not be read.
 */
function readImageAsDataUri(file: File): Promise<string | null> {
  return new Promise(resolve => {
    const reader: FileReader = new FileReader();
    reader.onload = () =>
      resolve(typeof reader.result === 'string' ? reader.result : null);
    reader.onerror = () => resolve(null);
    reader.readAsDataURL(file);
  });
}

/**
 * The Reco copilot: a full-height panel docked on the right edge of the
 * WordPress admin app. Collapsed, it is a floating launcher button bottom-right.
 * Open, it streams answers from the agent, shows a tool/thinking tree per turn,
 * and renders markdown with clickable navigation links. White surfaces with
 * Recomaze brand magenta accents.
 *
 * Onboarding state (pending steps, new-merchant) comes from the agent via the
 * quest overview, so it reads the authoritative setup progress.
 *
 * @param props - Component props.
 * @param props.token - The merchant's recomaze JWT.
 * @param props.clientId - The merchant's recomaze client id.
 * @return The launcher (collapsed) or the docked panel (open).
 */
export function CopilotPanel({
  token,
  clientId,
}: {
  token: string;
  clientId: string;
}): React.ReactElement {
  const location = useLocation();
  const navigate = useNavigate();
  const [input, setInput] = useState<string>('');
  const [isWide, setIsWide] = useState<boolean>(false);
  const [view, setView] = useState<'home' | 'chat'>('home');
  // Article id to show in the reader modal when the merchant taps "Open" on a
  // ready generation card.
  const [openArticleId, setOpenArticleId] = useState<string | null>(null);
  // Article currently being published to the blog, and those already published,
  // so the "Publish to blog" button reflects state per card.
  const [publishingArticleId, setPublishingArticleId] = useState<string | null>(
    null
  );
  const [publishedArticleIds, setPublishedArticleIds] = useState<Set<string>>(
    () => new Set()
  );
  // Articles whose last publish attempt failed, so the card can show "Retry".
  const [failedArticleIds, setFailedArticleIds] = useState<Set<string>>(
    () => new Set()
  );
  // A logo the merchant attached, queued to ride along the next turn so the
  // backend can read brand colors off it.
  const [pendingLogo, setPendingLogo] = useState<{
    name: string;
    dataUri: string;
  } | null>(null);

  const copilot = useCopilot();
  const isOpen: boolean = copilot.isOpen;
  const isStreaming: boolean = copilot.isStreaming;
  const questCount: number = copilot.questCount;
  const questNotification: string | null = copilot.questNotification;
  const questOverview: ICopilotQuestOverview | null = copilot.questOverview;
  const setupSteps = copilot.setupSteps;
  const messages: ICopilotMessage[] = copilot.messages;
  const setAuth = copilot.setAuth;
  const toggle = copilot.toggle;
  const close = copilot.close;
  const reset = copilot.reset;
  const setMessages = copilot.setMessages;
  const setQuestOverview = copilot.setQuestOverview;
  const dismissNudge = copilot.dismissNudge;
  const questNudge = copilot.questNudge;
  const requestCopilotTurn = copilot.requestCopilotTurn;
  const pendingPrompt: string | null = copilot.pendingPrompt;
  const clearPendingPrompt = copilot.clearPendingPrompt;
  const addUserMessage = copilot.addUserMessage;
  const startAssistantMessage = copilot.startAssistantMessage;
  const upsertStep = copilot.upsertStep;
  const setAssistantActions = copilot.setAssistantActions;
  const consumeMessageActions = copilot.consumeMessageActions;
  const setAssistantGeneration = copilot.setAssistantGeneration;
  const appendToAssistant = copilot.appendToAssistant;
  const setStreaming = copilot.setStreaming;
  const failLastAssistant = copilot.failLastAssistant;
  const setMessageGenerationReady = copilot.setMessageGenerationReady;
  const setKbDraftFileName = copilot.setKbDraftFileName;
  const setPendingPopupCustomization = copilot.setPendingPopupCustomization;
  const setPendingQuickActionIcons = copilot.setPendingQuickActionIcons;
  const copilotGetState = copilot.getState;
  const copilotSetState = copilot.setState;

  const threadRef = useRef<HTMLDivElement>(null);
  const abortRef = useRef<AbortController | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const prevPathRef = useRef<string | null>(null);

  // Keep the auth credentials in sync so background calls (quest dismiss)
  // authenticate with the agent the same way streamed calls do.
  useEffect(() => {
    setAuth(token, clientId);
  }, [token, clientId, setAuth]);

  // Push the WP admin content left while the panel is open so the fixed panel
  // never overlaps the page. Inject the layout CSS once, then toggle the body
  // class with the open state; always remove it on unmount.
  useEffect(() => {
    ensureCopilotLayoutCss();
    if (typeof document === 'undefined') return;
    document.body.classList.toggle(COPILOT_OPEN_BODY_CLASS, isOpen);

    return () => document.body.classList.remove(COPILOT_OPEN_BODY_CLASS);
  }, [isOpen]);

  // Re-sync quests + setup state when the merchant navigates between pages, so
  // completed steps reflect live instead of only after a full refresh.
  useEffect(() => {
    if (prevPathRef.current === null) {
      prevPathRef.current = location.pathname;

      return;
    }
    if (prevPathRef.current === location.pathname) return;
    prevPathRef.current = location.pathname;
    if (!copilotGetState().questsLoaded) return;
    const handle: number = window.setTimeout((): void => {
      void fetchCopilotQuests(token, clientId).then(overview => {
        if (overview) setQuestOverview(overview);
      });
    }, 1_500);

    return () => window.clearTimeout(handle);
  }, [location.pathname, setQuestOverview, token, clientId, copilotGetState]);

  // Refresh the setup checklist the instant any page marks a step complete,
  // without needing a navigation.
  useEffect(() => {
    const onSetupUpdated = (): void => {
      void fetchCopilotQuests(token, clientId).then(overview => {
        if (overview) setQuestOverview(overview);
      });
    };
    window.addEventListener('copilot:setup-updated', onSetupUpdated);

    return () =>
      window.removeEventListener('copilot:setup-updated', onSetupUpdated);
  }, [token, clientId, setQuestOverview]);

  // Poll each in-chat generation card until it finishes, so the card advances
  // on its own: articles flip to "ready", emailed product-content jobs flip to
  // "sent to your email".
  useEffect(() => {
    const pending: {
      kind: 'article' | 'content';
      id: string;
      index: number;
    }[] = messages
      .map((message: ICopilotMessage, index: number) => ({ message, index }))
      .filter(entry => entry.message.generationStatus === 'generating')
      .map(entry => ({
        kind: entry.message.generationKind ?? 'article',
        id:
          entry.message.generationKind === 'content'
            ? (entry.message.generationJobId ?? '')
            : (entry.message.generationArticleId ?? ''),
        index: entry.index,
      }))
      .filter(entry => Boolean(entry.id));
    if (pending.length === 0) return;

    let cancelled = false;
    const handle: number = window.setInterval((): void => {
      for (const { kind, id, index } of pending) {
        if (kind === 'content') {
          void fetchCopilotContentState(id, token, clientId).then(state => {
            if (cancelled || !state) return;
            if (DONE_CONTENT_STATES.has(state))
              setMessageGenerationReady(index);
          });
        } else {
          void fetchCopilotArticleStatus(id, token, clientId).then(status => {
            if (cancelled || !status) return;
            if (READY_ARTICLE_STATUSES.has(status.status)) {
              setMessageGenerationReady(index);
            }
          });
        }
      }
    }, GENERATION_POLL_INTERVAL_MS);

    return () => {
      cancelled = true;
      window.clearInterval(handle);
    };
  }, [messages, setMessageGenerationReady, token, clientId]);

  // Keep the thread pinned to the latest message, both as messages stream in
  // and whenever the panel is (re)opened with an existing conversation.
  useEffect(() => {
    if (!isOpen) return;
    const scrollToBottom = (): void => {
      const thread: HTMLDivElement | null = threadRef.current;
      if (thread) thread.scrollTop = thread.scrollHeight;
    };
    const frame: number = window.requestAnimationFrame(scrollToBottom);

    return () => window.cancelAnimationFrame(frame);
  }, [messages, isOpen]);

  useEffect(() => {
    return () => abortRef.current?.abort();
  }, []);

  // On first mount: load today's quests + the authoritative setup state (which
  // also auto-opens the panel for a brand-new merchant), then restore the saved
  // thread. A returning merchant with finished setup lands on their chat.
  useEffect(() => {
    if (copilotGetState().questsLoaded) return;
    copilotSetState({ questsLoaded: true });

    void (async () => {
      const overview = await refreshCopilotQuests(token, clientId);
      if (overview) setQuestOverview(overview);
      const isNew: boolean = overview?.is_new_merchant ?? false;

      const history: ICopilotMessage[] = await fetchCopilotHistory(
        token,
        clientId
      );
      if (history.length > 0 && copilotGetState().messages.length === 0) {
        setMessages(history);
        if (!isNew) setView('chat');
      }
      copilotSetState({ isHydrated: true });
    })();
  }, [
    setQuestOverview,
    setMessages,
    token,
    clientId,
    copilotGetState,
    copilotSetState,
  ]);

  const sendMessage = useCallback(
    async (text: string, hidden: boolean = false): Promise<void> => {
      const trimmed: string = text.trim();
      if (!trimmed || isStreaming) return;

      setView('chat');
      const history: ICopilotMessage[] = copilotGetState().messages;
      // Hidden turns (page events like "import confirmed") don't show a user
      // bubble - Reco just replies proactively, in the merchant's language.
      if (!hidden) addUserMessage(trimmed);
      startAssistantMessage();
      setStreaming(true);

      // Consume any queued logo so it rides along this single turn only.
      const logoImage: string | undefined = pendingLogo?.dataUri;
      if (pendingLogo) setPendingLogo(null);

      const controller: AbortController = new AbortController();
      abortRef.current = controller;
      let didWrite: boolean = false;

      try {
        await streamCopilot(
          {
            message: trimmed,
            history,
            context: { route: location.pathname, platform: 'WORDPRESS' },
            logo_image: logoImage,
          },
          {
            onStep: step => {
              if (WRITE_TOOL_IDS.has(step.id)) didWrite = true;
              upsertStep(step);
            },
            onToken: appendToAssistant,
            onDirective: directive => {
              if (directive.directive === 'navigate') {
                navigate(directive.path);
              } else if (directive.directive === 'actions') {
                setAssistantActions(directive.actions);
              } else if (directive.directive === 'generation') {
                setAssistantGeneration({
                  kind: directive.kind,
                  articleId: directive.articleId,
                  jobId: directive.jobId,
                });
              } else if (directive.directive === 'kb_document') {
                setKbDraftFileName(directive.fileName);
                if (location.pathname !== '/knowledge-base') {
                  navigate('/knowledge-base');
                }
              } else if (directive.directive === 'customize') {
                setPendingPopupCustomization(directive.suggestion);
                // The /customization page reads this and pre-fills the form for
                // the merchant to review and save.
                if (location.pathname !== '/customization') {
                  navigate('/customization');
                }
              } else if (directive.directive === 'quick_action_icons') {
                setPendingQuickActionIcons(directive.icons);
                // The /customization page reads this and sets the generated
                // icons on the quick actions for review.
                if (location.pathname !== '/customization') {
                  navigate('/customization');
                }
              }
            },
            onDone: () => setStreaming(false),
            onError: failLastAssistant,
          },
          token,
          clientId,
          controller.signal
        );
      } catch {
        // Aborted by the user (stop / new chat) or a network drop; partial
        // content stays in the thread. onError already covered HTTP failures.
      } finally {
        setStreaming(false);
        void saveCopilotConversation(
          copilotGetState().messages,
          token,
          clientId
        );
        // A write action changed server data - tell affected pages to re-fetch
        // (WordPress has no loaders, so this CustomEvent replaces a revalidate).
        if (didWrite) {
          window.dispatchEvent(new CustomEvent('copilot:data-changed'));
        }
      }
    },
    [
      isStreaming,
      pendingLogo,
      addUserMessage,
      startAssistantMessage,
      setStreaming,
      upsertStep,
      setAssistantActions,
      setAssistantGeneration,
      appendToAssistant,
      failLastAssistant,
      setKbDraftFileName,
      setPendingPopupCustomization,
      setPendingQuickActionIcons,
      copilotGetState,
      navigate,
      location.pathname,
      token,
      clientId,
    ]
  );

  const handleSubmit = useCallback(
    (event: React.FormEvent): void => {
      event.preventDefault();
      const text: string = input;
      setInput('');
      void sendMessage(text);
    },
    [input, sendMessage]
  );

  // A quick-action button under a reply: navigate, send a follow-up, or just
  // dismiss. Either way the buttons are consumed so they don't linger.
  const handleAction = useCallback(
    (action: ICopilotAction, messageIndex: number): void => {
      consumeMessageActions(messageIndex);
      if (action.action === 'navigate' && action.route) {
        navigate(action.route);
      } else if (action.action === 'prompt' && action.prompt) {
        void sendMessage(action.prompt);
      }
    },
    [consumeMessageActions, navigate, sendMessage]
  );

  // An internal-link chip inside a markdown reply. The model writes these for
  // BOTH real pages and for actions (confirm/generate) - and unreliably: a
  // "Confirm generation" chip often carries a real route href, so navigating on
  // a known route would silently jump pages instead of confirming. So we never
  // navigate from a markdown chip; we always hand the label to the copilot and
  // let it decide (it navigates via its own tool, or performs the action).
  // Genuine instant navigation comes from propose_actions buttons + the
  // navigate directive, not from these inline chips.
  const handleCopilotLink = useCallback(
    (_href: string, label: string): void => {
      void sendMessage(label);
    },
    [sendMessage]
  );

  // A page can queue a prompt for Reco to respond to - send it once the panel
  // is ready.
  useEffect(() => {
    if (!pendingPrompt || isStreaming) return;
    const prompt: string = pendingPrompt;
    clearPendingPrompt();
    void fetchCopilotQuests(token, clientId).then(overview => {
      if (overview) setQuestOverview(overview);
    });
    void sendMessage(prompt, true);
  }, [
    pendingPrompt,
    isStreaming,
    sendMessage,
    clearPendingPrompt,
    setQuestOverview,
    token,
    clientId,
  ]);

  const handleStop = useCallback((): void => {
    abortRef.current?.abort();
    setStreaming(false);
  }, [setStreaming]);

  // Publish a generated article to the blog. The agent (which holds the store
  // connector) does the publishing; the card reflects success / retry.
  const handlePublishToBlog = useCallback(
    async (articleId: string): Promise<void> => {
      if (!articleId) return;
      setPublishingArticleId(articleId);
      setFailedArticleIds(prev => {
        const next = new Set(prev);
        next.delete(articleId);

        return next;
      });
      const result = await publishCopilotArticleToBlog(
        articleId,
        token,
        clientId
      );
      if (result.ok) {
        setPublishedArticleIds(prev => new Set(prev).add(articleId));
      } else {
        setFailedArticleIds(prev => new Set(prev).add(articleId));
      }
      setPublishingArticleId(null);
    },
    [token, clientId]
  );

  // Wipe the thread: aborts any stream, clears persisted history, and lands on
  // the home surface (an empty thread renders home).
  const handleNewChat = useCallback((): void => {
    abortRef.current?.abort();
    reset();
    setView('home');
    void saveCopilotConversation([], token, clientId);
  }, [reset, token, clientId]);

  // Attach a logo image: read it as a data URI and queue it for the next turn,
  // which powers the backend's brand-color detection.
  const handleAttachLogo = useCallback(async (file: File): Promise<void> => {
    if (!file.type.startsWith('image/')) return;
    if (file.size > 5 * 1024 * 1024) return;
    const dataUri: string | null = await readImageAsDataUri(file);
    if (dataUri) setPendingLogo({ name: file.name, dataUri });
  }, []);

  if (!isOpen) {
    const setupIncomplete: boolean = setupSteps.some(step => !step.completed);

    return createPortal(
      <div className="recomaze-copilot-launcher fixed bottom-5 right-5 z-50 flex flex-col items-end gap-3">
        {questNotification && (
          <div className="flex max-w-[300px] items-start gap-3 rounded-2xl border border-gray-100 bg-white py-3 pl-4 pr-2 shadow-xl duration-300 animate-in fade-in slide-in-from-right-2">
            <button
              type="button"
              onClick={() =>
                questNudge
                  ? requestCopilotTurn(`Help me: ${questNudge.title}`)
                  : toggle()
              }
              className="flex items-start gap-3 text-left"
            >
              {setupIncomplete ? (
                <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-[#b7007c]/10">
                  <LuRocket className="h-4 w-4 text-[#b7007c]" />
                </span>
              ) : (
                <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-emerald-100">
                  <LuTrendingUp className="h-4 w-4 text-emerald-600" />
                </span>
              )}
              <p className="text-sm font-medium leading-snug text-gray-800">
                {questNotification}
              </p>
            </button>
            <button
              type="button"
              onClick={dismissNudge}
              className="shrink-0 rounded-lg p-1 text-gray-400 transition hover:bg-gray-100 hover:text-gray-600"
              aria-label="Dismiss notification"
            >
              <LuX className="h-3.5 w-3.5" />
            </button>
          </div>
        )}
        <button
          type="button"
          onClick={toggle}
          className="relative flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-[#e0379f] to-[#a1006a] shadow-[0_8px_30px_rgba(183,0,124,0.45)] ring-1 ring-white/40 transition-transform duration-300 hover:scale-105 animate-in fade-in zoom-in"
          aria-label="Open Recomaze Co-pilot"
        >
          {COPILOT_LOGO ? (
            <img
              src={COPILOT_LOGO}
              alt="Recomaze Co-pilot"
              className="h-8 w-8 object-contain"
            />
          ) : (
            <span className="text-lg font-bold text-white">R</span>
          )}
          {questCount > 0 && (
            <span className="absolute -right-1.5 -top-1.5 flex h-6 min-w-6 items-center justify-center rounded-full bg-white px-1.5 text-xs font-bold text-[#b7007c] shadow">
              {questCount}
            </span>
          )}
        </button>
      </div>,
      document.body
    );
  }

  return createPortal(
    <aside
      className={cn(
        'recomaze-copilot-panel fixed right-0 z-30 flex flex-col overflow-hidden border-l border-gray-200 bg-white shadow-2xl transition-[width] duration-300 animate-in slide-in-from-right fade-in',
        isWide ? 'w-full sm:w-[560px]' : 'w-full sm:w-[380px] lg:w-[400px]'
      )}
    >
      {/* Thin, subtle, auto-hiding scrollbar for the chat thread (the default
          chunky one looks rough inside the docked panel). */}
      <style>{`
        .copilot-scroll { scrollbar-width: thin; scrollbar-color: rgba(183,0,124,0.25) transparent; }
        .copilot-scroll::-webkit-scrollbar { width: 6px; }
        .copilot-scroll::-webkit-scrollbar-track { background: transparent; }
        .copilot-scroll::-webkit-scrollbar-thumb { background-color: rgba(183,0,124,0.25); border-radius: 9999px; }
        .copilot-scroll:hover::-webkit-scrollbar-thumb { background-color: rgba(183,0,124,0.45); }
      `}</style>
      <header className="relative flex items-center justify-between border-b border-[#b7007c]/20 bg-gradient-to-br from-[#e0379f] to-[#a1006a] px-4 py-3">
        <div className="flex items-center gap-2">
          <span className="flex h-7 w-7 items-center justify-center overflow-hidden rounded-lg bg-white/20">
            {COPILOT_LOGO ? (
              <img
                src={COPILOT_LOGO}
                alt="Recomaze"
                className="h-5 w-5 object-contain"
              />
            ) : (
              <span className="text-sm font-bold text-white">R</span>
            )}
          </span>
          <span className="text-[15px] font-semibold tracking-tight text-white">
            recomaze
          </span>
        </div>
        <div className="flex items-center gap-1 text-white/80">
          <button
            type="button"
            onClick={() => setView('home')}
            className="rounded-lg p-1.5 transition hover:bg-white/15 hover:text-white"
            aria-label="Home"
          >
            <LuHome className="h-4 w-4" />
          </button>
          {messages.length > 0 && (
            <button
              type="button"
              onClick={handleNewChat}
              className="rounded-lg p-1.5 transition hover:bg-white/15 hover:text-white"
              aria-label="Clear conversation"
              title="Clear conversation"
            >
              <LuTrash2 className="h-4 w-4" />
            </button>
          )}
          <button
            type="button"
            onClick={() => setIsWide(value => !value)}
            className="rounded-lg p-1.5 transition hover:bg-white/15 hover:text-white"
            aria-label={isWide ? 'Shrink panel' : 'Expand panel'}
          >
            {isWide ? (
              <LuMinimize2 className="h-4 w-4" />
            ) : (
              <LuMaximize2 className="h-4 w-4" />
            )}
          </button>
          <button
            type="button"
            onClick={close}
            className="rounded-lg p-1.5 transition hover:bg-white/15 hover:text-white"
            aria-label="Close copilot"
          >
            <LuX className="h-4 w-4" />
          </button>
        </div>
      </header>

      <div
        ref={threadRef}
        className="copilot-scroll relative flex-1 space-y-5 overflow-y-auto px-4 py-3"
      >
        {view === 'home' || messages.length === 0 ? (
          <CopilotHome
            setupSteps={setupSteps}
            questOverview={questOverview}
            onAsk={(text: string) => void sendMessage(text)}
          />
        ) : (
          messages.map((message: ICopilotMessage, index: number) =>
            message.role === 'user' ? (
              <div
                key={index}
                className="flex justify-end duration-300 animate-in fade-in slide-in-from-bottom-2"
              >
                <div className="max-w-[85%] rounded-2xl bg-gradient-to-br from-[#e0379f] to-[#a1006a] px-3.5 py-2 text-[13.5px] leading-relaxed text-white shadow-sm">
                  {message.content}
                </div>
              </div>
            ) : (
              <div
                key={index}
                className="flex gap-2.5 duration-300 animate-in fade-in slide-in-from-bottom-2"
              >
                <span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
                  {COPILOT_LOGO ? (
                    <img
                      src={COPILOT_LOGO}
                      alt="Recomaze Co-pilot"
                      className="h-5 w-5 object-contain"
                    />
                  ) : (
                    <span className="text-[11px] font-bold text-[#b7007c]">
                      R
                    </span>
                  )}
                </span>
                <div className="min-w-0 flex-1">
                  <CopilotSteps steps={message.steps ?? []} />
                  {message.content ? (
                    <CopilotMarkdown
                      content={message.content}
                      onLink={handleCopilotLink}
                    />
                  ) : (
                    (message.steps?.length ?? 0) === 0 && (
                      <span className="text-gray-400">Thinking...</span>
                    )
                  )}
                  {(message.steps ?? []).some(step =>
                    GENERATION_TOOL_IDS.has(step.id)
                  ) &&
                    (message.generationKind === 'content' ? (
                      message.generationStatus === 'ready' ? (
                        <div className="mt-3 overflow-hidden rounded-2xl border border-[#b7007c]/20 bg-[#b7007c]/5 p-3.5">
                          <div className="flex items-center gap-3">
                            <LuCheckCircle className="h-5 w-5 shrink-0 text-[#b7007c]" />
                            <div className="min-w-0 flex-1">
                              <p className="text-sm font-semibold text-gray-900">
                                Sent to your email
                              </p>
                              <p className="text-xs text-gray-500">
                                Your product content is on its way - check your
                                inbox.
                              </p>
                            </div>
                          </div>
                        </div>
                      ) : (
                        <div className="mt-3 overflow-hidden rounded-2xl border border-[#b7007c]/20 bg-[#b7007c]/5 p-3.5">
                          <div className="flex items-center gap-3">
                            <LuLoader2 className="h-5 w-5 shrink-0 animate-spin text-[#b7007c]" />
                            <div className="min-w-0 flex-1">
                              <p className="text-sm font-semibold text-gray-900">
                                Generating product content
                              </p>
                              <p className="text-xs text-gray-500">
                                We&apos;ll email you the result when it&apos;s
                                ready.
                              </p>
                            </div>
                          </div>
                          <div className="mt-3 h-1 overflow-hidden rounded-full bg-[#b7007c]/10">
                            <div className="h-full w-1/2 animate-pulse rounded-full bg-gradient-to-r from-[#e0379f] to-[#a1006a]" />
                          </div>
                        </div>
                      )
                    ) : message.generationStatus === 'ready' ? (
                      <div className="mt-3 overflow-hidden rounded-2xl border border-[#b7007c]/20 bg-[#b7007c]/5 p-3.5">
                        <div className="flex items-center gap-3">
                          <LuCheckCircle className="h-5 w-5 shrink-0 text-[#b7007c]" />
                          <div className="min-w-0 flex-1">
                            <p className="text-sm font-semibold text-gray-900">
                              Ready
                            </p>
                            <p className="text-xs text-gray-500">
                              Your content is ready to review.
                            </p>
                          </div>
                        </div>
                        <div className="mt-3 flex flex-wrap items-center gap-2">
                          <button
                            type="button"
                            onClick={() =>
                              setOpenArticleId(
                                message.generationArticleId ?? null
                              )
                            }
                            disabled={!message.generationArticleId}
                            className="flex items-center gap-1 rounded-full border border-[#b7007c]/30 bg-white px-3 py-1.5 text-xs font-semibold text-[#b7007c] transition hover:bg-[#b7007c]/5 disabled:opacity-50"
                          >
                            Open
                            <LuArrowRight className="h-3 w-3" />
                          </button>
                          {publishedArticleIds.has(
                            message.generationArticleId ?? ''
                          ) ? (
                            <span className="flex items-center gap-1 rounded-full bg-[#b7007c]/10 px-3 py-1.5 text-xs font-semibold text-[#b7007c]">
                              <LuCheckCircle className="h-3.5 w-3.5" />
                              Published to blog
                            </span>
                          ) : (
                            <button
                              type="button"
                              onClick={() =>
                                handlePublishToBlog(
                                  message.generationArticleId ?? ''
                                )
                              }
                              disabled={
                                !message.generationArticleId ||
                                publishingArticleId ===
                                  message.generationArticleId
                              }
                              className="flex items-center gap-1 rounded-full bg-[#b7007c] px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-[#a1006a] disabled:opacity-50"
                            >
                              {publishingArticleId ===
                              message.generationArticleId ? (
                                <>
                                  <LuLoader2 className="h-3.5 w-3.5 animate-spin" />
                                  Publishing...
                                </>
                              ) : failedArticleIds.has(
                                  message.generationArticleId ?? ''
                                ) ? (
                                'Retry publish'
                              ) : (
                                'Publish to blog'
                              )}
                            </button>
                          )}
                        </div>
                      </div>
                    ) : (
                      <div className="mt-3 overflow-hidden rounded-2xl border border-[#b7007c]/20 bg-[#b7007c]/5 p-3.5">
                        <div className="flex items-center gap-3">
                          <LuLoader2 className="h-5 w-5 shrink-0 animate-spin text-[#b7007c]" />
                          <div className="min-w-0 flex-1">
                            <p className="text-sm font-semibold text-gray-900">
                              Generating
                            </p>
                            <p className="text-xs text-gray-500">
                              In progress - it will appear in the list shortly.
                            </p>
                          </div>
                          <button
                            type="button"
                            onClick={() => navigate('/ai-visibility')}
                            className="flex shrink-0 items-center gap-1 rounded-full border border-[#b7007c]/30 bg-white px-3 py-1.5 text-xs font-semibold text-[#b7007c] transition hover:bg-[#b7007c]/5"
                          >
                            View in list
                            <LuArrowRight className="h-3 w-3" />
                          </button>
                        </div>
                        <div className="mt-3 h-1 overflow-hidden rounded-full bg-[#b7007c]/10">
                          <div className="h-full w-1/2 animate-pulse rounded-full bg-gradient-to-r from-[#e0379f] to-[#a1006a]" />
                        </div>
                      </div>
                    ))}
                  {(message.actions?.length ?? 0) > 0 && (
                    <div className="mt-3 flex flex-wrap gap-2 duration-300 animate-in fade-in slide-in-from-bottom-1">
                      {message.actions?.map(
                        (action: ICopilotAction, actionIndex: number) => (
                          <button
                            key={actionIndex}
                            type="button"
                            onClick={() => handleAction(action, index)}
                            className={cn(
                              'rounded-full px-3.5 py-1.5 text-xs font-semibold transition',
                              action.kind === 'primary'
                                ? 'bg-gradient-to-r from-[#e0379f] to-[#a1006a] text-white shadow-sm hover:opacity-90'
                                : 'border border-gray-200 bg-white text-gray-600 hover:border-[#b7007c]/40 hover:bg-[#b7007c]/5 hover:text-[#b7007c]'
                            )}
                          >
                            {action.label}
                          </button>
                        )
                      )}
                    </div>
                  )}
                </div>
              </div>
            )
          )
        )}
      </div>

      <form onSubmit={handleSubmit} className="relative px-3 pt-2">
        {pendingLogo && (
          <div className="mb-2 flex items-center gap-2 rounded-lg border border-[#b7007c]/20 bg-[#b7007c]/5 px-2.5 py-1.5">
            <img
              src={pendingLogo.dataUri}
              alt={pendingLogo.name}
              className="h-7 w-7 rounded object-contain"
            />
            <span className="flex-1 truncate text-xs text-gray-600">
              {pendingLogo.name}
            </span>
            <button
              type="button"
              onClick={() => setPendingLogo(null)}
              className="rounded p-1 text-gray-400 transition hover:bg-[#b7007c]/10 hover:text-gray-600"
              aria-label="Remove attached logo"
            >
              <LuX className="h-3.5 w-3.5" />
            </button>
          </div>
        )}
        <input
          ref={fileInputRef}
          type="file"
          accept="image/*"
          className="hidden"
          onChange={event => {
            const file: File | undefined = event.target.files?.[0];
            if (file) void handleAttachLogo(file);
            event.target.value = '';
          }}
        />
        <div className="flex items-center gap-2 rounded-full border border-gray-200 bg-white px-2 py-1.5 focus-within:border-[#b7007c]/60">
          <button
            type="button"
            onClick={() => fileInputRef.current?.click()}
            disabled={isStreaming}
            className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-gray-400 transition hover:bg-[#b7007c]/5 hover:text-[#b7007c] disabled:opacity-40"
            aria-label="Attach a logo image"
            title="Attach a logo (for brand colors)"
          >
            <LuImagePlus className="h-4 w-4" />
          </button>
          <input
            value={input}
            onChange={event => setInput(event.target.value)}
            placeholder="Ask anything..."
            className="flex-1 bg-transparent text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none"
          />
          {isStreaming ? (
            <button
              type="button"
              onClick={handleStop}
              className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-600 transition hover:bg-gray-200"
              aria-label="Stop"
            >
              <LuSquare className="h-3.5 w-3.5 fill-current" />
            </button>
          ) : (
            <button
              type="submit"
              disabled={input.trim().length === 0}
              className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#e0379f] to-[#a1006a] text-white shadow-sm transition hover:opacity-90 disabled:opacity-40"
              aria-label="Send message"
            >
              <LuArrowUp className="h-4 w-4" />
            </button>
          )}
        </div>
      </form>

      <p className="relative px-4 pb-3 pt-2 text-center text-[11px] text-gray-400">
        Recomaze Co-pilot can make mistakes. Check important info.
      </p>

      {/* Reader for a generated article opened from a ready generation card. */}
      <ArticleModal
        open={openArticleId !== null}
        articleId={openArticleId}
        onClose={() => setOpenArticleId(null)}
        clientId={clientId}
        token={token}
      />
    </aside>,
    document.body
  );
}
