/**
 * Per-operation view generator for ReactAppStarter
 *
 * Auto-generates `src/views/{Op}View.tsx` files for every non-standard
 * controller command and every service operation. Each generated view
 * is a thin shell that imports `OperationExecutor` from `../lib/` and
 * invokes it with the op's metadata.
 *
 * Logic ported from app-demo's runtime-engine.ts:380-427 — same skip
 * list for "standard" CURVED-protocol operations so we don't double
 * up on things that already have declared list/detail/form/dashboard
 * views.
 */

/**
 * CURVED + lifecycle + framework ops that should NOT get an operation
 * view. These either have dedicated CRUD views, are runtime helpers,
 * or are relationship-integrity checks. Copied verbatim from
 * app-demo/src/runtime/runtime-engine.ts:382-388.
 */
const STANDARD_OPS = new Set([
  'create', 'retrieve', 'retrieve_many', 'update', 'delete',
  'validate', 'evolve', 'list', 'query', 'get',
  'attachProfile', 'detachProfile', 'hasProfile',
  'handleChildAdded', 'handleChildRemoved',
  'validateRelationshipIntegrity', 'repairRelationshipIntegrity',
]);

export interface OperationViewDescriptor {
  /** File name without extension, e.g. `PublishView`. */
  viewName: string;
  /** Human-readable label for the page. */
  label: string;
  /** Service or controller class name (e.g. `AuthService`, `PostController`). */
  serviceName: string;
  /** Operation method name (e.g. `login`, `publish`). */
  operationName: string;
  /** Parameters declaration. Accepted in either `{ name: def }` or array form. */
  parameters?: Record<string, any> | Array<any>;
  /** Optional: parameter names that must be supplied before Execute. */
  requires?: string[];
  /** Optional: display return-type hint next to the title. */
  returns?: string;
  /** Source: determines which endpoint shape the executor uses. */
  source: 'service' | 'controller';
}

/**
 * Normalize a parameters block into `{ name: typeStr }`. Handles
 * every shape the parser / inference pipeline can emit:
 *   - `query: String`              → `{ query: "String" }`
 *   - `query: { type: "String" }`  → `{ query: "String" }`
 *   - `id: "UUID required"`        → `{ id: "UUID" }` (strip modifiers)
 *   - Broken shapes (e.g. inference bug producing `["object Object"]`
 *     for services) fall back to "String" so the executor renders
 *     a plain text input instead of blowing up.
 */
function normalizeParameters(raw: any): Record<string, string> | undefined {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
  const out: Record<string, string> = {};
  for (const [name, def] of Object.entries(raw)) {
    let type: string;
    if (typeof def === 'string') {
      type = def;
    } else if (def && typeof def === 'object' && 'type' in def && typeof (def as any).type === 'string') {
      type = (def as any).type;
    } else {
      // Malformed entry (e.g. inference bug on services) — render as text input.
      type = 'String';
    }
    // Strip space-delim modifiers: "UUID required" → "UUID".
    out[name] = (type.split(/\s+/)[0] || 'String');
  }
  return Object.keys(out).length > 0 ? out : undefined;
}

/**
 * Enumerate all operation views to generate for a spec. Returns an
 * array of descriptors — one per non-standard command + service op.
 * Consumers emit a `.tsx` per descriptor via `emitOperationView`.
 */
export function collectOperationViews(spec: {
  controllers?: Record<string, any>;
  services?: Record<string, any> | any[];
}): OperationViewDescriptor[] {
  const out: OperationViewDescriptor[] = [];
  const seen = new Set<string>();

  // Non-standard controller commands. A controller's operations object
  // may use `commands` or `operations` as its key — accept both so we
  // don't mis-classify a spec that uses either form.
  const controllers = spec.controllers ?? {};
  for (const [ctlName, ctl] of Object.entries(controllers)) {
    const ops = (ctl?.operations ?? ctl?.commands ?? {}) as Record<string, any>;
    for (const [opName, op] of Object.entries(ops)) {
      if (STANDARD_OPS.has(opName)) continue;
      const viewName = capitalize(opName) + 'View';
      if (seen.has(viewName)) continue;
      seen.add(viewName);
      out.push({
        viewName,
        label: humanize(opName),
        serviceName: ctlName,
        operationName: opName,
        parameters: normalizeParameters(op?.parameters ?? op?.params),
        requires: op?.requires,
        returns: op?.returns,
        source: 'controller',
      });
    }
  }

  // Services. Operations may be an object keyed by name OR an array
  // of { name, parameters, ... } objects.
  const services = spec.services ?? {};
  for (const [svcName, svc] of Object.entries(services)) {
    const rawOps = svc?.operations;
    const opEntries: Array<[string, any]> = Array.isArray(rawOps)
      ? rawOps.map((o: any) => [o.name, o])
      : Object.entries((rawOps ?? {}) as Record<string, any>);
    for (const [opName, op] of opEntries) {
      if (!opName) continue;
      if (STANDARD_OPS.has(opName)) continue;
      const viewName = capitalize(opName) + 'View';
      if (seen.has(viewName)) continue;
      seen.add(viewName);
      out.push({
        viewName,
        label: humanize(opName),
        serviceName: svcName,
        operationName: opName,
        parameters: normalizeParameters(op?.parameters ?? op?.params),
        requires: op?.requires,
        returns: op?.returns,
        source: 'service',
      });
    }
  }

  return out;
}

/**
 * Emit a single `src/views/{Op}View.tsx` file. A thin shell that
 * imports `OperationExecutor` from `../lib/` and passes the op's
 * metadata as props. Matches the structure of app-demo's
 * OperationView but inlined so users have one file per op to edit.
 */
export function emitOperationView(desc: OperationViewDescriptor): string {
  const paramsJson = desc.parameters
    ? JSON.stringify(desc.parameters, null, 2).replace(/\n/g, '\n        ')
    : 'undefined';
  const requiresJson = desc.requires
    ? JSON.stringify(desc.requires)
    : 'undefined';

  const returnsPill = desc.returns
    ? `\n        <span className="text-xs px-2 py-0.5 bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded font-mono">\n          → ${escapeJsx(desc.returns)}\n        </span>`
    : '';

  return `/**
 * ${desc.viewName} — generated by @specverse/realize (ReactAppStarter)
 *
 * Auto-generated operation view for ${desc.source} ${desc.serviceName}.${desc.operationName}.
 * Safe to edit; the generator will not overwrite a hand-edited file
 * (content-hashing in .specverse-gen/hashes.json).
 */
import { OperationExecutor } from '../lib/OperationExecutor';

export function ${desc.viewName}() {
  return (
    <div className="p-6 space-y-4">
      <div className="flex items-center gap-2">
        <h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
          ${escapeJsx(desc.label)}
        </h2>${returnsPill}
      </div>
      <p className="text-sm text-gray-500 dark:text-gray-400">
        Execute ${escapeJsx(desc.serviceName)}.${escapeJsx(desc.operationName)}
      </p>
      <div className="max-w-lg">
        <OperationExecutor
          serviceName="${desc.serviceName}"
          operationName="${desc.operationName}"
          parameters={${paramsJson}}
          requires={${requiresJson}}
          source="${desc.source}"
        />
      </div>
    </div>
  );
}
`;
}

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

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

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