using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using AxCopilot.Core;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;

namespace AxCopilot.ViewModels;

public partial class LauncherViewModel : INotifyPropertyChanged
{
    private static App? CurrentApp => System.Windows.Application.Current as App;

    private readonly CommandResolver _resolver;
    private readonly SettingsService _settings;

    private string _inputText = "";
    private LauncherItem? _selectedItem;
    private bool _isLoading;
    private CancellationTokenSource? _searchCts;
    private System.Threading.Timer? _debounceTimer;
    private const int DebounceMs = 30; // 30ms 디바운스 — 연속 입력 시 중간 검색 스킵
    private string _lastSearchQuery = ""; // IME 조합 완성 후 동일 쿼리 재검색 방지용

    // ─── 검색 히스토리 탐색 ──────────────────────────────────────────────────
    private bool _isHistoryNavigation; // InputText setter에서 히스토리 인덱스 리셋 방지
    private int  _historyIndex = -1;   // -1 = 탐색 중 아님

    // ─── 파일 액션 모드 ───────────────────────────────────────────────────────
    private bool _isActionMode;
    private LauncherItem? _actionSourceItem;
    private string _savedQuery = "";

    // ─── 클립보드 병합 ────────────────────────────────────────────────────────
    private readonly HashSet<ClipboardEntry> _mergeQueue = new();

    /// <summary>
    /// 검색 결과 컬렉션. ReplaceAll() 호출 시 단일 Reset 이벤트로 WPF 레이아웃 1회만 갱신.
    /// </summary>
    public BulkObservableCollection<LauncherItem> Results { get; } = new();

    /// <summary>
    /// 그룹핑이 적용된 결과 뷰. prefix 없는 일반 검색에서 앱/폴더/파일/단축키 섹션 구분 표시.
    /// Group == null 항목은 별도 그룹 헤더 없이 표시됩니다.
    /// </summary>
    public ICollectionView GroupedResults { get; }

    /// <summary>
    /// 퀵 액션 바 — 가장 많이 실행한 항목을 입력창 아래 칩으로 표시합니다.
    /// 입력창이 비어 있을 때만 표시됩니다.
    /// </summary>
    public ObservableCollection<QuickActionChip> QuickActionItems { get; } = new();

    /// <summary>퀵 액션 칩 표시 조건: 입력이 없고 칩이 하나 이상 있을 때</summary>
    public bool ShowQuickActions => string.IsNullOrEmpty(_inputText) && QuickActionItems.Count > 0;

    // ─── 기본 프로퍼티 ────────────────────────────────────────────────────────

    public string InputText
    {
        get => _inputText;
        set
        {
            if (_inputText == value) return;
            _inputText = value;
            // 사용자 직접 입력 시 히스토리 탐색 위치 초기화
            if (!_isHistoryNavigation) _historyIndex = -1;
            OnPropertyChanged();
            OnPropertyChanged(nameof(HasActivePrefix));
            OnPropertyChanged(nameof(ActivePrefixLabel));
            OnPropertyChanged(nameof(ActivePrefixSymbol));
            OnPropertyChanged(nameof(ActivePrefixBrush));
            OnPropertyChanged(nameof(IsClipboardMode));
            OnPropertyChanged(nameof(ShowMergeHint));
            OnPropertyChanged(nameof(MergeHintText));

            OnPropertyChanged(nameof(ShowQuickActions));

            // 연속 입력 시 이전 검색 즉시 취소 + 50ms 디바운스 후 실제 검색 시작
            _searchCts?.Cancel();
            _debounceTimer?.Dispose();
            if (string.IsNullOrWhiteSpace(value))
            {
                Results.Clear();
            }
            else
            {
                var captured = value;
                _debounceTimer = new System.Threading.Timer(
                    _ => Application.Current?.Dispatcher.InvokeAsync(() => _ = SearchAsync(captured)),
                    null, DebounceMs, System.Threading.Timeout.Infinite);
            }
        }
    }

    // ─── 미리보기 패널 ────────────────────────────────────────────────────────
    private string _previewText = "";
    private bool _hasPreview;
    private CancellationTokenSource? _previewCts;

    public string PreviewText
    {
        get => _previewText;
        private set { _previewText = value; OnPropertyChanged(); }
    }

