/**
 * App.tsx generator for ReactAppStarter
 *
 * Two-surface shell:
 *   1. Business views declared in the .specly spec (user's app).
 *   2. Dev GUI — 4-tab maintenance surface over the same backend.
 *
 * The 4 tabs:
 *   - Views: declared views + auto-inferred CRUD + operation views.
 *     BUSINESS group = views with an entry in spec.views.
 *     DEV group = inferred CRUD + operation views.
 *     Declaring a view in the spec graduates it from Dev → Business.
 *   - Models: raw CRUD for any model (R2 — placeholder for now).
 *   - Services: directory of services + their operations (R3).
 *   - Events: WebSocket event stream (R4).
 *
 * Not using react-router — the starter output is deliberately low-
 * magic. Users can add URL-level routing when they need it.
 */

import type { ExpandedSpec } from './views-generator.js';
import { collectOperationViews, type OperationViewDescriptor } from './operation-view-generator.js';

export interface AppGeneratorContext {
  spec: ExpandedSpec;
  manifest?: unknown;
}

type ViewType = 'list' | 'detail' | 'form' | 'dashboard';
const VIEW_TYPES: ViewType[] = ['list', 'detail', 'form', 'dashboard'];

function viewSuffix(v: ViewType): string {
  return v.charAt(0).toUpperCase() + v.slice(1) + 'View';
}

/** Per-view metadata the sidebar + routing need. */
interface ModelViewEntry {
  model: string;
  viewType: ViewType;
  viewName: string;       // "CategoryListView"
  componentName: string;  // same as viewName for our codegen
  declared: boolean;      // true iff spec.views[viewName] exists
}

