/**
 * Helpers emitter for ReactAppStarter
 *
 * Returns the source of small utility files that generated projects
 * use. Inlined into `src/lib/` at realize time rather than imported
 * from `@specverse/runtime` — Factory B's defining trait is that the
 * generated code has no @specverse/* runtime dependency.
 */

/**
 * Source of `src/lib/entity-display.ts`.
 *
 * Mirrors `getEntityDisplayName` from
 * `@specverse/runtime/views/core/entity-display`. Used by:
 * - The FK-resolution TODO in detail-view output (users who upgrade
 *   the plain-FK display to a resolved display name import this).
 * - Any user-written code that wants to label a record.
 */
export function emitEntityDisplay(): string {
  return `/**
 * Pick the best human-readable label for an entity.
 *
 * Walks a prioritized list of candidate fields. Falls back to a
 * truncated id if none match. Inlined into this project by
 * @specverse/realize (ReactAppStarter); edit freely — nothing in
 * the factory's regeneration will clobber your edits.
 */
export function getEntityDisplayName(entity: unknown): string {
  if (!entity || typeof entity !== 'object') return '';
  const record = entity as Record<string, unknown>;

  const candidates = ['name', 'title', 'displayName', 'label', 'username', 'email'];
  for (const key of candidates) {
    const value = record[key];
    if (typeof value === 'string' && value.trim().length > 0) {
      return value;
    }
  }

  const id = record.id;
  if (typeof id === 'string') {
    return id.length > 8 ? id.slice(0, 8) + '…' : id;
  }
  return id != null ? String(id) : '';
}

/**
 * Map an entity id to the display name of the record with that id in
 * a provided list. Handy for resolving belongsTo FK columns in
 * list / detail / dashboard views.
 *
 * Typed as \`readonly unknown[]\` so callers can pass arrays of
 * specific model types (e.g. \`User[]\`) without needing to cast; the
 * runtime check on \`.id\` narrows safely.
 */
export function resolveEntityDisplayName(
  id: unknown,
  records: readonly unknown[]
): string {
  if (id == null) return '';
  const match = records.find((r): r is Record<string, unknown> =>
    typeof r === 'object' && r !== null && (r as { id?: unknown }).id === id
  );
  return match ? getEntityDisplayName(match) : String(id);
}

/**
 * Format an arbitrary value for display in list cells, detail rows,
 * and dashboard previews. Handles the cases where \`String(v ?? '')\`
 * produces ugly output: nulls become em-dashes, booleans become
 * "Yes"/"No", nested objects become \`{k: v, ...}\` summaries
 * instead of "[object Object]", ISO datetimes are localized.
 *
 * Single source of truth for value display. Resolvers / emitters
 * call this from expression strings.
 */
export function formatDisplayValue(value: unknown): string {
  if (value === null || value === undefined) return '—';
  if (typeof value === 'boolean') return value ? 'Yes' : 'No';
  if (value instanceof Date) return value.toLocaleString();
  if (typeof value === 'string' && /^\\d{4}-\\d{2}-\\d{2}T/.test(value)) {
    return new Date(value).toLocaleString();
  }
  if (Array.isArray(value)) return \`[\${value.length} items]\`;
  if (typeof value === 'object') {
    const keys = Object.keys(value as Record<string, unknown>);
    if (keys.length === 0) return '{}';
    if (keys.length <= 5) {
      const rec = value as Record<string, unknown>;
      return keys.map(k => \`\${k}: \${formatDisplayValue(rec[k])}\`).join(', ');
    }
    return \`{\${keys.length} fields}\`;
  }
  return String(value);
}

/**
 * Format a stored DateTime (ISO string) for an <input type="datetime-local">.
 * The control requires exactly \`YYYY-MM-DDTHH:mm\`; a raw ISO string with
 * seconds + timezone ("…:00.000Z") renders the picker empty.
 */
export function formatDateTimeForInput(value: unknown): string {
  if (!value) return '';
  try { return new Date(value as string).toISOString().slice(0, 16); } catch { return ''; }
}

/**
 * Format a stored Date (ISO string) for an <input type="date">, which
 * requires exactly \`YYYY-MM-DD\`.
 */
export function formatDateForInput(value: unknown): string {
  if (!value) return '';
  try { return new Date(value as string).toISOString().slice(0, 10); } catch { return ''; }
}
`;
}

