/**
 * Deployment-emitter — engines 6.28.0+.
 *
 * After the analyse pipeline assembles the final spec (skeleton + filled
 * action bodies), this emits a deterministic `deployments:` block based
 * on what was extracted. One production environment with one instance
 * per detected controller / service / entity / view.
 *
 * Why this lives in analyse (not in `spv infer`): the inference-engine
 * deployment rules in `entities/src/core/deployments/inference/` are
 * for the from-scratch authoring path. They expand a minimal spec to
 * convention defaults. Analyse is the OPPOSITE — produces a faithful
 * mirror of the codebase. But the analysed spec WITHOUT a deployments
 * block is incomplete in a user-visible way (Dom Williams 2026-05-06:
 * "where does it say I'll have one of these and two of those and wire
 * them up"). This emitter closes that gap with a minimal mechanical
 * mapping users can hand-edit.
 *
 * Output shape (matches `entities/src/core/deployments/schema/`):
 *
 *   deployments:
 *     production:
 *       version: "1.0.0"
 *       description: "Default deployment topology emitted by spv analyse..."
 *       environment: production
 *       instances:
 *         controllers:
 *           authController:
 *             component: "ComponentName"
 *         services:
 *           authService:
 *             component: "ComponentName"
 *         storage:
 *           userStorage:
 *             component: "ComponentName"
 *             type: "document"
 *             persistence: "durable"
 *         views:
 *           gamePicker:
 *             component: "ComponentName"
 *
 * V1 keeps it minimal: no rateLimit / cache / retry / autoscaling /
 * resource-limits / monitoring / security / infrastructure. Those are
 * meaningful defaults but require manifest-aware decisions; emitting
 * them as `enabled: false` or with arbitrary defaults adds noise without
 * value. The user can layer those in via spec edits or via `spv infer`.
 */
/**
 * Convert a PascalCase class/entity/view name to a lowercase-first
 * identifier suitable as a deployment-instance key.
 *   AuthController → authController
 *   GamePicker     → gamePicker
 *   PaymentService → paymentService
 *   GameScreen     → gameScreen
 */
function toInstanceName(pascalName) {
    if (!pascalName)
        return pascalName;
    return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
}
/**
 * Emit a deterministic `deployments:` block from extracted facts.
 * The output is a complete top-level YAML fragment that can be appended
 * to a spec body.
 */