export async function generate(context: AppGeneratorContext): Promise<string> {
  const models = Object.keys(context.spec.models ?? {});
  const declaredViewNames = new Set(Object.keys(context.spec.views ?? {}));
  const operationViews = collectOperationViews(context.spec);

  const modelViews: ModelViewEntry[] = [];
  for (const m of models) {
    for (const v of VIEW_TYPES) {
      const viewName = `${m}${viewSuffix(v)}`;
      modelViews.push({
        model: m,
        viewType: v,
        viewName,
        componentName: viewName,
        declared: declaredViewNames.has(viewName),
      });
    }
  }

  const business = modelViews.filter(e => e.declared);
  // Dev = inferred CRUD + operation views
  const devModelViews = modelViews.filter(e => !e.declared);

  const imports = buildImports(modelViews, operationViews);
  const navBusiness = buildNavGroup('Business', business.map(e => ({
    label: e.viewName,
    onSelect: `setSelection({ kind: 'model', model: '${e.model}', view: '${e.viewType}' })`,
    activeExpr: `selection.kind === 'model' && selection.model === '${e.model}' && selection.view === '${e.viewType}'`,
  })), business.length > 0 ? undefined : 'No declared views in this spec. Declare one in spec.views to curate its layout.');
  const navDev = buildNavGroup('Dev', [
    ...devModelViews.map(e => ({
      label: e.viewName,
      onSelect: `setSelection({ kind: 'model', model: '${e.model}', view: '${e.viewType}' })`,
      activeExpr: `selection.kind === 'model' && selection.model === '${e.model}' && selection.view === '${e.viewType}'`,
    })),
    ...operationViews.map(op => ({
      label: op.viewName,
      onSelect: `setSelection({ kind: 'op', viewName: '${op.viewName}' })`,
      activeExpr: `selection.kind === 'op' && selection.viewName === '${op.viewName}'`,
    })),
  ]);

  const viewSwitch = buildViewSwitch(modelViews, operationViews);
  const initialSelection = buildInitialSelection(business, devModelViews, operationViews);

  return `/**
 * App.tsx — generated by @specverse/realize (ReactAppStarter)
 *
 * Safe to edit. Edits are preserved across regeneration via content
 * hashing. Four-tab shell (Views / Models / Services / Events):
 * the Views tab carries the user-facing app (declared + inferred
 * views), the other three are a maintenance / debug GUI over the
 * same backend.
 */
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useResizableSidebar } from './hooks/useResizableSidebar';
import { AppEventsProvider, useRecentEvents } from './hooks/useAppEvents';
${imports}

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 5 * 60 * 1000, retry: 1 },
  },
});

type Tab = 'views' | 'models' | 'services' | 'events';

type Selection =
  | { kind: 'model'; model: string; view: 'list' | 'detail' | 'form' | 'dashboard'; entityId?: string | number }
  | { kind: 'op'; viewName: string };

function Inner() {
  const [tab, setTab] = useState<Tab>('views');
  const [selection, setSelection] = useState<Selection>(${initialSelection});
  const [modelsTabPick, setModelsTabPick] = useState<string>(${models.length > 0 ? `'${models[0]}'` : `''`});
  const { width: sidebarWidth, isResizing, startResizing } = useResizableSidebar({ defaultWidth: 256 });

  return (
    <div className="min-h-screen flex flex-col bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100">
      <header className="flex items-center gap-4 px-6 py-3 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
        <h1 className="text-lg font-semibold">App</h1>
        <nav className="flex gap-1">
          <TabButton active={tab === 'views'} onClick={() => setTab('views')}>Views</TabButton>
          <TabButton active={tab === 'models'} onClick={() => setTab('models')}>Models</TabButton>
          <TabButton active={tab === 'services'} onClick={() => setTab('services')}>Services</TabButton>
          <TabButton active={tab === 'events'} onClick={() => setTab('events')}>Events</TabButton>
        </nav>
      </header>

      <div className="flex-1 flex overflow-hidden">
        {tab === 'views' && (
          <>
            <aside
              style={{ width: sidebarWidth }}
              className="shrink-0 overflow-auto border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
            >
${navBusiness}
${navDev}
            </aside>
            <div
              role="separator"
              aria-orientation="vertical"
              onMouseDown={startResizing}
              className={\`w-1 shrink-0 cursor-col-resize hover:bg-blue-500/40 \${isResizing ? 'bg-blue-500/60' : 'bg-transparent'}\`}
            />
            <main className="flex-1 overflow-auto">
${viewSwitch}
            </main>
          </>
        )}

        {tab === 'models' && (
${buildModelsTab(models)}
        )}
        {tab === 'services' && (
${buildServicesTab(operationViews)}
        )}
        {tab === 'events' && <EventsTab />}
      </div>
    </div>
  );
}

function TabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className={\`px-3 py-1.5 text-sm font-medium rounded-md transition-colors \${active ? 'bg-blue-500/10 text-blue-600 ring-1 ring-blue-500/40 dark:text-blue-400' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200'}\`}
    >
      {children}
    </button>
  );
}

/**
 * EventsTab — live view of the recent-events buffer populated by
 * AppEventsProvider. Newest at top, clear button resets the buffer
 * (doesn't affect server-side event state).
 */
function EventsTab() {
  const { recentEvents, clear } = useRecentEvents();
  const reversed = [...recentEvents].reverse();
  return (
    <main className="flex-1 flex flex-col overflow-hidden">
      <div className="flex items-center justify-between px-6 py-3 border-b border-gray-200 dark:border-gray-800">
        <div>
          <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">Events</h2>
          <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Live stream of CURED events over the WebSocket. Oldest buffered first.</p>
        </div>
        <button
          type="button"
          onClick={clear}
          disabled={recentEvents.length === 0}
          className="rounded border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-60 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
        >
          Clear buffer ({recentEvents.length})
        </button>
      </div>
      <div className="flex-1 overflow-auto p-6">
        {reversed.length === 0 ? (
          <p className="text-sm text-gray-400 italic">No events yet. Create/update/delete an entity in another tab and it will appear here.</p>
        ) : (
          <ul className="space-y-2">
            {reversed.map((e, idx) => (
              <li key={reversed.length - idx} className="rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900">
                <div className="flex items-center justify-between px-4 py-2 border-b border-gray-100 dark:border-gray-800">
                  <span className="font-mono text-sm text-blue-600 dark:text-blue-400">{e.event}</span>
                  <span className="text-xs text-gray-500 dark:text-gray-400">{new Date(e.timestamp).toLocaleTimeString()}</span>
                </div>
                <pre className="px-4 py-2 text-xs text-gray-700 dark:text-gray-300 overflow-x-auto">{JSON.stringify(e.payload, null, 2)}</pre>
              </li>
            ))}
          </ul>
        )}
      </div>
    </main>
  );
}

/** Placeholder used by Models / Services tabs when the spec declares
 *  no models or no services. Kept even when the current spec has both
 *  populated — users who later remove the declarations re-trigger the
 *  fallback branch without the component going missing. */
function TabPlaceholder({ label, note }: { label: string; note: string }) {
  return (
    <div className="flex-1 flex items-center justify-center p-12">
      <div className="text-center max-w-md">
        <h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200 mb-2">{label}</h2>
        <p className="text-sm text-gray-500 dark:text-gray-400">{note}</p>
      </div>
    </div>
  );
}
void TabPlaceholder;  // TS unused-warning suppression — see comment above.

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AppEventsProvider>
        <Inner />
      </AppEventsProvider>
    </QueryClientProvider>
  );
}
`;
}

