import type { Block, GithubVersion, Program, Value } from '@promptscript/core';
import { BaseFormatter } from '../base-formatter.js';
import type { ConventionRenderer } from '../convention-renderer.js';
import { GlobCategorizer } from '../extractors/glob-categorizer.js';
import type { FormatOptions, FormatterOutput } from '../types.js';

/**
 * GitHub formatter version information.
 */
export const GITHUB_VERSIONS = {
  simple: {
    name: 'simple',
    description: 'Single file output (.github/copilot-instructions.md)',
    outputPath: '.github/copilot-instructions.md',
  },
  multifile: {
    name: 'multifile',
    description: 'Main + path-specific instructions (.github/instructions/) + prompts',
    outputPath: '.github/copilot-instructions.md',
  },
  full: {
    name: 'full',
    description: 'Multifile + skills (.github/skills/) + agents (.github/agents/) + AGENTS.md',
    outputPath: '.github/copilot-instructions.md',
  },
} as const;

/**
 * Configuration for a path-specific instruction file.
 */
interface InstructionConfig {
  /** File name (without extension) */
  name: string;
  /** Glob patterns this instruction applies to */
  applyTo: string[];
  /** Optional agent exclusion */
  excludeAgent?: string;
  /** Description for the instruction */
  description: string;
  /** Content of the instruction */
  content: string;
  /** Resolved guard dependencies (injected by resolver) */
  __resolvedRequires?: Array<{ name: string; content: string }>;
}

/**
 * Configuration for a prompt file.
 */
interface PromptConfig {
  /** Prompt name */
  name: string;
  /** Description */
  description: string;
  /** Whether to run in agent mode */
  mode?: 'agent';
  /** Allowed tools in agent mode */
  tools?: string[];
  /** Handoff definitions for delegation to other agents */
  handoffs?: GitHubHandoff[];
  /** Prompt content */
  content: string;
}

/**
 * Configuration for a skill.
 */
interface SkillConfig {
  /** Skill name */
  name: string;
  /** Description */
  description: string;
  /** Whether to disable model invocation */
  disableModelInvocation?: boolean;
  /** Optional argument hint */
  argumentHint?: string;
  /** Skill content/instructions */
  content: string;
  /** Resource files to copy alongside SKILL.md */
  resources?: Array<{ relativePath: string; content: string }>;
  /** Raw frontmatter from source SKILL.md for pass-through */
  rawFrontmatter?: string;
  /** Optional relative output directory underneath .github/ */
  outputDir?: string;
}

/**
 * Configuration for a GitHub Copilot custom agent.
 *
 * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-custom-agents
 */
interface GitHubAgentConfig {
  /** Agent name (used for filename) */
  name: string;
  /** Description of what the agent does */
  description: string;
  /** Tools the agent can use */
  tools?: string[];
  /** AI model to use (e.g., 'gpt-4o', 'claude-3.5-sonnet') */
  model?: string;
  /** Model for Specification/planning mode (mixed models) */
  specModel?: string;
  /** System prompt content */
  content: string;
  /** Handoff definitions for delegation to other agents */
  handoffs?: GitHubHandoff[];
}

/**
 * Configuration for a handoff to another agent.
 */
interface GitHubHandoff {
  /** Display label for the handoff */
  label: string;
  /** Target agent name */
  agent: string;
  /** Prompt/instructions for the handoff */
  prompt: string;
  /** Whether to auto-send the handoff */
  send?: boolean;
}

/**
 * Mapping from PromptScript/Claude Code tool names to GitHub Copilot canonical tool names.
 *
 * GitHub Copilot uses specific tool names that differ from Claude Code aliases.
 * @see https://docs.github.com/en/copilot/reference/custom-agents-configuration
 */
const TOOL_NAME_MAPPING: Record<string, string> = {
  // Read tools
  Read: 'read',
  NotebookRead: 'read',
  // Edit tools
  Edit: 'edit',
  MultiEdit: 'edit',
  Write: 'edit',
  NotebookEdit: 'edit',
  // Search tools
  Grep: 'search',
  Glob: 'search',
  // Execute tools
  Bash: 'execute',
  // Web tools
  WebFetch: 'web',
  WebSearch: 'web',
  // Agent tools
  Task: 'agent',
  // Todo tools
  TodoWrite: 'todo',
  TodoRead: 'todo',
};

/**
 * Mapping from PromptScript/Claude Code model names to GitHub Copilot model names.
 *
 * GitHub Copilot uses full model names (e.g., "Claude Sonnet 4") while Claude Code
 * uses short aliases (e.g., "sonnet").
 *
 * @see https://code.visualstudio.com/docs/copilot/customization/custom-agents
 */
const MODEL_NAME_MAPPING: Record<string, string> = {
  // Claude models - default aliases map to latest versions (4.5)
  sonnet: 'Claude Sonnet 4.5',
  opus: 'Claude Opus 4.5',
  haiku: 'Claude Haiku 4.5',
  // Explicit version mappings
  'sonnet-4': 'Claude Sonnet 4',
  'sonnet-4.5': 'Claude Sonnet 4.5',
  'opus-4': 'Claude Opus 4',
  'opus-4.5': 'Claude Opus 4.5',
  'haiku-4': 'Claude Haiku 4',
  'haiku-4.5': 'Claude Haiku 4.5',
  // OpenAI models (pass through if already in correct format)
  'gpt-4o': 'GPT-4o',
  'gpt-4.1': 'GPT-4.1',
  'gpt-5': 'GPT-5',
  'gpt-5-mini': 'GPT-5 mini',
  // Special values
  inherit: '', // Empty string means omit the model property
  auto: 'Auto',
};