    public bool HasPreview
    {
        get => _hasPreview;
        private set { _hasPreview = value; OnPropertyChanged(); }
    }

    public LauncherItem? SelectedItem
    {
        get => _selectedItem;
        set
        {
            _selectedItem = value;
            OnPropertyChanged();
            _ = UpdatePreviewAsync(value);
        }
    }

    public bool IsLoading
    {
        get => _isLoading;
        set { _isLoading = value; OnPropertyChanged(); }
    }

    private string _placeholderText = AxCopilot.Services.L10n.Get("placeholder");
    public string PlaceholderText
    {
        get => _placeholderText;
        private set { _placeholderText = value; OnPropertyChanged(); }
    }

    /// <summary>런처가 열릴 때마다 호출 — 설정에 따라 랜덤 또는 고정 문구로 교체합니다.</summary>
    public void RefreshPlaceholder() =>
        PlaceholderText = _settings.Settings.Launcher.EnableRandomPlaceholder
            ? AxCopilot.Services.L10n.GetRandomPlaceholder()
            : AxCopilot.Services.L10n.Get("placeholder");

    /// <summary>아이콘 애니메이션 설정값</summary>
    public bool EnableIconAnimation => _settings.Settings.Launcher.EnableIconAnimation;
    /// <summary>런처 무지개 글로우 설정값</summary>
    public bool EnableRainbowGlow => _settings.Settings.Launcher.EnableRainbowGlow;
    /// <summary>선택 아이템 글로우 설정값</summary>
    public bool EnableSelectionGlow => _settings.Settings.Launcher.EnableSelectionGlow;
    /// <summary>런처 창 테두리 표시 설정값</summary>
    public bool ShowLauncherBorder => _settings.Settings.Launcher.ShowLauncherBorder;
    public string ThemeSetting => _settings.Settings.Launcher.Theme;
    public CustomThemeColors? CustomThemeColors => _settings.Settings.Launcher.CustomTheme;
    public string WindowPosition => _settings.Settings.Launcher.Position;

    // ─── Prefix 배지 ─────────────────────────────────────────────────────────

    private static readonly Dictionary<string, (string Label, string Symbol, string ColorHex)> PrefixMap = new()
    {
        // ─── 기본 프리픽스 ──────────────────────────────────────────────────────
        { "@",         ("URL",    Symbols.Globe,         "#0078D4") },
        { "~",         ("워크",   Symbols.Workspace,     "#C50F1F") },
        { ">",         ("명령",   Symbols.Terminal,      "#323130") },
        { "$",         ("클립",   Symbols.Clipboard,     "#8764B8") },
        { "cd",        ("폴더",   Symbols.Folder,        "#107C10") },
        { "#",         ("히스",   Symbols.History,       "#B7791F") },
        { ";",         ("스닛",   Symbols.Snippet,       "#0F6CBD") },
        { "=",         ("계산",   Symbols.Calculator,    "#4B5EFC") },
        { "!",         ("AI",     "\uE8BD",              "#8B2FC9") },
        { "?",         ("검색",   Symbols.Globe,         "#006EAF") },
        { "/",         ("시스템", Symbols.Power,         "#4A4A4A") },
        // ─── 시스템 도구 ─────────────────────────────────────────────────────────
        { "kill ",     ("킬",     Symbols.Error,         "#CC2222") },
        { "media ",    ("미디어", Symbols.MediaPlay,     "#1A6B3C") },
        { "info ",     ("시스템", Symbols.Computer,      "#5B4E7E") },
        { "port",      ("포트",   Symbols.Network,       "#006699") },
        // ─── v1.5 핸들러 ─────────────────────────────────────────────────────────
        { "emoji",     ("이모지", Symbols.Emoji,         "#F59E0B") },
        { "color",     ("색상",   Symbols.ColorPicker,   "#EC4899") },
        { "recent",    ("최근",   Symbols.RecentFiles,   "#059669") },
        { "note",      ("메모",   Symbols.Note,          "#7C3AED") },
        { "uninstall", ("제거",   Symbols.Uninstall,     "#DC2626") },
        // ─── v1.6 핸들러 ─────────────────────────────────────────────────────────
        { "env",       ("환경변수", Symbols.EnvVar,      "#0D9488") },
        { "json",      ("JSON",   Symbols.JsonValid,     "#D97706") },
        { "encode ",   ("인코딩", Symbols.EncodeIcon,    "#6366F1") },
        { "snap",      ("스냅",   Symbols.SnapLayout,    "#B45309") },
        { "cap",       ("캡처",   Symbols.CaptureIcon,   "#BE185D") },
        { "help",      ("도움말", Symbols.Info,          "#6B7280") },
        // ─── Phase L3-5 파일 태그 ──────────────────────────────────────────────
        { "tag",       ("태그",   Symbols.Tag,           "#6366F1") },
        // ─── Phase L3-8 알림 센터 ─────────────────────────────────────────────
        { "notif",     ("알림",   Symbols.ReminderBell,  "#F59E0B") },
        // ─── Phase L3-9 위젯 핸들러 ──────────────────────────────────────────
        { "pomo",      ("타이머", Symbols.Timer,         "#F59E0B") },
    };

