/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { load } from 'js-yaml';
import * as fs from 'node:fs/promises';
import {} from 'node:fs';
import * as path from 'node:path';
import * as crypto from 'node:crypto';
import { z } from 'zod';
import { DEFAULT_MAX_TURNS, DEFAULT_MAX_TIME_MINUTES, } from './types.js';
import { MCPServerConfig } from '../config/config.js';
import { isValidToolName } from '../tools/tool-names.js';
import { FRONTMATTER_REGEX } from '../skills/skillLoader.js';
import { getErrorMessage } from '../utils/errors.js';
/**
 * Error thrown when an agent definition is invalid or cannot be loaded.
 */
export class AgentLoadError extends Error {
    filePath;
    constructor(filePath, message) {
        super(`Failed to load agent from ${filePath}: ${message}`);
        this.filePath = filePath;
        this.name = 'AgentLoadError';
    }
}
const nameSchema = z
    .string()
    .regex(/^[a-z0-9-_]+$/, 'Name must be a valid slug');
const mcpServerSchema = z.object({
    command: z.string().optional(),
    args: z.array(z.string()).optional(),
    env: z.record(z.string()).optional(),
    cwd: z.string().optional(),
    url: z.string().optional(),
    http_url: z.string().optional(),
    headers: z.record(z.string()).optional(),
    tcp: z.string().optional(),
    type: z.enum(['sse', 'http']).optional(),
    timeout: z.number().optional(),
    trust: z.boolean().optional(),
    description: z.string().optional(),
    include_tools: z.array(z.string()).optional(),
    exclude_tools: z.array(z.string()).optional(),
});
const localAgentSchema = z
    .object({
    kind: z.literal('local').optional().default('local'),
    name: nameSchema,
    description: z.string().min(1),
    display_name: z.string().optional(),
    tools: z
        .array(z
        .string()
        .refine((val) => isValidToolName(val, { allowWildcards: true }), {
        message: 'Invalid tool name',
    }))
        .optional(),
    mcp_servers: z.record(mcpServerSchema).optional(),
    model: z.string().optional(),
    temperature: z.number().optional(),
    max_turns: z.number().int().positive().optional(),
    timeout_mins: z.number().int().positive().optional(),
})
    .strict();
// Base fields shared by all auth configs.
const baseAuthFields = {};
const apiKeyAuthSchema = z.object({
    ...baseAuthFields,
    type: z.literal('apiKey'),
    key: z.string().min(1, 'API key is required'),
    name: z.string().optional(),
});
const httpAuthSchema = z.object({
    ...baseAuthFields,
    type: z.literal('http'),
    scheme: z.string().min(1),
    token: z.string().min(1).optional(),
    username: z.string().min(1).optional(),
    password: z.string().min(1).optional(),
    value: z.string().min(1).optional(),
});
const googleCredentialsAuthSchema = z.object({
    ...baseAuthFields,
    type: z.literal('google-credentials'),
    scopes: z.array(z.string()).optional(),
});
const oauth2AuthSchema = z.object({
    ...baseAuthFields,
    type: z.literal('oauth'),
    client_id: z.string().optional(),
    client_secret: z.string().optional(),
    scopes: z.array(z.string()).optional(),
    authorization_url: z.string().url().optional(),
    token_url: z.string().url().optional(),
});
const authConfigSchema = z
    .discriminatedUnion('type', [
    apiKeyAuthSchema,
    httpAuthSchema,
    googleCredentialsAuthSchema,
    oauth2AuthSchema,
])
    .superRefine((data, ctx) => {
    if (data.type === 'http') {
        if (data.value)
            return;
        if (data.scheme === 'Bearer') {
            if (!data.token) {
                ctx.addIssue({
                    code: z.ZodIssueCode.custom,
                    message: 'Bearer scheme requires "token"',
                    path: ['token'],
                });
            }
        }
        else if (data.scheme === 'Basic') {
            if (!data.username) {
                ctx.addIssue({
                    code: z.ZodIssueCode.custom,
                    message: 'Basic authentication requires "username"',
                    path: ['username'],
                });
            }
            if (!data.password) {
                ctx.addIssue({
                    code: z.ZodIssueCode.custom,
                    message: 'Basic authentication requires "password"',
                    path: ['password'],
                });
            }
        }
        else {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: `HTTP scheme "${data.scheme}" requires "value"`,
                path: ['value'],
            });
        }
    }
});
const baseRemoteAgentSchema = z.object({
    kind: z.literal('remote').optional().default('remote'),
    name: nameSchema,
    description: z.string().optional(),
    display_name: z.string().optional(),
    auth: authConfigSchema.optional(),
});
const remoteAgentUrlSchema = baseRemoteAgentSchema
    .extend({
    agent_card_url: z.string().url(),
    agent_card_json: z.undefined().optional(),
})
    .strict();
