// src/chat/export.ts
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0

import fs from "node:fs/promises";
import path from "node:path";

import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai";
import { formatError } from "@gadget/ai";
import type {
  IChatSession,
  IChatTurn,
  IChatTurnBlock,
  IChatTurnBlockThinking,
  IChatTurnBlockResponding,
  IChatTurnBlockTool,
  IChatToolCall,
  IChatSubagentProcess,
  IChatTurnStats,
  IAiProvider,
  IDroneModelConfig,
  IUser,
} from "@gadget/api";
import { ChatTurnStatus } from "@gadget/api";

import { GadgetTool } from "../tool.ts";

const VALID_FORMATS = ["markdown", "json"] as const;
type ExportFormat = (typeof VALID_FORMATS)[number];

/**
 * Safe user representation — PII stripped.
 * Only _id and displayName are included. The persona is included because
 * it is user-authored and non-sensitive.
 */
export interface ISafeUser {
  _id: string;
  displayName: string;
  persona?: string;
}

/**
 * Safe AI provider representation — secrets and infrastructure stripped.
 * baseUrl is excluded (internal network info). apiKey is excluded (secret).
 */
export interface ISafeAiProvider {
  _id: string;
  name: string;
  apiType: string;
  enabled: boolean;
}

/**
 * Safe project representation — only non-sensitive metadata.
 * gitUrl is excluded (could contain auth tokens in practice).
 * system is excluded (may contain infrastructure details).
 */
export interface ISafeProject {
  _id: string;
  name: string;
  slug: string;
  description?: string;
}

/**
 * A single turn scrubbed for export. `prompts.system` is always excluded.
 */
export interface IExportTurn {
  _id: string;
  createdAt: string;
  mode: string;
  status: string;
  prompts: {
    user: string;
  };
  blocks: IChatTurnBlock[];
  toolCalls: IChatToolCall[];
  subagents: IChatSubagentProcess[];
  stats: IChatTurnStats;
  errorMessage?: string;
}

/**
 * The full export payload delivered by the AgentService data provider.
 */
export interface IChatExportData {
  session: IChatSession;
  turns: IChatTurn[];
  user: IUser;
  provider: IAiProvider;
  project: { name: string; slug: string; description?: string };
  modelConfig: Omit<IDroneModelConfig, "provider">;
  gadgetCodeVersion: string;
  exportedAt: Date;
}

// ── Utility helpers ────────────────────────────────────────────────────

function sanitizeSessionName(name: string): string {
  return name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 50) || "session";
}

function formatTimestamp(date: Date): string {
  const d = date instanceof Date ? date : new Date(date);
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}

function toISO(date: Date | string): string {
  return new Date(date).toISOString();
}

/** Strip PII, credentials, infrastructure info from a User record. */
function safeUser(user: IUser): ISafeUser {
  return {
    _id: String(user._id),
    displayName: user.displayName,
    persona: user.persona,
  };
}

/** Strip secrets and infrastructure info from an AiProvider record. */
function safeProvider(provider: IAiProvider): ISafeAiProvider {
  return {
    _id: String(provider._id),
    name: provider.name,
    apiType: provider.apiType,
    enabled: provider.enabled,
  };
}

/** Strip sensitive info from a project record. */
function safeProject(project: { name: string; slug: string; description?: string; gitUrl?: string; system?: string }): ISafeProject {
  return {
    _id: String((project as any)._id),
    name: project.name,
    slug: project.slug,
    description: project.description,
  };
}

/** Strip `prompts.system` from a turn and normalize dates. */
function scrubTurn(turn: IChatTurn): IExportTurn {
  return {
    _id: String(turn._id),
    createdAt: toISO(turn.createdAt),
    mode: turn.mode,
    status: turn.status,
    prompts: {
      user: turn.prompts.user,
    },
    blocks: turn.blocks || [],
    toolCalls: (turn.toolCalls || []).map(scrubToolCall),
    subagents: (turn.subagents || []).map(scrubSubagent),
    stats: turn.stats,
    errorMessage: turn.errorMessage,
  };
}

/** Scrub a tool call for export — no changes needed to the core structure,
 *  but we strip any subagent-embedded provider/user references. */
