using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using UglyToad.PdfPig;
using WpfColor = System.Windows.Media.Color;

namespace AxCopilot.Views;

/// <summary>
/// F3 파일 빠른 미리보기 창.
/// 선택된 파일/폴더의 내용을 이미지·텍스트·정보 3가지 뷰로 표시합니다.
/// </summary>
public partial class QuickLookWindow : Window
{
    // ─── 지원 확장자 ──────────────────────────────────────────────────────────

    private static readonly HashSet<string> ImageExts = new(StringComparer.OrdinalIgnoreCase)
    {
        ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif"
    };

    private static readonly HashSet<string> TextExts = new(StringComparer.OrdinalIgnoreCase)
    {
        ".txt", ".md", ".cs", ".vb", ".fs", ".py", ".js", ".ts", ".jsx", ".tsx",
        ".json", ".xml", ".xaml", ".yaml", ".yml", ".toml", ".ini", ".conf",
        ".log", ".csv", ".html", ".htm", ".css", ".scss", ".less",
        ".sql", ".sh", ".bash", ".bat", ".cmd", ".ps1",
        ".config", ".env", ".gitignore", ".editorconfig",
        ".java", ".cpp", ".c", ".h", ".hpp", ".rs", ".go", ".rb", ".php", ".swift",
        ".vue", ".svelte", ".dockerfile"
    };

    private static readonly HashSet<string> CodeExts = new(StringComparer.OrdinalIgnoreCase)
    {
        ".cs", ".vb", ".fs", ".py", ".js", ".ts", ".jsx", ".tsx",
        ".json", ".xml", ".xaml", ".yaml", ".yml", ".toml",
        ".sql", ".sh", ".bash", ".bat", ".cmd", ".ps1",
        ".java", ".cpp", ".c", ".h", ".hpp", ".rs", ".go", ".rb", ".php", ".swift",
        ".css", ".scss", ".less", ".html", ".htm", ".vue", ".svelte"
    };

    private static readonly HashSet<string> PdfExts = new(StringComparer.OrdinalIgnoreCase)
        { ".pdf" };

    private static readonly HashSet<string> WordExts = new(StringComparer.OrdinalIgnoreCase)
        { ".docx", ".doc" };

    private static readonly HashSet<string> ExcelExts = new(StringComparer.OrdinalIgnoreCase)
        { ".xlsx", ".xls" };

    // ─── 편집 상태 ────────────────────────────────────────────────────────────

    private string  _currentFilePath = "";   // 현재 표시 중인 파일 경로
    private string  _currentFileExt  = "";   // 현재 파일 확장자
    private bool    _isEditMode;             // 편집 모드 활성 여부
    private bool    _isModified;             // 미저장 변경 있음
    private bool    _suppressTextChanged;    // TextChanged 이벤트 억제 플래그

    // ─── 생성 ─────────────────────────────────────────────────────────────────

    public QuickLookWindow(string path, Window owner)
    {
        InitializeComponent();
        Owner = owner;
        KeyDown += OnKeyDown;
        Loaded  += (_, _) => LoadPreview(path);
    }

    // ─── 이벤트 ──────────────────────────────────────────────────────────────

    private void OnKeyDown(object sender, KeyEventArgs e)
    {
        // ─ 편집 모드 단축키 ─────────────────────────────────────────
        if (_isEditMode)
        {
            // Ctrl+S → 저장
            if (e.Key == Key.S && Keyboard.Modifiers == ModifierKeys.Control)
            {
                SaveEditedFile();
                e.Handled = true;
                return;
            }
            // Ctrl+E → 편집 모드 토글
            if (e.Key == Key.E && Keyboard.Modifiers == ModifierKeys.Control)
            {
                ToggleEditMode();
                e.Handled = true;
                return;
            }
            // Esc → 편집 모드 종료 (미저장 확인)
            if (e.Key == Key.Escape)
            {
                if (_isModified)
                {
                    var result = CustomMessageBox.Show(
                        "저장하지 않은 변경 내용이 있습니다.\n편집을 취소하고 미리보기로 돌아갈까요?",
                        "AX Copilot — 편집 취소",
                        MessageBoxButton.YesNo,
                        MessageBoxImage.Question);
                    if (result != MessageBoxResult.Yes) { e.Handled = true; return; }
                }
                ExitEditMode();
                e.Handled = true;
                return;
            }
            return; // 편집 모드에서 F3 등은 무시 (TextBox가 처리)
        }

        // ─ 미리보기 모드 단축키 ─────────────────────────────────────
        if (e.Key == Key.E && Keyboard.Modifiers == ModifierKeys.Control)
        {
            ToggleEditMode();
            e.Handled = true;
            return;
        }

        if (e.Key is Key.Escape or Key.F3)
        {
            Close();
            e.Handled = true;
        }
    }