export function emitDefaultDeployments(input) {
    const { facts, classesByComponent, entityHome } = input;
    // Buckets: component name → instances, keyed by instance name.
    const controllers = [];
    const services = [];
    const storage = [];
    const views = [];
    // Engines 6.30.1+ (TODO #51 phase 2A): index implements relationships
    // by className so we can attach `advertises: [InterfaceName]` to
    // controller/service instances. This populates the previously-empty
    // capability-advertising slot on extracted specs.
    const implementsByClass = new Map();
    for (const rel of facts.implementsRelationships ?? []) {
        if (!implementsByClass.has(rel.className))
            implementsByClass.set(rel.className, []);
        implementsByClass.get(rel.className).push(rel.interfaceName);
    }
    // Controllers + services from per-component class lists.
    // The skeleton-emitter splits classes into controllers (Controller suffix)
    // vs services (everything else); replicate that here so the deployment
    // instances match the spec's logical layout.
    //
    // GLOBAL dedup of instance names (engines 6.29.4+). Earlier we deduped
    // per-component, but instance names are at the deployment-instances
    // namespace level (top of `instances.controllers`/`instances.services`
    // map keys), so two `GameService` classes in different components would
    // both emit `gameService:` keys — yaml-loads as a duplicated mapping key
    // and validate fails. First-name-wins; the loser is silently dropped
    // (the lost class is still in its component's services block in the
    // spec — only the deployment instance is suppressed).
    const seenGlobalControllerNames = new Set();
    const seenGlobalServiceNames = new Set();
    for (const [bucketKey, classes] of classesByComponent) {
        // Map bucket key → component spec name. The bucket keys may be
        // `_unassigned` (catch-all) or component-suggested names. We use the
        // entityHome map's values to find the canonical spec name; fall
        // back to bucketKey for non-`_unassigned` keys.
        const specName = bucketKey === '_unassigned' ? 'Application' : bucketKey;
        for (const cls of classes) {
            // Skip entries that are also entities — the skeleton-emitter's
            // owner-vs-model dedup keeps only one (model wins), and we mirror
            // that here. Otherwise we'd emit a controller instance for a
            // model that the spec doesn't declare as a controller.
            if (entityHome.has(cls.entityName))
                continue;
            const instanceName = toInstanceName(cls.entityName);
            const advertises = implementsByClass.get(cls.entityName);
            if (/Controller$/.test(cls.entityName)) {
                if (seenGlobalControllerNames.has(instanceName))
                    continue;
                seenGlobalControllerNames.add(instanceName);
                controllers.push({ instanceName, component: specName, advertises });
            }
            else {
                if (seenGlobalServiceNames.has(instanceName))
                    continue;
                seenGlobalServiceNames.add(instanceName);
                services.push({ instanceName, component: specName, advertises });
            }
        }
    }
    // Storage from entities — one instance per entity, attributed to its
    // owning component.
    const seenStorageNames = new Set();
    for (const ent of facts.entities ?? []) {
        const owner = entityHome.get(ent.name);
        if (!owner)
            continue;
        // Convention: <entity>Storage. Matches Dom's reference loosely
        // (`mongoUsers`-style names depend on manifest knowledge we don't
        // have at analyse-time; `userStorage` is the manifest-agnostic shape).
        const instanceName = `${toInstanceName(ent.name)}Storage`;
        if (seenStorageNames.has(instanceName))
            continue;
        seenStorageNames.add(instanceName);
        storage.push({ instanceName, component: owner, entityName: ent.name });
    }
    // Views — one instance per detected view, attributed by source-tree
    // topology. The skeleton-emitter has already done the attribution in
    // its component bodies; we replicate the same logic here. Without a
    // structural sourceDir, we fall back to `Application`.
    const seenViewNames = new Set();
    for (const v of facts.views ?? []) {
        const instanceName = toInstanceName(v.name);
        if (seenViewNames.has(instanceName))
            continue;
        seenViewNames.add(instanceName);
        // Find the component whose structural.sourceDir is a prefix of v.filePath.
        let owner = 'Application';
        for (const c of facts.suggestedComponents ?? []) {
            const sd = c.structural?.sourceDir;
            if (sd && v.filePath.startsWith(sd)) {
                owner = c.suggestedName;
                break;
            }
        }
        views.push({ instanceName, component: owner });
    }
    // V2 Phase 2 (2026-05-08): Non-library dependency hints. Each detected
    // pattern lands in one of three deployment-instance categories per the
    // hint's `deploymentHint.category` field — `infrastructure` for
    // subprocess + managed-SDK, `services` for HTTP/RPC clients,
    // `communications` for messaging brokers. The hint's
    // `instanceName` / `advertises` / `config` go through verbatim.
    //
    // Components are derived from the consumer key in
    // facts.nonLibraryImportsByComponent. Multiple consumers depending on
    // the same provider (e.g. two components both calling `gitnexus`)
    // share ONE deployment instance (the first consumer wins; subsequent
    // hits are deduped on instanceName).
    //
    // The existing services bucket above is REUSED for service-client
    // hints — they coexist with class-derived services in the same
    // category. Class-derived services are added first (above), so a
    // service-client hint with the same instanceName is silently dropped
    // (the consumer's class wins).
    const infrastructure = [];
    const communications = [];
    const seenGlobalInfrastructureNames = new Set();
    const seenGlobalCommunicationsNames = new Set();
    // Service-client hints reuse the existing services bucket; dedup
    // against the same names already used by class-derived services.
    for (const [consumerName, hints] of Object.entries(facts.nonLibraryImportsByComponent ?? {})) {
        for (const hint of hints) {
            const inst = hint.deploymentHint;
            const cat = inst.category;
            if (cat === 'infrastructure') {
                if (seenGlobalInfrastructureNames.has(inst.instanceName))
                    continue;
                seenGlobalInfrastructureNames.add(inst.instanceName);
                infrastructure.push({
                    instanceName: inst.instanceName,
                    component: consumerName,
                    advertises: inst.advertises,
                    config: inst.config,
                });
            }
            else if (cat === 'communications') {
                if (seenGlobalCommunicationsNames.has(inst.instanceName))
                    continue;
                seenGlobalCommunicationsNames.add(inst.instanceName);
                communications.push({
                    instanceName: inst.instanceName,
                    component: consumerName,
                    advertises: inst.advertises,
                    config: inst.config,
                });
            }
            else if (cat === 'services') {
                if (seenGlobalServiceNames.has(inst.instanceName))
                    continue;
                seenGlobalServiceNames.add(inst.instanceName);
                services.push({
                    instanceName: inst.instanceName,
                    component: consumerName,
                    advertises: inst.advertises,
                    config: inst.config,
                });
            }
        }
    }
    // Build the YAML. Indented to be appendable to a top-level spec body.
    const lines = [];
    lines.push('deployments:');
    lines.push('  production:');
    lines.push('    version: "1.0.0"');
    lines.push('    description: "Default deployment topology emitted by `spv ai analyse`. One instance per detected controller / service / entity / view. Hand-edit policies (rateLimit, cache, retry, autoscaling) and infrastructure-specific instances (security, monitoring) as needed."');
    lines.push('    environment: production');
    lines.push('    instances:');
    if (controllers.length > 0) {
        lines.push('      controllers:');
        for (const c of controllers) {
            lines.push(`        ${c.instanceName}:`);
            lines.push(`          component: "${c.component}"`);
            if (c.advertises && c.advertises.length > 0) {
                lines.push(`          advertises: [${c.advertises.map((a) => `"${a}"`).join(', ')}]`);
            }
        }
    }
    if (services.length > 0) {
        lines.push('      services:');
        for (const s of services) {
            lines.push(`        ${s.instanceName}:`);
            lines.push(`          component: "${s.component}"`);
            if (s.advertises && s.advertises.length > 0) {
                lines.push(`          advertises: [${s.advertises.map((a) => `"${a}"`).join(', ')}]`);
            }
            if (s.config && Object.keys(s.config).length > 0) {
                lines.push('          config:');
                emitConfigBlock(lines, s.config, '            ');
            }
        }
    }
    if (storage.length > 0) {
        lines.push('      storage:');
        for (const s of storage) {
            lines.push(`        ${s.instanceName}:`);
            lines.push(`          component: "${s.component}"`);
            lines.push('          type: "document"');
            lines.push('          persistence: "durable"');
        }
    }
    if (views.length > 0) {
        lines.push('      views:');
        for (const v of views) {
            lines.push(`        ${v.instanceName}:`);
            lines.push(`          component: "${v.component}"`);
        }
    }
    // V2 Phase 2: infrastructure (subprocess + managed-SDK).
    if (infrastructure.length > 0) {
        lines.push('      infrastructure:');
        for (const inst of infrastructure) {
            lines.push(`        ${inst.instanceName}:`);
            lines.push(`          component: "${inst.component}"`);
            if (inst.advertises.length > 0) {
                lines.push(`          advertises: [${inst.advertises.map((a) => `"${a}"`).join(', ')}]`);
            }
            if (Object.keys(inst.config).length > 0) {
                lines.push('          config:');
                emitConfigBlock(lines, inst.config, '            ');
            }
        }
    }
    // V2 Phase 2: communications (messaging brokers).
    if (communications.length > 0) {
        lines.push('      communications:');
        for (const inst of communications) {
            lines.push(`        ${inst.instanceName}:`);
            lines.push(`          component: "${inst.component}"`);
            if (inst.advertises.length > 0) {
                lines.push(`          advertises: [${inst.advertises.map((a) => `"${a}"`).join(', ')}]`);
            }
            if (Object.keys(inst.config).length > 0) {
                lines.push('          config:');
                emitConfigBlock(lines, inst.config, '            ');
            }
        }
    }
    return {
        deploymentsYaml: lines.join('\n'),
        counts: {
            controllers: controllers.length,
            services: services.length,
            storage: storage.length,
            views: views.length,
            infrastructure: infrastructure.length,
            communications: communications.length,
        },
    };
}
/**
 * Emit a free-form config object as YAML at the given indent. Handles
 * primitives (strings, numbers, booleans), arrays of primitives, and
 * one level of nested objects (which is all the V2 Phase 2 hints
 * produce — `endpoint: { env: ... }` is the deepest nesting). Non-
 * supported types (functions, symbols) are skipped silently.
 */
