// ============================================================================
// StyleKro — CHAT module (split from app.jsx on 2026-06-26).
// ChatPage + helpers (renderMarkdown, formatTime, TryonProgress, ImageFeedback,
// ZoomableImage). Depends on symbols from common.jsx (loaded first).
// ============================================================================
// ===== Chat page =====
const SUGGESTIONS = [
  { key: 'date', labelKey: 'chat.suggestion1', promptKey: 'chat.suggestion1Prompt' },
  { key: 'roast', labelKey: 'chat.suggestion2', promptKey: 'chat.suggestion2Prompt' },
  { key: 'autumn', labelKey: 'chat.suggestion3', promptKey: 'chat.suggestion3Prompt' },
  { key: 'colors', labelKey: 'chat.suggestion4', promptKey: 'chat.suggestion4Prompt' },
  { key: 'suits', labelKey: 'chat.suggestion5', promptKey: 'chat.suggestion5Prompt' },
];

// Lightweight markdown for assistant text: **bold**, *italic*, `code`, links, line breaks.
function renderMarkdown(text) {
  if (!text) return null;
  const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
  let html = escape(text);
  // links [label](url) and bare urls
  html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
  html = html.replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g, '$1<a href="$2" target="_blank" rel="noopener">$2</a>');
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
  html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
  html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
  html = html.replace(/\n/g, '<br/>');
  return <span dangerouslySetInnerHTML={{ __html: html }} />;
}

function formatTime(iso) {
  if (!iso) return '';
  const d = new Date(iso.includes('T') || iso.includes('Z') ? iso : iso.replace(' ', 'T') + 'Z');
  if (isNaN(d.getTime())) return '';
  const now = new Date();
  const sameDay = d.toDateString() === now.toDateString();
  if (sameDay) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
  return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' · ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}

// ===== Try-on progress (estimated) =====
// The image generation API has no streaming progress, so we render an
// asymptotic 0 → 95% curve over `etaMs`. The component unmounts the
// moment the result arrives, which feels like "snap to 100%".
function TryonProgress({ etaMs = 30000, label }) {
  const { t } = useAuth();
  const finalLabel = label || t('chat.generatingTryOn');
  const [pct, setPct] = useState(0);
  useEffect(() => {
    const start = Date.now();
    const compute = () => {
      const elapsed = Date.now() - start;
      setPct(95 * (1 - Math.exp(-(elapsed / etaMs) * 2.6)));
    };
    compute();
    // setInterval keeps ticking on mobile during scroll/touch (RAF is paused).
    const id = setInterval(compute, 200);
    return () => clearInterval(id);
  }, [etaMs]);
  return (
    <div className="tryon-progress-pill" role="status" aria-live="polite">
      <div className="tryon-progress-track">
        <div className="tryon-progress-fill" style={{ width: pct + '%' }} />
      </div>
      <div className="tryon-progress-meta">
        <span className="tryon-progress-label">{finalLabel}</span>
        <span className="tryon-progress-pct">{Math.round(pct)}%</span>
      </div>
    </div>
  );
}

// ===== Image feedback (thumbs up / down on generated try-ons) =====
// Single module-level cache + subscriber set so every button sees the
// same ratings without each one hitting the API on mount.
const __imageFeedbackCache = { messages: {}, merges: {} };
let __imageFeedbackLoaded = false;
let __imageFeedbackLoading = null;
const __imageFeedbackSubs = new Set();
function __notifyImageFeedback() { for (const fn of __imageFeedbackSubs) fn(); }

function ImageFeedback({ messageId, mergeId, className = '', compact = false }) {
  const { apiCall, token } = useAuth();
  const key = messageId || mergeId;
  const readRating = useCallback(() => {
    if (!key) return null;
    if (messageId) return __imageFeedbackCache.messages[messageId] || null;
    return __imageFeedbackCache.merges[mergeId] || null;
  }, [key, messageId, mergeId]);
  const [rating, setRating] = useState(readRating);
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    const sub = () => setRating(readRating());
    __imageFeedbackSubs.add(sub);
    return () => { __imageFeedbackSubs.delete(sub); };
  }, [readRating]);

  useEffect(() => {
    if (!token || __imageFeedbackLoaded || __imageFeedbackLoading) return;
    __imageFeedbackLoading = (async () => {
      try {
        const data = await apiCall('/feedback/image/mine');
        __imageFeedbackCache.messages = data?.messages || {};
        __imageFeedbackCache.merges = data?.merges || {};
        __imageFeedbackLoaded = true;
        __notifyImageFeedback();
      } catch (_e) { /* non-fatal */ }
      finally { __imageFeedbackLoading = null; }
    })();
  }, [token, apiCall]);

  if (!key) return null;

  async function send(next) {
    if (busy) return;
    const desired = rating === next ? 'clear' : next;
    const prev = rating;
    const optimistic = desired === 'clear' ? null : desired;
    setRating(optimistic);
    if (messageId) __imageFeedbackCache.messages[messageId] = optimistic;
    if (mergeId) __imageFeedbackCache.merges[mergeId] = optimistic;
    __notifyImageFeedback();
    setBusy(true);
    try {
      await apiCall('/feedback/image', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message_id: messageId || null,
          merge_id: mergeId || null,
          rating: desired,
        }),
      });
    } catch (_e) {
      setRating(prev);
      if (messageId) __imageFeedbackCache.messages[messageId] = prev;
      if (mergeId) __imageFeedbackCache.merges[mergeId] = prev;
      __notifyImageFeedback();
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className={`img-fb${compact ? ' compact' : ''}${className ? ' ' + className : ''}`} onClick={(e) => e.stopPropagation()}>
      <button
        type="button"
        className={`img-fb-btn up${rating === 'up' ? ' on' : ''}`}
        aria-pressed={rating === 'up'}
        aria-label="Good result"
        title="Good result"
        disabled={busy}
        onClick={(e) => { e.stopPropagation(); send('up'); }}
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 22V11"/><path d="M21 14c0-1.1-.9-2-2-2h-5.5l1-4.5c.2-.9-.5-1.5-1.5-1.5-.5 0-1 .2-1.3.6L7 11v11h11.3c.9 0 1.7-.6 1.9-1.5l1.5-5.5c0-.2.1-.4.1-.5z"/></svg>
      </button>
      <button
        type="button"
        className={`img-fb-btn down${rating === 'down' ? ' on' : ''}`}
        aria-pressed={rating === 'down'}
        aria-label="Bad result"
        title="Bad result"
        disabled={busy}
        onClick={(e) => { e.stopPropagation(); send('down'); }}
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 2v11"/><path d="M3 10c0 1.1.9 2 2 2h5.5l-1 4.5c-.2.9.5 1.5 1.5 1.5.5 0 1-.2 1.3-.6L17 13V2H5.7c-.9 0-1.7.6-1.9 1.5L2.3 9c0 .2-.1.4-.1.5z"/></svg>
      </button>
    </div>
  );
}