    // ─── 설정 기능 토글 (런처 실동작 연결) ──────────────────────────────────
    /// <summary>번호 배지(1~9) 표시 여부 — LauncherWindow.xaml 번호 뱃지 Visibility 바인딩</summary>
    public bool ShowNumberBadges => _settings.Settings.Launcher.ShowNumberBadges;
    /// <summary>포커스 잃으면 런처 닫기 — Window_Deactivated에서 읽음</summary>
    public bool CloseOnFocusLost  => _settings.Settings.Launcher.CloseOnFocusLost;
    /// <summary>액션 모드(→) 허용 여부</summary>
    public bool EnableActionMode  => _settings.Settings.Launcher.EnableActionMode;

    /// <summary>AI 기능 활성화 여부 (AiEnabled 설정). false이면 ! prefix 숨김.</summary>
    private static bool IsAiEnabled() =>
        CurrentApp?.SettingsService?.Settings.AiEnabled ?? true;

    public bool HasActivePrefix =>
        _settings.Settings.Launcher.ShowPrefixBadge &&
        _inputText.Length > 0 && PrefixMap.Keys.Any(k =>
            (k != "!" || IsAiEnabled()) &&
            _inputText.StartsWith(k, StringComparison.OrdinalIgnoreCase));

    public string? ActivePrefix =>
        PrefixMap.Keys.FirstOrDefault(k =>
            (k != "!" || IsAiEnabled()) &&
            _inputText.StartsWith(k, StringComparison.OrdinalIgnoreCase));