function scrubToolCall(tc: IChatToolCall): IChatToolCall {
  const result: IChatToolCall = {
    callId: tc.callId,
    name: tc.name,
    parameters: tc.parameters,
    response: tc.response,
  };
  if (tc.subagent) {
    result.subagent = scrubSubagent(tc.subagent);
  }
  return result;
}

/** Scrub a subagent process for export. */
function scrubSubagent(sa: IChatSubagentProcess): IChatSubagentProcess {
  return {
    prompt: sa.prompt,
    thinking: sa.thinking,
    response: sa.response,
    toolCalls: (sa.toolCalls || []).map(scrubToolCall),
    stats: sa.stats,
  };
}

// ── Markdown formatter ─────────────────────────────────────────────────

function formatAsMarkdown(data: IChatExportData): string {
  const { session, turns, user, provider, project, modelConfig, gadgetCodeVersion } = data;
  const safeUser_ = safeUser(user);
  const safeProvider_ = safeProvider(provider);

  const finishedTurns = turns.filter(
    (t) => t.status !== ChatTurnStatus.Processing,
  );
  const totalInputTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.inputTokens || 0), 0);
  const totalOutputTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.responseTokens || 0), 0);
  const totalThinkingTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.thinkingTokenCount || 0), 0);

  const lines: string[] = [];

  // ── Header ──
  lines.push(`# ${session.name}`);
  lines.push("");
  lines.push(`**Session ID:** ${session._id}`);
  lines.push(`**Created:** ${toISO(session.createdAt)}`);
  if (session.lastMessageAt) {
    lines.push(`**Last Message:** ${toISO(session.lastMessageAt)}`);
  }
  lines.push(`**Mode:** ${session.mode}`);
  lines.push(`**Project:** ${project.name} (${project.slug})`);
  lines.push(`**AI Provider:** ${safeProvider_.name} (${safeProvider_.apiType})`);
  lines.push(`**Model:** ${session.selectedModel}`);
  lines.push(`**Reasoning Effort:** ${session.reasoningEffort || "off"}`);
  lines.push(`**Context Window:** ${session.numCtx || modelConfig.params.numCtx}`);
  lines.push(`**Exported by:** Gadget Code v${gadgetCodeVersion}`);
  lines.push(`**Exported at:** ${data.exportedAt.toISOString()}`);
  lines.push("");
  lines.push(`**Turns:** ${finishedTurns.length}`);
  lines.push(`**Tool Calls:** ${session.stats?.toolCallCount || 0}`);
  lines.push(`**Total Input Tokens:** ${totalInputTokens.toLocaleString()}`);
  lines.push(`**Total Output Tokens:** ${totalOutputTokens.toLocaleString()}`);
  if (totalThinkingTokens > 0) {
    lines.push(`**Total Thinking Tokens:** ${totalThinkingTokens.toLocaleString()}`);
  }
  lines.push("");

  // ── Pins ──
  if (session.pins && session.pins.length > 0) {
    lines.push("## Pinned Context");
    lines.push("");
    for (const pin of session.pins) {
      lines.push(`- ${pin.content}`);
    }
    lines.push("");
  }

  lines.push("---");
  lines.push("");

  // ── Turns ──
  for (const [i, turn] of finishedTurns.entries()) {
    const scrubbed = scrubTurn(turn);
    lines.push(`## Turn ${i + 1}`);
    lines.push("");
    lines.push(`**Timestamp:** ${scrubbed.createdAt}`);
    lines.push(`**Mode:** ${scrubbed.mode}`);
    lines.push(`**Status:** ${scrubbed.status}`);
    if (scrubbed.stats?.durationLabel) {
      lines.push(`**Duration:** ${scrubbed.stats.durationLabel}`);
    }
    lines.push("");

    // User prompt
    lines.push("### User");
    lines.push("");
    lines.push(scrubbed.prompts.user);
    lines.push("");

    // Thinking blocks
    const thinkingBlocks = scrubbed.blocks.filter(
      (b) => b.mode === "thinking",
    ) as IChatTurnBlockThinking[];
    if (thinkingBlocks.length > 0) {
      lines.push("### Thinking");
      lines.push("");
      for (const block of thinkingBlocks) {
        lines.push(block.content);
        lines.push("");
      }
    }

    // Response blocks
    const responseBlocks = scrubbed.blocks.filter(
      (b) => b.mode === "responding",
    ) as IChatTurnBlockResponding[];
    if (responseBlocks.length > 0) {
      lines.push("### Assistant");
      lines.push("");
      for (const block of responseBlocks) {
        lines.push(block.content);
        lines.push("");
      }
    }

    // Tool call blocks
    const toolBlocks = scrubbed.blocks.filter(
      (b) => b.mode === "tool",
    ) as IChatTurnBlockTool[];
    if (toolBlocks.length > 0 || scrubbed.toolCalls.length > 0) {
      lines.push("### Tool Calls");
      lines.push("");

      // From blocks
      for (const block of toolBlocks) {
        const tc = block.content;
        lines.push(`#### ${tc.name}`);
        lines.push("");
        lines.push("**Call ID:** " + tc.callId);
        lines.push("");
        lines.push("**Parameters:**");
        lines.push("```json");
        try {
          lines.push(JSON.stringify(JSON.parse(tc.parameters), null, 2));
        } catch {
          lines.push(tc.parameters);
        }
        lines.push("```");
        lines.push("");
        if (tc.response) {
          lines.push("**Response:**");
          lines.push("```");
          // Truncate very long responses for readability
          const resp = tc.response.length > 5000
            ? tc.response.slice(0, 5000) + "\n... (truncated for readability)"
            : tc.response;
          lines.push(resp);
          lines.push("```");
          lines.push("");
        }
        if (tc.subagent) {
          lines.push(formatSubagentMarkdown(tc.subagent));
        }
      }

      // From toolCalls array (if not already covered by blocks)
      const blockCallIds = new Set(toolBlocks.map((b) => b.content.callId));
      for (const tc of scrubbed.toolCalls) {
        if (blockCallIds.has(tc.callId)) continue;
        lines.push(`#### ${tc.name}`);
        lines.push("");
        lines.push("**Call ID:** " + tc.callId);
        lines.push("");
        lines.push("**Parameters:**");
        lines.push("```json");
        try {
          lines.push(JSON.stringify(JSON.parse(tc.parameters), null, 2));
        } catch {
          lines.push(tc.parameters);
        }
        lines.push("```");
        lines.push("");
        if (tc.response) {
          lines.push("**Response:**");
          lines.push("```");
          const resp = tc.response.length > 5000
            ? tc.response.slice(0, 5000) + "\n... (truncated for readability)"
            : tc.response;
          lines.push(resp);
          lines.push("```");
          lines.push("");
        }
        if (tc.subagent) {
          lines.push(formatSubagentMarkdown(tc.subagent));
        }
      }
    }

    // Subagents
    if (scrubbed.subagents.length > 0) {
      lines.push("### Subagents");
      lines.push("");
      for (const sa of scrubbed.subagents) {
        lines.push(formatSubagentMarkdown(sa));
      }
    }

    // Error
    if (scrubbed.errorMessage) {
      lines.push("### Error");
      lines.push("");
      lines.push(`> ${scrubbed.errorMessage}`);
      lines.push("");
    }

    // Turn stats
    lines.push(`*Token usage: ${scrubbed.stats?.inputTokens || 0} input / ${scrubbed.stats?.responseTokens || 0} output${scrubbed.stats?.thinkingTokenCount ? ` / ${scrubbed.stats.thinkingTokenCount} thinking` : ""}*`);
    lines.push("");
    lines.push("---");
    lines.push("");
  }

  return lines.join("\n");
}

