// Host-side App. After the module extraction this shrunk from 99KB
// (the original editor) to ~150 lines (mount the module + host chrome
// + cart state + toast surface).
//
// What stayed in the host
// ───────────────────────
//   • Site header (logo, preview, download, share, cart icon + badge)
//   • PWA install prompt + offline indicator
//   • Brand colors / fonts / spacing — passed in as `theme`
//   • Product catalog (shirt colors, sizes, prices)
//   • Template catalog
//   • Cart state (count + items)
//   • App-wide toast surface (link-copied, upload-failed, etc.)
//   • Share handler (navigator.share / clipboard / toast fallback)
//
// What moved to the module (goods-editor)
// ────────────────────────────────────────
//   • Canvas + everything that attaches to it (toolbar, crop overlay)
//   • Sidebar (Text/Upload/Stickers/Emoji tabs)
//   • LayersPanel, HistoryControls
//   • All editor state management + keyboard handlers
//   • Persistence of in-progress design (localStorage)
//   • Export pipeline (via goods-editor/server in server.js)
//   • Mobile bottom sheet + FAB

import { useState, useCallback, useRef, useEffect } from 'react';
import { ApparelDesigner } from 'goods-editor';
import 'goods-editor/styles.css';

import { Header } from './components/Header';
import { PWAInstall } from './components/PWAInstall';
import { OfflineIndicator } from './components/OfflineIndicator';
import { Toast } from './components/Toast';

import { SHIRT_COLORS, SHIRT_SIZES, SHIRT_BASE_PRICE_USD } from './constants/shirt';
import { TEMPLATES } from './constants/templates';
import { t } from './i18n/t';

// URL param parsing for deep links — the host owns routing.
function readTemplateFromUrl() {
  if (typeof window === 'undefined') return null;
  try {
    const params = new URLSearchParams(window.location.search);
    const id = params.get('template');
    return id && id.trim() ? id.trim() : null;
  } catch { return null; }
}

// Default auto-dismiss for toasts. Long enough to read a short
// message; short enough that stacked toasts don't pile up. Callers
// that need longer (e.g. the copy-failed fallback that shows a URL
// for manual copy) override per-call.
const TOAST_DEFAULT_DURATION_MS = 3000;