    private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed) DragMove();
    }

    private void BtnClose_Click(object sender, MouseButtonEventArgs e)
    {
        if (_isEditMode && _isModified)
        {
            var result = CustomMessageBox.Show(
                "저장하지 않은 변경 내용이 있습니다.\n저장하지 않고 닫을까요?",
                "AX Copilot — 저장 확인",
                MessageBoxButton.YesNo,
                MessageBoxImage.Question);
            if (result != MessageBoxResult.Yes) return;
        }
        Close();
    }

    // ─── 편집 버튼 클릭 ─────────────────────────────────────────────────────

    private void BtnEdit_Click(object sender, MouseButtonEventArgs e) => ToggleEditMode();

    // ─── 편집 텍스트 변경 감지 ───────────────────────────────────────────────

    private void TextEditBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
    {
        if (_suppressTextChanged) return;
        if (!_isModified)
        {
            _isModified = true;
            // 타이틀에 ● 수정 마커 추가
            if (!FileNameText.Text.StartsWith("● "))
                FileNameText.Text = "● " + FileNameText.Text;
        }
    }

    // ─── 편집 모드 진입/종료 ─────────────────────────────────────────────────

    private void ToggleEditMode()
    {
        if (!_isEditMode)
            EnterEditMode();
        else
        {
            if (_isModified)
            {
                var result = CustomMessageBox.Show(
                    "저장하지 않은 변경 내용이 있습니다.\n저장하고 미리보기로 돌아갈까요?",
                    "AX Copilot — 편집 모드 종료",
                    MessageBoxButton.YesNoCancel,
                    MessageBoxImage.Question);
                if (result == MessageBoxResult.Cancel) return;
                if (result == MessageBoxResult.Yes) SaveEditedFile();
            }
            ExitEditMode();
        }
    }

    private void EnterEditMode()
    {
        if (string.IsNullOrEmpty(_currentFilePath) || !TextExts.Contains(_currentFileExt)) return;

        try
        {
            // 파일 전체 내용 읽기 (미리보기의 300줄 제한 없이)
            string fullContent;
            try   { fullContent = File.ReadAllText(_currentFilePath, Encoding.UTF8); }
            catch { fullContent = File.ReadAllText(_currentFilePath); }

            _suppressTextChanged = true;
            TextEditBox.Text = fullContent;
            _suppressTextChanged = false;

            // UI 전환: 미리보기 → 편집
            TextScrollViewer.Visibility = Visibility.Collapsed;
            TextEditBox.Visibility      = Visibility.Visible;
            TextEditBox.Focus();

            // 편집 버튼 아이콘 → 👁 (보기 모드로 전환 의미)
            BtnEditIcon.Text = "\uE890"; // Eye / View icon
            BtnEdit.ToolTip  = "미리보기 모드로 전환 (Ctrl+E)";
            BtnEditIcon.Foreground = TryFindResource("AccentColor") as Brush
                                     ?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(75, 94, 252));

            _isEditMode = true;
            _isModified = false;

            // 상태 힌트를 하단 FooterMeta에 추가
            FooterMeta.Text = FooterMeta.Text + "  ·  ✏ 편집 모드  (Ctrl+S 저장 · Esc 취소)";
        }
        catch (Exception ex)
        {
            Services.LogService.Warn($"편집 모드 진입 실패: {ex.Message}");
            Services.NotificationService.Notify("AX Copilot", $"파일을 열 수 없습니다: {ex.Message}");
        }
    }

    private void ExitEditMode()
    {
        TextEditBox.Visibility      = Visibility.Collapsed;
        TextScrollViewer.Visibility = Visibility.Visible;

        // 편집 버튼 아이콘 복원 → ✏
        BtnEditIcon.Text       = "\uE70F";
        BtnEdit.ToolTip        = "편집 모드 전환 (Ctrl+E)";
        BtnEditIcon.Foreground = TryFindResource("SecondaryText") as Brush
                                 ?? Brushes.Gray;

        _isEditMode = false;
        _isModified = false;

        // 미리보기 다시 로드 (편집된 내용 반영)
        ReloadPreview();
    }

    private void SaveEditedFile()
    {
        if (string.IsNullOrEmpty(_currentFilePath)) return;
        try
        {
            File.WriteAllText(_currentFilePath, TextEditBox.Text, Encoding.UTF8);
            _isModified = false;
            // ● 마커 제거
            if (FileNameText.Text.StartsWith("● "))
                FileNameText.Text = FileNameText.Text[2..];

            // 하단 상태 갱신
            var info = new FileInfo(_currentFilePath);
            FooterMeta.Text = $"{FormatSize(info.Length)}  ·  {info.LastWriteTime:yyyy-MM-dd HH:mm}  ·  ✏ 편집 모드  (Ctrl+S 저장 · Esc 취소)";

            Services.NotificationService.Notify("저장 완료", $"{Path.GetFileName(_currentFilePath)} 저장됨");
            Services.LogService.Info($"인라인 편집 저장: {_currentFilePath}");
        }
        catch (Exception ex)
        {
            Services.LogService.Error($"파일 저장 실패: {ex.Message}");
            CustomMessageBox.Show(
                $"파일을 저장하지 못했습니다.\n{ex.Message}",
                "저장 오류",
                MessageBoxButton.OK,
                MessageBoxImage.Error);
        }
    }

    private void ReloadPreview()
    {
        // 편집 후 미리보기 새로고침 (변경된 내용 반영)
        if (string.IsNullOrEmpty(_currentFilePath) || !File.Exists(_currentFilePath)) return;

        // 파일명 ● 마커 제거
        if (FileNameText.Text.StartsWith("● "))
            FileNameText.Text = FileNameText.Text[2..];

        // 상태 바 복원
        var info = new FileInfo(_currentFilePath);
        FooterMeta.Text = $"{FormatSize(info.Length)}  ·  {info.LastWriteTime:yyyy-MM-dd HH:mm}";

        // 텍스트 뷰 갱신
        LoadTextPreview(_currentFilePath, _currentFileExt);
    }

    // ─── 미리보기 로드 ───────────────────────────────────────────────────────

    private void LoadPreview(string path)
    {
        try
        {
            FileNameText.Text = Path.GetFileName(path);

            if (Directory.Exists(path))
            {
                LoadFolderInfo(path);
                return;
            }

            if (!File.Exists(path))
            {
                ShowInfo("\uE7BA", "파일을 찾을 수 없습니다.", "");
                return;
            }

            var ext  = Path.GetExtension(path);
            var info = new FileInfo(path);

            // 편집 상태 저장
            _currentFilePath = path;
            _currentFileExt  = ext;

            FooterPath.Text = path;
            FooterMeta.Text = $"{FormatSize(info.Length)}  ·  {info.LastWriteTime:yyyy-MM-dd HH:mm}";

            if (ImageExts.Contains(ext))
            {
                BtnEdit.Visibility = Visibility.Collapsed;
                LoadImagePreview(path, info);
            }
            else if (PdfExts.Contains(ext))
            {
                BtnEdit.Visibility = Visibility.Collapsed;
                LoadPdfPreview(path, info);
            }
            else if (WordExts.Contains(ext))
            {
                BtnEdit.Visibility = Visibility.Collapsed;
                LoadWordPreview(path, info);
            }
            else if (ExcelExts.Contains(ext))
            {
                BtnEdit.Visibility = Visibility.Collapsed;
                LoadExcelPreview(path, info);
            }
            else if (TextExts.Contains(ext))
            {
                BtnEdit.Visibility = Visibility.Visible; // ✏ 편집 버튼 표시
                LoadTextPreview(path, ext);
            }
            else
            {
                BtnEdit.Visibility = Visibility.Collapsed;
                LoadFileInfo(path, ext, info);
            }
        }
        catch (Exception ex)
        {
            ShowInfo("\uE783", $"미리보기 오류", ex.Message);
        }
    }

    // ─── 이미지 ──────────────────────────────────────────────────────────────

    private void LoadImagePreview(string path, FileInfo info)
    {
        FileTypeIcon.Text = "\uEB9F";
        try
        {
            var bi = new BitmapImage();
            bi.BeginInit();
            bi.CacheOption    = BitmapCacheOption.OnLoad;
            bi.UriSource      = new Uri(path, UriKind.Absolute);
            bi.DecodePixelWidth = 800; // 메모리 절약
            bi.EndInit();
            bi.Freeze();

            PreviewImage.Source = bi;
            // 이미지 해상도를 타이틀바에 추가
            FileNameText.Text = $"{Path.GetFileName(path)}  ({bi.PixelWidth}×{bi.PixelHeight})";
            ImageScrollViewer.Visibility = Visibility.Visible;
        }
        catch
        {
            ShowInfo("\uEB9F", "이미지를 불러올 수 없습니다.", info.Name);
        }
    }

    // ─── 텍스트 / 코드 ────────────────────────────────────────────────────────

    private void LoadTextPreview(string path, string ext)
    {
        FileTypeIcon.Text = "\uE8A5";
        try
        {
            const int MaxLines = 300;
            List<string> lines;
            try
            {
                lines = File.ReadLines(path, Encoding.UTF8).Take(MaxLines).ToList();
            }
            catch
            {
                lines = File.ReadLines(path).Take(MaxLines).ToList();
            }

            var text = string.Join("\n", lines);
            if (lines.Count == MaxLines) text += "\n\n… (이하 생략, 최대 300줄)";

            PreviewText.Text = text;

            // 코드 파일: 줄 번호 + 배경 강조
            if (CodeExts.Contains(ext))
            {
                ApplyCodeStyle(lines, ext);
            }

            TextScrollViewer.Visibility = Visibility.Visible;
        }
        catch
        {
            ShowInfo("\uE8A5", "텍스트를 불러올 수 없습니다.", path);
        }
    }

    private void ApplyCodeStyle(List<string> lines, string ext)
    {
        // 줄 번호 표시
        LineNumbers.Text    = string.Join("\n", Enumerable.Range(1, lines.Count));
        LineNumbers.Visibility = Visibility.Visible;
        LineNumBg.Visibility   = Visibility.Visible;

        // 코드 본문을 줄 번호 너비만큼 우측으로 밀기
        PreviewText.Margin = new Thickness(50, 12, 14, 12);

        // 확장자별 배경 색조
        var (accent, dim) = ext.ToLowerInvariant() switch
        {
            ".cs" or ".vb" or ".fs"       => ("#0A6ABDE8", "#10143A57"), // 파랑 — C#/VB/F#
            ".py"                          => ("#0AF5D55C", "#10254A10"), // 초록 — Python
            ".js" or ".ts" or ".jsx" or ".tsx" => ("#0AF0B429", "#10453200"), // 앰버 — JS/TS
            ".json" or ".yaml" or ".yml" or ".toml" => ("#0AB0B0C0", "#10202030"), // 회색 — 데이터
            ".sql"                         => ("#0AFF8C69", "#10401A00"), // 주황 — SQL
            ".html" or ".htm" or ".xml" or ".xaml" => ("#0AFF7878", "#10400000"), // 빨강 — 마크업
            ".css" or ".scss" or ".less"   => ("#0AFF69B4", "#10400030"), // 핑크 — 스타일
            ".sh" or ".bash" or ".ps1" or ".bat" => ("#0A90FF90", "#10003010"), // 연두 — 쉘
            ".cpp" or ".c" or ".h" or ".hpp" or ".rs" => ("#0AD499FF", "#10181830"), // 보라 — C/C++/Rust
            ".go"                          => ("#0A00BCD4", "#10001E2A"), // 청록 — Go
            _                              => ("#0AFFFFFF", "#10181828"),  // 기본
        };

        try
        {
            var accentBrush = new SolidColorBrush(
                (WpfColor)ColorConverter.ConvertFromString(accent));
            var dimBrush = new SolidColorBrush(
                (WpfColor)ColorConverter.ConvertFromString(dim));

            // TextScrollViewer 배경에 코드 색조 적용
            TextScrollViewer.Background = dimBrush;
            LineNumBg.Background        = accentBrush;
        }
        catch { /* 색 변환 실패 무시 */ }
    }

    // ─── PDF ─────────────────────────────────────────────────────────────────

    private void LoadPdfPreview(string path, FileInfo info)
    {
        FileTypeIcon.Text = "\uEA90";
        try
        {
            using var doc = PdfDocument.Open(path);
            var totalPages = doc.NumberOfPages;
            var sb = new StringBuilder();
            const int PreviewPages = 10;
            var pages = Math.Min(totalPages, PreviewPages);

            for (int i = 1; i <= pages; i++)
            {
                var page = doc.GetPage(i);
                var pageText = page.Text;
                if (!string.IsNullOrWhiteSpace(pageText))
                {
                    sb.AppendLine($"── 페이지 {i} ──");
                    sb.AppendLine(pageText.Trim());
                    sb.AppendLine();
                }
            }

            if (totalPages > PreviewPages)
                sb.AppendLine($"… (전체 {totalPages}페이지 중 {PreviewPages}페이지 표시)");

            PdfMetaText.Text    = $"{totalPages}페이지  ·  {FormatSize(info.Length)}";
            PdfPreviewText.Text = sb.Length > 0 ? sb.ToString() : "(텍스트 추출 불가 — 스캔 PDF)";
            PdfScrollViewer.Visibility = Visibility.Visible;
        }
        catch (Exception ex)
        {
            ShowInfo("\uEA90", "PDF를 불러올 수 없습니다.", ex.Message);
        }
    }

    // ─── Word (.docx) ────────────────────────────────────────────────────────

    private void LoadWordPreview(string path, FileInfo info)
    {
        FileTypeIcon.Text     = "\uE8A5";
        OfficeTypeIcon.Text   = "\uE8A5";
        try
        {
            using var doc = WordprocessingDocument.Open(path, false);
            var body = doc.MainDocumentPart?.Document?.Body;
            if (body == null) { ShowInfo("\uE8A5", "Word 문서를 열 수 없습니다.", path); return; }

            var sb = new StringBuilder();
            foreach (var para in body.Descendants<DocumentFormat.OpenXml.Wordprocessing.Paragraph>())
            {
                var line = para.InnerText;
                if (!string.IsNullOrWhiteSpace(line))
                    sb.AppendLine(line);
                if (sb.Length > 8000) { sb.AppendLine("\n… (이하 생략)"); break; }
            }

            OfficeMetaText.Text     = $"Word 문서  ·  {FormatSize(info.Length)}";
            OfficePreviewText.Text  = sb.Length > 0 ? sb.ToString() : "(내용 없음)";
            OfficeScrollViewer.Visibility = Visibility.Visible;
        }
        catch (Exception ex)
        {
            ShowInfo("\uE8A5", "Word 문서를 불러올 수 없습니다.", ex.Message);
        }
    }

    // ─── Excel (.xlsx) ────────────────────────────────────────────────────────

    private void LoadExcelPreview(string path, FileInfo info)
    {
        FileTypeIcon.Text     = "\uE9F9";
        OfficeTypeIcon.Text   = "\uE9F9";
        try
        {
            using var doc = SpreadsheetDocument.Open(path, false);
            var wb = doc.WorkbookPart;
            if (wb == null) { ShowInfo("\uE9F9", "Excel 문서를 열 수 없습니다.", path); return; }

            // 공유 문자열 테이블
            var sharedStrings = wb.SharedStringTablePart?.SharedStringTable
                .Elements<SharedStringItem>()
                .Select(x => x.InnerText)
                .ToList() ?? new List<string>();

            var sb = new StringBuilder();
            int sheetCount = 0;

            foreach (var sheetPart in wb.WorksheetParts.Take(3))
            {
                sheetCount++;
                sb.AppendLine($"── 시트 {sheetCount} ──");
                var rows = sheetPart.Worksheet.Descendants<Row>().Take(50);
                foreach (var row in rows)
                {
                    var cells = row.Elements<Cell>().Select(c =>
                    {
                        if (c.DataType?.Value == CellValues.SharedString &&
                            int.TryParse(c.InnerText, out int idx) && idx < sharedStrings.Count)
                            return sharedStrings[idx];
                        return c.InnerText;
                    });
                    sb.AppendLine(string.Join("\t", cells));
                    if (sb.Length > 8000) { sb.AppendLine("… (이하 생략)"); goto done; }
                }
            }
            done:
            OfficeMetaText.Text    = $"Excel 문서  ·  {FormatSize(info.Length)}";
            OfficePreviewText.Text = sb.Length > 0 ? sb.ToString() : "(내용 없음)";
            OfficeScrollViewer.Visibility = Visibility.Visible;
        }
        catch (Exception ex)
        {
            ShowInfo("\uE9F9", "Excel 문서를 불러올 수 없습니다.", ex.Message);
        }
    }

    // ─── 폴더 ────────────────────────────────────────────────────────────────

    private void LoadFolderInfo(string path)
    {
        FileTypeIcon.Text    = "\uE8B7";
        InfoTypeIcon.Text    = "\uE8B7";
        FooterPath.Text      = path;

        try
        {
            var di    = new DirectoryInfo(path);
            var items = di.GetFileSystemInfos();
            var files = items.Count(i => i is FileInfo);
            var dirs  = items.Count(i => i is DirectoryInfo);

            InfoTypeName.Text = di.Name;
            InfoSubText.Text  = $"파일 {files}개  ·  폴더 {dirs}개";
            FooterMeta.Text   = $"수정: {di.LastWriteTime:yyyy-MM-dd HH:mm}";
        }
        catch
        {
            InfoTypeName.Text = Path.GetFileName(path);
            InfoSubText.Text  = "폴더";
        }

        InfoPanel.Visibility = Visibility.Visible;
    }

    // ─── 기타 파일 정보 ──────────────────────────────────────────────────────

    private void LoadFileInfo(string path, string ext, FileInfo info)
    {
        var (icon, typeName) = ext.ToLowerInvariant() switch
        {
            ".exe" or ".msi" or ".appx"       => ("\uE756", "실행 파일"),
            ".pdf"                             => ("\uEA90", "PDF 문서"),
            ".docx" or ".doc"                  => ("\uE8A5", "Word 문서"),
            ".xlsx" or ".xls"                  => ("\uE9F9", "Excel 문서"),
            ".pptx" or ".ppt"                  => ("\uE8A5", "PowerPoint"),
            ".zip" or ".7z" or ".rar" or ".gz" => ("\uED25", "압축 파일"),
            ".mp4" or ".avi" or ".mkv" or ".mov" or ".wmv" => ("\uE714", "동영상"),
            ".mp3" or ".wav" or ".flac" or ".aac"           => ("\uE767", "오디오"),
            ".lnk"                             => ("\uE71B", "바로 가기"),
            ".dll" or ".sys"                   => ("\uECAA", "시스템 파일"),
            _ => ("\uE7C3", ext.TrimStart('.').ToUpperInvariant() + " 파일")
        };

        FileTypeIcon.Text    = icon;
        InfoTypeIcon.Text    = icon;
        InfoTypeName.Text    = typeName;
        InfoSubText.Text     = info.Name;
        InfoPanel.Visibility = Visibility.Visible;
    }

    // ─── 오류/정보 패널 ───────────────────────────────────────────────────────

    private void ShowInfo(string icon, string title, string sub)
    {
        InfoTypeIcon.Text    = icon;
        InfoTypeName.Text    = title;
        InfoSubText.Text     = sub;
        InfoPanel.Visibility = Visibility.Visible;
    }

    // ─── 파일 크기 포맷 ───────────────────────────────────────────────────────

    private static string FormatSize(long bytes) => bytes switch
    {
        < 1_024L              => $"{bytes} B",
        < 1_024L * 1_024      => $"{bytes / 1_024.0:F1} KB",
        < 1_024L * 1_024 * 1_024 => $"{bytes / 1_024.0 / 1_024.0:F1} MB",
        _                     => $"{bytes / 1_024.0 / 1_024.0 / 1_024.0:F1} GB"
    };
}
