/**
 * ReactAppStarter view emitter
 *
 * Given a view spec + model spec + expanded spec, emits a complete
 * idiomatic React component source file as a string.
 *
 * Strategy 3 ("skeleton + rendered interior"): a hand-written
 * skeleton template provides the idiomatic React outer structure
 * (imports, hooks, props, layout). The canonical Tailwind adapter
 * renders the pattern's interior as HTML. The html-to-jsx transformer
 * converts the HTML to JSX. The emitter composes these.
 *
 * See README.md in this directory for the architecture.
 */

import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { walkPatternSection } from '@specverse/runtime/views/core';
import { extractBelongsToTargets, extractHasManyTargets, pluralize as pluralizeShared } from './belongs-to.js';
import { emitJsxSource } from './emit-jsx-source.js';

/**
 * The minimal shape of a view spec this emitter needs. Matches the
 * inferred spec format — `view.type` drives skeleton selection,
 * `view.model` identifies the primary model, `view.uiComponents` (if
 * present) overrides inference defaults.
 */
export interface ViewSpec {
  type: string;
  model?: string;
  uiComponents?: Record<string, unknown>;
  [key: string]: unknown;
}

/**
 * The minimal shape of a model spec this emitter needs. Field details
 * flow through to the Tailwind adapter which decides column selection.
 */
export interface ModelSpec {
  name: string;
  attributes: Record<string, unknown>;
  relationships?: Record<string, unknown>;
  lifecycles?: Record<string, unknown>;
}

export interface EmitContext {
  view: ViewSpec;
  viewName: string;          // e.g. "PostListView"
  model: ModelSpec;
  modelSchemas: Record<string, ModelSpec>;
  /** Pluggable so we can stub the renderer in unit tests. */
  renderBody: RenderBodyFn;
}

/**
 * Render the body of a view as JSX-safe source — the exact string
 * that is dropped at `{{BODY}}` in the skeleton.
 *
 * The renderBody implementation is responsible for the whole body
 * pipeline: invoke the canonical Tailwind adapter for static shell
 * rendering, run that HTML through `htmlToJsx`, and inject any JSX
 * expressions (`.map()`, event handlers). Keeping it here means
 * view-emitter stays a pure orchestrator and can't accidentally
 * double-transform JSX that's already JSX.
 */
export type RenderBodyFn = (context: EmitContext) => string;

/**
 * Emit a complete .tsx file for a single view. Pure function — all
 * inputs come in via the context argument so it's easy to test.
 */
export function emitView(context: EmitContext): string {
  const skeleton = loadSkeleton(context.view.type);
  const bodyJsx = context.renderBody(context);

  const substitutions = buildSubstitutions(context, bodyJsx);
  return applySubstitutions(skeleton, substitutions);
}

// ──────────────────────────────────────────────────────────────────────
// Skeleton loading
// ──────────────────────────────────────────────────────────────────────

const SKELETON_BY_VIEW_TYPE: Record<string, string> = {
  list: 'list.tsx.template',
  detail: 'detail.tsx.template',
  form: 'form.tsx.template',
  dashboard: 'dashboard.tsx.template',
  // Specialist types (board, timeline, calendar, analytics, workflow,
  // wizard, comparison, settings, map, feed, profile) come online in
  // Phase 2e.
};

function loadSkeleton(viewType: string): string {
  const filename = SKELETON_BY_VIEW_TYPE[viewType.toLowerCase()];
  if (!filename) {
    throw new Error(
      `No skeleton registered for view type "${viewType}". ` +
      `Known types: ${Object.keys(SKELETON_BY_VIEW_TYPE).join(', ')}.`
    );
  }

  const here = dirname(fileURLToPath(import.meta.url));
  const path = join(here, 'skeletons', filename);
  return readFileSync(path, 'utf8');
}

// ──────────────────────────────────────────────────────────────────────
// Substitution
// ──────────────────────────────────────────────────────────────────────