function formatSubagentMarkdown(sa: IChatSubagentProcess): string {
  const lines: string[] = [];
  lines.push(`##### Subagent`);
  lines.push("");
  lines.push("**Prompt:** " + sa.prompt.slice(0, 200) + (sa.prompt.length > 200 ? "..." : ""));
  lines.push("");
  if (sa.thinking) {
    lines.push("**Thinking:**");
    lines.push("> " + sa.thinking.slice(0, 1000).replace(/\n/g, "\n> "));
    lines.push("");
  }
  if (sa.response) {
    lines.push("**Response:**");
    lines.push(sa.response.slice(0, 2000) + (sa.response.length > 2000 ? "\n... (truncated)" : ""));
    lines.push("");
  }
  if (sa.stats) {
    lines.push(`*Subagent tokens: ${sa.stats.inputTokens || 0} input / ${sa.stats.responseTokens || 0} output / ${sa.stats.thinkingTokenCount || 0} thinking — ${sa.stats.durationLabel || "unknown"}*`);
    lines.push("");
  }
  return lines.join("\n");
}

// ── JSON formatter ─────────────────────────────────────────────────────

function formatAsJson(data: IChatExportData): string {
  const { session, turns, user, provider, project, modelConfig, gadgetCodeVersion } = data;

  const finishedTurns = turns.filter(
    (t) => t.status !== ChatTurnStatus.Processing,
  );
  const totalInputTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.inputTokens || 0), 0);
  const totalOutputTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.responseTokens || 0), 0);

  const exportObj = {
    exportMetadata: {
      format: "gadget-code-chat-export-v1",
      version: "1.0.0",
      exportedAt: data.exportedAt.toISOString(),
      gadgetCodeVersion,
      gadgetCodeUrl: "https://g4dge7.com/",
    },
    session: {
      _id: String(session._id),
      name: session.name,
      mode: session.mode,
      createdAt: toISO(session.createdAt),
      lastMessageAt: session.lastMessageAt ? toISO(session.lastMessageAt) : undefined,
      selectedModel: session.selectedModel,
      reasoningEffort: session.reasoningEffort || "off",
      numCtx: session.numCtx || modelConfig.params.numCtx,
      stats: session.stats,
      pins: session.pins || [],
    },
    user: safeUser(user),
    provider: safeProvider(provider),
    project: safeProject(project),
    modelConfig: {
      modelId: modelConfig.modelId,
      params: modelConfig.params,
    },
    turns: finishedTurns.map(scrubTurn),
    aggregateStats: {
      turnCount: finishedTurns.length,
      totalInputTokens,
      totalOutputTokens,
    },
  };

  return JSON.stringify(exportObj, null, 2);
}

