/**
 * Runtime App.tsx Generator
 *
 * Generates a slim App.tsx that imports DevShell from @specverse/runtime
 * and wires it up with instance-specific API hooks.
 *
 * This replaces the 185-file static generation with ~10 files.
 */

import type { TemplateContext } from '@specverse/types';

/** Does the spec declare a `type: timeline` view? */
function hasTimelineView(spec: any): boolean {
  const scan = (views: any) => views && typeof views === 'object'
    && Object.values(views).some((v: any) => String(v?.type).toLowerCase() === 'timeline');
  if (scan(spec?.views)) return true;
  for (const c of Object.values(spec?.components ?? {})) {
    if (scan((c as any)?.views)) return true;
  }
  return false;
}

/** Read a capability → factory from the manifest (#view-component-factories). */
function manifestChoice(manifest: any, capability: string): string | undefined {
  const inner = manifest?.manifests ? Object.values(manifest.manifests)[0] : manifest;
  const maps = (inner as any)?.capabilityMappings;
  if (Array.isArray(maps)) {
    const m = maps.find((x: any) => x?.capability === capability);
    if (m) return m.implementation ?? m.instanceFactory;
  }
  return (inner as any)?.[capability] ?? manifest?.[capability];
}

export default function generateRuntimeAppTsx(context: TemplateContext): string {
  const { spec } = context;
  const appName = (spec as any)?.metadata?.name || (spec as any)?.name || 'SpecVerse App';
  // The manifest selects the timeline renderer factory; default to VisTimeline
  // when a timeline view exists and the manifest is silent (back-compat).
  const timelineRenderer = manifestChoice((context as any).manifest, 'view.timeline')
    ?? (hasTimelineView(spec) ? 'VisTimeline' : undefined);
  const useVisTimeline = timelineRenderer === 'VisTimeline';

  return `import { useState, useEffect, useMemo, useCallback } from 'react';
import { QueryClient, QueryClientProvider, useQueryClient } from '@tanstack/react-query';
import yaml from 'js-yaml';
import { RuntimeViewProvider, useEntitySync } from '@specverse/runtime/views/react';
import { UserShell, DevShell${useVisTimeline ? ', VisTimelineView' : ''} } from '@specverse/runtime/views/react';
import {
  useEntitiesQuery,
  useModelSchemaQuery,
  useExecuteOperationMutation,
  useTransitionStateMutation,
} from './hooks/useApi';
import { listEntities, getRuntimeInfo } from './lib/apiClient';
import devSpecRaw from './dev.specly?raw';

// Parse YAML spec
function parseSpec(raw: string): Record<string, unknown> {
  try {
    const parsed = yaml.load(raw);
    if (parsed && typeof parsed === 'object') {
      const obj = parsed as Record<string, unknown>;
      if (obj.components && typeof obj.components === 'object') {
        const componentName = Object.keys(obj.components)[0];
        if (componentName) {
          return { ...(obj.components as any)[componentName], componentName, _raw: obj };
        }
      }
      return obj;
    }
    return {};
  } catch {
    return {};
  }
}

const devSpec = parseSpec(devSpecRaw);

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

function AppContent() {
  // Fetch app spec from backend API (avoids bundling spec file path issues)
  const [appSpec, setAppSpec] = useState<Record<string, unknown>>({});
  useEffect(() => {
    fetch('/api/spec')
      .then(r => r.json())
      .then(data => {
        if (data && typeof data === 'object') {
          // Flatten components wrapper if present
          if (data.components) {
            const name = Object.keys(data.components)[0];
            if (name) {
              setAppSpec({ ...data.components[name], componentName: name });
              return;
            }
          }
          setAppSpec(data);
        }
      })
      .catch(() => {});
  }, []);

  // State-sync contract: WebSocket entity events → React Query cache
  // invalidation. This is what guarantees "delete in one view is visible
  // in every other view within one WS round-trip" — it runs at the App
  // level so it's always on, regardless of which tab is active.
  const queryClient = useQueryClient();
  const invalidateEntities = useCallback((modelName: string) => {
    queryClient.invalidateQueries({ queryKey: ['entities', modelName] });
  }, [queryClient]);
  useEntitySync({ invalidateEntities, apiBaseUrl: '/api' });

  // Name resolver for OperationResultView. Kept in the host so runtime
  // view components never issue raw fetch() calls — all HTTP goes
  // through apiClient's canonical layer.
  const resolveEntityNames = useCallback(async (ids: string[]): Promise<Record<string, string>> => {
    const names: Record<string, string> = {};
    try {
      const info = await getRuntimeInfo();
      const models = info?.models || [];
      await Promise.all(models.map(async (model: string) => {
        try {
          const entities = await listEntities(model + 'Controller');
          for (const entity of entities) {
            const id = (entity as any)?.id;
            const dataId = (entity as any)?.data?.id;
            const display = (entity as any)?.name || (entity as any)?.title || (entity as any)?.label || id;
            if (id && ids.includes(id)) names[id] = String(display);
            if (dataId && ids.includes(dataId)) names[dataId] = String(display);
          }
        } catch { /* skip this model */ }
      }));
    } catch { /* ignore — caller falls back to raw IDs */ }
    return names;
  }, []);

  const runtimeValue = useMemo(() => ({
    useEntitiesQuery,
    useModelSchemaQuery,
    useExecuteOperationMutation,
    useTransitionStateMutation,
    entities: {},
    modelSchemas: {},
    views: [],
    spec: appSpec,
    apiBaseUrl: '/api',
    invalidateEntities,
    resolveEntityNames,${useVisTimeline ? `
    // view.timeline → VisTimeline (#view-component-factories): the runtime
    // resolves the generic 'Timeline' slot to this renderer.
    viewComponents: { Timeline: VisTimelineView },` : ''}
  }), [appSpec, invalidateEntities, resolveEntityNames]);

  // End-user shell by default; the developer harness (Views/Models/Services/
  // Events) stays reachable at ?dev=1 (or via the "Developer view" link).
  const [devMode, setDevMode] = useState<boolean>(
    () => typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('dev')
  );

  return (
    <RuntimeViewProvider value={runtimeValue}>
      {devMode ? (
        <DevShell devSpec={devSpec} appSpec={appSpec} title="${appName}" />
      ) : (
        <UserShell
          devSpec={devSpec}
          appSpec={appSpec}
          title="${appName}"
          onOpenDevShell={() => setDevMode(true)}
        />
      )}
    </RuntimeViewProvider>
  );
}

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