function App() {
  const initialTemplate = readTemplateFromUrl();

  // ── Editor bridge ───────────────────────────────────────────────
  // The Header's Preview and Download buttons live in the host but
  // drive flows the editor module owns (server-rendered preview
  // modal, print-resolution PNG download). The module exposes both
  // via a ref-based imperative API and reports its `exporting` flag
  // via callback so we can disable the buttons during the round-trip.
  //
  // Why ref + callback rather than passing handler props in: the
  // module already owns the state (current elements, current template,
  // export progress). Threading that back through props would mean
  // duplicating the module's internal state in the host. The ref is
  // a one-way trigger; the callback is a one-way notification.
  const editorRef = useRef(null);
  const [isExporting, setIsExporting] = useState(false);

  // ── Toast surface ───────────────────────────────────────────────
  // See src/components/Toast.jsx for the renderer + architecture
  // docblock. Shape: { message, kind, id } | null.
  //
  // Timer lives in a ref because nothing else needs to react to its
  // changes — it's just a handle for clearTimeout. Scheduling a new
  // toast cancels the previous timer first, so back-to-back toasts
  // don't trip over each other (the new one stays for its own full
  // duration rather than getting cut short by the prior timer).
  const [toast, setToast] = useState(null);
  const toastTimerRef = useRef(null);

  const showToast = useCallback((message, kind = 'info', duration = TOAST_DEFAULT_DURATION_MS) => {
    if (toastTimerRef.current) {
      clearTimeout(toastTimerRef.current);
      toastTimerRef.current = null;
    }
    setToast({ message, kind, id: Date.now() });
    if (duration > 0) {
      toastTimerRef.current = setTimeout(() => {
        setToast(null);
        toastTimerRef.current = null;
      }, duration);
    }
  }, []);

  const dismissToast = useCallback(() => {
    if (toastTimerRef.current) {
      clearTimeout(toastTimerRef.current);
      toastTimerRef.current = null;
    }
    setToast(null);
  }, []);

  // Cancel any pending toast timer on unmount to avoid a setState
  // call on an unmounted component. Hot-reload during dev exercises
  // this path frequently; without the cleanup, the console fills
  // with warnings.
  useEffect(() => {
    return () => {
      if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
    };
  }, []);

  // ── Editor action proxies ───────────────────────────────────────
  // Preview and Download both fire the editor's export pipeline.
  //
  // No empty-canvas guard here, even though it would be friendlier
  // than letting the pipeline render a blank PNG. The host doesn't
  // own the elements array (it lives inside the module), so checking
  // emptiness from out here would require the module to emit a
  // count via callback. That's a module API extension worth doing
  // alongside the Clear button reintroduction — both want the same
  // "host can query editor state" pattern. Until then, the worst
  // case is the user gets an empty PNG in Downloads.
  const handleHeaderPreview = useCallback(() => {
    editorRef.current?.preview();
  }, []);

  const handleHeaderTestDownload = useCallback(() => {
    editorRef.current?.testDownload();
  }, []);

  // ── Share handler ───────────────────────────────────────────────
  // Three-level fallback: navigator.share (native OS share sheet,
  // ideal on mobile + recent desktop), then navigator.clipboard
  // (silent copy with a toast confirmation), then a long-duration
  // toast showing the URL for manual copy.
  //
  // The middle branch's success toast leans on a sparkles emoji to
  // make "your action succeeded" feel a bit warmer than a plain
  // "Done." Worth 6 bytes.
  //
  // The user-cancel case in navigator.share is swallowed silently —
  // they deliberately backed out of the share, no toast needed.
  // Other share errors (no support, permission denied, etc.) fall
  // through to the clipboard branch.
  const handleShare = useCallback(async () => {
    const url = window.location.href;
    if (typeof navigator !== 'undefined' && navigator.share) {
      try {
        await navigator.share({
          title: t('share.title'),
          text: t('share.text'),
          url,
        });
        return;
      } catch {
        // User cancelled, or share unsupported on this site config.
        // Fall through to clipboard.
      }
    }
    if (typeof navigator !== 'undefined' && navigator.clipboard) {
      try {
        await navigator.clipboard.writeText(url);
        showToast(t('toast.link-copied'), 'success');
        return;
      } catch {
        // Clipboard write blocked (e.g. insecure context, permissions).
        // Fall through to manual-copy toast.
      }
    }
    // Last resort: show the URL in a longer-lived toast so the user
    // can read it and copy by hand. 8s default in the i18n catalog's
    // docblock is the rationale; we pass it explicitly here so the
    // intent reads at the call site.
    showToast(t('toast.copy-failed', { url }), 'info', 8000);
  }, [showToast]);

  // ── Cart state (host-owned) ──────────────────────────────────────
  // The editor module calls `onAddToCart(snapshot)` when the user
  // clicks "Add to Cart" inside the editor. The host owns what
  // happens next — persisting the item, syncing to a backend,
  // navigating to checkout, etc.
  //
  // For now the cart lives in component state (lost on page reload).
  // When wiring up a real cart, the typical evolution is:
  //   1. Persist `cartItems` to localStorage so it survives reload.
  //   2. POST each addition to a `/api/cart` endpoint on the server,
  //      keyed by a per-user / per-session id.
  //   3. Replace the local state with a cart Context the rest of the
  //      site (header, checkout page) reads from, so the header's
  //      badge and the checkout page agree on contents.
  //
  // The snapshot the module passes is:
  //   { elements, shirtColorId, shirtSize, currentTemplate }
  //
  // It does NOT include the rendered PNG. If the host needs the PNG
  // to display a cart thumbnail or send to a fulfillment backend,
  // call the editor's `/api/export` endpoint with the same elements
  // and template — that's the same endpoint the module's built-in
  // export pipeline uses.
  const [cartItems, setCartItems] = useState([]);

  const handleAddToCart = useCallback((snapshot) => {
    setCartItems((prev) => [
      ...prev,
      {
        id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
        addedAt: Date.now(),
        ...snapshot,
      },
    ]);
    // TODO: when integrating with the real cart backend, POST to
    // /api/cart here. The optimistic local update above keeps the
    // UI responsive while the server round-trip is in flight.
  }, []);

  const handleOpenCart = useCallback(() => {
    // TODO: open the cart drawer / navigate to /cart. For now a
    // placeholder so the header button is wired to something.
    console.log('[host] Open cart', cartItems);
  }, [cartItems]);

  // ── Upload handler (host-owned) ────────────────────────────────
  // The module's UploadTab calls this when the user drops a file
  // into the editor's upload dropzone. We POST to the host's
  // /api/upload endpoint (defined in server.js) and return the
  // resulting URLs so the editor can place the image on the canvas.
  //
  // The host owns this rather than the module because storage,
  // authentication, virus scanning, file-size quotas, and so on
  // are all host concerns. The module's contract is just "give me
  // a URL the canvas can load."
  //
  // Error handling: we surface a host toast AND re-throw so the
  // module can drop its own "upload failed" UI (e.g. clearing the
  // dropzone's loading state). The toast lets the user see what
  // went wrong; the throw lets the module handle the failure path
  // in its own state machine.
  const handleUploadFile = useCallback(async (file) => {
    const formData = new FormData();
    formData.append('image', file);
    try {
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });
      if (!response.ok) {
        const errorText = await response.text().catch(() => '');
        const message = `${response.status}${errorText ? `: ${errorText}` : ''}`;
        showToast(t('toast.upload-failed', { error: message }), 'error', 5000);
        throw new Error(`Upload failed (${message})`);
      }
      const data = await response.json();
      // The host's /api/upload returns { original: { url }, preview: { url } }.
      // The editor uses the preview (smaller, faster to render) for the
      // canvas, and stores the original on the element so the photo
      // editor can round-trip back to full resolution.
      return {
        url: data.preview.url,
        originalUrl: data.original.url,
      };
    } catch (err) {
      // Network errors (offline, DNS, etc.) land here without an
      // explicit status. Surface a generic message and re-throw —
      // the module's caller still needs to see the failure to clean
      // up its dropzone state.
      if (!err.message?.startsWith('Upload failed')) {
        showToast(t('toast.upload-failed', { error: err.message || 'network error' }), 'error', 5000);
      }
      throw err;
    }
  }, [showToast]);

  return (
    <div className="app-shell">
      <OfflineIndicator />
      <PWAInstall />
      <Toast toast={toast} onDismiss={dismissToast} />
      <Header
        cartCount={cartItems.length}
        onShare={handleShare}
        onOpenCart={handleOpenCart}
        onTestDownload={handleHeaderTestDownload}
        isExporting={isExporting}
        onPreview={handleHeaderPreview}
      />

      <main style={{ flex: 1, minHeight: 0 }}>
        <ApparelDesigner
          ref={editorRef}
          onExportingChange={setIsExporting}
          shirtColors={SHIRT_COLORS}
          shirtSizes={SHIRT_SIZES}
          basePrice={SHIRT_BASE_PRICE_USD}
          templates={TEMPLATES}
          initialTemplate={initialTemplate}
          designFilenamePrefix="pawfectly-yours-design"
          shareTitle={t('share.title')}
          /*
           * The host's index.html already declares the Google Fonts
           * <link> for the entire design system. That loads in <head>
           * during HTML parse — earlier than the module's mount-time
           * loader could run. Passing loadFonts={false} suppresses the
           * module's runtime injection so we don't double-request the
           * stylesheet (browsers would dedupe at the cache level, but
           * the duplicate preconnects + DOM nodes are noise).
           *
           * If you ever drop the <link> from index.html (e.g. moving
           * to self-hosted fonts, or deciding the module should own
           * fonts), flip this to true (or just delete the prop — it
           * defaults to true).
           */
          loadFonts={false}
          onUploadFile={handleUploadFile}
          onSave={(elements, name, template) => {
            // Editor-emitted save callback — fires when the user
            // commits their design from inside the module (Cmd+S,
            // or a save action in the editor's own UI). The host
            // doesn't have a backend save yet, so we just log it.
            // When wiring up a real save endpoint, POST here and
            // use the toast surface for success/failure feedback.
            console.log('[host] Save:', { elementCount: elements.length, name, templateId: template?.id });
          }}
          onAddToCart={handleAddToCart}
        />
      </main>
    </div>
  );
}

export default App;
