/**
 * ExecManifest type + generation.
 * The manifest is a JSON file that describes the bundled app
 * for installation and runtime.
 */

import { type FrontmcpExecConfig } from './config';
import { type SetupStep, zodSchemaToJsonSchema, idToEnvName } from './setup';

export interface ExecManifest {
  name: string;
  version: string;
  nodeVersion: string;
  storage: {
    type: 'sqlite' | 'redis' | 'none';
    required: boolean;
  };
  /**
   * Server bind config. Omitted entirely for `--target cli` (CLI builds don't
   * bind a port — the manifest is consumed by `frontmcp install`, container
   * `EXPOSE`, and proxy generators).
   */
  network?: {
    defaultPort: number;
    supportsSocket: boolean;
  };
  dependencies: {
    system: string[];
    nativeAddons: string[];
  };
  bundle: string; // bundle filename
  setup?: {
    steps: ManifestSetupStep[];
  };
  cli?: {
    enabled: boolean;
    cliBundle: string;
    outputDefault: 'text' | 'json';
    authRequired: boolean;
    toolCount: number;
    resourceCount: number;
    templateCount: number;
    promptCount: number;
    oauthEnabled: boolean;
    skillsEnabled?: boolean;
    jobsEnabled?: boolean;
    workflowsEnabled?: boolean;
  };
}

export interface ManifestSetupStep {
  id: string;
  prompt: string;
  description?: string;
  jsonSchema: Record<string, unknown>;
  env: string;
  sensitive?: boolean;
  group?: string;
  next?: string | Record<string, string>;
  showWhen?: Record<string, string | string[]>;
}

export interface GenerateManifestContext {
  /** Build target the manifest is being written for. */
  target?: 'node' | 'cli';
  /** Port declared via `@FrontMcp({ http: { port } })` on the user's entry. */
  decoratorHttpPort?: number;
  /** `cli.outputDefault` from frontmcp.config.deployments[].cli (per-deployment). */
  outputDefault?: 'text' | 'json';
}

export function generateManifest(
  config: FrontmcpExecConfig,
  bundleFilename: string,
  context: GenerateManifestContext = {},
): ExecManifest {
  // Resolve port precedence (highest → lowest):
  //   1. `frontmcp.config.network.defaultPort`         (build-config override)
  //   2. `@FrontMcp({ http: { port } })`               (decorator metadata)
  //   3. 3000                                          (Express default)
  // The legacy `3001` was a phantom default that didn't match what the
  // server actually bound (#371).
  const resolvedPort = config.network?.defaultPort ?? context.decoratorHttpPort ?? 3000;

  const manifest: ExecManifest = {
    name: config.name,
    version: config.version || '1.0.0',
    nodeVersion: config.nodeVersion || '>=22.0.0',
    storage: {
      type: config.storage?.type || 'none',
      required: config.storage?.required ?? false,
    },
    dependencies: {
      system: config.dependencies?.system || [],
      nativeAddons: config.dependencies?.nativeAddons || [],
    },
    bundle: bundleFilename,
  };

  // Network section is server-build-specific. CLI builds don't bind a port,
  // so emitting `network.defaultPort` confuses tooling that drives container
  // `EXPOSE` / reverse proxies (#371 note).
  if (context.target !== 'cli') {
    manifest.network = {
      defaultPort: resolvedPort,
      supportsSocket: config.network?.supportsSocket ?? true,
    };
  }

  // Convert setup steps — serialize Zod schemas to JSON Schema
  if (config.setup?.steps && config.setup.steps.length > 0) {
    manifest.setup = {
      steps: config.setup.steps.map((step) => serializeStep(step)),
    };
  }

  return manifest;
}

function serializeStep(step: SetupStep): ManifestSetupStep {
  let jsonSchema: Record<string, unknown> = { type: 'string' };

  if (step.jsonSchema) {
    // Already serialized (e.g., from JSON config)
    jsonSchema = step.jsonSchema;
  } else if (step.schema) {
    // Convert Zod schema to JSON Schema
    jsonSchema = zodSchemaToJsonSchema(step.schema);
  }

  return {
    id: step.id,
    prompt: step.prompt,
    description: step.description,
    jsonSchema,
    env: step.env || idToEnvName(step.id),
    sensitive: step.sensitive,
    group: step.group,
    next: step.next,
    showWhen: step.showWhen,
  };
}