const remoteAgentJsonSchema = baseRemoteAgentSchema
    .extend({
    agent_card_url: z.undefined().optional(),
    agent_card_json: z.string().refine((val) => {
        try {
            JSON.parse(val);
            return true;
        }
        catch {
            return false;
        }
    }, { message: 'agent_card_json must be valid JSON' }),
})
    .strict();
const remoteAgentSchema = z.union([
    remoteAgentUrlSchema,
    remoteAgentJsonSchema,
]);
const agentUnionOptions = [
    { label: 'Local Agent' },
    { label: 'Remote Agent' },
    { label: 'Remote Agent' },
];
const remoteAgentsListSchema = z.array(remoteAgentSchema);
const markdownFrontmatterSchema = z.union([
    localAgentSchema,
    remoteAgentUrlSchema,
    remoteAgentJsonSchema,
]);
function guessIntendedKind(rawInput) {
    if (typeof rawInput !== 'object' || rawInput === null)
        return undefined;
    const input = rawInput;
    if (input.kind === 'local')
        return 'local';
    if (input.kind === 'remote')
        return 'remote';
    const hasLocalKeys = 'tools' in input ||
        'mcp_servers' in input ||
        'model' in input ||
        'temperature' in input ||
        'max_turns' in input ||
        'timeout_mins' in input;
    const hasRemoteKeys = 'agent_card_url' in input || 'auth' in input || 'agent_card_json' in input;
    if (hasLocalKeys && !hasRemoteKeys)
        return 'local';
    if (hasRemoteKeys && !hasLocalKeys)
        return 'remote';
    return undefined;
}
function formatZodError(error, context, rawInput) {
    const intendedKind = rawInput ? guessIntendedKind(rawInput) : undefined;
    const formatIssues = (issues, unionPrefix) => issues.flatMap((i) => {
        // Handle union errors specifically to give better context
        if (i.code === z.ZodIssueCode.invalid_union) {
            return i.unionErrors.flatMap((unionError, index) => {
                const label = unionPrefix
                    ? unionPrefix
                    : (agentUnionOptions[index]?.label ??
                        `Branch #${index + 1}`);
                if (intendedKind === 'local' && label === 'Remote Agent')
                    return [];
                if (intendedKind === 'remote' && label === 'Local Agent')
                    return [];
                return formatIssues(unionError.issues, label);
            });
        }
        const prefix = unionPrefix ? `(${unionPrefix}) ` : '';
        const path = i.path.length > 0 ? `${i.path.join('.')}: ` : '';
        return `${prefix}${path}${i.message}`;
    });
    const formatted = Array.from(new Set(formatIssues(error.issues))).join('\n');
    return `${context}:\n${formatted}`;
}
/**
 * Parses and validates an agent Markdown file with frontmatter.
 *
 * @param filePath Path to the Markdown file.
 * @param content Optional pre-loaded content of the file.
 * @returns An array containing the single parsed agent definition.
 * @throws AgentLoadError if parsing or validation fails.
 */