function ZoomableImage({ src, alt = '' }) {
  const ref = React.useRef(null);
  const state = React.useRef({
    scale: 1, tx: 0, ty: 0,
    startDist: 0, startScale: 1,
    startTx: 0, startTy: 0,
    panning: false, panX: 0, panY: 0,
  });

  function apply() {
    const el = ref.current;
    if (!el) return;
    const s = state.current;
    el.style.transform = `translate(${s.tx}px, ${s.ty}px) scale(${s.scale})`;
  }

  function onTouchStart(e) {
    const s = state.current;
    if (e.touches.length === 2) {
      const [a, b] = e.touches;
      s.startDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
      s.startScale = s.scale;
      s.startTx = s.tx;
      s.startTy = s.ty;
      e.preventDefault();
    } else if (e.touches.length === 1 && s.scale > 1) {
      s.panning = true;
      s.panX = e.touches[0].clientX;
      s.panY = e.touches[0].clientY;
      s.startTx = s.tx;
      s.startTy = s.ty;
    }
  }

  function onTouchMove(e) {
    const s = state.current;
    if (e.touches.length === 2) {
      const [a, b] = e.touches;
      const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
      const next = Math.min(5, Math.max(1, s.startScale * (dist / (s.startDist || 1))));
      s.scale = next;
      apply();
      e.preventDefault();
    } else if (e.touches.length === 1 && s.panning) {
      s.tx = s.startTx + (e.touches[0].clientX - s.panX);
      s.ty = s.startTy + (e.touches[0].clientY - s.panY);
      apply();
      e.preventDefault();
    }
  }

  function onTouchEnd(e) {
    const s = state.current;
    if (e.touches.length < 2) s.panning = false;
    if (s.scale <= 1.01) { s.scale = 1; s.tx = 0; s.ty = 0; apply(); }
  }

  function onDoubleClick(e) {
    e.stopPropagation();
    const s = state.current;
    if (s.scale > 1) { s.scale = 1; s.tx = 0; s.ty = 0; } else { s.scale = 2; }
    apply();
  }

  return (
    <img
      ref={ref}
      src={src}
      alt={alt}
      className="lightbox-img-zoom"
      onClick={(e) => e.stopPropagation()}
      onDoubleClick={onDoubleClick}
      onTouchStart={onTouchStart}
      onTouchMove={onTouchMove}
      onTouchEnd={onTouchEnd}
      onTouchCancel={onTouchEnd}
    />
  );
}