/**
 * Formatter for GitHub Copilot instructions.
 *
 * Supports three versions:
 * - **simple**: Single `.github/copilot-instructions.md` file
 * - **multifile**: Main + `.github/instructions/*.instructions.md` + `.github/prompts/*.prompt.md`
 * - **full** (default): Multifile + `.github/skills/<name>/SKILL.md` + `AGENTS.md`
 *
 * @example
 * ```yaml
 * targets:
 *   - github  # uses full mode (default)
 *   - github:
 *       version: multifile
 *   - github:
 *       version: full
 * ```
 *
 * @see https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot
 */
export class GitHubFormatter extends BaseFormatter {
  readonly name = 'github';
  readonly outputPath = '.github/copilot-instructions.md';
  readonly description = 'GitHub Copilot instructions (Markdown)';
  readonly defaultConvention = 'markdown';

  /**
   * Get supported versions for this formatter.
   */
  static getSupportedVersions(): typeof GITHUB_VERSIONS {
    return GITHUB_VERSIONS;
  }

  override getSkillBasePath(): string | null {
    return '.github/skills';
  }

  override getSkillFileName(): string | null {
    return 'SKILL.md';
  }

  override referencesMode(): 'directory' | 'inline' | 'none' {
    return 'directory';
  }

  format(ast: Program, options?: FormatOptions): FormatterOutput {
    const version = this.resolveVersion(options?.version);

    if (version === 'full') {
      return this.formatFull(ast, options);
    }

    if (version === 'multifile') {
      return this.formatMultifile(ast, options);
    }

    return this.formatSimple(ast, options);
  }

  /**
   * Resolve version string to GithubVersion.
   */
  private resolveVersion(version?: string): GithubVersion {
    if (version === 'simple') return 'simple';
    if (version === 'multifile') return 'multifile';
    return 'full';
  }

  // ============================================================
  // Simple Mode (single file)
  // ============================================================

  private formatSimple(ast: Program, options?: FormatOptions): FormatterOutput {
    const renderer = this.createRenderer(options);
    const sections: string[] = [];

    if (renderer.getConvention().name === 'markdown') {
      sections.push(this.header(ast));
    }

    this.addCommonSections(ast, renderer, sections);

    return {
      path: this.getOutputPath(options),
      content: sections.join('\n\n') + '\n',
    };
  }

  // ============================================================
  // Multifile Mode
  // ============================================================

  private formatMultifile(ast: Program, options?: FormatOptions): FormatterOutput {
    const renderer = this.createRenderer(options);
    const additionalFiles: FormatterOutput[] = [];

    // Generate path-specific instruction files
    const instructions = this.extractInstructions(ast);
    for (const instruction of instructions) {
      additionalFiles.push(this.generateInstructionFile(instruction));
    }

    // Generate prompt files
    const prompts = this.extractPrompts(ast);
    for (const prompt of prompts) {
      additionalFiles.push(this.generatePromptFile(prompt));
    }

    // Main file content
    const sections: string[] = [];
    if (renderer.getConvention().name === 'markdown') {
      sections.push(this.header(ast));
    }
    this.addCommonSections(ast, renderer, sections);

    return {
      path: this.getOutputPath(options),
      content: sections.join('\n\n') + '\n',
      additionalFiles: additionalFiles.length > 0 ? additionalFiles : undefined,
    };
  }

  // ============================================================
  // Full Mode
  // ============================================================

  private formatFull(ast: Program, options?: FormatOptions): FormatterOutput {
    const renderer = this.createRenderer(options);
    const additionalFiles: FormatterOutput[] = [];

    // Generate path-specific instruction files
    const instructions = this.extractInstructions(ast);
    for (const instruction of instructions) {
      additionalFiles.push(this.generateInstructionFile(instruction));
    }

    // Generate prompt files
    const prompts = this.extractPrompts(ast);
    for (const prompt of prompts) {
      additionalFiles.push(this.generatePromptFile(prompt));
    }

    // Generate skill files
    const skills = this.extractSkills(ast, options);
    for (const skill of skills) {
      additionalFiles.push(this.generateSkillFile(skill, options));
    }

    // Generate custom agent files (.github/agents/)
    const customAgents = this.extractCustomAgents(ast);
    for (const agent of customAgents) {
      additionalFiles.push(this.generateCustomAgentFile(agent));
    }

    // Generate AGENTS.md
    const agentsFile = this.generateAgentsFile(ast);
    if (agentsFile) {
      additionalFiles.push(agentsFile);
    }

    // Main file content
    const sections: string[] = [];
    if (renderer.getConvention().name === 'markdown') {
      sections.push(this.header(ast));
    }
    this.addCommonSections(ast, renderer, sections);

    return {
      path: this.getOutputPath(options),
      content: sections.join('\n\n') + '\n',
      additionalFiles: additionalFiles.length > 0 ? additionalFiles : undefined,
    };
  }

  // ============================================================
  // Instruction File Generation
  // ============================================================