/**
 * Source of `src/hooks/useAppEvents.ts`.
 *
 * Connects to the backend's `/ws` endpoint, subscribes to every
 * CURED event, and invalidates the matching React Query cache on
 * Created / Updated / Deleted / Evolved events. Drives live-refresh
 * everywhere in the app.
 *
 * Message shapes the backend sends:
 *   - `{ type: 'connected', timestamp }` — open handshake.
 *   - `{ type: 'subscribed-all', events }` — ack of subscribe-all.
 *   - `{ type: 'event', event: '{Model}{Action}', payload, timestamp }`.
 *
 * Message the client sends:
 *   - `{ type: 'subscribe-all' }` once the socket opens.
 */
export function emitUseAppEvents(): string {
  return `/**
 * AppEventsProvider + useAppEvents + useRecentEvents
 *
 * One WebSocket connection to the backend's /ws, shared across the
 * app. Two consumers:
 *   - useAppEvents(): React Query invalidation on mutations (no-op
 *     call — side effect is via the provider).
 *   - useRecentEvents(): the Events tab's scrolling stream display.
 *
 * Inlined into this project by @specverse/realize (ReactAppStarter).
 * Edit freely.
 */
import { useEffect, useState, useContext, createContext, type ReactNode } from 'react';
import { useQueryClient } from '@tanstack/react-query';

export interface AppEvent {
  event: string;
  payload: any;
  timestamp: string;
}

interface AppEventsCtx {
  recentEvents: AppEvent[];
  clear: () => void;
}

const EventsContext = createContext<AppEventsCtx | null>(null);

function pluralize(s: string): string {
  if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + 'ies';
  if (/(s|x|z|ch|sh)$/i.test(s)) return s + 'es';
  return s + 's';
}

function deriveWsUrl(): string {
  const apiBase = import.meta.env.VITE_API_BASE_URL || '';
  if (apiBase) return apiBase.replace(/^http/, 'ws') + '/ws';
  const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  return \`\${proto}//\${window.location.host}/ws\`;
}

/** Parse "CategoryCreated" → "Category". Returns null for event names
 *  that don't end in one of the 4 CURED verbs. */
function modelFromEvent(eventName: string): string | null {
  const m = eventName.match(/^(.+)(Created|Updated|Deleted|Evolved)$/);
  return m ? m[1] : null;
}

/**
 * Provider — wrap the app once. Maintains a single WS connection,
 * invalidates React Query caches on mutation events, and keeps a
 * buffer of recent events (default 100) for the Events tab.
 */
export function AppEventsProvider({ children, bufferSize = 100 }: { children: ReactNode; bufferSize?: number }) {
  const qc = useQueryClient();
  const [recentEvents, setRecentEvents] = useState<AppEvent[]>([]);

  const clear = () => setRecentEvents([]);

  useEffect(() => {
    let cancelled = false;
    let ws: WebSocket | null = null;
    let reconnectDelay = 500;
    let reconnectTimer: ReturnType<typeof setTimeout> | null = null;

    const connect = () => {
      if (cancelled) return;
      try {
        ws = new WebSocket(deriveWsUrl());
      } catch {
        scheduleReconnect();
        return;
      }

      ws.addEventListener('open', () => {
        if (cancelled) { ws?.close(); return; }
        reconnectDelay = 500;
        ws?.send(JSON.stringify({ type: 'subscribe-all' }));
      });

      ws.addEventListener('message', (evt) => {
        if (cancelled) return;
        let data: any;
        try { data = JSON.parse(evt.data as string); } catch { return; }
        if (data?.type !== 'event') return;
        const appEvent: AppEvent = {
          event: data.event,
          payload: data.payload,
          timestamp: data.timestamp ?? new Date().toISOString(),
        };
        // Append + cap at bufferSize.
        setRecentEvents(prev => {
          const next = [...prev, appEvent];
          return next.length > bufferSize ? next.slice(-bufferSize) : next;
        });
        // Invalidate React Query cache for affected model.
        const model = modelFromEvent(data.event);
        if (model) {
          const resource = pluralize(model).toLowerCase();
          qc.invalidateQueries({ queryKey: [resource] });
        }
      });

      ws.addEventListener('close', () => {
        if (!cancelled) scheduleReconnect();
      });

      ws.addEventListener('error', () => {
        try { ws?.close(); } catch { /* ignore */ }
      });
    };

    const scheduleReconnect = () => {
      if (cancelled) return;
      if (reconnectTimer) clearTimeout(reconnectTimer);
      reconnectTimer = setTimeout(() => {
        reconnectDelay = Math.min(reconnectDelay * 2, 10_000);
        connect();
      }, reconnectDelay);
    };

    // Microtask-deferred connect — StrictMode-safe (synchronous
    // mount/unmount/remount won't leave a half-open socket).
    Promise.resolve().then(() => { if (!cancelled) connect(); });

    return () => {
      cancelled = true;
      if (reconnectTimer) clearTimeout(reconnectTimer);
      if (ws && ws.readyState === WebSocket.CONNECTING) {
        ws.addEventListener('open', () => ws?.close(), { once: true });
      } else if (ws) {
        try { ws.close(); } catch { /* ignore */ }
      }
    };
  }, [qc, bufferSize]);

  return (
    <EventsContext.Provider value={{ recentEvents, clear }}>
      {children}
    </EventsContext.Provider>
  );
}

/**
 * Shorthand for components that don't care about the buffer — they
 * get live-refresh behavior "for free" because the provider runs
 * query invalidation. Kept as a no-op hook so sites that called it
 * under the old API still type-check and still document intent.
 */
export function useAppEvents() {
  // No-op — the Provider does the work. Retained to preserve intent
  // at call sites like "the app needs live refresh".
  useContext(EventsContext);
}

/**
 * Read the recent-events buffer — used by the Events tab to render
 * a scrolling stream. Returns [] if no AppEventsProvider is mounted.
 */
export function useRecentEvents(): AppEventsCtx {
  return useContext(EventsContext) ?? { recentEvents: [], clear: () => {} };
}
`;
}

