• ExportStatisticsButton provides a dropdown-driven control for exporting statistics data. Users can choose formats and which data sections to include.

    Parameters

    Returns Element

    Dropdown button element.

    export function ExportStatisticsButton({
    importStats,
    syncStats,
    matchResults,
    disabled = false,
    size = "default",
    variant = "outline",
    appliedFilters,
    comparisonMode,
    isFiltered,
    }: Readonly<ExportStatisticsButtonProps>) {
    const [format, setFormat] = useState<StatisticsExportFormat>("json");
    const [selectedSections, setSelectedSections] = useState<Set<ExportSection>>(
    () => new Set<ExportSection>(["import", "sync", "matches"]),
    );
    const [isMenuOpen, setIsMenuOpen] = useState(false);

    const matchCount = useMemo(() => matchResults.length, [matchResults]);

    const toggleExportSection = useCallback((section: ExportSection) => {
    setSelectedSections((prev) => {
    const next = new Set(prev);
    if (next.has(section)) {
    next.delete(section);
    } else {
    next.add(section);
    }
    return next;
    });
    }, []);

    const buildJsonPayload = useCallback(() => {
    const baseMetadata = buildExportMetadata(
    "json",
    matchResults.length,
    undefined,
    Array.from(selectedSections),
    );

    const enrichedMetadata = {
    ...baseMetadata,
    ...(appliedFilters && { filters: appliedFilters }),
    ...(comparisonMode?.enabled && { comparison: comparisonMode }),
    ...(isFiltered && { isFiltered: true }),
    };

    const payload: Record<string, unknown> = {
    metadata: enrichedMetadata,
    generatedAt: new Date().toISOString(),
    };

    if (selectedSections.has("import") && importStats) {
    payload.importStats = importStats;
    }

    if (selectedSections.has("sync") && syncStats) {
    payload.syncStats = syncStats;
    }

    if (selectedSections.has("matches") && matchResults.length > 0) {
    payload.matchResults = matchResults;
    }

    return payload;
    }, [
    importStats,
    syncStats,
    matchResults,
    selectedSections,
    appliedFilters,
    comparisonMode,
    isFiltered,
    ]);

    const buildTabularRows = useCallback((): ExportRow[] => {
    const summaryRows = buildSummaryRows(
    importStats,
    syncStats,
    selectedSections,
    );
    if (selectedSections.has("matches") && matchResults.length > 0) {
    summaryRows.push(...buildMatchRows(matchResults));
    }
    return summaryRows;
    }, [importStats, syncStats, selectedSections, matchResults]);

    /**
    * Handles JSON export format.
    * @returns Promise that resolves when export is complete
    */
    const handleJsonExport = useCallback(async (): Promise<void> => {
    const payload = buildJsonPayload();
    const file = await exportToJson(payload, "statistics");
    toast.success(`Statistics exported to ${file}`);
    setIsMenuOpen(false);
    }, [buildJsonPayload]);

    /**
    * Builds metadata comments for CSV export including filters and comparison mode.
    * @param baseMetadata - Base export metadata
    * @param sectionNames - Array of export section names
    * @returns Array of comment rows with metadata
    */
    const buildCsvMetadataComments = useCallback(
    (
    baseMetadata: Record<string, unknown>,
    sectionNames: string[],
    ): Array<Record<string, unknown>> => {
    const baseComments: Array<Record<string, unknown>> = [
    {
    comment: `Exported: ${baseMetadata.exportedAt ?? new Date().toISOString()}`,
    },
    { comment: `App Version: v${baseMetadata.appVersion ?? "unknown"}` },
    { comment: `Sections: ${sectionNames.join(", ")}` },
    ];

    const filterComments: Array<Record<string, unknown>> = appliedFilters
    ? [
    { comment: "" },
    { comment: "Filters Applied:" },
    {
    comment:
    appliedFilters.genres.length > 0
    ? ` Genres: ${appliedFilters.genres.join(", ")}`
    : " Genres: None",
    },
    {
    comment:
    appliedFilters.formats.length > 0
    ? ` Formats: ${appliedFilters.formats.join(", ")}`
    : " Formats: None",
    },
    {
    comment:
    appliedFilters.statuses.length > 0
    ? ` Statuses: ${appliedFilters.statuses.join(", ")}`
    : " Statuses: None",
    },
    {
    comment:
    appliedFilters.dateRange.start || appliedFilters.dateRange.end
    ? ` Date Range: ${appliedFilters.dateRange.start?.toISOString().split("T")[0] ?? "N/A"} to ${appliedFilters.dateRange.end?.toISOString().split("T")[0] ?? "N/A"}`
    : " Date Range: None",
    },
    {
    comment: ` Confidence: ${appliedFilters.confidenceRange.min} - ${appliedFilters.confidenceRange.max}`,
    },
    ]
    : [];

    const comparisonComments: Array<Record<string, unknown>> =
    comparisonMode?.enabled
    ? [
    { comment: "" },
    { comment: "Comparison Mode:" },
    { comment: ` Primary Range: ${comparisonMode.primaryRange}` },
    {
    comment: ` Secondary Range: ${comparisonMode.secondaryRange}`,
    },
    { comment: ` Metric: ${comparisonMode.metric}` },
    ]
    : [];

    return [
    ...baseComments,
    ...filterComments,
    ...comparisonComments,
    { comment: "" },
    ];
    },
    [appliedFilters, comparisonMode],
    );

    /**
    * Handles CSV export format.
    * @param sectionNames - Array of export section names
    * @returns Promise that resolves when export is complete
    */
    const handleCsvExport = useCallback(
    async (selectedSectionNames: string[]): Promise<void> => {
    const rows = buildTabularRows();

    if (rows.length === 0) {
    toast.error("No data available for the selected export format");
    return;
    }

    const baseMetadata = buildExportMetadata(
    "csv",
    matchResults.length,
    undefined,
    selectedSectionNames,
    );

    const metadataComments = buildCsvMetadataComments(
    baseMetadata as unknown as Record<string, unknown>,
    selectedSectionNames,
    );
    const withMetadata = [...metadataComments, ...rows];

    const tabularData = withMetadata as unknown as Record<string, unknown>[];
    const file = await exportToCSV(tabularData, "statistics");

    toast.success(`Statistics exported to ${file}`);
    setIsMenuOpen(false);
    },
    [buildTabularRows, matchResults.length, buildCsvMetadataComments],
    );

    /**
    * Handles Markdown export format.
    * @param sectionNames - Array of export section names
    * @param totalEntries - Total number of entries being exported
    * @returns Promise that resolves when export is complete
    */
    const handleMarkdownExport = useCallback(
    async (
    selectedSectionNames: string[],
    totalEntries: number,
    ): Promise<void> => {
    const baseMetadata = buildExportMetadata(
    "markdown",
    totalEntries,
    undefined,
    selectedSectionNames,
    );

    const markdownData: Record<string, unknown> = {};

    if (appliedFilters) {
    markdownData.appliedFilters = appliedFilters;
    }
    if (comparisonMode?.enabled) {
    markdownData.comparisonMode = comparisonMode;
    }
    if (isFiltered) {
    markdownData.isFiltered = true;
    }

    if (selectedSections.has("import") && importStats) {
    markdownData.importStats = importStats;
    }

    if (selectedSections.has("sync") && syncStats) {
    markdownData.syncStats = syncStats;
    }

    if (selectedSections.has("matches") && matchResults.length > 0) {
    const flattened = matchResults.map(flattenMatchResult);
    markdownData.matchResults = flattened;
    }

    const file = exportToMarkdown(markdownData, "statistics", baseMetadata);
    toast.success(`Statistics exported to ${file}`);
    setIsMenuOpen(false);
    },
    [
    selectedSections,
    appliedFilters,
    comparisonMode,
    isFiltered,
    importStats,
    syncStats,
    matchResults,
    ],
    );

    const handleExport = useCallback(async () => {
    if (selectedSections.size === 0) {
    toast.error("Select at least one dataset to export");
    return;
    }

    try {
    const selectedSectionNames = Array.from(selectedSections);
    const totalEntries = matchResults.length;

    if (format === "json") {
    await handleJsonExport();
    return;
    }

    if (format === "markdown") {
    await handleMarkdownExport(selectedSectionNames, totalEntries);
    return;
    }

    await handleCsvExport(selectedSectionNames);
    } catch (error) {
    console.error("[ExportStatistics] ❌ Export failed", error);
    toast.error("Failed to export statistics");
    }
    }, [
    selectedSections,
    format,
    matchResults,
    handleJsonExport,
    handleMarkdownExport,
    handleCsvExport,
    ]);

    return (
    <DropdownMenu open={isMenuOpen} onOpenChange={setIsMenuOpen}>
    <DropdownMenuTrigger asChild>
    <Button
    variant={variant}
    size={size}
    disabled={disabled}
    className="gap-2"
    >
    <Download className="h-4 w-4" aria-hidden="true" />
    <span>
    {isFiltered ? "Export Filtered Statistics" : "Export Statistics"}
    </span>
    <span className="text-muted-foreground text-xs font-medium">
    {selectedSections.size} dataset
    {selectedSections.size === 1 ? "" : "s"}
    </span>
    </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end" className="w-64">
    <DropdownMenuLabel className="flex items-center gap-2">
    <BarChart3 className="h-4 w-4 text-blue-500" aria-hidden="true" />
    Export Options
    </DropdownMenuLabel>
    <DropdownMenuRadioGroup
    value={format}
    onValueChange={(value) => setFormat(value as StatisticsExportFormat)}
    >
    <DropdownMenuRadioItem value="json">
    <FileJson
    className="mr-2 h-4 w-4 text-emerald-500"
    aria-hidden="true"
    />
    JSON
    </DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="csv">
    <FileSpreadsheet
    className="mr-2 h-4 w-4 text-blue-500"
    aria-hidden="true"
    />
    CSV
    </DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="markdown">
    <FileText
    className="mr-2 h-4 w-4 text-purple-500"
    aria-hidden="true"
    />
    Markdown
    </DropdownMenuRadioItem>
    </DropdownMenuRadioGroup>

    <DropdownMenuSeparator />

    {/* Filter Summary Section */}
    {isFiltered && appliedFilters && (
    <>
    <DropdownMenuLabel className="flex items-center gap-2 text-xs">
    <span className="font-medium">Applied Filters</span>
    </DropdownMenuLabel>
    {appliedFilters.genres.length > 0 && (
    <div className="px-2 py-1 text-xs text-slate-600 dark:text-slate-400">
    Genres: {appliedFilters.genres.join(", ")}
    </div>
    )}
    {appliedFilters.formats.length > 0 && (
    <div className="px-2 py-1 text-xs text-slate-600 dark:text-slate-400">
    Formats: {appliedFilters.formats.join(", ")}
    </div>
    )}
    {appliedFilters.statuses.length > 0 && (
    <div className="px-2 py-1 text-xs text-slate-600 dark:text-slate-400">
    Statuses: {appliedFilters.statuses.join(", ")}
    </div>
    )}
    {(appliedFilters.dateRange.start ||
    appliedFilters.dateRange.end) && (
    <div className="px-2 py-1 text-xs text-slate-600 dark:text-slate-400">
    Date Range:{" "}
    {appliedFilters.dateRange.start?.toISOString().split("T")[0] ??
    "N/A"}{" "}
    to{" "}
    {appliedFilters.dateRange.end?.toISOString().split("T")[0] ??
    "N/A"}
    </div>
    )}
    {(appliedFilters.confidenceRange.min > 0 ||
    appliedFilters.confidenceRange.max < 100) && (
    <div className="px-2 py-1 text-xs text-slate-600 dark:text-slate-400">
    Confidence: {appliedFilters.confidenceRange.min} -{" "}
    {appliedFilters.confidenceRange.max}
    </div>
    )}
    <DropdownMenuSeparator />
    </>
    )}

    <DropdownMenuLabel className="flex items-center gap-2">
    <ListChecks className="h-4 w-4 text-slate-500" aria-hidden="true" />
    Include Sections
    </DropdownMenuLabel>
    <DropdownMenuCheckboxItem
    checked={selectedSections.has("import")}
    onCheckedChange={() => toggleExportSection("import")}
    >
    <BarChart2
    className="mr-2 h-4 w-4 text-blue-500"
    aria-hidden="true"
    />
    Import statistics
    </DropdownMenuCheckboxItem>
    <DropdownMenuCheckboxItem
    checked={selectedSections.has("sync")}
    onCheckedChange={() => toggleExportSection("sync")}
    >
    <Activity
    className="mr-2 h-4 w-4 text-emerald-500"
    aria-hidden="true"
    />
    Sync performance
    </DropdownMenuCheckboxItem>
    <DropdownMenuCheckboxItem
    checked={selectedSections.has("matches")}
    onCheckedChange={() => toggleExportSection("matches")}
    disabled={matchCount === 0}
    >
    <BarChart3
    className="mr-2 h-4 w-4 text-purple-500"
    aria-hidden="true"
    />
    Match results
    </DropdownMenuCheckboxItem>

    <DropdownMenuSeparator />

    <DropdownMenuItem
    onClick={handleExport}
    className="bg-primary/5 text-primary hover:bg-primary/10 cursor-pointer font-medium"
    >
    <Download className="mr-2 h-4 w-4" aria-hidden="true" />
    Export now
    </DropdownMenuItem>
    </DropdownMenuContent>
    </DropdownMenu>
    );
    }