function emitConfigBlock(lines, config, indent) {
    for (const [key, value] of Object.entries(config)) {
        if (value == null)
            continue;
        if (Array.isArray(value)) {
            if (value.length === 0) {
                lines.push(`${indent}${key}: []`);
                continue;
            }
            const allPrimitive = value.every((v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean');
            if (allPrimitive) {
                const flow = value.map((v) => (typeof v === 'string' ? `"${v}"` : String(v))).join(', ');
                lines.push(`${indent}${key}: [${flow}]`);
            }
            else {
                lines.push(`${indent}${key}:`);
                for (const v of value) {
                    if (typeof v === 'object' && v != null) {
                        lines.push(`${indent}  -`);
                        emitConfigBlock(lines, v, indent + '    ');
                    }
                    else {
                        lines.push(`${indent}  - ${typeof v === 'string' ? `"${v}"` : String(v)}`);
                    }
                }
            }
        }
        else if (typeof value === 'object') {
            lines.push(`${indent}${key}:`);
            emitConfigBlock(lines, value, indent + '  ');
        }
        else if (typeof value === 'string') {
            lines.push(`${indent}${key}: "${value}"`);
        }
        else {
            lines.push(`${indent}${key}: ${value}`);
        }
    }
}
//# sourceMappingURL=deployment-emitter.js.map