/**
 * Source of `src/hooks/useResizableSidebar.ts`.
 *
 * Ported verbatim from `@specverse/runtime/views/react/hooks/
 * useResizableSidebar` (app-demo re-exports the same hook). Drag-to-
 * resize sidebar width with localStorage persistence.
 */
export function emitUseResizableSidebar(): string {
  return `/**
 * useResizableSidebar — drag-to-resize sidebar width.
 *
 * Inlined into this project by @specverse/realize (ReactAppStarter);
 * edit freely — nothing in the factory's regeneration will clobber
 * your edits.
 *
 * Returns { width, isResizing, startResizing } — width is the current
 * pixel value, isResizing is true during an active drag, startResizing
 * is an onMouseDown handler to attach to the drag handle element.
 */
import { useState, useEffect, useCallback } from 'react';

export interface ResizableSidebarOptions {
  defaultWidth?: number;
  minWidth?: number;
  maxWidth?: number;
}

export function useResizableSidebar(options: ResizableSidebarOptions = {}) {
  const { defaultWidth = 256, minWidth = 200, maxWidth = 500 } = options;
  const [width, setWidth] = useState(defaultWidth);
  const [isResizing, setIsResizing] = useState(false);

  const startResizing = useCallback(() => {
    setIsResizing(true);
  }, []);

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

    const handleMouseMove = (e: MouseEvent) => {
      const newWidth = Math.min(maxWidth, Math.max(minWidth, e.clientX));
      setWidth(newWidth);
    };
    const handleMouseUp = () => {
      setIsResizing(false);
      document.body.style.cursor = '';
      document.body.style.userSelect = '';
    };

    document.body.style.cursor = 'col-resize';
    document.body.style.userSelect = 'none';
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);

    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
      document.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isResizing, minWidth, maxWidth]);

  return { width, isResizing, startResizing };
}
`;
}