// ──────────────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────────────

function buildImports(modelViews: ModelViewEntry[], operationViews: OperationViewDescriptor[]): string {
  const lines: string[] = [];
  for (const e of modelViews) {
    lines.push(`import { ${e.componentName} } from './views/${e.viewName}';`);
  }
  for (const op of operationViews) {
    lines.push(`import { ${op.viewName} } from './views/${op.viewName}';`);
  }
  return lines.join('\n');
}

function buildInitialSelection(
  business: ModelViewEntry[],
  devModelViews: ModelViewEntry[],
  operationViews: OperationViewDescriptor[],
): string {
  // Prefer the first business view; fall back to the first dev model
  // view; then operation view; else empty.
  if (business.length > 0) {
    return `{ kind: 'model', model: '${business[0].model}', view: '${business[0].viewType}' }`;
  }
  if (devModelViews.length > 0) {
    return `{ kind: 'model', model: '${devModelViews[0].model}', view: '${devModelViews[0].viewType}' }`;
  }
  if (operationViews.length > 0) {
    return `{ kind: 'op', viewName: '${operationViews[0].viewName}' }`;
  }
  return `{ kind: 'model', model: '', view: 'list' }`;
}

interface NavEntry {
  label: string;
  /** Full expression string: `setSelection({...})`. */
  onSelect: string;
  /** JS boolean expression that is true when this entry is active. */
  activeExpr: string;
}

function buildNavGroup(heading: string, entries: NavEntry[], emptyNote?: string): string {
  const headingLine = `        <div className="px-4 pb-2 pt-3 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
          ${heading}
        </div>`;
  const baseCls = 'block w-full text-left rounded-md px-3 py-1.5 text-sm transition-colors';
  const activeCls = 'bg-blue-500/10 text-blue-600 ring-1 ring-blue-500/40 dark:text-blue-400';
  const inactiveCls = 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200';

  if (entries.length === 0) {
    const note = emptyNote
      ? `          <p className="px-3 py-2 text-xs text-gray-400 italic">${emptyNote}</p>`
      : '';
    return `${headingLine}
        <nav className="px-2 space-y-0.5">
${note}
        </nav>`;
  }
  const buttons = entries
    .map(e => `          <button type="button" onClick={() => ${e.onSelect}} className={\`${baseCls} \${${e.activeExpr} ? '${activeCls}' : '${inactiveCls}'}\`}>${e.label}</button>`)
    .join('\n');
  return `${headingLine}
        <nav className="px-2 space-y-0.5">
${buttons}
        </nav>`;
}

/**
 * Services tab — directory of services + their operations.
 * Each operation is a button that jumps to the corresponding
 * operation view in the Views tab (no executor duplication — the
 * actual parameter form lives with the view).
 */
function buildServicesTab(operationViews: OperationViewDescriptor[]): string {
  const services = operationViews.filter(op => op.source === 'service');
  if (services.length === 0) {
    return `          <TabPlaceholder label="Services" note="No services declared in this spec. Add a 'services:' block to main.specly to populate this directory." />`;
  }
  // Group operations by serviceName.
  const byService = new Map<string, OperationViewDescriptor[]>();
  for (const op of services) {
    if (!byService.has(op.serviceName)) byService.set(op.serviceName, []);
    byService.get(op.serviceName)!.push(op);
  }

  const serviceCards = [...byService.entries()]
    .map(([svcName, ops]) => {
      const opButtons = ops
        .map(op => {
          const paramsLabel = op.parameters
            ? Object.keys(op.parameters as any).length === 0
              ? 'no params'
              : Object.keys(op.parameters as any).join(', ')
            : 'no params';
          const returns = op.returns ? ` → ${escapeJsx(op.returns)}` : '';
          return `                <button type="button" onClick={() => { setTab('views'); setSelection({ kind: 'op', viewName: '${op.viewName}' }); }} className="block w-full text-left rounded-md border border-gray-200 px-4 py-3 text-sm hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800 transition-colors">
                  <div className="font-mono text-sm text-blue-600 dark:text-blue-400">${op.operationName}</div>
                  <div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
                    ${paramsLabel}${returns}
                  </div>
                </button>`;
        })
        .join('\n');
      return `            <div className="mb-6">
              <h3 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">${svcName}</h3>
              <div className="space-y-2">
${opButtons}
              </div>
            </div>`;
    })
    .join('\n');

  return `          <main className="flex-1 overflow-auto p-6">
            <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-6">Services</h2>
${serviceCards}
          </main>`;
}