interface Substitutions {
  MODEL_NAME: string;
  PLURAL_MODEL: string;
  PLURAL_LOWER: string;
  SINGULAR_LOWER: string;
  BODY: string;
  /**
   * Extra import lines that pull in the hooks and display-name
   * helpers this view needs for related models (belongsTo dropdowns,
   * belongsTo FK resolution, hasMany lists in detail views). Empty
   * string when there are no related models to wire.
   */
  RELATED_IMPORTS: string;
  /**
   * Extra hook calls inside the component body — one per related
   * target. belongsTo → \`${relName}Options\`, hasMany → \`${relName}All\`.
   * Empty string when none.
   */
  RELATED_HOOKS: string;
  /**
   * Detail-view tabbed-related-lists state. A \`useState\` line
   * declaring \`relatedTab\` + \`setRelatedTab\` when the detail has
   * more than one hasMany. Empty otherwise.
   */
  RELATED_TAB_STATE: string;
  /**
   * Detail-view tabbed-related-lists import line. \`import { useState }
   * from 'react';\` when tabs are emitted, empty otherwise (strict TS
   * rejects unused imports).
   */
  RELATED_TAB_IMPORT: string;
  /**
   * Form-view lifecycle substitutions. Populated only when the model
   * has >=1 lifecycle. Drive the Edit/Evolve tab block in form.tsx.template.
   */
  LIFECYCLE_IMPORTS: string;
  LIFECYCLE_HOOK: string;
  LIFECYCLE_STATE: string;
  LIFECYCLE_HANDLER: string;
  LIFECYCLE_PANEL: string;
  HAS_LIFECYCLES: string;
  /**
   * Form-view body split into two slots so the Create/Update button
   * row can sit BETWEEN the fields and the existing-entities list
   * (matching app-demo's layout). FIELDS_BODY goes inside <form>,
   * EXISTING_LIST_BODY after the button row, outside <form>. Empty
   * for non-form views.
   */
  FIELDS_BODY: string;
  EXISTING_LIST_BODY: string;
}

function buildSubstitutions(context: EmitContext, body: string): Substitutions {
  const modelName = context.model.name;
  const pluralModel = pluralize(modelName);
  const { imports, hooks } = buildBelongsToWiring(context);
  const lifecycle = buildLifecycleSubstitutions(context);
  const { fieldsBody, existingListBody } = buildFormSplitBodies(context);
  return {
    MODEL_NAME: modelName,
    PLURAL_MODEL: pluralModel,
    PLURAL_LOWER: pluralModel.toLowerCase(),
    SINGULAR_LOWER: modelName.toLowerCase(),
    BODY: body,
    RELATED_IMPORTS: imports,
    RELATED_HOOKS: hooks,
    RELATED_TAB_STATE: buildRelatedTabState(context),
    RELATED_TAB_IMPORT: buildRelatedTabImport(context),
    FIELDS_BODY: fieldsBody,
    EXISTING_LIST_BODY: existingListBody,
    ...lifecycle,
  };
}

/**
 * Walk the form pattern's `fields` and `existing-entities-list`
 * sections separately so the skeleton can place them in different
 * DOM locations (fields inside <form>, existing-list outside +
 * below the button row). Empty for non-form views.
 */
function buildFormSplitBodies(context: EmitContext): { fieldsBody: string; existingListBody: string } {
  if (context.view.type.toLowerCase() !== 'form') {
    return { fieldsBody: '', existingListBody: '' };
  }
  const walkCtx = {
    model: context.model,
    modelSchemas: context.modelSchemas as any,
    view: context.view as any,
  };
  const imports = new Set<string>();
  const fieldsNodes = walkPatternSection('form-view', 'fields', walkCtx);
  const listNodes = walkPatternSection('form-view', 'existing-entities-list', walkCtx);
  return {
    fieldsBody: emitJsxSource(fieldsNodes, { imports, indent: 10 }),
    existingListBody: emitJsxSource(listNodes, { imports, indent: 6 }),
  };
}

/**
 * Compute the form-view lifecycle block. Empty across the board for
 * non-form views or models without declared lifecycles — strict TS
 * rejects unused imports, so all placeholders must be conditional.
 *
 * The generated Evolve tab: a state picker + Save button that invokes
 * \`useEvolve{Model}Mutation\` with { id, toState, lifecycleName }.
 * Uses the primary (first-declared) lifecycle to drive the picker —
 * multi-lifecycle support (a picker-per-lifecycle) is a later extension.
 */