// ── Companion Markdown (for JSON exports) ───────────────────────────────

function formatCompanionMarkdown(data: IChatExportData): string {
  const { session, user, provider, modelConfig, gadgetCodeVersion } = data;
  const safeUser_ = safeUser(user);
  const safeProvider_ = safeProvider(provider);

  const finishedTurns = (data.turns || []).filter(
    (t) => t.status !== ChatTurnStatus.Processing,
  );
  const totalInputTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.inputTokens || 0), 0);
  const totalOutputTokens = finishedTurns.reduce((sum, t) => sum + (t.stats?.responseTokens || 0), 0);

  const lines: string[] = [];

  lines.push("# Chat Export — Companion Document");
  lines.push("");
  lines.push("## Export Details");
  lines.push("");
  lines.push(`- **Format:** Gadget Code Chat Export v1 (JSON)`);
  lines.push(`- **Exported:** ${data.exportedAt.toISOString()}`);
  lines.push(`- **Gadget Code Version:** ${gadgetCodeVersion}`);
  lines.push(`- **Get Gadget Code:** https://g4dge7.com/`);
  lines.push("");

  lines.push("## Authors");
  lines.push("");
  lines.push("### Human Author");
  lines.push(`- **Name:** ${safeUser_.displayName}`);
  lines.push("");

  lines.push("### AI Agent");
  lines.push(`- **Agent:** Gadget Code version ${gadgetCodeVersion}`);
  lines.push(`- **Provider:** ${safeProvider_.name} (${safeProvider_.apiType})`);
  lines.push(`- **Model:** ${modelConfig.modelId}`);
  lines.push(`- **Parameters:**`);
  lines.push(`  - Temperature: ${modelConfig.params.temperature}`);
  lines.push(`  - Top P: ${modelConfig.params.topP}`);
  lines.push(`  - Top K: ${modelConfig.params.topK}`);
  lines.push(`  - Context Window: ${modelConfig.params.numCtx}`);
  lines.push(`  - Max Completion Tokens: ${modelConfig.params.maxCompletionTokens}`);
  lines.push(`  - Num Predict: ${modelConfig.params.numPredict}`);
  lines.push(`  - Reasoning: ${modelConfig.params.reasoning}`);
  lines.push("");

  lines.push("## Session Summary");
  lines.push("");
  lines.push(`- **Session:** ${session.name}`);
  lines.push(`- **Session ID:** ${session._id}`);
  lines.push(`- **Mode:** ${session.mode}`);
  lines.push(`- **Turns:** ${finishedTurns.length}`);
  lines.push(`- **Tool Calls:** ${session.stats?.toolCallCount || 0}`);
  lines.push(`- **Total Input Tokens:** ${totalInputTokens.toLocaleString()}`);
  lines.push(`- **Total Output Tokens:** ${totalOutputTokens.toLocaleString()}`);
  lines.push("");

  lines.push("## About This Export");
  lines.push("");
  lines.push("This Markdown document accompanies a JSON export of a Gadget Code chat session.");
  lines.push("The JSON file contains the complete machine-readable conversation data, suitable");
  lines.push("for import into other systems, analysis, or archival purposes.");
  lines.push("");
  lines.push("The JSON export format is designed to be self-contained and interoperable. It");
  lines.push("includes all conversation turns, tool calls, subagent interactions, and session");
  lines.push("metadata needed to reconstruct the full conversation in another system.");
  lines.push("");
  lines.push("For more information about Gadget Code, visit https://g4dge7.com/");

  return lines.join("\n");
}

