/**
 * View files generator for ReactAppStarter
 *
 * Called by the realize engine. Iterates over the expanded spec's
 * models × view types, dispatches to the right composer, and returns
 * a filename → source map. Realize writes each file.
 *
 * Also emits the local helpers library (src/lib/entity-display.ts)
 * so generated projects have zero @specverse/runtime runtime deps.
 */

import { emitView, type EmitContext, type ModelSpec, type ViewSpec, type RenderBodyFn } from './view-emitter.js';
import { composeListBody } from './list-body-composer.js';
import { composeDetailBody } from './detail-body-composer.js';
import { composeFormBody } from './form-body-composer.js';
import { composeDashboardBody } from './dashboard-body-composer.js';
import { emitEntityDisplay, emitUseResizableSidebar, emitUseAppEvents } from './helpers-emitter.js';
import { emitFieldInput, emitOperationExecutor, emitOperationResultView } from './operation-emitters.js';
import { collectOperationViews, emitOperationView } from './operation-view-generator.js';

/**
 * The four primary view types Factory B currently emits. Specialist
 * types (board, timeline, calendar, etc.) are not yet implemented —
 * if the expanded spec declares one, we skip it and log a TODO.
 * These users can add composers under Phase 2e.
 */
const PRIMARY_VIEW_TYPES = ['list', 'detail', 'form', 'dashboard'] as const;
type PrimaryViewType = typeof PRIMARY_VIEW_TYPES[number];

const COMPOSER_BY_TYPE: Record<PrimaryViewType, RenderBodyFn> = {
  list: composeListBody,
  detail: composeDetailBody,
  form: composeFormBody,
  dashboard: composeDashboardBody,
};

/** Generator contract — the shape realize expects. */
export interface ViewsGeneratorContext {
  /** The expanded spec (post-inference). */
  spec: ExpandedSpec;
  /** The resolved manifest configuration. Not currently read by this
   *  generator but passed by realize for parity with other generators. */
  manifest?: unknown;
}

export interface ExpandedSpec {
  /** Models keyed by name. */
  models?: Record<string, ModelSpec>;
  /** Views keyed by view name (e.g. "PostListView"). */
  views?: Record<string, ViewSpec & { type: string; model?: string }>;
  /** Controllers keyed by name (e.g. "PostController"). Non-standard
   *  commands get auto-generated operation views. */
  controllers?: Record<string, any>;
  /** Services keyed by name (e.g. "AuthService"). Every operation
   *  gets an auto-generated operation view. */
  services?: Record<string, any> | any[];
}

export interface GeneratedFiles {
  [relativePath: string]: string;
}

/**
 * Generate all view files + local helpers.
 *
 * Paths are RELATIVE to the frontend root (`frontendDir` in the
 * manifest). Realize prepends the absolute output path when writing.
 */
export async function generate(context: ViewsGeneratorContext): Promise<GeneratedFiles> {
  const files: GeneratedFiles = {};
  const spec = context.spec;
  const models = spec.models ?? {};
  const viewsBySpec = spec.views ?? {};

  // Group the spec's views by model+type so we can decide, for each
  // (model, type) pair, whether to use the user's view spec or emit
  // a default.
  const viewIndex = indexViews(viewsBySpec);
  const skippedSpecialists: string[] = [];

  for (const [modelName, model] of Object.entries(models)) {
    for (const viewType of PRIMARY_VIEW_TYPES) {
      const viewName = `${modelName}${capitalize(viewType)}View`;
      // Prefer a user-defined view of that shape if one exists; fall
      // back to a synthesized default.
      const view = viewIndex.get(viewName) ?? synthesizeDefault(modelName, viewType);
      const source = emitView({
        view,
        viewName,
        model,
        modelSchemas: models,
        renderBody: COMPOSER_BY_TYPE[viewType],
      });
      files[`src/views/${viewName}.tsx`] = source;
    }
  }

  // Surface any specialist-type views the user declared but we can't
  // emit yet. The runtime (ReactAppRuntime) handles these; in starter
  // kit mode the user has to write them manually for now.
  for (const [name, view] of Object.entries(viewsBySpec)) {
    if (!PRIMARY_VIEW_TYPES.includes(view.type as PrimaryViewType)) {
      skippedSpecialists.push(`${name} (type=${view.type})`);
    }
  }
  if (skippedSpecialists.length > 0) {
    console.warn(
      `[ReactAppStarter] Skipped ${skippedSpecialists.length} specialist view(s) — ` +
      `implement composers for: ${[...new Set(Object.values(viewsBySpec)
        .filter(v => !PRIMARY_VIEW_TYPES.includes(v.type as PrimaryViewType))
        .map(v => v.type))].join(', ')}. ` +
      `Skipped: ${skippedSpecialists.join(', ')}`
    );
  }

  // Auto-generated operation views — one per non-standard controller
  // command + service operation. Uses the same lib components as
  // handwritten operation views would; emitted into src/views/ alongside
  // declared views. App.tsx sidebar groups them as Dev (see below).
  const operationViews = collectOperationViews(spec);
  for (const desc of operationViews) {
    files[`src/views/${desc.viewName}.tsx`] = emitOperationView(desc);
  }

  // Local helpers
  files['src/lib/entity-display.ts'] = emitEntityDisplay();
  files['src/hooks/useResizableSidebar.ts'] = emitUseResizableSidebar();
  files['src/hooks/useAppEvents.tsx'] = emitUseAppEvents();
  // Operation-view lib (only needed when at least one operation view
  // is emitted — otherwise FieldInput / OperationExecutor / etc. go
  // unused and trigger TS errors for unused imports from nowhere).
  if (operationViews.length > 0) {
    files['src/lib/FieldInput.tsx'] = emitFieldInput();
    files['src/lib/OperationExecutor.tsx'] = emitOperationExecutor();
    files['src/lib/OperationResultView.tsx'] = emitOperationResultView();
  }

  return files;
}

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

function indexViews(
  views: Record<string, ViewSpec & { type: string; model?: string }>
): Map<string, ViewSpec & { type: string; model?: string }> {
  const out = new Map<string, ViewSpec & { type: string; model?: string }>();
  for (const [name, view] of Object.entries(views)) {
    out.set(name, view);
  }
  return out;
}

function synthesizeDefault(modelName: string, viewType: PrimaryViewType): ViewSpec {
  return {
    type: viewType,
    model: modelName,
  };
}

function capitalize(s: string): string {
  return s.charAt(0).toUpperCase() + s.slice(1);
}