function buildLifecycleSubstitutions(context: EmitContext): {
  LIFECYCLE_IMPORTS: string;
  LIFECYCLE_HOOK: string;
  LIFECYCLE_STATE: string;
  LIFECYCLE_HANDLER: string;
  LIFECYCLE_PANEL: string;
  HAS_LIFECYCLES: string;
} {
  const isForm = context.view.type.toLowerCase() === 'form';
  const lifecycles = extractLifecycles(context.model);
  if (!isForm || lifecycles.length === 0) {
    return {
      LIFECYCLE_IMPORTS: '',
      LIFECYCLE_HOOK: '',
      LIFECYCLE_STATE: '',
      LIFECYCLE_HANDLER: '',
      LIFECYCLE_PANEL: '',
      HAS_LIFECYCLES: 'false',
    };
  }

  const modelName = context.model.name;
  const primary = lifecycles[0];
  const lifecyclesConst = `${modelName.toUpperCase()}_LIFECYCLES`;

  return {
    LIFECYCLE_IMPORTS: `  useEvolve${modelName}Mutation,\n  ${lifecyclesConst},`,
    LIFECYCLE_HOOK: `  const evolveItem = useEvolve${modelName}Mutation();`,
    LIFECYCLE_STATE: `  const [targetState, setTargetState] = useState<string>('');`,
    LIFECYCLE_HANDLER: `  const handleEvolveSubmit = async (event: React.FormEvent) => {
    event.preventDefault();
    if (!targetState) return;
    try {
      const result = await evolveItem.mutateAsync({
        id: entityId as any,
        toState: targetState,
        lifecycleName: '${primary.name}',
      });
      onSuccess?.(result as ${modelName});
    } catch {
      // Evolve errors are surfaced via evolveItem.isError.
    }
  };`,
    LIFECYCLE_PANEL: buildLifecyclePanel(modelName, primary, lifecyclesConst),
    HAS_LIFECYCLES: 'true',
  };
}

function buildLifecyclePanel(
  modelName: string,
  primary: { name: string; states: string[] },
  lifecyclesConst: string,
): string {
  return `      {showTabs && activeTab === 'evolve' && (
        <form onSubmit={handleEvolveSubmit} className="space-y-6">
          <div className="rounded-lg border border-gray-200 bg-white p-6 dark:border-gray-700 dark:bg-gray-900">
            <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
              <div>
                <dt className="text-sm font-medium text-gray-500 dark:text-gray-400">Current ${humanize(primary.name)}</dt>
                <dd className="mt-1">
                  <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
                    {String((existing as any)?.${primary.name} ?? '')}
                  </span>
                </dd>
              </div>
              <div>
                <label htmlFor="evolve-target" className="block text-sm font-medium text-gray-500 dark:text-gray-400">Next ${humanize(primary.name)}</label>
                <select
                  id="evolve-target"
                  value={targetState}
                  onChange={e => setTargetState(e.target.value)}
                  className="mt-1 w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
                >
                  <option value="">— choose —</option>
                  {${lifecyclesConst}.${primary.name}.states.map(s => (
                    <option key={s} value={s}>{s}</option>
                  ))}
                </select>
              </div>
            </dl>
          </div>

          <div>
            <button
              type="submit"
              disabled={evolveItem.isPending || !targetState}
              className="rounded bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700 disabled:opacity-60"
            >
              {evolveItem.isPending ? 'Evolving…' : \`Evolve to \${targetState || '…'}\`}
            </button>
          </div>

          {evolveItem.isError && (
            <div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
              Evolve failed: {String(evolveItem.error)}
            </div>
          )}
        </form>
      )}`;
}

/**
 * Local copy of extractLifecycles to avoid importing from the hook
 * generator. Matches the runtime helper in lifecycle-helpers.ts.
 */
function extractLifecycles(model: any): Array<{ name: string; states: string[] }> {
  const out: Array<{ name: string; states: string[] }> = [];
  const lifecycles = (model?.lifecycles ?? {}) as Record<string, any>;
  for (const [name, def] of Object.entries(lifecycles)) {
    if (!def || typeof def !== 'object') continue;
    let states: string[] = [];
    if (Array.isArray(def.states)) {
      states = def.states
        .map((s: any) => (typeof s === 'string' ? s : s?.name ?? s?.id ?? ''))
        .filter(Boolean);
    } else if (typeof def.flow === 'string') {
      states = def.flow.split(/\s*(?:->|,)\s*/).map((s: string) => s.trim()).filter(Boolean);
    }
    if (states.length > 0) out.push({ name, states });
  }
  return out;
}