function escapeJsx(s: string): string {
  return s.replace(/[<>&]/g, ch => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[ch]!));
}

/**
 * Models tab — an admin-style CRUD surface over every model in the spec.
 * Sub-sidebar listing each model; main area renders that model's
 * {Model}FormView, which already contains the create/update form plus
 * the embedded existing-entities list. Reuses existing components; no
 * new rendering path required.
 */
function buildModelsTab(models: string[]): string {
  if (models.length === 0) {
    return `          <TabPlaceholder label="Models" note="No models in this spec — add one to start." />`;
  }
  const modelButtons = models
    .map(m =>
      `              <button type="button" onClick={() => setModelsTabPick('${m}')} className={\`block w-full text-left rounded-md px-3 py-1.5 text-sm transition-colors \${modelsTabPick === '${m}' ? 'bg-blue-500/10 text-blue-600 ring-1 ring-blue-500/40 dark:text-blue-400' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200'}\`}>${m}</button>`
    )
    .join('\n');
  const switchBranches = models
    .map(m => `            {modelsTabPick === '${m}' && <${m}FormView />}`)
    .join('\n');

  return `          <>
            <aside className="w-56 shrink-0 overflow-auto border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
              <div className="px-4 pt-5 pb-3">
                <div className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Models</div>
              </div>
              <nav className="px-2 space-y-0.5">
${modelButtons}
              </nav>
            </aside>
            <main className="flex-1 overflow-auto">
${switchBranches}
            </main>
          </>`;
}

function buildViewSwitch(modelViews: ModelViewEntry[], operationViews: OperationViewDescriptor[]): string {
  const models = [...new Set(modelViews.map(e => e.model))];
  if (models.length === 0 && operationViews.length === 0) {
    return '        <p className="p-6 text-sm text-gray-400">No models or operations — add to your .specly file and run <code>spv realize</code>.</p>';
  }
  const navHandler =
    `(navModel: string, navId: string | number) => setSelection({ kind: 'model', model: navModel, view: 'detail', entityId: navId })`;
  const branches: string[] = [];

  for (const m of models) {
    branches.push(
      `        {selection.kind === 'model' && selection.model === '${m}' && selection.view === 'list' && (
          <${m}ListView onSelect={item => setSelection({ kind: 'model', model: '${m}', view: 'detail', entityId: (item as any).id })} onCreate={() => setSelection({ kind: 'model', model: '${m}', view: 'form' })} onNavigate={${navHandler}} />
        )}`,
      `        {selection.kind === 'model' && selection.model === '${m}' && selection.view === 'detail' && (
          <${m}DetailView entityId={selection.entityId} onEntityChange={id => setSelection({ kind: 'model', model: '${m}', view: 'detail', entityId: id })} onNavigate={${navHandler}} />
        )}`,
      `        {selection.kind === 'model' && selection.model === '${m}' && selection.view === 'form' && (
          <${m}FormView mode={selection.entityId ? 'update' : 'create'} entityId={selection.entityId} onSuccess={() => setSelection({ kind: 'model', model: '${m}', view: 'list' })} onDeleted={() => setSelection({ kind: 'model', model: '${m}', view: 'list' })} onSelectEntity={item => setSelection({ kind: 'model', model: '${m}', view: 'form', entityId: (item as any).id })} />
        )}`,
      `        {selection.kind === 'model' && selection.model === '${m}' && selection.view === 'dashboard' && (
          <${m}DashboardView onSelect={item => setSelection({ kind: 'model', model: '${m}', view: 'detail', entityId: (item as any).id })} />
        )}`,
    );
  }

  for (const op of operationViews) {
    branches.push(
      `        {selection.kind === 'op' && selection.viewName === '${op.viewName}' && (
          <${op.viewName} />
        )}`,
    );
  }

  return branches.join('\n');
}