  /**
   * Extract instruction configurations from @guards block.
   */
  private extractInstructions(ast: Program): InstructionConfig[] {
    const guards = this.findBlock(ast, 'guards');
    if (!guards) return [];

    const instructions: InstructionConfig[] = [];
    const props = this.getProps(guards.content);

    // Handle globs property via GlobCategorizer
    const globPatterns = props['globs'];
    if (Array.isArray(globPatterns)) {
      const standards = this.findBlock(ast, 'standards');
      const categorizer = new GlobCategorizer(this.standardsExtractor);
      const categorized = categorizer.categorize(
        globPatterns.filter((p): p is string => typeof p === 'string'),
        standards?.content ?? null
      );

      for (const cat of categorized) {
        instructions.push({
          name: cat.name,
          applyTo: cat.patterns,
          description: cat.description,
          content: cat.content,
        });
      }
    }

    // Handle named instruction blocks within guards
    for (const [key, value] of Object.entries(props)) {
      if (key === 'globs') continue;

      if (value && typeof value === 'object' && !Array.isArray(value)) {
        if (!this.isSafeSkillName(key)) continue;
        const obj = value as Record<string, Value>;
        const applyTo = obj['applyTo'];
        const excludeAgent = obj['excludeAgent'];
        const description = obj['description'];
        const content = obj['content'];

        if (applyTo && Array.isArray(applyTo)) {
          const resolved = obj['__resolvedRequires'];
          instructions.push({
            name: key,
            applyTo: applyTo.map((p) => this.valueToString(p)),
            excludeAgent: excludeAgent ? this.valueToString(excludeAgent) : undefined,
            description: description ? this.valueToString(description) : `${key} rules`,
            content: content ? this.valueToString(content) : '',
            __resolvedRequires: Array.isArray(resolved)
              ? (resolved as Array<{ name: string; content: string }>)
              : undefined,
          });
        }
      }
    }

    return instructions;
  }