function humanize(name: string): string {
  return name
    .replace(/([A-Z])/g, ' $1')
    .replace(/^./, c => c.toUpperCase())
    .trim();
}

/**
 * Return the \`useState\` import line for detail views with tabs.
 * Empty for everything else — strict TS rejects unused imports, so
 * we emit the import only when the state is actually declared.
 */
function buildRelatedTabImport(context: EmitContext): string {
  if (context.view.type.toLowerCase() !== 'detail') return '';
  const hasMany = extractHasManyTargets(context.model, context.modelSchemas);
  if (hasMany.length < 2) return '';
  return `import { useState } from 'react';`;
}

/**
 * Emit a \`useState\` call only when the detail view will render
 * tabs — i.e. when the model has 2+ hasMany relationships. The tab
 * state defaults to the first relationship's name. For other view
 * types or single-hasMany detail views, no state is needed.
 */
function buildRelatedTabState(context: EmitContext): string {
  if (context.view.type.toLowerCase() !== 'detail') return '';
  const hasMany = extractHasManyTargets(context.model, context.modelSchemas);
  if (hasMany.length < 2) return '';
  const firstName = hasMany[0].name;
  return `  const [relatedTab, setRelatedTab] = useState<string>('${firstName}');`;
}

/**
 * Compute the belongsTo wiring for a view: one import line per unique
 * target hook, one hook call per relationship (even if two relations
 * share a target — each needs its own options variable).
 *
 * The entity-display helper import set is view-type-dependent to
 * avoid unused-import TS errors:
 *   - form uses `getEntityDisplayName(opt)` (has whole entity in hand)
 *   - list / detail / dashboard use `resolveEntityDisplayName(id, list)`
 *     (has FK id, looks up in the list)
 */