    public string ActivePrefixLabel =>
        ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out var info) ? info.Label : "";

    public string ActivePrefixSymbol =>
        ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out var info) ? info.Symbol : Symbols.Search;

    public SolidColorBrush ActivePrefixBrush
    {
        get
        {
            if (ActivePrefix != null && PrefixMap.TryGetValue(ActivePrefix, out var info))
            {
                var color = (Color)ColorConverter.ConvertFromString(info.ColorHex);
                return new SolidColorBrush(color);
            }
            return new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
        }
    }

    // ─── 파일 액션 모드 프로퍼티 ─────────────────────────────────────────────

    public bool IsActionMode
    {
        get => _isActionMode;
        private set
        {
            _isActionMode = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(ShowActionModeBar));
            OnPropertyChanged(nameof(ActionModeBreadcrumb));
        }
    }

    public bool ShowActionModeBar => IsActionMode;
    public string ActionModeBreadcrumb => _actionSourceItem?.Title ?? "";

    /// <summary>현재 선택 항목이 파일 액션 가능 여부</summary>
    public bool CanEnterActionMode() =>
        !IsActionMode && SelectedItem?.Data is IndexEntry;

    // ─── 클립보드 병합 모드 프로퍼티 ─────────────────────────────────────────

    public bool IsClipboardMode => _inputText.StartsWith("#", StringComparison.Ordinal);

    public int MergeCount => _mergeQueue.Count;

    public bool ShowMergeHint => _mergeQueue.Count > 0 && IsClipboardMode;

    public string MergeHintText =>
        _mergeQueue.Count > 0
            ? $"{_mergeQueue.Count}개 선택됨  ·  Shift+Enter로 합치기  ·  Esc로 취소"
            : "";

    public bool IsItemMarkedForMerge(LauncherItem item) =>
        item.Data is ClipboardEntry e && _mergeQueue.Contains(e);

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

    public event EventHandler? CloseRequested;
    public event EventHandler<string>? NotificationRequested;

    public LauncherViewModel(CommandResolver resolver, SettingsService settings)
    {
        _resolver = resolver;
        _settings = settings;

        // 그룹핑 뷰 초기화 (UI 스레드에서 생성 보장)
        GroupedResults = CollectionViewSource.GetDefaultView(Results);
        GroupedResults.GroupDescriptions.Add(
            new PropertyGroupDescription(nameof(LauncherItem.Group)));
    }

    // ─── 런처 표시 시 초기화 ──────────────────────────────────────────────────

    /// <summary>런처가 표시될 때 호출 — 이전 상태 초기화</summary>
    public void OnShown()
    {
        if (IsActionMode) { IsActionMode = false; _actionSourceItem = null; }
        Results.Clear();
        _lastSearchQuery = "";
        _historyIndex = -1;
        ClearMerge();
        LoadQuickActions();
    }

    // ─── 검색 히스토리 탐색 ──────────────────────────────────────────────────

    /// <summary>
    /// ↑ 키 — 이전(오래된) 히스토리 항목으로 이동.
    /// null이면 히스토리 없음.
    /// </summary>
    public string? NavigateHistoryPrev()
    {
        var history = SearchHistoryService.GetAll();
        if (history.Count == 0) return null;
        _historyIndex = Math.Min(_historyIndex + 1, history.Count - 1);
        return history[_historyIndex];
    }

    /// <summary>
    /// ↓ 키 — 최신 히스토리 항목으로 이동.
    /// _historyIndex가 0 이하면 빈 문자열(현재 입력으로 복귀).
    /// </summary>
    public string? NavigateHistoryNext()
    {
        if (_historyIndex <= 0) { _historyIndex = -1; return ""; }
        _historyIndex--;
        var history = SearchHistoryService.GetAll();
        return _historyIndex >= 0 && _historyIndex < history.Count
            ? history[_historyIndex] : "";
    }

    /// <summary>
    /// 히스토리에서 탐색한 텍스트를 입력창에 설정합니다.
    /// InputText setter의 히스토리 인덱스 리셋을 억제합니다.
    /// </summary>
    public void SetInputFromHistory(string text)
    {
        _isHistoryNavigation = true;
        try { InputText = text; }
        finally { _isHistoryNavigation = false; }
    }

    /// <summary>
    /// UsageRankingService 상위 항목에서 퀵 액션 칩을 생성합니다.
    /// 실제로 존재하는 파일/폴더만 표시하며 최대 8개로 제한합니다.
    /// </summary>
    public void LoadQuickActions()
    {
        QuickActionItems.Clear();
        var topItems = UsageRankingService.GetTopItems(16); // 여유분 확보 (일부 경로가 없을 수 있음)
        var added = 0;
        foreach (var (path, _) in topItems)
        {
            if (added >= 8) break;
            var expanded = Environment.ExpandEnvironmentVariables(path);
            var isFolder = Directory.Exists(expanded);
            var isFile   = !isFolder && File.Exists(expanded);
            if (!isFolder && !isFile) continue; // 삭제된 항목 건너뜀

            var ext   = Path.GetExtension(expanded).ToLowerInvariant();
            var title = Path.GetFileNameWithoutExtension(expanded);
            if (string.IsNullOrEmpty(title)) title = Path.GetFileName(expanded);

            var symbol = isFolder           ? Symbols.Folder
                       : ext == ".exe"      ? Symbols.App
                       : ext is ".lnk"
                              or ".url"     ? Symbols.App
                       : Symbols.File;

            var color  = isFolder           ? Color.FromRgb(0x10, 0x7C, 0x10)
                       : ext == ".exe"      ? Color.FromRgb(0x4B, 0x5E, 0xFC)
                       : ext is ".lnk"
                              or ".url"     ? Color.FromRgb(0x4B, 0x5E, 0xFC)
                       : Color.FromRgb(0x5B, 0x4E, 0x7E);

            var bg = new SolidColorBrush(Color.FromArgb(0x26, color.R, color.G, color.B));
            QuickActionItems.Add(new QuickActionChip(title, symbol, path, bg));
            added++;
        }
        OnPropertyChanged(nameof(ShowQuickActions));
    }

    // ─── 검색 ────────────────────────────────────────────────────────────────

    /// <summary>
    /// IME 조합 중 코드비하인드에서 직접 호출하는 검색 트리거.
    /// InputText 바인딩을 건드리지 않으므로 한글 조합 상태가 유지됩니다.
    /// 동일 쿼리로 재호출 시 (IME 조합 완성 이벤트 중복) 검색을 건너뜁니다.
    /// </summary>
    internal Task TriggerImeSearchAsync(string text)
    {
        var key = text.Trim().ToLowerInvariant();
        // IME 조합 완성으로 인한 동일 쿼리 재검색이면 선택 위치 유지 (건너뜀)
        if (key == _lastSearchQuery && Results.Count > 0) return Task.CompletedTask;
        return SearchAsync(text);
    }

    private async Task SearchAsync(string query)
    {
        // CTS 취소는 setter에서 이미 처리됨. 새 토큰만 발급.
        _searchCts = new CancellationTokenSource();
        var ct = _searchCts.Token;

        var queryKey = query.Trim().ToLowerInvariant();
        // 동일 쿼리 재검색(IME 커밋 후 WPF 바인딩 갱신 등)이면 결과를 일단 유지
        var isSameQuery = queryKey == _lastSearchQuery && Results.Count > 0;

        if (!isSameQuery) Results.Clear();
        if (string.IsNullOrWhiteSpace(query)) { _lastSearchQuery = ""; return; }

        // 기능 비활성화 시 해당 프리픽스 쿼리는 빈 결과 반환
        if (!_settings.Settings.Launcher.EnableFavorites &&
            query.StartsWith("fav", StringComparison.OrdinalIgnoreCase)) return;
        if (!_settings.Settings.Launcher.EnableRecent &&
            query.StartsWith("recent", StringComparison.OrdinalIgnoreCase)) return;

        // 동일 쿼리면 현재 선택 항목을 기억해 복원 시도
        var prevSelected = isSameQuery ? SelectedItem : null;

        IsLoading = true;
        try
        {
            var items = await _resolver.ResolveAsync(query, ct);
            if (ct.IsCancellationRequested) return;

            // ReplaceAll: Clear+AddRange를 단일 Reset 이벤트로 → WPF 레이아웃 1회만 갱신
            Results.ReplaceAll(items);
            _lastSearchQuery = queryKey;

            if (isSameQuery && prevSelected != null)
            {
                // 결과 목록이 같으면 이전 선택 복원 (ReferenceEquals: 동일 데이터 객체 비교)
                var restored = Results.FirstOrDefault(r =>
                    ReferenceEquals(r.Data, prevSelected.Data) || r.Title == prevSelected.Title);
                SelectedItem = restored ?? Results.FirstOrDefault();
            }
            else
            {
                SelectedItem = Results.FirstOrDefault();
            }
        }
        catch (OperationCanceledException) { /* 정상 취소 */ }
        catch (Exception ex)
        {
            LogService.Error($"검색 오류: {ex.Message}");
        }
        finally
        {
            if (!ct.IsCancellationRequested) IsLoading = false;
        }
    }

    // ─── 실행 ────────────────────────────────────────────────────────────────

    public async Task ExecuteSelectedAsync()
    {
        // 파일 액션 모드: 선택한 액션 실행
        if (IsActionMode && SelectedItem?.Data is FileActionData fileAction)
        {
            ExecuteFileAction(fileAction);
            ExitActionMode();
            CloseRequested?.Invoke(this, EventArgs.Empty);
            return;
        }

        if (SelectedItem == null) return;

        // 실행 전 검색어 히스토리 저장 (2자 이상, prefix 포함)
        if (InputText.Trim().Length >= 2)
            SearchHistoryService.Add(InputText.Trim());

        // 창을 먼저 닫아 체감 속도 확보 → 실행은 백그라운드
        CloseRequested?.Invoke(this, EventArgs.Empty);

        try
        {
            await _resolver.ExecuteAsync(SelectedItem, InputText, CancellationToken.None);
        }
        catch (Exception ex)
        {
            NotificationRequested?.Invoke(this, $"실행 실패: {ex.Message}");
            LogService.Error($"Execute 오류: {ex.Message}");
        }
    }

    /// <summary>
    /// 캡처 모드에서 Shift+Enter 시 지연 캡처 타이머 선택 목록을 표시합니다.
    /// </summary>
    public bool ShowDelayTimerItems()
    {
        if (SelectedItem?.Data is not string mode) return false;

        // 이미 delay 아이템이면 실행으로 넘기기
        if (mode.StartsWith("delay:")) return false;

        // ScreenCaptureHandler 찾기
        if (!_resolver.RegisteredHandlers.TryGetValue(
                ActivePrefix ?? "", out var handler) ||
            handler is not AxCopilot.Handlers.ScreenCaptureHandler capHandler)
            return false;

        var items = capHandler.GetDelayItems(mode).ToList();
        Results.Clear();
        foreach (var item in items) Results.Add(item);
        SelectedItem = Results.FirstOrDefault();
        return true;
    }

    public void SelectNext()
    {
        if (Results.Count == 0) return;
        var idx = SelectedItem != null ? Results.IndexOf(SelectedItem) : -1;
        SelectedItem = Results[(idx + 1) % Results.Count];
    }

    public void SelectPrev()
    {
        if (Results.Count == 0) return;
        var idx = SelectedItem != null ? Results.IndexOf(SelectedItem) : 0;
        SelectedItem = Results[(idx - 1 + Results.Count) % Results.Count];
    }

    // ─── Large Type ───────────────────────────────────────────────────────────

    /// <summary>Shift+Enter 시 Large Type으로 표시할 텍스트 반환</summary>
    public string GetLargeTypeText()
    {
        if (SelectedItem == null) return "";
        // 계산기: 결과값 문자열
        if (SelectedItem.Data is string s && !string.IsNullOrWhiteSpace(s)) return s;
        // 클립보드 히스토리: 전체 텍스트
        if (SelectedItem.Data is ClipboardEntry entry && entry.IsText) return entry.Text;
        // 기본: 제목
        return SelectedItem.Title;
    }

    // ─── 미리보기 패널 업데이트 ───────────────────────────────────────────────

    private async Task UpdatePreviewAsync(LauncherItem? item)
    {
        _previewCts?.Cancel();
        _previewCts = new CancellationTokenSource();
        var ct = _previewCts.Token;

        if (item == null) { HasPreview = false; return; }

        try
        {
            await Task.Delay(80, ct); // 빠른 탐색 시 불필요한 파일 읽기 방지

            // 클립보드 텍스트
            if (item.Data is ClipboardEntry clipEntry && clipEntry.IsText)
            {
                var text = clipEntry.Text ?? "";
                PreviewText = text.Length > 400 ? text[..400] + "…" : text;
                HasPreview = !string.IsNullOrEmpty(PreviewText);
                return;
            }

            // 인덱스 파일 (IndexEntry)
            if (item.Data is IndexEntry indexEntry)
            {
                var ext = System.IO.Path.GetExtension(indexEntry.Path).ToLowerInvariant();

                // 텍스트 파일: 첫 6줄 미리보기
                if (IsPreviewableTextFile(ext) && System.IO.File.Exists(indexEntry.Path))
                {
                    var lines = await ReadFirstLinesAsync(indexEntry.Path, 6, ct);
                    if (ct.IsCancellationRequested) return;
                    PreviewText = string.Join("\n", lines);
                    HasPreview = !string.IsNullOrEmpty(PreviewText.Trim());
                    return;
                }

                // 이미지 파일: 해상도·크기 메타데이터
                if (IsImageFile(ext) && System.IO.File.Exists(indexEntry.Path))
                {
                    var meta = await Task.Run(() => GetImageMeta(indexEntry.Path), ct);
                    if (ct.IsCancellationRequested) return;
                    if (meta != null) { PreviewText = meta; HasPreview = true; return; }
                }

                // PDF 파일: 페이지 수·크기·첫 페이지 텍스트
                if (ext == ".pdf" && System.IO.File.Exists(indexEntry.Path))
                {
                    var meta = await Task.Run(() => GetPdfMeta(indexEntry.Path), ct);
                    if (ct.IsCancellationRequested) return;
                    if (meta != null) { PreviewText = meta; HasPreview = true; return; }
                }

                // 미디어 파일: 파일 크기
                if (IsMediaFile(ext) && System.IO.File.Exists(indexEntry.Path))
                {
                    var meta = GetFileSizeMeta(indexEntry.Path, ext);
                    if (meta != null) { PreviewText = meta; HasPreview = true; return; }
                }
            }

            HasPreview = false;
        }
        catch (OperationCanceledException) { }
        catch { HasPreview = false; }
    }

    private static bool IsPreviewableTextFile(string ext) => ext is
        ".txt" or ".md" or ".log" or ".csv" or ".json" or ".xml"
        or ".yaml" or ".yml" or ".ini" or ".cfg" or ".conf"
        or ".cs" or ".py" or ".js" or ".ts" or ".html" or ".css";

    private static bool IsImageFile(string ext) => ext is
        ".jpg" or ".jpeg" or ".png" or ".gif" or ".bmp" or ".webp"
        or ".svg" or ".ico" or ".tiff" or ".tif";

    private static bool IsMediaFile(string ext) => ext is
        ".mp3" or ".wav" or ".flac" or ".aac" or ".ogg" or ".wma"
        or ".mp4" or ".avi" or ".mkv" or ".mov" or ".wmv" or ".webm";

    private static string? GetImageMeta(string path)
    {
        try
        {
            var fi = new System.IO.FileInfo(path);
            var size = FormatFileSize(fi.Length);
            var ext = fi.Extension.TrimStart('.').ToUpperInvariant();

            // BitmapDecoder로 해상도 읽기
            using var stream = new System.IO.FileStream(path,
                System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.Read);
            var decoder = System.Windows.Media.Imaging.BitmapDecoder.Create(
                stream, System.Windows.Media.Imaging.BitmapCreateOptions.DelayCreation,
                System.Windows.Media.Imaging.BitmapCacheOption.None);
            var frame = decoder.Frames[0];
            return $"🖼 {ext} 이미지  ·  {frame.PixelWidth}×{frame.PixelHeight}  ·  {size}\n" +
                   $"수정: {fi.LastWriteTime:yyyy-MM-dd HH:mm}";
        }
        catch { return null; }
    }

    private static string? GetPdfMeta(string path)
    {
        try
        {
            var fi = new System.IO.FileInfo(path);
            var size = FormatFileSize(fi.Length);

            using var doc = UglyToad.PdfPig.PdfDocument.Open(path);
            int pages = doc.NumberOfPages;
            var firstPageText = "";
            if (pages > 0)
            {
                var page = doc.GetPage(1);
                firstPageText = page.Text;
                if (firstPageText.Length > 200)
                    firstPageText = firstPageText[..200] + "…";
                firstPageText = firstPageText.Replace("\r\n", " ").Replace("\n", " ");
            }

            var meta = $"📄 PDF  ·  {pages}페이지  ·  {size}";
            if (!string.IsNullOrWhiteSpace(firstPageText))
                meta += $"\n{firstPageText}";

            return meta;
        }
        catch { return null; }
    }

    private static string? GetFileSizeMeta(string path, string ext)
    {
        try
        {
            var fi = new System.IO.FileInfo(path);
            var size = FormatFileSize(fi.Length);
            var type = ext switch
            {
                ".mp3" or ".wav" or ".flac" or ".aac" or ".ogg" or ".wma" => "🎵 오디오",
                ".mp4" or ".avi" or ".mkv" or ".mov" or ".wmv" or ".webm" => "🎬 동영상",
                _ => "📁 파일"
            };
            return $"{type}  ·  {ext.TrimStart('.').ToUpperInvariant()}  ·  {size}\n" +
                   $"수정: {fi.LastWriteTime:yyyy-MM-dd HH:mm}";
        }
        catch { return null; }
    }

    private static string FormatFileSize(long bytes) => bytes switch
    {
        < 1024        => $"{bytes} B",
        < 1048576     => $"{bytes / 1024.0:F1} KB",
        < 1073741824  => $"{bytes / 1048576.0:F1} MB",
        _             => $"{bytes / 1073741824.0:F2} GB"
    };

    private static async Task<IEnumerable<string>> ReadFirstLinesAsync(
        string path, int maxLines, CancellationToken ct)
    {
        var lines = new List<string>();
        using var reader = new System.IO.StreamReader(path,
            System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
        for (int i = 0; i < maxLines && !reader.EndOfStream; i++)
        {
            ct.ThrowIfCancellationRequested();
            var line = await reader.ReadLineAsync(ct);
            if (line != null) lines.Add(line);
        }
        return lines;
    }
}