function ChatPage() {
  const { apiCall, profile, signOut, user, lang, token, t: tr, photoVersion } = useAuth();
  const ensureProfilePhoto = useRequireProfilePhoto();
  const confirmAction = useConfirmDialog();
  const navigate = useNavigate();
  const [messages, setMessages] = useState([]);
  const [text, setText] = useState('');
  const [attached, setAttached] = useState(null);
  const [attachedPreview, setAttachedPreview] = useState('');
  const [typing, setTyping] = useState(false);
  const [sending, setSending] = useState(false);
  const [linkBusy, setLinkBusy] = useState(false);
  const [historyLoading, setHistoryLoading] = useState(true);
  const [hasMoreHistory, setHasMoreHistory] = useState(false);
  const [loadingMore, setLoadingMore] = useState(false);
  const [lightbox, setLightbox] = useState(null); // url string
  const [sheet360MsgId, setSheet360MsgId] = useState(null);
  const fetchChatSheet360 = useCallback(
    () => fetchSheet360(`/chat/messages/${sheet360MsgId}/sheet360`),
    [sheet360MsgId],
  );
  const [showScrollBtn, setShowScrollBtn] = useState(false);
  const [dragOver, setDragOver] = useState(false);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [quickOpen, setQuickOpen] = useState(false);
  const [quickPrompts, setQuickPrompts] = useState([]);
  const listRef = useRef(null);
  const fileRef = useRef(null);
  const cameraRef = useRef(null);
  const taRef = useRef(null);
  const initialScrollDone = useRef(false);

  // Build attachment preview when file changes.
  useEffect(() => {
    if (!attached) { setAttachedPreview(''); return; }
    const url = URL.createObjectURL(attached);
    setAttachedPreview(url);
    return () => URL.revokeObjectURL(url);
  }, [attached]);

  // Load quick prompts from API.
  useEffect(() => {
    let cancelled = false;
    apiCall('/prompts/quick')
      .then((data) => {
        if (cancelled || !data) return;
        const list = Array.isArray(data) ? data : (data.prompts || []);
        setQuickPrompts(list);
      })
      .catch(() => { /* ignore */ });
    return () => { cancelled = true; };
  }, [apiCall]);

  // Load history.
  useEffect(() => {
    (async () => {
      try {
        const PAGE = 5;
        const history = await apiCall(`/chat/history?limit=${PAGE}`);
        if (!history.length) {
          setMessages([{ role: 'assistant', content: `${tr('chat.welcome.hi')} ${profile?.name || ''} — ${tr('chat.welcome.body')}`, welcome: true, created_at: new Date().toISOString() }]);
        } else {
          setMessages(history.map(m => ({ id: m.id, role: m.role, content: m.content, image_url: m.image_url ? imgUrl(m.image_url) : null, created_at: m.created_at, pending: !!m.pending })));
          setHasMoreHistory(history.length >= PAGE);
        }
      } catch (e) {
        setMessages([{ role: 'assistant', content: `Couldn't load history: ${e.message}` }]);
      } finally {
        setHistoryLoading(false);
      }
    })();
  }, []); // eslint-disable-line

  // While any message is pending (e.g. tryon still running after a refresh),
  // poll history every few seconds so the image shows up once ready.
  const hasPendingMessage = messages.some(m => m.pending);
  useEffect(() => {
    if (!hasPendingMessage) return;
    const PAGE = 5;
    let cancelled = false;
    const tick = async () => {
      try {
        const history = await apiCall(`/chat/history?limit=${PAGE}`);
        if (cancelled) return;
        const byId = new Map(history.map(m => [m.id, m]));
        setMessages(prev => prev.map(m => {
          if (!m.pending) return m;
          const fresh = byId.get(m.id);
          if (!fresh) return m;
          return {
            ...m,
            content: fresh.content,
            image_url: fresh.image_url ? imgUrl(fresh.image_url) : null,
            pending: !!fresh.pending,
          };
        }));
      } catch { /* ignore */ }
    };
    const id = setInterval(tick, 4000);
    return () => { cancelled = true; clearInterval(id); };
  }, [hasPendingMessage]);

  // Auto-scroll on message change (only if user is near bottom).
  useEffect(() => {
    const el = listRef.current;
    if (!el) return;
    const jumpToBottom = () => { el.scrollTop = el.scrollHeight; };
    // First render after messages load: always jump to the latest message.
    if (!initialScrollDone.current && messages.length > 0) {
      initialScrollDone.current = true;
      requestAnimationFrame(jumpToBottom);
      // Re-scroll as images load in (any image inside the list).
      [60, 200, 500, 1000, 2000].forEach((ms) => setTimeout(jumpToBottom, ms));
      el.querySelectorAll('img').forEach((img) => {
        if (!img.complete) img.addEventListener('load', jumpToBottom, { once: true });
      });
      return;
    }
    const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 200;
    if (nearBottom) jumpToBottom();
  }, [messages, typing]);

  // Show "scroll to bottom" pill when scrolled up. Also lazy-load older
  // messages from the server when the user scrolls near the top.
  useEffect(() => {
    const el = listRef.current;
    if (!el) return;
    async function loadOlder() {
      const oldest = messages.find((m) => m.id);
      if (!oldest) return;
      setLoadingMore(true);
      const prevHeight = el.scrollHeight;
      try {
        const PAGE = 5;
        const older = await apiCall(`/chat/history?limit=${PAGE}&before=${encodeURIComponent(oldest.id)}`);
        if (older.length) {
          setMessages((cur) => [
            ...older.map(m => ({ id: m.id, role: m.role, content: m.content, image_url: m.image_url ? imgUrl(m.image_url) : null, created_at: m.created_at })),
            ...cur,
          ]);
        }
        setHasMoreHistory(older.length >= PAGE);
        requestAnimationFrame(() => {
          const delta = el.scrollHeight - prevHeight;
          if (delta > 0) el.scrollTop = el.scrollTop + delta;
        });
      } catch { /* ignore */ }
      finally { setLoadingMore(false); }
    }
    function onScroll() {
      const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
      setShowScrollBtn(distance > 240);
      if (el.scrollTop < 80 && hasMoreHistory && !loadingMore) {
        loadOlder();
      }
    }
    el.addEventListener('scroll', onScroll);
    return () => el.removeEventListener('scroll', onScroll);
  }, [messages, hasMoreHistory, loadingMore]); // eslint-disable-line

  // Paste image from clipboard.
  useEffect(() => {
    function onPaste(e) {
      const items = e.clipboardData?.items || [];
      for (const it of items) {
        if (it.kind === 'file' && it.type.startsWith('image/')) {
          const f = it.getAsFile();
          if (f) { setAttached(f); break; }
        }
      }
    }
    window.addEventListener('paste', onPaste);
    return () => window.removeEventListener('paste', onPaste);
  }, []);

  // Receive an image shared from another Android app (gallery, WhatsApp,
  // browser → "Share with Vogue"). The native shell stages the image and
  // exposes it through window.AndroidShare.consumePendingShare() as a
  // base64 data URL with mime type prefix.
  useEffect(() => {
    function dataUrlToFile(dataUrl, name) {
      try {
        const [head, b64] = dataUrl.split(',');
        const mime = (head.match(/data:(.*?);base64/) || [, 'image/jpeg'])[1];
        const bin = atob(b64);
        const buf = new Uint8Array(bin.length);
        for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
        const ext = mime.split('/')[1] || 'jpg';
        return new File([buf], name || `shared.${ext}`, { type: mime });
      } catch { return null; }
    }
    function consumeOnce() {
      try {
        const bridge = window.AndroidShare;
        if (!bridge || typeof bridge.consumePendingShare !== 'function') return false;
        const payload = bridge.consumePendingShare();
        if (!payload) return false;
        const file = dataUrlToFile(payload, 'shared-image.jpg');
        if (file) { setAttached(file); return true; }
      } catch { /* ignore */ }
      return false;
    }
    // Allow the page to be invoked from native code too.
    window.__styleyouOnSharedImage = (dataUrl) => {
      const f = dataUrlToFile(dataUrl, 'shared-image.jpg');
      if (f) setAttached(f);
    };
    // First, try right away.
    let consumed = consumeOnce();
    // Then keep polling — a cold-start launch from "Share to Vogue" can
    // take 5-15s before ChatPage actually mounts (Babel + React boot +
    // any auth redirect chain). We poll fast for the first ~10s and then
    // back off, so the staged image still lands in the composer when the
    // page finally appears.
    let ticks = 0;
    const poll = setInterval(() => {
      ticks += 1;
      if (consumed) { clearInterval(poll); return; }
      if (consumeOnce()) { consumed = true; clearInterval(poll); return; }
      // After the initial burst, slow down to once every 2s and cap the
      // total polling window at ~2 minutes to avoid running forever.
      if (ticks === 40) {
        clearInterval(poll);
        const slow = setInterval(() => {
          if (consumed || consumeOnce()) { consumed = true; clearInterval(slow); }
        }, 2000);
        // Stop the slow poll after ~2 minutes total.
        setTimeout(() => clearInterval(slow), 110000);
      }
    }, 250);
    function onFocus() { if (!consumed) consumed = consumeOnce(); }
    window.addEventListener('focus', onFocus);
    document.addEventListener('visibilitychange', onFocus);
    return () => {
      clearInterval(poll);
      window.removeEventListener('focus', onFocus);
      document.removeEventListener('visibilitychange', onFocus);
      try { delete window.__styleyouOnSharedImage; } catch { window.__styleyouOnSharedImage = undefined; }
    };
  }, []);

  // Auto-resize textarea.
  useEffect(() => {
    if (taRef.current) {
      taRef.current.style.height = 'auto';
      taRef.current.style.height = Math.min(140, taRef.current.scrollHeight) + 'px';
    }
  }, [text]);

  function onAttach(e) {
    const f = e.target.files[0];
    setAttached(f || null);
  }

  function clearAttachment() {
    setAttached(null);
    if (fileRef.current) fileRef.current.value = '';
    if (cameraRef.current) cameraRef.current.value = '';
  }

  async function newChat() {
    if (!(await confirmAction(tr('chat.confirmClearHistory'), { confirmLabel: tr('common.clear'), tone: 'danger' }))) return;
    try {
      await apiCall('/chat/history', { method: 'DELETE' });
      setMessages([{ role: 'assistant', content: `${tr('chat.welcome.hi')} ${profile?.name || ''} — ${tr('chat.welcome.fresh')}`, welcome: true, created_at: new Date().toISOString() }]);
    } catch (e) {
      alert(tr('chat.clearHistoryError') + ' ' + e.message);
    }
  }

  // Map mockup tabs -> existing taxonomy fields (category_group / subcategory).
  function garmentMatchesCategory(g, cat) {
    const grp = (g.category_group || '').toLowerCase();
    const sub = (g.subcategory || '').toLowerCase();
    switch (cat) {
      case 'Dresses':
        return grp.includes('dress') || sub.includes('dress') || sub.includes('gown')
          || sub.includes('saree') || sub.includes('lehenga') || sub.includes('salwar');
      case 'Tops':
        return grp === 'topwear' || sub.includes('top') || sub.includes('shirt')
          || sub.includes('blouse') || sub.includes('kurti') || sub.includes('kurta');
      case 'Bottoms':
        return grp === 'bottomwear' || sub.includes('jean') || sub.includes('trouser')
          || sub.includes('legging') || sub.includes('skirt') || sub.includes('short')
          || sub.includes('palazzo') || sub.includes('chino');
      case 'Shoes':
        return grp === 'footwear' || sub.includes('shoe') || sub.includes('sneaker')
          || sub.includes('boot') || sub.includes('heel') || sub.includes('flat')
          || sub.includes('sandal') || sub.includes('slipper');
      case 'Bags':
        return sub.includes('bag') || sub.includes('handbag') || sub.includes('wallet')
          || sub.includes('backpack') || sub.includes('clutch');
      case 'Accessories':
        return grp === 'accessories' || sub.includes('watch') || sub.includes('jewel')
          || sub.includes('belt') || sub.includes('sunglass') || sub.includes('scarf')
          || sub.includes('cap') || sub.includes('hair');
      default:
        return true;
    }
  }

  function onDrop(e) {
    e.preventDefault();
    setDragOver(false);
    const f = e.dataTransfer?.files?.[0];
    if (f && f.type.startsWith('image/')) setAttached(f);
  }

  function copyMessage(content) {
    navigator.clipboard?.writeText(content || '');
  }

  function onKeyDown(e) {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      submit();
    }
  }

  async function submit(overrideText, overrideImage, displayText) {
    const t = (typeof overrideText === 'string' ? overrideText : text).trim();
    const image = overrideImage !== undefined ? overrideImage : attached;
    if (!t && !image) return;

    // If the message is just a URL (and no image attached), treat it as a try-on link.
    if (!image && /^https?:\/\/\S+$/i.test(t)) {
      const started = await tryOnLink(t);
      if (started) setText('');
      return;
    }

    if (image && !(await ensureProfilePhoto())) return;

    setSending(true);

    const optimisticUrl = image ? URL.createObjectURL(image) : null;
    const now = new Date().toISOString();
    const shownContent = (typeof displayText === 'string' && displayText.trim())
      ? displayText.trim()
      : (t || '(image)');
    setMessages((m) => [...m, { role: 'user', content: shownContent, image_url: optimisticUrl, created_at: now }]);
    setText('');
    setAttached(null);
    if (fileRef.current) fileRef.current.value = '';
    if (cameraRef.current) cameraRef.current.value = '';
    setTyping(true);

    try {
      const fd = new FormData();
      fd.append('text', t);
      if (image) fd.append('image', image);
      if (lang) fd.append('lang', lang);
      if (typeof displayText === 'string' && displayText.trim()) {
        fd.append('display_text', displayText.trim());
      }
      await streamChat(fd, token, (evt, data) => {
        if (evt === 'assistant') {
          setTyping(false);
          setMessages((m) => [...m, {
            id: data.id, role: 'assistant', content: data.content,
            image_url: data.image_url ? imgUrl(data.image_url) : null,
            created_at: data.created_at,
          }]);
        } else if (evt === 'tryon_pending') {
          setTyping(false);
          setMessages((m) => [...m, {
            id: data.id, role: 'assistant', content: data.content,
            image_url: null, pending: true,
            created_at: data.created_at,
          }]);
        } else if (evt === 'tryon_fast' || evt === 'tryon_final') {
          setMessages((m) => m.map((msg) => msg.id === data.id ? {
            ...msg, content: data.content,
            image_url: data.image_url ? imgUrl(data.image_url) : msg.image_url,
            pending: evt === 'tryon_fast',
          } : msg));
        } else if (evt === 'tryon_error') {
          setMessages((m) => m.map((msg) => msg.id === data.id ? {
            ...msg, content: `Try-on failed: ${data.detail || 'unknown error'}`,
            image_url: null, pending: false,
          } : msg));
        } else if (evt === 'error') {
          setMessages((m) => [...m, { role: 'assistant', content: `Sorry, something went wrong: ${data.detail}`, created_at: new Date().toISOString() }]);
        }
      });
    } catch (e) {
      setMessages((m) => [...m, { role: 'assistant', content: `Sorry, something went wrong: ${e.message}`, created_at: new Date().toISOString() }]);
    } finally {
      setTyping(false);
      setSending(false);
    }
  }

  function logout() {
    signOut();
    navigate('/login', { replace: true });
  }

  async function tryOnLink(urlArg) {
    const u = (urlArg || '').trim();
    if (!u) return false;
    if (!/^https?:\/\//i.test(u)) {
      setMessages((m) => [...m, { role: 'assistant', content: "That doesn't look like a valid URL. Make sure it starts with http:// or https://", created_at: new Date().toISOString() }]);
      return false;
    }
    if (!(await ensureProfilePhoto())) return false;
    setLinkBusy(true);
    setSending(true);
    setMessages((m) => [...m, { role: 'user', content: `Try this on for me: ${u}`, created_at: new Date().toISOString() }]);
    setTyping(true);
    try {
      const res = await apiCall('/chat/tryon-url', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url: u }),
      });
      setMessages((m) => {
        const next = [...m];
        if (res.user_message?.image_url) {
          for (let i = next.length - 1; i >= 0; i--) {
            if (next[i].role === 'user' && next[i].content?.startsWith('Try this on for me:')) {
              next[i] = { role: 'user', content: res.user_message.content, image_url: imgUrl(res.user_message.image_url), created_at: res.user_message.created_at };
              break;
            }
          }
        }
        next.push({ role: 'assistant', content: res.assistant_message.content, created_at: res.assistant_message.created_at });
        if (res.tryon_message) {
          next.push({ role: 'assistant', content: res.tryon_message.content, image_url: imgUrl(res.tryon_message.image_url), created_at: res.tryon_message.created_at });
        }
        return next;
      });
    } catch (e) {
      setMessages((m) => [...m, { role: 'assistant', content: `Couldn't try that on: ${e.message}`, created_at: new Date().toISOString() }]);
    } finally {
      setTyping(false);
      setLinkBusy(false);
      setSending(false);
    }
    return true;
  }

  return (
    <section className="screen">
      <NavBar active="chat" />
      <main className="chat-shell">
        {sidebarOpen && <div className="chat-side-scrim" onClick={() => setSidebarOpen(false)} />}
        <aside className={`chat-side${sidebarOpen ? ' open' : ''}`} onClick={(e) => { if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') setSidebarOpen(false); }}>
          <div className="agent-card">
            <div className="agent-avatar">V</div>
            <div>
              <div className="agent-name">{tr('chat.agentName')}</div>
              <div className="agent-title">{tr('chat.agentTitle')}</div>
            </div>
          </div>
          <div className="suggestions">
            {SUGGESTIONS.map((s) => (
              <button key={s.key} className="chip" onClick={() => { setText(tr(s.promptKey)); taRef.current?.focus(); }}>{tr(s.labelKey)}</button>
            ))}
          </div>
          {profile && (
            <div className="profile-mini">
              <img
                src={bust(imgUrl(profile.photo_url), photoVersion)}
                alt=""
                loading="lazy"
                decoding="async"
                style={{ cursor: 'zoom-in' }}
                onClick={() => profile.photo_url && setLightbox(bust(imgUrl(profile.photo_url), photoVersion))}
              />
              <div><strong>{profile.name}</strong><small>{tr('chat.profilePhotoHint')}</small></div>
            </div>
          )}
          <button className="btn btn-ghost small" onClick={logout}>{tr('common.signOut')}</button>
        </aside>
        <section className="chat-main">
          <button
            type="button"
            className="chat-clear-btn"
            onClick={newChat}
            title={tr('chat.confirmClearHistory')}
            aria-label={tr('chat.confirmClearHistory')}
          >
            <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M3 6h18" />
              <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
              <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
              <path d="M10 11v6" />
              <path d="M14 11v6" />
            </svg>
          </button>
          {/* Hero stage — large model image with chat bubbles overlaid */}
          {(() => {
            const lastImg = [...messages].reverse().find((m) => m.image_url)?.image_url;
            const heroSrc = lastImg || (profile?.photo_url ? bust(imgUrl(profile.photo_url), photoVersion) : null);
            return (
              <div className="hero-stage">
                {!heroSrc && (
                  <div className="hero-stage-placeholder" aria-hidden="true">
                    <svg viewBox="0 0 80 120" width="120" height="180" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                      <circle cx="40" cy="18" r="10"/>
                      <path d="M20 50 L40 32 L60 50 L54 78 L50 116 L30 116 L26 78 Z"/>
                      <path d="M20 50 L10 70"/><path d="M60 50 L70 70"/>
                    </svg>
                  </div>
                )}
                <div
                  className={`chat-list overlay${dragOver ? ' drag-over' : ''}`}
                  ref={listRef}
                  onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
                  onDragLeave={() => setDragOver(false)}
                  onDrop={onDrop}
                >
            {historyLoading ? (
              <div className="chat-skeleton">
                <div className="sk-bubble assistant" />
                <div className="sk-bubble user" />
                <div className="sk-bubble assistant" />
              </div>
            ) : (<>
              {(loadingMore || hasMoreHistory) && (
                <div className="chat-load-more" aria-hidden="true">
                  <span className="chat-load-more-hint">{loadingMore ? tr('chat.loadingOlder') : tr('chat.scrollOlder')}</span>
                </div>
              )}
              {messages.map((m, i) => {
              const onlyImage = (!m.content || m.content === '(image)') && m.image_url;
              const isUser = m.role === 'user';
              const cls = `bubble-row ${m.role}`;
              return (
                <div key={m.id || i} className={cls}>
                  {!isUser && (
                    <div className="msg-avatar assistant" title={tr('chat.agentName')}>V</div>
                  )}
                  <div className="msg-col">
                    <div className={`bubble ${m.role}${m.welcome ? ' welcome' : ''}${onlyImage ? ' image-only' : ''}`}>
                      {m.content && !onlyImage && (
                        <div className="bubble-text">
                          {isUser ? m.content : renderMarkdown(m.content)}
                        </div>
                      )}
                      {m.image_url && (
                        <img
                          src={m.image_url}
                          alt=""
                          className="bubble-image"
                          loading="lazy"
                          decoding="async"
                          onClick={() => setLightbox(m.image_url)}
                        />
                      )}
                      {m.pending && (
                        <div className="upgrade-hint">
                          <span className="upgrade-hint-spinner" aria-hidden="true" />
                          <span className="upgrade-hint-title">{tr('chat.refiningLook')}</span>
                        </div>
                      )}
                    </div>
                    <div className="msg-meta">
                      <span className="msg-time">{formatTime(m.created_at)}</span>
                      {m.content && !onlyImage && (
                        <button className="msg-action" title="Copy" onClick={() => copyMessage(m.content)}>⧉</button>
                      )}
                      {m.image_url && (
                        <a className="msg-action" title="Open image" href={m.image_url} target="_blank" rel="noopener">↗</a>
                      )}
                      {m.role === 'assistant' && m.image_url && !m.pending && (
                        <button className="msg-action" title="360° view" onClick={() => setSheet360MsgId(m.id)}>360°</button>
                      )}
                    </div>
                  </div>
                  {isUser && (
                    <div
                      className="msg-avatar user"
                      title={profile?.name || user?.email}
                      style={profile?.photo_url ? { cursor: 'zoom-in' } : undefined}
                      onClick={() => { if (profile?.photo_url) setLightbox(bust(imgUrl(profile.photo_url), photoVersion)); }}
                    >
                      {profile?.photo_url ? <img src={bust(imgUrl(profile.photo_url), photoVersion)} alt="" /> : (profile?.name?.[0] || user?.email?.[0] || 'U').toUpperCase()}
                    </div>
                  )}
                </div>
              );
            })}
            </>)}
            {typing && (
              <div className="bubble-row assistant">
                <div className="msg-avatar assistant">V</div>
                <div className="msg-col">
                  <div className="bubble assistant typing">
                    {(linkBusy || sending) ? (
                      <TryonProgress label={tr('chat.loading')} />
                    ) : (
                      <><span></span><span></span><span></span></>
                    )}
                  </div>
                </div>
              </div>
            )}
            {dragOver && (
              <div className="drop-overlay">{tr('chat.dropHint')}</div>
            )}
          </div>
              </div>
            );
          })()}
          {attached && (
            <div className="composer-attach-row">
              {attachedPreview && <img className="car-thumb" src={attachedPreview} alt="" />}
              <div className="car-info">
                <span className="car-name">{attached.name || 'image.png'}</span>
                <span className="car-size">{(attached.size / 1024).toFixed(0)} KB</span>
              </div>
              <button type="button" className="car-remove" onClick={clearAttachment} aria-label={tr('chat.remove')}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </div>
          )}
          {quickPrompts.length > 0 && (
            <div className={`quick-prompts${quickOpen ? ' open' : ''}`}>
              {quickOpen && (
                <div className="quick-prompts-panel" role="dialog" aria-label="Quick prompts">
                  <div className="quick-prompts-head">
                    <span className="quick-prompts-title">✨ Quick ideas</span>
                    <button
                      type="button"
                      className="quick-prompts-close"
                      onClick={() => setQuickOpen(false)}
                      aria-label="Close"
                    >
                      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                    </button>
                  </div>
                  <div className="quick-prompts-grid">
                    {quickPrompts.map((p, i) => (
                      <button
                        type="button"
                        key={p.id || i}
                        className="quick-prompt-card"
                        style={{ animationDelay: `${i * 40}ms` }}
                        disabled={sending}
                        onClick={async () => {
                          setQuickOpen(false);
                          let img;
                          if (p.attach_profile_photo) {
                            // Prefer the primary profile photo. Fall back to the
                            // most recent assistant try-on image already in the
                            // chat so the prompt always has a face to work from.
                            const candidates = [];
                            if (profile?.photo_url) candidates.push(bust(imgUrl(profile.photo_url), photoVersion));
                            for (let i = messages.length - 1; i >= 0 && candidates.length < 3; i--) {
                              const m = messages[i];
                              if (m.role === 'assistant' && m.image_url && !m.pending) {
                                candidates.push(m.image_url);
                              }
                            }
                            for (const url of candidates) {
                              try {
                                const bust = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now();
                                const res = await fetch(bust, { cache: 'no-store' });
                                if (!res.ok) continue;
                                const blob = await res.blob();
                                img = new File([blob], 'portrait.jpg', { type: blob.type || 'image/jpeg' });
                                break;
                              } catch { /* try next candidate */ }
                            }
                          }
                          submit(p.prompt, img, p.name);
                        }}
                      >
                        <span className="quick-prompt-icon" aria-hidden="true">{p.icon || '✨'}</span>
                        <span className="quick-prompt-text">
                          <span className="quick-prompt-name">{p.name}</span>
                          {p.description && <span className="quick-prompt-desc">{p.description}</span>}
                        </span>
                      </button>
                    ))}
                  </div>
                </div>
              )}
              <button
                type="button"
                className={`quick-prompts-fab${quickOpen ? ' active' : ''}`}
                onClick={() => setQuickOpen((v) => !v)}
                aria-expanded={quickOpen}
                aria-label={quickOpen ? 'Close quick ideas' : 'Open quick ideas'}
                title="Quick ideas"
              >
                {quickOpen ? (
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                ) : (
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 3l1.6 4.4L18 9l-4.4 1.6L12 15l-1.6-4.4L6 9l4.4-1.6z"/><path d="M19 14l.8 2.2L22 17l-2.2.8L19 20l-.8-2.2L16 17l2.2-.8z"/></svg>
                )}
              </button>
            </div>
          )}
          <form className="composer composer-pill" onSubmit={(e) => { e.preventDefault(); submit(); }}>
            <input id="chat-attach-input" ref={fileRef} type="file" accept="image/*" hidden onChange={onAttach} />
            <label htmlFor="chat-attach-input" className="composer-attach" title={tr('chat.uploadOutfit')}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
            </label>
            <textarea ref={taRef} rows="1" placeholder={tr('chat.composerPlaceholder')} value={text} onChange={(e) => setText(e.target.value)} onKeyDown={onKeyDown} />
            <button type="submit" aria-label={tr('chat.sendAria')} disabled={sending || (!text.trim() && !attached)} className="composer-send">
              {sending ? (
                <span className="composer-send-dot" />
              ) : (
                <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
              )}
            </button>
          </form>
          <p className="composer-hint">{tr('chat.composerHint')}</p>
        </section>
      </main>
      {lightbox && (
        <div className="lightbox" onClick={() => setLightbox(null)}>
          <button className="lightbox-close" title="Close">×</button>
          <ZoomableImage src={lightbox} />
          <LightboxActions
            url={lightbox}
            filename="stylekro-tryon.png"
            onRotate360={(() => {
              const m = /\/chat\/messages\/([^/?]+)\/image/.exec(lightbox);
              return m ? (e) => { e.stopPropagation(); setLightbox(null); setSheet360MsgId(m[1]); } : null;
            })()}
          />
        </div>
      )}
      <Sheet360Modal
        open={!!sheet360MsgId}
        fetchSheet={fetchChatSheet360}
        onClose={() => setSheet360MsgId(null)}
      />
    </section>
  );
}