  /**
   * Generate a .github/instructions/*.instructions.md file.
   */
  private generateInstructionFile(config: InstructionConfig): FormatterOutput {
    const lines: string[] = [];

    // YAML frontmatter
    lines.push('---');
    lines.push(`applyTo:`);
    for (const pattern of config.applyTo) {
      lines.push(`  - "${pattern}"`);
    }
    if (config.excludeAgent) {
      lines.push(`excludeAgent: "${config.excludeAgent}"`);
    }
    lines.push('---');
    lines.push('');
    lines.push(`# ${config.description}`);
    lines.push('');
    if (config.content) {
      // Dedent content and normalize for Prettier compatibility
      const dedentedContent = this.dedent(config.content);
      const normalizedContent = this.normalizeMarkdownForPrettier(dedentedContent);
      lines.push(normalizedContent);
    }

    // Append required context from resolved guard dependencies
    const resolvedReqs = Array.isArray(config.__resolvedRequires)
      ? (config.__resolvedRequires as unknown[]).filter(
          (d): d is { name: string; content: string } =>
            d != null &&
            typeof d === 'object' &&
            typeof (d as Record<string, unknown>)['name'] === 'string' &&
            typeof (d as Record<string, unknown>)['content'] === 'string'
        )
      : [];
    if (resolvedReqs.length > 0) {
      lines.push('');
      lines.push('## Required Context');
      lines.push('');
      for (const dep of resolvedReqs) {
        // Sanitize dep.name: strip heading markers, newlines, and YAML frontmatter separators
        const safeName = dep.name
          .replace(/^#+\s*/g, '')
          .replace(/[\r\n]/g, ' ')
          .replace(/---/g, '—');
        lines.push(`### ${safeName}`);
        lines.push('');
        // Sanitize dep.content: dedent and strip leading --- that could be mistaken for frontmatter
        const safeContent = this.dedent(dep.content).replace(/^---/gm, '\\---');
        lines.push(safeContent);
        lines.push('');
      }
    }

    return {
      path: `.github/instructions/${config.name}.instructions.md`,
      content: lines.join('\n'),
    };
  }

  // ============================================================
  // Prompt File Generation
  // ============================================================

  /**
   * Extract prompt configurations from @shortcuts block.
   */
  private extractPrompts(ast: Program): PromptConfig[] {
    const shortcuts = this.findBlock(ast, 'shortcuts');
    if (!shortcuts) return [];

    const prompts: PromptConfig[] = [];
    const props = this.getProps(shortcuts.content);

    for (const [name, value] of Object.entries(props)) {
      if (value && typeof value === 'object' && !Array.isArray(value)) {
        const obj = value as Record<string, Value>;

        // Check if this shortcut should be a prompt file
        if (obj['prompt'] === true || obj['type'] === 'prompt') {
          const handoffs = this.extractHandoffs(obj['handoffs']);
          prompts.push({
            name,
            description: obj['description'] ? this.valueToString(obj['description']) : name,
            mode: obj['mode'] === 'agent' ? 'agent' : undefined,
            tools:
              obj['tools'] && Array.isArray(obj['tools'])
                ? obj['tools'].map((t) => this.valueToString(t))
                : undefined,
            handoffs: handoffs.length > 0 ? handoffs : undefined,
            content: obj['content'] ? this.valueToString(obj['content']) : '',
          });
        }
      }
    }

    return prompts;
  }

  /**
   * Generate a .github/prompts/*.prompt.md file.
   */
  private generatePromptFile(config: PromptConfig): FormatterOutput {
    const lines: string[] = [];

    // YAML frontmatter - use single quotes (Prettier default for YAML strings)
    lines.push('---');
    // Use double quotes if description contains apostrophe, single quotes otherwise
    const descQuote = config.description.includes("'") ? '"' : "'";
    lines.push(`description: ${descQuote}${config.description}${descQuote}`);
    if (config.mode === 'agent') {
      lines.push('mode: agent');
    }
    if (config.tools && config.tools.length > 0) {
      // Map tool names and use inline YAML array format
      const mappedTools = config.tools
        .map((tool) => TOOL_NAME_MAPPING[tool] ?? tool.toLowerCase())
        .filter((t, i, arr) => arr.indexOf(t) === i); // deduplicate
      const toolsArray = mappedTools.map((t) => `'${t}'`).join(', ');
      lines.push(`tools: [${toolsArray}]`);
    }
    if (config.handoffs && config.handoffs.length > 0) {
      lines.push(...this.renderHandoffsYaml(config.handoffs));
    }
    lines.push('---');
    lines.push('');
    if (config.content) {
      // Dedent content and normalize for Prettier compatibility
      const dedentedContent = this.dedent(config.content);
      const normalizedContent = this.normalizeMarkdownForPrettier(dedentedContent);
      lines.push(normalizedContent);
    }

    // Strip leading slashes from name for clean file paths
    const cleanName = config.name.replace(/^\/+/, '');
    return {
      path: `.github/prompts/${cleanName}.prompt.md`,
      content: lines.join('\n') + '\n',
    };
  }

  // ============================================================
  // Skill File Generation
  // ============================================================

  /**
   * Extract skill configurations from @skills block.
   */
  private extractSkills(ast: Program, options?: FormatOptions): SkillConfig[] {
    const skillsBlock = this.findBlock(ast, 'skills');
    if (!skillsBlock) return [];

    const skills: SkillConfig[] = [];
    const props = this.getProps(skillsBlock.content);

    for (const [name, value] of Object.entries(props)) {
      if (value && typeof value === 'object' && !Array.isArray(value)) {
        if (!this.shouldIncludeSkill(name, options)) continue;
        const obj = value as Record<string, Value>;
        skills.push({
          name,
          description: obj['description'] ? this.valueToString(obj['description']) : name,
          disableModelInvocation: obj['disableModelInvocation'] === true,
          argumentHint: obj['argumentHint'] ? this.valueToString(obj['argumentHint']) : undefined,
          content: obj['content'] ? this.valueToString(obj['content']) : '',
          resources:
            obj['resources'] && Array.isArray(obj['resources'])
              ? (obj['resources'] as Array<Record<string, Value>>).map((r) => ({
                  relativePath: r['relativePath'] as string,
                  content: r['content'] as string,
                }))
              : undefined,
          rawFrontmatter:
            typeof obj['__rawFrontmatter'] === 'string' ? obj['__rawFrontmatter'] : undefined,
          outputDir: typeof obj['__outputDir'] === 'string' ? obj['__outputDir'] : undefined,
        });
      }
    }

    return skills;
  }

  /**
   * Generate a .github/skills/<name>/SKILL.md file.
   */
  private generateSkillFile(config: SkillConfig, options?: FormatOptions): FormatterOutput {
    const lines: string[] = [];

    // YAML frontmatter
    lines.push('---');
    if (config.rawFrontmatter) {
      lines.push(config.rawFrontmatter);
    } else {
      lines.push(`name: ${config.name}`);
      // Use double quotes if description contains apostrophe, single quotes otherwise
      const descQuote = config.description.includes("'") ? '"' : "'";
      lines.push(`description: ${descQuote}${config.description}${descQuote}`);
      if (config.disableModelInvocation) {
        lines.push('disable-model-invocation: true');
      }
      if (config.argumentHint) {
        const hintQuote = config.argumentHint.includes("'") ? '"' : "'";
        lines.push(`argument-hint: ${hintQuote}${config.argumentHint}${hintQuote}`);
      }
    }
    lines.push('---');
    lines.push('');
    if (config.content) {
      // First dedent (using lines 2+ for indent calculation since line 1 may be trimmed)
      // Then normalize for Prettier compatibility
      const dedentedContent = this.dedent(config.content);
      const normalizedContent = this.normalizeMarkdownForPrettier(dedentedContent);
      lines.push(normalizedContent);
    }

    // Strip leading slashes from name for clean file paths
    const cleanName = config.name.replace(/^\/+/, '');
    const skillDirPath = this.resolveSkillDir(
      '.github/skills',
      cleanName,
      config.outputDir,
      options
    );
    const resourceFiles = this.sanitizeResourceFiles(config.resources, skillDirPath);

    const resourcesWithProvenance = resourceFiles.map((f) => {
      if (f.path.includes('/references/')) {
        return {
          ...f,
          content: `${this.referenceProvenance(f.path)}\n\n${f.content}`,
        };
      }
      return f;
    });

    return {
      path: `${skillDirPath}/SKILL.md`,
      content: lines.join('\n') + '\n',
      additionalFiles: resourcesWithProvenance.length > 0 ? resourcesWithProvenance : undefined,
    };
  }

  // ============================================================
  // AGENTS.md Generation
  // ============================================================

  /**
   * Generate AGENTS.md file with agent-specific instructions.
   */
  private generateAgentsFile(ast: Program): FormatterOutput | null {
    const identity = this.findBlock(ast, 'identity');
    if (!identity) return null;

    const lines: string[] = [];
    lines.push('# Agent Instructions');
    lines.push('');

    // Add identity content
    // Use stripAllIndent for AGENTS.md since identity content comes from merged sources with inconsistent indentation
    const identityText = this.extractText(identity.content);
    if (identityText) {
      lines.push('## Identity');
      lines.push('');
      lines.push(this.stripAllIndent(identityText));
      lines.push('');
    }

    // Add context if available
    const context = this.findBlock(ast, 'context');
    if (context) {
      const contextText = this.extractText(context.content);
      if (contextText) {
        lines.push('## Context');
        lines.push('');
        lines.push(this.stripAllIndent(contextText));
        lines.push('');
      }
    }

    // Add restrictions if available
    const restrictions = this.findBlock(ast, 'restrictions');
    if (restrictions) {
      const items = this.extractRestrictionItems(restrictions.content);
      if (items.length > 0) {
        lines.push('## Restrictions');
        lines.push('');
        for (const item of items) {
          lines.push(`- ${item}`);
        }
        // No trailing empty line - we add '\n' at the end
      }
    }

    return {
      path: 'AGENTS.md',
      content: lines.join('\n') + '\n',
    };
  }

  /**
   * Extract restriction items from block content.
   */
  private extractRestrictionItems(content: Block['content']): string[] {
    if (content.type === 'ArrayContent') {
      return content.elements.map((item) => this.valueToString(item));
    }

    if (content.type === 'TextContent') {
      return content.value
        .trim()
        .split('\n')
        .map((line) => line.trim().replace(/^-\s*/, ''))
        .filter((line) => line.length > 0);
    }

    if (content.type === 'ObjectContent') {
      const itemsArray = this.getProp(content, 'items');
      if (Array.isArray(itemsArray)) {
        return itemsArray.map((item: unknown) => this.valueToString(item as Value));
      }
    }

    return [];
  }

  // ============================================================
  // Custom Agent File Generation (.github/agents/)
  // ============================================================

  /**
   * Extract custom agent configurations from @agents block.
   *
   * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-custom-agents
   */
  private extractCustomAgents(ast: Program): GitHubAgentConfig[] {
    const agentsBlock = this.findBlock(ast, 'agents');
    if (!agentsBlock) return [];

    const agents: GitHubAgentConfig[] = [];
    const props = this.getProps(agentsBlock.content);

    for (const [name, value] of Object.entries(props)) {
      if (value && typeof value === 'object' && !Array.isArray(value)) {
        const obj = value as Record<string, Value>;
        const description = obj['description'] ? this.valueToString(obj['description']) : '';
        if (!description) continue; // description is required

        const handoffs = this.extractHandoffs(obj['handoffs']);
        agents.push({
          name,
          description,
          tools: this.parseToolsArray(obj['tools']),
          model: obj['model'] ? this.valueToString(obj['model']) : undefined,
          specModel: obj['specModel'] ? this.valueToString(obj['specModel']) : undefined,
          handoffs: handoffs.length > 0 ? handoffs : undefined,
          content: obj['content'] ? this.valueToString(obj['content']) : '',
        });
      }
    }

    return agents;
  }

  /**
   * Parse tools array from a Value and map to GitHub Copilot canonical names.
   *
   * @see https://docs.github.com/en/copilot/reference/custom-agents-configuration
   */
  private parseToolsArray(value: Value | undefined): string[] | undefined {
    if (!value || !Array.isArray(value)) return undefined;

    const mapped = value
      .map((v) => this.valueToString(v))
      .filter((s) => s.length > 0)
      .map((tool) => TOOL_NAME_MAPPING[tool] ?? tool.toLowerCase());

    // Deduplicate (e.g., Grep and Glob both map to 'search')
    const unique = [...new Set(mapped)];

    return unique.length > 0 ? unique : undefined;
  }

  /**
   * Extract handoff definitions from a handoffs value.
   */
  private extractHandoffs(value: Value | undefined): GitHubHandoff[] {
    if (!value || !Array.isArray(value)) return [];

    const handoffs: GitHubHandoff[] = [];
    for (const item of value) {
      if (item && typeof item === 'object' && !Array.isArray(item)) {
        const obj = item as Record<string, Value>;
        const label = obj['label'] ? this.valueToString(obj['label']) : '';
        const agent = obj['agent'] ? this.valueToString(obj['agent']) : '';
        const prompt = obj['prompt'] ? this.valueToString(obj['prompt']) : '';

        if (label && agent) {
          handoffs.push({
            label,
            agent,
            prompt,
            send: obj['send'] === true ? true : undefined,
          });
        }
      }
    }

    return handoffs;
  }

  /**
   * Render handoffs as YAML lines for frontmatter.
   */
  private renderHandoffsYaml(handoffs: GitHubHandoff[]): string[] {
    const lines: string[] = [];
    lines.push('handoffs:');
    for (const handoff of handoffs) {
      // Use double quotes if label contains apostrophe, single quotes otherwise
      const labelQuote = handoff.label.includes("'") ? '"' : "'";
      lines.push(`  - label: ${labelQuote}${handoff.label}${labelQuote}`);
      lines.push(`    agent: ${handoff.agent}`);
      if (handoff.prompt) {
        const promptQuote = handoff.prompt.includes("'") ? '"' : "'";
        lines.push(`    prompt: ${promptQuote}${handoff.prompt}${promptQuote}`);
      }
      if (handoff.send) {
        lines.push('    send: true');
      }
    }
    return lines;
  }

  /**
   * Map a model name from PromptScript/Claude Code format to GitHub Copilot format.
   *
   * @returns The mapped model name, or undefined if the model should be omitted (e.g., "inherit")
   */
  private mapModelName(model: string | undefined): string | undefined {
    if (!model) return undefined;

    const mapped = MODEL_NAME_MAPPING[model.toLowerCase()];

    // If mapped to empty string, omit the model property
    if (mapped === '') return undefined;

    // If we have a mapping, use it; otherwise pass through as-is
    // (allows users to specify GitHub Copilot model names directly)
    return mapped ?? model;
  }

  /**
   * Generate a .github/agents/<name>.md file.
   *
   * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-custom-agents
   */
  private generateCustomAgentFile(config: GitHubAgentConfig): FormatterOutput {
    const lines: string[] = [];

    // YAML frontmatter
    lines.push('---');
    lines.push(`name: ${config.name}`);
    lines.push(`description: ${config.description}`);

    if (config.tools && config.tools.length > 0) {
      // Use inline YAML array format as per GitHub Copilot spec
      const toolsArray = config.tools.map((t) => `'${t}'`).join(', ');
      lines.push(`tools: [${toolsArray}]`);
    }

    // Map model name to GitHub Copilot format
    const mappedModel = this.mapModelName(config.model);
    if (mappedModel) {
      lines.push(`model: ${mappedModel}`);
    }

    // Map spec mode model name (mixed models)
    const mappedSpecModel = this.mapModelName(config.specModel);
    if (mappedSpecModel) {
      lines.push(`specModel: ${mappedSpecModel}`);
    }

    if (config.handoffs && config.handoffs.length > 0) {
      lines.push(...this.renderHandoffsYaml(config.handoffs));
    }

    lines.push('---');
    lines.push('');

    if (config.content) {
      // Dedent content and normalize for Prettier compatibility
      const dedentedContent = this.dedent(config.content);
      const normalizedContent = this.normalizeMarkdownForPrettier(dedentedContent);
      lines.push(normalizedContent);
    }

    // Strip leading slashes from name for clean file paths
    const cleanName = config.name.replace(/^\/+/, '');
    return {
      path: `.github/agents/${cleanName}.md`,
      content: lines.join('\n') + '\n',
    };
  }

  // ============================================================
  // Common Section Methods
  // ============================================================

  private addCommonSections(ast: Program, renderer: ConventionRenderer, sections: string[]): void {
    const project = this.project(ast, renderer);
    if (project) sections.push(project);

    const techStack = this.techStack(ast, renderer);
    if (techStack) sections.push(techStack);

    const architecture = this.architecture(ast, renderer);
    if (architecture) sections.push(architecture);

    const codeStandards = this.codeStandards(ast, renderer);
    if (codeStandards) sections.push(codeStandards);

    const shortcuts = this.shortcutsSection(ast, renderer);
    if (shortcuts) sections.push(shortcuts);

    const commands = this.commands(ast, renderer);
    if (commands) sections.push(commands);

    const gitCommits = this.gitCommits(ast, renderer);
    if (gitCommits) sections.push(gitCommits);

    const configFiles = this.configFiles(ast, renderer);
    if (configFiles) sections.push(configFiles);

    const documentation = this.documentation(ast, renderer);
    if (documentation) sections.push(documentation);

    const postWork = this.postWork(ast, renderer);
    if (postWork) sections.push(postWork);

    const restrictions = this.restrictions(ast, renderer);
    if (restrictions) sections.push(restrictions);

    const diagrams = this.diagrams(ast, renderer);
    if (diagrams) sections.push(diagrams);

    const knowledge = this.knowledgeContent(ast, renderer);
    if (knowledge) sections.push(knowledge);

    const examples = this.examples(ast, renderer);
    if (examples) sections.push(examples);
  }

  private header(_ast: Program): string {
    return `# GitHub Copilot Instructions`;
  }

  private project(ast: Program, renderer: ConventionRenderer): string | null {
    const identity = this.findBlock(ast, 'identity');

    let content = '';
    if (identity) {
      content = this.extractText(identity.content);
    } else {
      const context = this.findBlock(ast, 'context');
      if (context?.content.type === 'MixedContent' && context.content.text) {
        content = context.content.text.value.trim();
      }
    }

    if (!content) return null;

    // Apply stripAllIndent to normalize merged identity content for Prettier compatibility
    return renderer.renderSection('project', this.stripAllIndent(content));
  }

  private techStack(ast: Program, renderer: ConventionRenderer): string | null {
    const context = this.findBlock(ast, 'context');
    if (context) {
      const items = this.extractTechStackFromContext(context);
      if (items.length > 0) {
        const content = renderer.renderList(items);
        return renderer.renderSection('tech-stack', content);
      }
    }

    // Fallback: extract from @standards.code (languages, frameworks, testing)
    const standards = this.findBlock(ast, 'standards');
    if (standards) {
      const items = this.extractTechStackFromStandards(standards);
      if (items.length > 0) {
        const content = renderer.renderList(items);
        return renderer.renderSection('tech-stack', content);
      }
    }

    return null;
  }

  /**
   * Extract tech stack items from @context block.
   */
  private extractTechStackFromContext(
    context: NonNullable<ReturnType<typeof this.findBlock>>
  ): string[] {
    const props = this.getProps(context.content);
    const items: string[] = [];

    const languages = props['languages'];
    if (languages) {
      const arr = Array.isArray(languages) ? languages : [languages];
      items.push(`**Language:** ${this.formatTechItem(arr)}`);
    }

    const runtime = props['runtime'];
    if (runtime) {
      items.push(`**Runtime:** ${this.valueToString(runtime)}`);
    }

    const monorepo = props['monorepo'];
    if (monorepo && typeof monorepo === 'object' && !Array.isArray(monorepo)) {
      const mr = monorepo as Record<string, Value>;
      const tool = mr['tool'];
      const pm = mr['packageManager'];
      if (tool && pm) {
        items.push(
          `**Monorepo:** ${this.valueToString(tool)} with ${this.valueToString(pm)} workspaces`
        );
      }
    }

    return items;
  }

  /**
   * Extract tech stack items from @standards.code (languages, frameworks, testing).
   */
  private extractTechStackFromStandards(
    standards: NonNullable<ReturnType<typeof this.findBlock>>
  ): string[] {
    const code = this.getProp(standards.content, 'code');
    if (!code || typeof code !== 'object' || Array.isArray(code)) return [];

    const codeObj = code as Record<string, Value>;
    const items: string[] = [];

    for (const key of ['languages', 'frameworks', 'testing']) {
      const val = codeObj[key];
      if (val) items.push(...(Array.isArray(val) ? val : [val]).map(String));
    }

    return items;
  }

  private architecture(ast: Program, renderer: ConventionRenderer): string | null {
    const context = this.findBlock(ast, 'context');
    if (!context) return null;

    const text = this.extractText(context.content);
    const archMatch = this.extractSectionWithCodeBlock(text, '## Architecture');
    if (!archMatch) return null;

    const content = archMatch.replace('## Architecture', '').trim();
    // Apply stripAllIndent to normalize content for Prettier compatibility
    return renderer.renderSection('architecture', this.stripAllIndent(content));
  }

  private codeStandards(ast: Program, renderer: ConventionRenderer): string | null {
    const standards = this.findBlock(ast, 'standards');
    if (!standards) return null;

    // Use shared extractor for consistent handling of all @standards keys
    const extracted = this.standardsExtractor.extract(standards.content);
    const subsections: string[] = [];

    // Dynamically iterate over ALL extracted code standards (not just hardcoded keys)
    for (const entry of extracted.codeStandards.values()) {
      if (entry.items.length > 0) {
        subsections.push(
          renderer.renderSection(entry.sectionName, renderer.renderList(entry.items), 2)
        );
      }
    }

    if (subsections.length === 0) return null;

    return renderer.renderSection('code-standards', subsections.join('\n\n'));
  }

  /**
   * Generate shortcuts section for copilot-instructions.md.
   * Includes shortcuts that don't have prompt: true (those go to .prompt.md files).
   */
  private shortcutsSection(ast: Program, renderer: ConventionRenderer): string | null {
    const block = this.findBlock(ast, 'shortcuts');
    if (!block) return null;

    const props = this.getProps(block.content);
    const items: string[] = [];

    for (const [name, value] of Object.entries(props)) {
      // Skip shortcuts with prompt: true (those generate separate .prompt.md files)
      if (value && typeof value === 'object' && !Array.isArray(value)) {
        const obj = value as Record<string, Value>;
        if (obj['prompt'] === true || obj['type'] === 'prompt') {
          continue;
        }
        // Object without prompt: true - use description or first line of content
        const desc = obj['description'] || obj['content'] || name;
        items.push(`${name}: ${this.valueToString(desc).split('\n')[0]}`);
      } else {
        // Simple string shortcut - use first line only
        items.push(`${name}: ${this.valueToString(value).split('\n')[0]}`);
      }
    }

    if (items.length === 0) return null;
    return renderer.renderSection('shortcuts', renderer.renderList(items));
  }

  private commands(ast: Program, renderer: ConventionRenderer): string | null {
    const knowledge = this.findBlock(ast, 'knowledge');
    if (!knowledge) return null;

    const text = this.extractText(knowledge.content);
    const commandsMatch = this.extractSectionWithCodeBlock(text, '## Development Commands');
    if (!commandsMatch) return null;

    const content = commandsMatch.replace('## Development Commands', '').trim();
    // Apply stripAllIndent to normalize content for Prettier compatibility
    return renderer.renderSection('commands', this.stripAllIndent(content));
  }

  private gitCommits(ast: Program, renderer: ConventionRenderer): string | null {
    const standards = this.findBlock(ast, 'standards');
    if (!standards) return null;

    const props = this.getProps(standards.content);
    const git = props['git'];
    if (!git || typeof git !== 'object' || Array.isArray(git)) return null;

    const gitObj = git as Record<string, Value>;
    const items: string[] = [];

    const format = gitObj['format'];
    if (format) {
      items.push(
        `Use [${this.valueToString(format)}](https://www.conventionalcommits.org/) format`
      );
    }

    const maxLen = gitObj['maxSubjectLength'];
    if (maxLen) {
      items.push(`Keep commit message subject line max ${this.valueToString(maxLen)} characters`);
    }

    items.push('Format: `<type>(<scope>): <description>`');

    const types = gitObj['types'];
    if (types && Array.isArray(types)) {
      items.push(`Types: \`${types.map(String).join('`, `')}\``);
    }

    const scope = gitObj['scope'];
    if (scope) {
      items.push(`Scope: ${this.valueToString(scope)}`);
    }

    const example = gitObj['example'];
    if (example) {
      items.push(`Example: \`${this.valueToString(example)}\``);
    }

    if (items.length === 0) return null;

    return renderer.renderSection('git-commits', renderer.renderList(items));
  }

  private configFiles(ast: Program, renderer: ConventionRenderer): string | null {
    const standards = this.findBlock(ast, 'standards');
    if (!standards) return null;

    const props = this.getProps(standards.content);
    const config = props['config'];
    if (!config || typeof config !== 'object' || Array.isArray(config)) return null;

    const configObj = config as Record<string, Value>;
    const subsections: string[] = [];

    const eslint = configObj['eslint'];
    if (eslint) {
      const value = this.valueToString(eslint);
      subsections.push(renderer.renderSection('eslint', `ESLint: ${value}`, 2));
    }

    const vite = configObj['viteRoot'];
    if (vite) {
      const value = this.valueToString(vite);
      subsections.push(renderer.renderSection('vite-vitest', `Vite root: ${value}`, 2));
    }

    if (subsections.length === 0) return null;

    return renderer.renderSection('configuration-files', subsections.join('\n\n'));
  }

  private documentation(ast: Program, renderer: ConventionRenderer): string | null {
    const standards = this.findBlock(ast, 'standards');
    if (!standards) return null;

    const props = this.getProps(standards.content);
    const docs = props['documentation'];
    if (!docs || typeof docs !== 'object' || Array.isArray(docs)) return null;

    const docsObj = docs as Record<string, Value>;
    const items: string[] = [];

    if (docsObj['verifyBefore']) {
      items.push(
        '**Before** making code changes, review `README.md` and relevant files in `docs/` to understand documented behavior'
      );
    }
    if (docsObj['verifyAfter']) {
      items.push(
        '**After** making code changes, verify consistency with `README.md` and `docs/` - update documentation if needed'
      );
    }
    if (docsObj['codeExamples']) {
      items.push('Ensure code examples in documentation remain accurate after modifications');
    }
    items.push('If adding new features, add corresponding documentation in `docs/`');
    items.push('If changing existing behavior, update affected documentation sections');

    if (items.length === 0) return null;

    return renderer.renderSection('documentation-verification', renderer.renderList(items));
  }

  private postWork(ast: Program, renderer: ConventionRenderer): string | null {
    const knowledge = this.findBlock(ast, 'knowledge');
    if (!knowledge) return null;

    const text = this.extractText(knowledge.content);
    const postMatch = this.extractSectionWithCodeBlock(text, '## Post-Work Verification');
    if (!postMatch) return null;

    const intro =
      'After completing any code changes, run the following commands to ensure code quality:';
    const content = postMatch.replace('## Post-Work Verification', '').trim();
    // Apply stripAllIndent to normalize content for Prettier compatibility
    return renderer.renderSection(
      'post-work-verification',
      `${intro}\n${this.stripAllIndent(content)}`
    );
  }

  private restrictions(ast: Program, renderer: ConventionRenderer): string | null {
    const block = this.findBlock(ast, 'restrictions');
    if (!block) return null;

    let items: string[] = [];

    if (block.content.type === 'ArrayContent') {
      items = block.content.elements.map((item) =>
        this.formatRestriction(this.valueToString(item))
      );
    } else if (block.content.type === 'TextContent') {
      items = block.content.value
        .trim()
        .split('\n')
        .filter((line) => line.trim())
        .map((line) => this.formatRestriction(line.trim()));
    } else if (block.content.type === 'ObjectContent') {
      const itemsArray = this.getProp(block.content, 'items');
      if (Array.isArray(itemsArray)) {
        items = itemsArray.map((item: unknown) =>
          this.formatRestriction(this.valueToString(item as Value))
        );
      }
    }

    if (items.length === 0) return null;

    return renderer.renderSection('donts', renderer.renderList(items));
  }

  private diagrams(ast: Program, renderer: ConventionRenderer): string | null {
    const standards = this.findBlock(ast, 'standards');
    if (!standards) return null;

    const props = this.getProps(standards.content);
    const diag = props['diagrams'];
    if (!diag || typeof diag !== 'object' || Array.isArray(diag)) return null;

    const diagObj = diag as Record<string, Value>;
    const items: string[] = [];

    const format = diagObj['format'];
    if (format) {
      items.push(
        `Always use **${this.valueToString(format)}** syntax for diagrams in documentation`
      );
    }

    const types = diagObj['types'];
    if (types && Array.isArray(types)) {
      items.push(`Supported diagram types: ${types.map(String).join(', ')}, etc.`);
    }

    items.push('Wrap diagrams in markdown code blocks with `mermaid` language identifier');
    items.push(
      'Example:\n  ```mermaid\n  flowchart LR\n    A[Input] --> B[Process] --> C[Output]\n  ```'
    );

    if (items.length === 0) return null;

    return renderer.renderSection('diagrams', renderer.renderList(items));
  }

  /**
   * Render remaining @knowledge content not already consumed by
   * commands() (## Development Commands) or postWork() (## Post-Work Verification).
   */
  private knowledgeContent(ast: Program, _renderer: ConventionRenderer): string | null {
    const knowledge = this.findBlock(ast, 'knowledge');
    if (!knowledge) return null;

    const text = this.extractText(knowledge.content);
    if (!text) return null;

    // Remove sections already consumed by other methods
    const consumedHeaders = ['## Development Commands', '## Post-Work Verification'];
    let remaining = text;

    for (const header of consumedHeaders) {
      const headerIndex = remaining.indexOf(header);
      if (headerIndex === -1) continue;

      const afterHeader = remaining.indexOf('\n', headerIndex);
      if (afterHeader === -1) {
        remaining = remaining.substring(0, headerIndex).trimEnd();
        continue;
      }

      const nextSection = remaining.indexOf('\n## ', afterHeader);
      if (nextSection === -1) {
        remaining = remaining.substring(0, headerIndex).trimEnd();
      } else {
        remaining = remaining.substring(0, headerIndex) + remaining.substring(nextSection + 1);
      }
    }

    remaining = remaining.trim();
    if (!remaining) return null;

    return this.stripAllIndent(remaining);
  }

  private examples(ast: Program, renderer: ConventionRenderer): string | null {
    return this.renderExamplesSection(ast, renderer, 'examples');
  }

  // Helper methods

  private formatTechItem(arr: Value[]): string {
    return arr.map(String).join(', ');
  }

  private formatRestriction(text: string): string {
    const formatted = text
      .replace(/^-\s*/, '')
      .replace(/^"/, '')
      .replace(/"$/, '')
      .replace(/^Never\s+/i, "Don't ");

    return formatted;
  }
}