export async function parseAgentMarkdown(filePath, content) {
    let fileContent;
    if (content !== undefined) {
        fileContent = content;
    }
    else {
        try {
            fileContent = await fs.readFile(filePath, 'utf-8');
        }
        catch (error) {
            throw new AgentLoadError(filePath, `Could not read file: ${getErrorMessage(error)}`);
        }
    }
    // Split frontmatter and body
    const match = fileContent.match(FRONTMATTER_REGEX);
    if (!match) {
        throw new AgentLoadError(filePath, 'Invalid agent definition: Missing mandatory YAML frontmatter. Agent Markdown files MUST start with YAML frontmatter enclosed in triple-dashes "---" (e.g., ---\nname: my-agent\n---).');
    }
    const frontmatterStr = match[1];
    const body = match[2] || '';
    let rawFrontmatter;
    try {
        rawFrontmatter = load(frontmatterStr);
    }
    catch (error) {
        throw new AgentLoadError(filePath, `YAML frontmatter parsing failed: ${getErrorMessage(error)}`);
    }
    // Handle array of remote agents
    if (Array.isArray(rawFrontmatter)) {
        const result = remoteAgentsListSchema.safeParse(rawFrontmatter);
        if (!result.success) {
            throw new AgentLoadError(filePath, `Validation failed: ${formatZodError(result.error, 'Remote Agents List')}`);
        }
        return result.data.map((agent) => ({
            ...agent,
            kind: 'remote',
        }));
    }
    const result = markdownFrontmatterSchema.safeParse(rawFrontmatter);
    if (!result.success) {
        throw new AgentLoadError(filePath, `Validation failed: ${formatZodError(result.error, 'Agent Definition', rawFrontmatter)}`);
    }
    const frontmatter = result.data;
    if (frontmatter.kind === 'remote') {
        return [
            {
                ...frontmatter,
                kind: 'remote',
            },
        ];
    }
    // Construct the local agent definition
    return [
        {
            ...frontmatter,
            kind: 'local',
            system_prompt: body.trim(),
        },
    ];
}
/**
 * Converts frontmatter auth config to the internal A2AAuthConfig type.
 * This handles the mapping from snake_case YAML to the internal type structure.
 */
function convertFrontmatterAuthToConfig(frontmatter) {
    switch (frontmatter.type) {
        case 'apiKey':
            return {
                type: 'apiKey',
                key: frontmatter.key,
                name: frontmatter.name,
            };
        case 'google-credentials':
            return {
                type: 'google-credentials',
                scopes: frontmatter.scopes,
            };
        case 'http':
            if (frontmatter.value) {
                return {
                    type: 'http',
                    scheme: frontmatter.scheme,
                    value: frontmatter.value,
                };
            }
            switch (frontmatter.scheme) {
                case 'Bearer':
                    // Token is required by schema validation
                    return {
                        type: 'http',
                        scheme: 'Bearer',
                        token: frontmatter.token,
                    };
                case 'Basic':
                    // Username/password are required by schema validation
                    return {
                        type: 'http',
                        scheme: 'Basic',
                        username: frontmatter.username,
                        password: frontmatter.password,
                    };
                default:
                    throw new Error(`Unknown HTTP scheme: ${frontmatter.scheme}`);
            }
        case 'oauth':
            return {
                type: 'oauth2',
                client_id: frontmatter.client_id,
                client_secret: frontmatter.client_secret,
                scopes: frontmatter.scopes,
                authorization_url: frontmatter.authorization_url,
                token_url: frontmatter.token_url,
            };
        default: {
            const exhaustive = frontmatter;
            const raw = exhaustive;
            if (typeof raw === 'object' && raw !== null && 'type' in raw) {
                throw new Error(`Unknown auth type: ${String(raw['type'])}`);
            }
            throw new Error('Unknown auth type');
        }
    }
}
/**
 * Converts a FrontmatterAgentDefinition DTO to the internal AgentDefinition structure.
 *
 * @param markdown The parsed Markdown/Frontmatter definition.
 * @param metadata Optional metadata including hash and file path.
 * @returns The internal AgentDefinition.
 */