// ── Tool implementation ───────────────────────────────────────────────

export class ExportTool extends GadgetTool {
  private _getSessionData: (() => Promise<IChatExportData>) | null = null;

  setSessionDataProvider(fn: () => Promise<IChatExportData>): void {
    this._getSessionData = fn;
  }

  get name(): string {
    return "chat_export";
  }

  get category(): string {
    return "chat";
  }

  get definition(): IToolDefinition {
    return {
      type: "function",
      function: {
        name: this.name,
        description:
          "Export the current chat session and all its conversation turns to disk as a document. " +
          "The default format is Markdown, which produces a well-formatted conversation log including " +
          "tool calls, responses, thinking blocks, and related metadata. " +
          "The JSON format produces a complete machine-readable export suitable for import into " +
          "other systems, analysis, or archival — it is always accompanied by a companion Markdown " +
          "document that describes and credits the export. " +
          "All exports exclude sensitive information (emails, API keys, hostnames, system prompts).",
        parameters: {
          type: "object",
          properties: {
            format: {
              type: "string",
              enum: [...VALID_FORMATS],
              description:
                "Export format: 'markdown' for a human-readable conversation log, " +
                "'json' for a complete machine-readable export with a companion Markdown document. " +
                "Defaults to 'markdown'.",
            },
          },
          required: ["format"],
        },
      },
    };
  }

  async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
    const { format } = args;

    // ── Validate format parameter ──
    if (!format) {
      return formatError({
        code: "MISSING_PARAMETER",
        message: "The 'format' parameter is required.",
        parameter: "format",
        expected: "Either 'markdown' or 'json'",
        recoveryHint: "Specify 'markdown' for a readable log or 'json' for a machine-readable export.",
      });
    }

    if (!VALID_FORMATS.includes(format as ExportFormat)) {
      return formatError({
        code: "INVALID_PARAMETER",
        message: `Invalid format: '${format}'. Must be one of: ${VALID_FORMATS.join(", ")}`,
        parameter: "format",
        expected: `One of: ${VALID_FORMATS.join(", ")}`,
        recoveryHint: "Use 'markdown' for a human-readable conversation log or 'json' for a complete data export.",
      });
    }

    // ── Check session data provider ──
    if (!this._getSessionData) {
      return formatError({
        code: "OPERATION_FAILED",
        message: "ExportTool has not been initialized with a session data provider.",
        recoveryHint: "This is an internal initialization error. The agent service must wire the data provider during startup.",
      });
    }

    // ── Get export data ──
    let data: IChatExportData;
    try {
      data = await this._getSessionData();
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      logger.error("failed to get export data", { error: message });
      return formatError({
        code: "OPERATION_FAILED",
        message: `Failed to retrieve session data for export: ${message}`,
        recoveryHint: "Ensure the chat session is active and try again.",
      });
    }

