/**
 * Build-time schema extraction.
 * After the server bundle is produced, boots a DirectClient via connect(),
 * extracts tool/resource/prompt schemas, and serializes them for CLI code generation.
 */

export interface ExtractedTool {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
}

export interface ExtractedResource {
  uri: string;
  name: string;
  description?: string;
  mimeType?: string;
}

export interface ExtractedResourceTemplate {
  uriTemplate: string;
  name: string;
  description?: string;
}

export interface ExtractedPrompt {
  name: string;
  description?: string;
  arguments?: Array<{
    name: string;
    description?: string;
    required?: boolean;
  }>;
}

export interface ExtractedJob {
  name: string;
  description?: string;
  inputSchema?: Record<string, unknown>;
  tags?: string[];
}

export interface ExtractedCapabilities {
  skills: boolean;
  jobs: boolean;
  workflows: boolean;
}

export interface ExtractedSkillAsset {
  skillName: string;
  baseDir?: string;
  instructionFile?: string;
  resourceDirs?: {
    references?: string;
    examples?: string;
    scripts?: string;
    assets?: string;
  };
  /** Mirrors `SkillAssetEntry.description` so consumers can compose frontmatter. */
  description?: string;
  /** Mirrors `SkillAssetEntry.tags`. */
  tags?: string[];
  /** Mirrors `SkillAssetEntry.license`. */
  license?: string;
}

export interface ExtractedSchema {
  tools: ExtractedTool[];
  resources: ExtractedResource[];
  resourceTemplates: ExtractedResourceTemplate[];
  prompts: ExtractedPrompt[];
  jobs: ExtractedJob[];
  capabilities: ExtractedCapabilities;
  skillAssets: ExtractedSkillAsset[];
  /**
   * Server bind config extracted from `@FrontMcp({ http })` (or its
   * decorator-attached `__frontmcp:config` metadata). Used by the manifest
   * writer so the published manifest reflects the port the server will
   * actually listen on, not a hard-coded SDK default.
   */
  httpPort?: number;
}

/**
 * Known system tool names injected by SDK features (jobs, workflows).
 *
 * Canonical names are snake_case per issue #408; hyphenated aliases are
 * kept transitionally so older built bundles continue to be recognized by
 * the schema extractor.
 */
export const SYSTEM_TOOL_NAMES = new Set([
  'list_jobs',
  'execute_job',
  'get_job_status',
  'register_job',
  'remove_job',
  'list_workflows',
  'execute_workflow',
  'get_workflow_status',
  'register_workflow',
  'remove_workflow',
  // Legacy hyphen names — accepted for back-compat with previously built bundles.
  'list-jobs',
  'execute-job',
  'get-job-status',
  'register-job',
  'remove-job',
  'list-workflows',
  'execute-workflow',
  'get-workflow-status',
  'register-workflow',
  'remove-workflow',
]);

/**
 * Extract schemas from a compiled server bundle.
 * Requires the bundle to export a FrontMcp-decorated class as default export
 * or a config object usable by connect().
 */