function buildBelongsToWiring(context: EmitContext): { imports: string; hooks: string } {
  const belongsTo = extractBelongsToTargets(context.model);
  // hasMany wiring is only needed by the detail view (for its
  // related-lists section). Other view types skip it to avoid
  // unused-import TS errors.
  const isDetail = context.view.type.toLowerCase() === 'detail';
  const hasMany = isDetail
    ? extractHasManyTargets(context.model, context.modelSchemas)
    : [];

  // For detail's related tables to resolve FK columns (e.g. Task.project
  // in a User → Tasks table), we also need query hooks for each
  // hasMany-target's own belongsTo targets. Skip the back-reference
  // target (pointing at us) since that column is filtered out.
  const crossBelongsTo: BelongsToLike[] = [];
  if (isDetail) {
    for (const rel of hasMany) {
      const targetModel = context.modelSchemas[rel.target];
      if (!targetModel) continue;
      const targetFK = rel.foreignKey;
      for (const btm of extractBelongsToTargets(targetModel)) {
        if (`${btm.name}Id` === targetFK) continue;  // skip back-ref
        crossBelongsTo.push(btm);
      }
    }
  }

  if (belongsTo.length === 0 && hasMany.length === 0 && crossBelongsTo.length === 0) {
    // Detail view still needs getEntityDisplayName for its picker +
    // formatDisplayValue for field rows, even when the model has no
    // relationships.
    const vt = context.view.type.toLowerCase();
    if (vt === 'detail') {
      return {
        imports: `import { getEntityDisplayName, formatDisplayValue } from '../lib/entity-display';`,
        hooks: '',
      };
    }
    // List / dashboard without relationships still need formatDisplayValue
    // for non-FK cells.
    if (vt === 'list' || vt === 'dashboard') {
      return {
        imports: `import { formatDisplayValue } from '../lib/entity-display';`,
        hooks: '',
      };
    }
    return { imports: '', hooks: '' };
  }

  // Dedupe hook imports by target — relationships to the same model
  // share the same query hook.
  const hookImportNames = new Set<string>();
  for (const rel of belongsTo) {
    hookImportNames.add(`use${pluralizeShared(rel.target)}Query`);
  }
  for (const rel of hasMany) {
    hookImportNames.add(`use${pluralizeShared(rel.target)}Query`);
  }
  for (const rel of crossBelongsTo) {
    hookImportNames.add(`use${pluralizeShared(rel.target)}Query`);
  }

  // Helpers needed per view type (strict TS rejects unused imports,
  // so we only list what the emitted code actually references):
  //   - form uses getEntityDisplayName(opt) for dropdown labels
  //   - list / dashboard use resolveEntityDisplayName(id, list) for
  //     FK columns
  //   - detail main section uses resolveEntityDisplayName when there
  //     are belongsTo rows; the related-lists section uses
  //     resolveEntityDisplayName for FK columns in the full-column
  //     path, or getEntityDisplayName in the one-column fallback
  //     path (used when a hasMany target's schema isn't in
  //     context.modelSchemas).
  const viewType = context.view.type.toLowerCase();
  const helperImports = new Set<string>();
  if (viewType === 'form') {
    helperImports.add('getEntityDisplayName');
  } else if (viewType === 'detail') {
    // Detail view always uses getEntityDisplayName for the entity
    // picker dropdown (per-skeleton, not per-section).
    helperImports.add('getEntityDisplayName');
    helperImports.add('formatDisplayValue');  // field-list + related-tables cells.
    if (belongsTo.length > 0 || crossBelongsTo.length > 0) {
      helperImports.add('resolveEntityDisplayName');
    }
  } else if (viewType === 'list' || viewType === 'dashboard') {
    helperImports.add('resolveEntityDisplayName');
    helperImports.add('formatDisplayValue');  // table-content + recent-items cells.
  } else {
    helperImports.add('resolveEntityDisplayName');
  }

  const importLines: string[] = [];
  importLines.push(
    `import { ${[...hookImportNames].join(', ')} } from '../hooks/useApi';`
  );
  if (helperImports.size > 0) {
    importLines.push(`import { ${[...helperImports].join(', ')} } from '../lib/entity-display';`);
  }

  // belongsTo hooks → \`${relName}Options\` arrays used in dropdowns
  // and FK resolvers. hasMany hooks → \`${relName}All\` arrays that
  // the detail composer filters by the back-reference FK. Cross-
  // belongsTo hooks → \`${relName}Options\` so related tables in the
  // detail view can resolve their FK columns to display names.
  const hookLines: string[] = [];
  for (const rel of belongsTo) {
    hookLines.push(
      `  const { data: ${rel.name}Options = [] } = use${pluralizeShared(rel.target)}Query();`
    );
  }
  for (const rel of hasMany) {
    hookLines.push(
      `  const { data: ${rel.name}All = [] } = use${pluralizeShared(rel.target)}Query();`
    );
  }
  // Dedupe cross-belongsTo options by name: if two hasMany targets
  // both have a \`project\` belongsTo, we only need one \`projectOptions\`.
  const emittedOptionsVars = new Set(belongsTo.map(r => `${r.name}Options`));
  for (const rel of crossBelongsTo) {
    const varName = `${rel.name}Options`;
    if (emittedOptionsVars.has(varName)) continue;
    emittedOptionsVars.add(varName);
    hookLines.push(
      `  const { data: ${varName} = [] } = use${pluralizeShared(rel.target)}Query();`
    );
  }

  return {
    imports: importLines.join('\n'),
    hooks: hookLines.join('\n'),
  };
}

/**
 * Minimal "belongsTo-like" shape — both extractBelongsToTargets
 * results and the cross-target equivalent use this. Declared here
 * just to keep the wiring function's signatures tight.
 */
type BelongsToLike = { name: string; target: string };

function applySubstitutions(template: string, subs: Substitutions): string {
  let out = template;
  for (const [key, value] of Object.entries(subs)) {
    // Replace every occurrence of {{KEY}} — plain string substitution,
    // no expression evaluation. Escape regex special chars in the key
    // (defensive; keys are ASCII constants).
    const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
    out = out.replace(pattern, value);
  }
  return out;
}

/**
 * Minimal English pluralizer matching the adapter's conventions.
 * Good enough for generated code — users can edit the output if they
 * want a different pluralization.
 */
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';
}