    // ── Determine file paths ──
    const projectDir = this.toolbox.env.workspace?.projectDir;
    if (!projectDir) {
      return formatError({
        code: "OPERATION_FAILED",
        message: "Workspace project directory is not available. The workspace must be initialized before exporting.",
        recoveryHint: "Ensure the agent is running within an active workspace.",
      });
    }

    const exportDir = path.join(projectDir, ".gadget", "exports");
    const baseName = sanitizeSessionName(data.session.name);
    const timestamp = formatTimestamp(data.exportedAt);
    const exportFormat = format as ExportFormat;

    logger.debug("exporting chat session", {
      sessionId: String(data.session._id),
      format: exportFormat,
      turnCount: data.turns.length,
    });

    try {
      // Ensure export directory exists
      await fs.mkdir(exportDir, { recursive: true });

      const results: string[] = [];

      if (exportFormat === "markdown") {
        // ── Markdown export ──
        const content = formatAsMarkdown(data);
        const fileName = `${baseName}-${timestamp}.md`;
        const filePath = path.join(exportDir, fileName);
        await fs.writeFile(filePath, content, "utf-8");
        const byteCount = Buffer.byteLength(content, "utf-8");
        results.push(`.gadget/exports/${fileName}`);

        // Build success response
        const finishedTurns = data.turns.filter(
          (t) => t.status !== ChatTurnStatus.Processing,
        );
        const totalInput = finishedTurns.reduce((s, t) => s + (t.stats?.inputTokens || 0), 0);
        const totalOutput = finishedTurns.reduce((s, t) => s + (t.stats?.responseTokens || 0), 0);

        return [
          "CHAT EXPORT SUCCESS",
          `Format: markdown`,
          `Session: ${data.session.name}`,
          `Turns: ${finishedTurns.length}`,
          `Total Input Tokens: ${totalInput.toLocaleString()}`,
          `Total Output Tokens: ${totalOutput.toLocaleString()}`,
          `Bytes: ${byteCount.toLocaleString()}`,
          `Files:`,
          ...results.map((r) => `  - ${r}`),
        ].join("\n");
      }

      // ── JSON export (always with companion markdown) ──
      const jsonContent = formatAsJson(data);
      const jsonFileName = `${baseName}-${timestamp}.json`;
      const jsonFilePath = path.join(exportDir, jsonFileName);
      await fs.writeFile(jsonFilePath, jsonContent, "utf-8");
      const jsonBytes = Buffer.byteLength(jsonContent, "utf-8");
      results.push(`.gadget/exports/${jsonFileName}`);

      const companionContent = formatCompanionMarkdown(data);
      const companionFileName = `${baseName}-${timestamp}-readme.md`;
      const companionFilePath = path.join(exportDir, companionFileName);
      await fs.writeFile(companionFilePath, companionContent, "utf-8");
      const companionBytes = Buffer.byteLength(companionContent, "utf-8");
      results.push(`.gadget/exports/${companionFileName}`);

      // Build success response
      const finishedTurns = data.turns.filter(
        (t) => t.status !== ChatTurnStatus.Processing,
      );
      const totalInput = finishedTurns.reduce((s, t) => s + (t.stats?.inputTokens || 0), 0);
      const totalOutput = finishedTurns.reduce((s, t) => s + (t.stats?.responseTokens || 0), 0);

      return [
        "CHAT EXPORT SUCCESS",
        `Format: json + companion markdown`,
        `Session: ${data.session.name}`,
        `Turns: ${finishedTurns.length}`,
        `Total Input Tokens: ${totalInput.toLocaleString()}`,
        `Total Output Tokens: ${totalOutput.toLocaleString()}`,
        `JSON Bytes: ${jsonBytes.toLocaleString()}`,
        `Readme Bytes: ${companionBytes.toLocaleString()}`,
        `Files:`,
        ...results.map((r) => `  - ${r}`),
      ].join("\n");
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      logger.error("failed to write export file", { error: message });
      return formatError({
        code: "OPERATION_FAILED",
        message: `Failed to write export file: ${message}`,
        recoveryHint: "Check filesystem permissions and available disk space, then try again.",
      });
    }
  }
}