export function markdownToAgentDefinition(markdown, metadata) {
    const inputConfig = {
        inputSchema: {
            type: 'object',
            properties: {
                query: {
                    type: 'string',
                    description: 'The task for the agent.',
                },
            },
            // query is not required because it defaults to "Get Started!" if not provided
            required: [],
        },
    };
    if (markdown.kind === 'remote') {
        const base = {
            kind: 'remote',
            name: markdown.name,
            description: markdown.description || '',
            displayName: markdown.display_name,
            auth: markdown.auth
                ? convertFrontmatterAuthToConfig(markdown.auth)
                : undefined,
            inputConfig,
            metadata,
        };
        if ('agent_card_json' in markdown &&
            markdown.agent_card_json !== undefined) {
            base.agentCardJson = markdown.agent_card_json;
            return base;
        }
        if ('agent_card_url' in markdown && markdown.agent_card_url !== undefined) {
            base.agentCardUrl = markdown.agent_card_url;
            return base;
        }
        throw new AgentLoadError(metadata?.filePath || 'unknown', 'Unexpected state: neither agent_card_json nor agent_card_url present on remote agent');
    }
    // If a model is specified, use it. Otherwise, inherit
    const modelName = markdown.model || 'inherit';
    const mcpServers = {};
    if (markdown.mcp_servers) {
        for (const [name, config] of Object.entries(markdown.mcp_servers)) {
            mcpServers[name] = new MCPServerConfig(config.command, config.args, config.env, config.cwd, config.url, config.http_url, config.headers, config.tcp, config.type, config.timeout, config.trust, config.description, config.include_tools, config.exclude_tools);
        }
    }
    return {
        kind: 'local',
        name: markdown.name,
        description: markdown.description,
        displayName: markdown.display_name,
        promptConfig: {
            systemPrompt: markdown.system_prompt,
            query: '${query}',
        },
        modelConfig: {
            model: modelName,
            generateContentConfig: {
                temperature: markdown.temperature ?? 1,
                topP: 0.95,
            },
        },
        runConfig: {
            maxTurns: markdown.max_turns ?? DEFAULT_MAX_TURNS,
            maxTimeMinutes: markdown.timeout_mins ?? DEFAULT_MAX_TIME_MINUTES,
        },
        toolConfig: markdown.tools
            ? {
                tools: markdown.tools,
            }
            : undefined,
        mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined,
        inputConfig,
        metadata,
    };
}
/**
 * Loads all agents from a specific directory.
 * Ignores files starting with _ and non-supported extensions.
 * Supported extensions: .md
 *
 * @param dir Directory path to scan.
 * @returns Object containing successfully loaded agents and any errors.
 */
export async function loadAgentsFromDirectory(dir) {
    const result = {
        agents: [],
        errors: [],
    };
    let dirEntries;
    try {
        dirEntries = await fs.readdir(dir, { withFileTypes: true });
    }
    catch (error) {
        // If directory doesn't exist, just return empty
        if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
            return result;
        }
        result.errors.push(new AgentLoadError(dir, `Could not list directory: ${getErrorMessage(error)}`));
        return result;
    }
    const files = dirEntries.filter((entry) => entry.isFile() &&
        !entry.name.startsWith('_') &&
        entry.name.endsWith('.md'));
    for (const entry of files) {
        const filePath = path.join(dir, entry.name);
        try {
            const content = await fs.readFile(filePath, 'utf-8');
            const hash = crypto.createHash('sha256').update(content).digest('hex');
            const agentDefs = await parseAgentMarkdown(filePath, content);
            for (const def of agentDefs) {
                const agent = markdownToAgentDefinition(def, { hash, filePath });
                result.agents.push(agent);
            }
        }
        catch (error) {
            if (error instanceof AgentLoadError) {
                result.errors.push(error);
            }
            else {
                result.errors.push(new AgentLoadError(filePath, `Unexpected error: ${getErrorMessage(error)}`));
            }
        }
    }
    return result;
}
//# sourceMappingURL=agentLoader.js.map