export async function extractSchemas(bundlePath: string): Promise<ExtractedSchema> {
  // Suppress @FrontMcp() decorator bootstrap — we only need metadata, not a running server
  const prev = process.env['FRONTMCP_SCHEMA_EXTRACT'];
  process.env['FRONTMCP_SCHEMA_EXTRACT'] = '1';

  let mod: Record<string, unknown>;
  try {
    mod = require(bundlePath);
  } finally {
    if (prev === undefined) {
      delete process.env['FRONTMCP_SCHEMA_EXTRACT'];
    } else {
      process.env['FRONTMCP_SCHEMA_EXTRACT'] = prev;
    }
  }

  const configOrClass = mod.default || mod;

  // Use @frontmcp/sdk connect() to boot in-memory client
  let connect: (config: unknown, options?: { mode?: string }) => Promise<unknown>;
  try {
    const sdk = require('@frontmcp/sdk');
    connect = sdk.connect || sdk.direct?.connect;
    if (!connect) {
      throw new Error('connect() not found in @frontmcp/sdk');
    }
  } catch {
    throw new Error(
      '@frontmcp/sdk is required for CLI schema extraction. Ensure it is installed.',
    );
  }

  const client = await connect(configOrClass, { mode: 'cli' }) as {
    listTools(): Promise<unknown>;
    listResources(): Promise<{ resources: Array<{ uri: string; name?: string; description?: string; mimeType?: string }> }>;
    listResourceTemplates?(): Promise<{ resourceTemplates: Array<{ uriTemplate: string; name?: string; description?: string }> }>;
    listPrompts(): Promise<{ prompts: Array<{ name: string; description?: string; arguments?: unknown[] }> }>;
    listJobs?(): Promise<{ jobs: Array<{ name: string; description?: string; inputSchema?: Record<string, unknown>; tags?: string[] }>; count: number }>;
    collectSkillAssets?(): Promise<{
      entries: Array<{
        skillName: string;
        baseDir?: string;
        instructionFile?: string;
        resources?: Record<string, string | undefined>;
        description?: string;
        tags?: string[];
        license?: string;
      }>;
    }>;
    close(): Promise<void>;
  };

  try {
    const [toolsRaw, resourcesResult, promptsResult] = await Promise.all([
      client.listTools().catch(() => []),
      client.listResources().catch(() => ({ resources: [] })),
      client.listPrompts().catch(() => ({ prompts: [] })),
    ]);

    let resourceTemplates: ExtractedResourceTemplate[] = [];
    if (client.listResourceTemplates) {
      try {
        const templatesResult = await client.listResourceTemplates();
        resourceTemplates = (templatesResult.resourceTemplates || []).map((t) => ({
          uriTemplate: t.uriTemplate,
          name: t.name || t.uriTemplate,
          description: t.description,
        }));
      } catch {
        // Resource templates not supported
      }
    }

    // DirectClient.listTools() returns FormattedTools (array) directly,
    // not { tools: [...] } like the raw MCP client
    const toolsList = Array.isArray(toolsRaw)
      ? toolsRaw as Array<{ name: string; description?: string; inputSchema?: unknown }>
      : ((toolsRaw as { tools?: unknown[] })?.tools || []) as Array<{ name: string; description?: string; inputSchema?: unknown }>;

    const tools: ExtractedTool[] = toolsList.map((t) => ({
      name: t.name,
      description: t.description || '',
      inputSchema: (t.inputSchema as Record<string, unknown>) || { type: 'object', properties: {} },
    }));

    const resources: ExtractedResource[] = (resourcesResult.resources || []).map((r) => ({
      uri: r.uri,
      name: r.name || r.uri,
      description: r.description,
      mimeType: r.mimeType,
    }));

    const prompts: ExtractedPrompt[] = (promptsResult.prompts || []).map((p) => ({
      name: p.name,
      description: p.description,
      arguments: p.arguments as ExtractedPrompt['arguments'],
    }));

    const toolNameSet = new Set(tools.map((t) => t.name));
    // SEP-2640: skills are exposed under the singular `skill://` URI scheme.
    const hasSkillsResources = resourceTemplates.some(
      (rt) => rt.uriTemplate.startsWith('skill://'),
    );
    const capabilities: ExtractedCapabilities = {
      skills: hasSkillsResources,
      jobs:
        toolNameSet.has('execute_job') ||
        toolNameSet.has('get_job_status') ||
        toolNameSet.has('execute-job') ||
        toolNameSet.has('get-job-status'),
      workflows:
        toolNameSet.has('execute_workflow') ||
        toolNameSet.has('get_workflow_status') ||
        toolNameSet.has('execute-workflow') ||
        toolNameSet.has('get-workflow-status'),
    };

    // Extract job schemas if jobs capability is available
    let jobs: ExtractedJob[] = [];
    if (capabilities.jobs && client.listJobs) {
      try {
        const jobsResult = await client.listJobs();
        jobs = (jobsResult.jobs || []).map((j) => ({
          name: j.name,
          description: j.description,
          inputSchema: j.inputSchema,
          tags: j.tags,
        }));
      } catch {
        // Jobs listing not available at build time
      }
    }

    // Collect skill file assets for copying to dist
    let skillAssets: ExtractedSkillAsset[] = [];
    if (capabilities.skills && client.collectSkillAssets) {
      try {
        const manifest = await client.collectSkillAssets();
        skillAssets = manifest.entries.map((e) => ({
          skillName: e.skillName,
          baseDir: e.baseDir,
          instructionFile: e.instructionFile,
          resourceDirs: e.resources as ExtractedSkillAsset['resourceDirs'],
          description: e.description,
          tags: e.tags,
          license: e.license,
        }));
      } catch {
        // Skill asset collection not available
      }
    }

    // Extract http.port from the @FrontMcp() decorator metadata. Returns
    // undefined when the entry is a plain config object (no decorator) or
    // when no port was declared — caller falls back to its own default.
    let httpPort: number | undefined;
    try {
      const sdk = require('@frontmcp/sdk') as { getDecoratorConfig?: (t: unknown) => Record<string, unknown> | undefined };
      const cfg = typeof sdk.getDecoratorConfig === 'function'
        ? sdk.getDecoratorConfig(configOrClass)
        : (configOrClass as Record<string, unknown> | undefined);
      const http = cfg?.['http'] as { port?: number } | undefined;
      if (http && typeof http.port === 'number') httpPort = http.port;
    } catch {
      // best-effort — manifest writer will fall back to its own default
    }

    return { tools, resources, resourceTemplates, prompts, jobs, capabilities, skillAssets, httpPort };
  } finally {
    await client.close().catch(() => {});
  }
}
