# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/core.ipynb.

# %% auto 0
__all__ = ['DISCORD_TOKEN', 'console', 'prints', 'fetch_channel_complete_history', 'organize_messages_with_threads',
           'create_thread_aware_conversations', 'fetch_discord_msgs', 'fetch_discord_msgs_cli', 'main',
           'list_all_guilds', 'list_all_channels', 'interactive_discord_fetch', 'interactive_cli']

# %% ../nbs/core.ipynb 4
import discord
import asyncio
import json
from datetime import datetime
from fastcore.utils import L
import os, sys
from fastcore.utils import in_notebook
from fastcore.parallel import parallel
import typer
from typing_extensions import Annotated
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn
from rich.prompt import Prompt, IntPrompt
from rich.table import Table
from dotenv import load_dotenv
import os
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

# %% ../nbs/core.ipynb 5
load_dotenv()

def prints(*args, **kwargs): print(*args, file=sys.stderr, **kwargs)

DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
if not DISCORD_TOKEN: raise ValueError("DISCORD_TOKEN env variable not found")

# %% ../nbs/core.ipynb 8
async def fetch_channel_complete_history(channel_id, limit=None, save_to_file=True, print_summary=True):
    """
    Fetch complete history from a single Discord channel including threads and replies
    
    Args:
        channel_id (int): The Discord channel ID
        limit (int, optional): Max messages to fetch. None = all messages
        save_to_file (bool): Whether to save results to JSON file
        print_summary (bool): Whether to print summary information
    
    Returns:
        dict: Complete channel data with messages, threads, and reply hierarchies
    """
    
    # Set up Discord client
    intents = discord.Intents.default()
    intents.message_content = True
    intents.guilds = True
    intents.guild_messages = True
    client = discord.Client(intents=intents)
    
    result = None
    
    def format_message(message):
        """Format a single message with all relevant data"""
        return {
            "id": str(message.id),
            "author": {
                "name": str(message.author),
                "id": str(message.author.id),
                "display_name": getattr(message.author, 'display_name', str(message.author))
            },
            "content": message.content,
            "timestamp": message.created_at.isoformat(),
            "edited_timestamp": message.edited_at.isoformat() if message.edited_at else None,
            "attachments": [
                {
                    "url": att.url,
                    "filename": att.filename,
                    "content_type": getattr(att, 'content_type', None)
                } for att in message.attachments
            ],
            "embeds": [
                {
                    "title": embed.title,
                    "description": embed.description,
                    "url": embed.url
                } for embed in message.embeds
            ],
            "reactions": [
                {
                    "emoji": str(reaction.emoji),
                    "count": reaction.count
                } for reaction in message.reactions
            ],
            "mentions": [
                {
                    "name": str(user),
                    "id": str(user.id)
                } for user in message.mentions
            ],
            "pinned": message.pinned,
            "reply_to": None  # Will be filled if this is a reply
        }
    
    @client.event
    async def on_ready():
        nonlocal result
        prints(f'Connected as {client.user}')
        
        try:
            # Get the channel
            channel = client.get_channel(channel_id)
            if not channel:
                prints(f"Channel {channel_id} not found or no access")
                await client.close()
                return
            
            if print_summary: prints(f"Fetching from #{channel.name} in {channel.guild.name}")
            
            # Initialize result structure
            result = {
                "channel_info": {
                    "id": str(channel.id),
                    "name": channel.name,
                    "topic": channel.topic,
                    "guild_name": channel.guild.name,
                    "guild_id": str(channel.guild.id)
                },
                "messages": [],
                "threads": {}
            }
            
            # Fetch all main channel messages
            if print_summary: prints("Fetching main channel messages...")
            message_count = 0
            
            async for message in channel.history(limit=limit, oldest_first=True):
                msg_data = format_message(message)
                
                # Handle replies
                if message.reference and message.reference.message_id:
                    msg_data["reply_to"] = {
                        "message_id": str(message.reference.message_id),
                        "channel_id": str(message.reference.channel_id)
                    }
                    
                    # Get referenced message content if available
                    if message.reference.resolved:
                        ref_msg = message.reference.resolved
                        msg_data["reply_to"]["referenced_content"] = ref_msg.content
                        msg_data["reply_to"]["referenced_author"] = str(ref_msg.author)
                
                result["messages"].append(msg_data)
                message_count += 1
                
                # Check for thread on this message
                if hasattr(message, 'thread') and message.thread:
                    await fetch_thread_messages(message.thread, msg_data)
            
            if print_summary: prints(f"Fetched {message_count} main messages")
            
            # Fetch all threads (both active and archived)
            all_threads = []
            
            # Get active threads
            for thread in channel.threads:
                all_threads.append(thread)
            
            # Get archived threads
            try:
                async for thread in channel.archived_threads(limit=None):
                    all_threads.append(thread)
            except Exception as e:
                prints(f"Could not fetch archived threads: {e}")
            
            # Process all threads and match them to parent messages
            for thread in all_threads:
                if str(thread.id) not in result["threads"]:
                    # Check if thread ID matches a message ID (Discord's way of linking threads to messages)
                    parent_msg = None
                    for msg in result["messages"]:
                        if msg["id"] == str(thread.id):
                            parent_msg = msg
                            break
                    
                    await fetch_thread_messages(thread, parent_msg)
            
            if print_summary: prints(f"Total threads found: {len(result['threads'])}")
            
            # Print summary if requested
            if print_summary and result:
                prints(f"\n=== Channel: #{result['channel_info']['name']} ===")
                prints(f"Guild: {result['channel_info']['guild_name']}")
                prints(f"Total messages: {len(result['messages'])}")
                prints(f"Total threads: {len(result['threads'])}")
                
                # Show thread summary
                for thread_id, thread_info in result['threads'].items():
                    prints(f"  Thread '{thread_info['name']}': {len(thread_info['messages'])} messages")
            
            # Save to file if requested
            if save_to_file and result:
                timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                filename = f"discord_channel_{result['channel_info']['name']}_{timestamp}.json"
                
                with open(filename, 'w', encoding='utf-8') as f:
                    json.dump(result, f, indent=2, ensure_ascii=False)
                
                if print_summary:
                    prints(f"\nComplete channel data saved to: {filename}")
                    prints(f"File size: {os.path.getsize(filename) / 1024:.1f} KB")
            
        except Exception as e:
            prints(f"Error: {e}")
            await client.close()
        
        finally: 
            if not client.is_closed():await client.close()
            await asyncio.sleep(0.1)  # Give time for cleanup
        
    
    async def fetch_thread_messages(thread, parent_message=None):
        """Fetch all messages from a thread"""
        if print_summary: prints(f"  Fetching thread: {thread.name}")
        
        thread_data = {
            "id": str(thread.id),
            "name": thread.name,
            "parent_message_id": str(parent_message["id"]) if parent_message else str(thread.id),  # Use thread ID if no parent found
            "archived": getattr(thread, 'archived', False),
            "messages": []
        }
        
        try:
            async for thread_msg in thread.history(limit=limit, oldest_first=True):
                thread_msg_data = format_message(thread_msg)
                
                # Handle replies within threads
                if thread_msg.reference and thread_msg.reference.message_id:
                    thread_msg_data["reply_to"] = {
                        "message_id": str(thread_msg.reference.message_id),
                        "channel_id": str(thread_msg.reference.channel_id)
                    }
                    
                    if thread_msg.reference.resolved:
                        ref_msg = thread_msg.reference.resolved
                        thread_msg_data["reply_to"]["referenced_content"] = ref_msg.content
                        thread_msg_data["reply_to"]["referenced_author"] = str(ref_msg.author)
                
                thread_data["messages"].append(thread_msg_data)
            
            result["threads"][str(thread.id)] = thread_data
            if print_summary: prints(f"    Found {len(thread_data['messages'])} thread messages")
            
        except discord.Forbidden:
            if print_summary: prints(f"    No permission to read thread {thread.name}")
        except Exception as e:
            if print_summary: prints(f"    Error reading thread {thread.name}: {e}")
    
    # Start the client
    try: await client.start(DISCORD_TOKEN)
    except Exception as e: prints(f"Failed to start client: {e}")
    finally:
        if not client.is_closed(): await client.close()
    await asyncio.sleep(0.5)
    return result

# %% ../nbs/core.ipynb 10
def organize_messages_with_threads(channel_data):
    """
    Organize messages into a hierarchical structure where threads are nested under their parent messages.
    
    Returns a list of conversation objects where each conversation has:
    - main_message: The top-level message
    - thread_messages: Array of messages in the thread (if any)
    - direct_replies: Array of direct replies to the message (not in a thread)
    """
    
    if not channel_data:
        return []
    
    # Create lookup maps
    message_by_id = {msg['id']: msg for msg in channel_data['messages']}
    threads_by_parent = {}
    
    # Map threads by their parent message ID
    for thread_id, thread_info in channel_data['threads'].items():
        if thread_info['parent_message_id']:
            threads_by_parent[thread_info['parent_message_id']] = thread_info
    
    # Track which messages have been processed
    processed_ids = set()
    conversations = []
    
    # Process each message
    for msg in channel_data['messages']:
        # Skip if already processed as a reply
        if msg['id'] in processed_ids:
            continue
        
        # Skip if this is a reply to another message (will be processed with its parent)
        if msg.get('reply_to') and msg['reply_to'].get('message_id') in message_by_id:
            continue
        
        # Create conversation object
        conversation = {
            'main_message': {
                'id': msg['id'],
                'author': msg['author']['name'],
                'content': msg['content'],
                'timestamp': msg['timestamp'],
                'attachments': msg.get('attachments', []),
                'embeds': msg.get('embeds', [])
            },
            'thread_messages': [],
            'direct_replies': []
        }
        
        processed_ids.add(msg['id'])
        
        # Add thread messages if this message has a thread
        if msg['id'] in threads_by_parent:
            thread_info = threads_by_parent[msg['id']]
            conversation['thread_messages'] = [
                {
                    'id': thread_msg['id'],
                    'author': thread_msg['author']['name'],
                    'content': thread_msg['content'],
                    'timestamp': thread_msg['timestamp'],
                    'attachments': thread_msg.get('attachments', []),
                    'embeds': thread_msg.get('embeds', []),
                    'is_reply_to': thread_msg.get('reply_to', {}).get('message_id') if thread_msg.get('reply_to') else None
                }
                for thread_msg in thread_info['messages']
            ]
        
        # Find direct replies (not in threads)
        for other_msg in channel_data['messages']:
            if (other_msg.get('reply_to') and 
                other_msg['reply_to'].get('message_id') == msg['id'] and
                other_msg['id'] not in processed_ids):
                
                conversation['direct_replies'].append({
                    'id': other_msg['id'],
                    'author': other_msg['author']['name'],
                    'content': other_msg['content'],
                    'timestamp': other_msg['timestamp'],
                    'attachments': other_msg.get('attachments', []),
                    'embeds': other_msg.get('embeds', [])
                })
                processed_ids.add(other_msg['id'])
        
        conversations.append(conversation)
    
    return conversations

# %% ../nbs/core.ipynb 12
def create_thread_aware_conversations(channel_data, include_metadata=False):
    """
    Create a clean conversation structure where threads are properly nested.
    
    Args:
        channel_data: Raw channel data from Discord
        include_metadata: Whether to include IDs, timestamps, etc.
        
    Returns:
        Dictionary with channel info and organized conversations
    """
    
    organized = organize_messages_with_threads(channel_data)
    
    result = {
        'channel': channel_data['channel_info']['name'],
        'guild': channel_data['channel_info']['guild_name'],
        'conversations': []
    }
    
    for conv in organized:
        # Skip empty messages
        if not conv['main_message']['content'].strip() and not conv['thread_messages'] and not conv['direct_replies']:
            continue
        
        conversation = {
            'question': {
                'author': conv['main_message']['author'],
                'content': conv['main_message']['content']
            }
        }
        
        if include_metadata:
            conversation['question']['id'] = conv['main_message']['id']
            conversation['question']['timestamp'] = conv['main_message']['timestamp']
        
        # Add thread responses if any
        if conv['thread_messages']:
            conversation['thread'] = []
            for thread_msg in conv['thread_messages']:
                if len(thread_msg['content']) >= 5:
                    msg_data = {
                        'author': thread_msg['author'],
                        'content': thread_msg['content']
                    }
                    if include_metadata:
                        msg_data['id'] = thread_msg['id']
                        msg_data['timestamp'] = thread_msg['timestamp']
                    conversation['thread'].append(msg_data)
        
        # Add direct replies if any
        if conv['direct_replies']:
            conversation['replies'] = []
            for reply in conv['direct_replies']:
                reply_data = {
                    'author': reply['author'],
                    'content': reply['content']
                }
                if include_metadata:
                    reply_data['id'] = reply['id']
                    reply_data['timestamp'] = reply['timestamp']
                conversation['replies'].append(reply_data)
        
        if len(conversation['question']['content']) >= 60: result['conversations'].append(conversation)
            
    return result

# %% ../nbs/core.ipynb 14
async def fetch_discord_msgs(
    channel_id: int,
    limit: int = None,
    save_original: bool = True,
    save_simplified: bool = True,
    print_summary: bool = True,
    emit_to_stdout: bool = False,
    use_thread_structure: bool = True,
):
    """
    Fetch all messages from a Discord channel including threads and reply hierarchies.
    
    This tool fetches complete Discord channel history and can save both detailed
    and simplified versions of the data. The simplified version is ideal for LLMs.
    
    Requirements:
    - Discord bot token in DISCORD_TOKEN env variable
    - Bot must have Read Message History permissions
    - Bot must be a member of the server
    
    Args:
        channel_id: Discord channel ID to fetch messages from
        limit: Maximum number of messages to fetch (None = all messages)
        save_original: Save complete channel data with all metadata
        save_simplified: Save simplified conversation-focused data
        print_summary: Print summary of fetched data
        emit_to_stdout: Output simplified JSON data to stdout
        use_thread_structure: Use new thread-aware structure (recommended)
    
    Returns:
        tuple: (original_data, simplified_data)
    """
    print_flag = print_summary and not emit_to_stdout
    
    try:
        # Fetch complete channel data
        if print_flag: prints("Fetching complete channel data...")
        original_data = await fetch_channel_complete_history(
            channel_id, 
            limit=limit, 
            save_to_file=save_original, 
            print_summary=print_flag
        )
        
        if not original_data:
            if print_flag: prints("❌ Failed to fetch channel data")
            return None, None
        
        # Use thread-aware structure
        simplified_data = create_thread_aware_conversations(
            original_data, 
            include_metadata=True
        )
        
        # Save if requested
        if save_simplified:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"discord_thread_aware_{simplified_data['channel']}_{timestamp}.json"
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(simplified_data, f, indent=2, ensure_ascii=False)
            if print_flag:
                prints(f"Thread-aware data saved to: {filename}")
                prints(f"File size: {os.path.getsize(filename) / 1024:.1f} KB")
        
        # Emit to stdout if requested
        if emit_to_stdout and simplified_data:
            print(json.dumps(simplified_data, indent=2, ensure_ascii=False))
        
        if print_flag:
            prints(f"\n✅ Successfully fetched channel data!")
            if save_original:
                prints(f"📄 Original data: {len(original_data['messages'])} messages, {len(original_data['threads'])} threads")
            if save_simplified or use_thread_structure:
                prints(f"💬 Simplified data: {len(simplified_data['conversations'])} conversations")
            
        return original_data, simplified_data
        
    except Exception as e:
        prints(f"❌ Error: {e}")
        return None, None

# %% ../nbs/core.ipynb 18
def fetch_discord_msgs_cli(
    channel_id: Annotated[int, typer.Argument(help="Discord channel ID to fetch messages from")],
    limit: Annotated[int, typer.Option(help="Maximum number of messages to fetch")] = None,
    verbose: Annotated[bool, typer.Option("--verbose", help="Show detailed logs")] = False,
    save_to_files: Annotated[bool, typer.Option("--save-to-files", help="Save both original and simplified data to files and print summary")] = False,
    use_thread_structure: Annotated[bool, typer.Option("--threads/--no-threads", help="Use thread-aware structure (recommended)")] = True,
):
    """
    Fetch all messages from a Discord channel including threads and reply hierarchies.
    
    By default, outputs simplified conversation data as JSON to stdout (suitable for piping).
    Use --save-to-files to save both original and simplified data to files with summary output.
    """
    import asyncio
    return asyncio.run(fetch_discord_msgs(
        channel_id=channel_id,
        limit=limit,
        save_original=save_to_files,
        save_simplified=save_to_files,
        print_summary=verbose,
        emit_to_stdout=not save_to_files,
        use_thread_structure=use_thread_structure
    ))


def main(): typer.run(fetch_discord_msgs_cli)

# %% ../nbs/core.ipynb 20
async def list_all_guilds():
    """
    List all guilds (servers) the bot has access to
    
    Returns:
        list: List of dicts with guild name, id, and member count
    """
    
    intents = discord.Intents.default()
    intents.message_content = True
    intents.guilds = True
    client = discord.Client(intents=intents)
    
    guilds = []
    
    @client.event
    async def on_ready():
        nonlocal guilds
        
        for guild in client.guilds:
            guilds.append({
                "guild_name": guild.name,
                "guild_id": str(guild.id),
                "member_count": guild.member_count,
                "text_channels_count": len(guild.text_channels)
            })
        
        await client.close()
    
    await client.start(DISCORD_TOKEN)
    return guilds

# %% ../nbs/core.ipynb 21
async def list_all_channels():
    """
    List all channels the bot has access to across all servers
    
    Returns:
        list: List of dicts with channel name, id, guild name, and guild id
    """
    
    intents = discord.Intents.default()
    intents.message_content = True
    intents.guilds = True
    client = discord.Client(intents=intents)
    
    channels = []
    
    @client.event
    async def on_ready():
        nonlocal channels
        
        for guild in client.guilds:
            for channel in guild.text_channels:
                permissions = channel.permissions_for(guild.me)
                if permissions.read_messages:
                    channels.append({
                        "channel_name": channel.name,
                        "channel_id": str(channel.id),
                        "guild_name": guild.name,
                        "guild_id": str(guild.id),
                        "category": channel.category.name if channel.category else None
                    })
        
        await client.close()
    
    await client.start(DISCORD_TOKEN)
    return channels

# %% ../nbs/core.ipynb 23
console = Console()

def interactive_discord_fetch():
    """
    Interactive CLI to fetch Discord messages from selected channels
    """
    import asyncio
    
    console.print("\n[bold cyan]Discord Channel Fetcher[/bold cyan]\n")
    
    # Step 1: Get all channels
    with console.status("[cyan]Connecting to Discord..."):
        channels = asyncio.run(list_all_channels())
    
    if not channels:
        console.print("[red]No channels found or no permissions[/red]")
        return
    
    # Group channels by guild
    guilds = {}
    for ch in channels:
        guild_name = ch['guild_name']
        if guild_name not in guilds:
            guilds[guild_name] = []
        guilds[guild_name].append(ch)
    
    # Step 2: Choose guild
    console.print("[yellow]Available Guilds:[/yellow]")
    guild_list = list(guilds.keys())
    for i, guild in enumerate(guild_list, 1):
        console.print(f"{i}. {guild} ({len(guilds[guild])} channels)")
    
    guild_choice = IntPrompt.ask("\nSelect guild number", default=1)
    if guild_choice < 1 or guild_choice > len(guild_list):
        console.print("[red]Invalid guild selection[/red]")
        return
    
    selected_guild = guild_list[guild_choice - 1]
    guild_channels = guilds[selected_guild]
    
    # Step 3: Choose channels
    console.print(f"\n[yellow]Channels in {selected_guild}:[/yellow]")
    
    # Display channels in a table
    table = Table(show_header=True, header_style="bold magenta")
    table.add_column("#", style="dim", width=4)
    table.add_column("Channel", style="cyan")
    table.add_column("Category", style="green")
    table.add_column("ID", style="dim")
    
    table.add_row("0", "ALL CHANNELS", "—", "—")
    for i, ch in enumerate(guild_channels, 1):
        table.add_row(
            str(i),
            ch['channel_name'],
            ch['category'] or "None",
            ch['channel_id']
        )
    
    console.print(table)
    
    channel_choice = IntPrompt.ask("\nSelect channel number (0 for all)", default=0)
    
    if channel_choice < 0 or channel_choice > len(guild_channels):
        console.print("[red]Invalid channel selection[/red]")
        return
    
    # Determine which channels to fetch
    if channel_choice == 0:
        channels_to_fetch = guild_channels
        console.print(f"\n[green]Selected: All {len(channels_to_fetch)} channels[/green]")
    else:
        channels_to_fetch = [guild_channels[channel_choice - 1]]
        console.print(f"\n[green]Selected: #{channels_to_fetch[0]['channel_name']}[/green]")
    
    # Step 4: Output options
    output_filename = None
    output_dir = None
    separate_files = False
    
    if len(channels_to_fetch) > 1:
        console.print("\n[yellow]Output Options:[/yellow]")
        console.print("1. Concatenate to single file")
        console.print("2. Separate files in directory")
        
        output_choice = IntPrompt.ask("Select output option", default=1)
        
        if output_choice == 1:
            # Single file - ask for filename
            default_filename = f"discord_{selected_guild.replace('/', '-')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
            output_filename = Prompt.ask("Enter output filename (full path allowed)", default=default_filename)
        elif output_choice == 2:
            # Multiple files - ask for directory
            separate_files = True
            output_dir = Prompt.ask("Enter output directory", default="./discord_output")
            Path(output_dir).mkdir(parents=True, exist_ok=True)
        else:
            console.print("[red]Invalid option[/red]")
            return
    else:
        # Single channel - always ask for filename
        default_filename = f"discord_{channels_to_fetch[0]['channel_name'].replace('/', '-')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        output_filename = Prompt.ask("Enter output filename (full path allowed)", default=default_filename)
    
    # Step 5: Fetch messages
    all_messages = []
    progress_lock = threading.Lock()
    
    # Fetch with progress tracking
    if len(channels_to_fetch) > 1:
        # Use Rich progress bar for multiple channels
        with Progress(
            TextColumn("[bold blue]{task.fields[channel_name]:30}", justify="left"),
            SpinnerColumn(),
            BarColumn(bar_width=40),
            TaskProgressColumn(),
            TextColumn("{task.fields[status]}"),
            console=console,
            refresh_per_second=10
        ) as progress:
            
            # Create a progress task for each channel
            task_map = {}
            for ch in channels_to_fetch:
                task = progress.add_task(
                    f"Channel", 
                    total=100,  # Indeterminate progress
                    channel_name=f"#{ch['channel_name'][:28]}",
                    status="[dim]Waiting...[/dim]"
                )
                task_map[ch['channel_id']] = (task, ch)
            
            # Overall summary task
            overall_task = progress.add_task(
                f"Overall Progress",
                total=len(channels_to_fetch),
                channel_name="[bold]Total[/bold]",
                status=f"[cyan]0/{len(channels_to_fetch)} completed[/cyan]"
            )
            
            completed_count = 0
            
            # Helper function to fetch a single channel with progress updates
            def fetch_with_progress(ch, task_id):
                nonlocal completed_count
                try:
                    # Update status to fetching
                    progress.update(task_id, status="[yellow]Fetching...[/yellow]", completed=30)
                    
                    # Fetch the data
                    original, simplified = asyncio.run(fetch_discord_msgs(
                        int(ch['channel_id']),
                        save_original=False,
                        save_simplified=False,
                        print_summary=False,
                        emit_to_stdout=False
                    ))
                    
                    if simplified:
                        # Update status to success
                        progress.update(task_id, status="[green]✓ Complete[/green]", completed=100)
                        
                        with progress_lock:
                            completed_count += 1
                            progress.update(
                                overall_task, 
                                advance=1,
                                status=f"[cyan]{completed_count}/{len(channels_to_fetch)} completed[/cyan]"
                            )
                        
                        return {
                            "guild": selected_guild,
                            "channel": ch['channel_name'],
                            "channel_id": ch['channel_id'],
                            "data": simplified
                        }
                    else:
                        progress.update(task_id, status="[red]✗ No data[/red]", completed=100)
                        with progress_lock:
                            completed_count += 1
                            progress.update(overall_task, advance=1)
                except Exception as e:
                    progress.update(task_id, status=f"[red]✗ Error: {str(e)[:20]}[/red]", completed=100)
                    with progress_lock:
                        completed_count += 1
                        progress.update(overall_task, advance=1)
                return None
            
            # Use ThreadPoolExecutor for parallel fetching
            with ThreadPoolExecutor(max_workers=min(4, len(channels_to_fetch))) as executor:
                # Submit all tasks
                futures = []
                for ch_id, (task_id, ch) in task_map.items():
                    future = executor.submit(fetch_with_progress, ch, task_id)
                    futures.append(future)
                
                # Collect results
                for future in as_completed(futures):
                    result = future.result()
                    if result:
                        all_messages.append(result)
            
            # Final status update
            progress.update(
                overall_task, 
                status=f"[bold green]Completed: {len(all_messages)} successful, {len(channels_to_fetch) - len(all_messages)} failed[/bold green]"
            )
    else:
        # Single channel - show simple status
        with console.status(f"[cyan]Fetching #{channels_to_fetch[0]['channel_name']}..."):
            def fetch_channel_data(ch):
                try:
                    original, simplified = asyncio.run(fetch_discord_msgs(
                        int(ch['channel_id']),
                        save_original=False,
                        save_simplified=False,
                        print_summary=False,
                        emit_to_stdout=False
                    ))
                    
                    if simplified:
                        return {
                            "guild": selected_guild,
                            "channel": ch['channel_name'],
                            "channel_id": ch['channel_id'],
                            "data": simplified
                        }
                except Exception as e:
                    console.print(f"[red]Error fetching #{ch['channel_name']}: {e}[/red]")
                return None
            
            result = fetch_channel_data(channels_to_fetch[0])
            if result:
                all_messages = [result]
                console.print(f"[green]✓[/green] Fetched #{channels_to_fetch[0]['channel_name']}")
    
    # Step 6: Save results
    if separate_files:
        # Write each channel to separate file
        saved_count = 0
        for msg in all_messages:
            if msg:
                filename = Path(output_dir) / f"{msg['channel'].replace('/', '-')}.json"
                with open(filename, 'w', encoding='utf-8') as f:
                    json.dump(msg['data'], f, indent=2, ensure_ascii=False)
                saved_count += 1
        console.print(f"\n[green]✓[/green] Saved {saved_count} files to {output_dir}")
    elif all_messages:
        # Concatenate to single file
        if len(all_messages) == 1:
            # Single channel - output just the simplified data
            output = all_messages[0]['data']
        else:
            # Multiple channels - wrap in structured format
            output = {
                "guild": selected_guild,
                "channels": []
            }
            for msg in all_messages:
                output["channels"].append({
                    "name": msg["channel"],
                    "id": msg["channel_id"],
                    "conversations": msg["data"]["conversations"]
                })
        
        output_json = json.dumps(output, indent=2, ensure_ascii=False)
        
        # Write to file
        with open(output_filename, 'w', encoding='utf-8') as f:
            f.write(output_json)
        console.print(f"\n[green]✓[/green] Saved to {output_filename}")
    
    console.print(f"\n[bold green]Successfully fetched {len(all_messages)} out of {len(channels_to_fetch)} channels![/bold green]")

def interactive_cli():
    """Entry point for interactive Discord fetcher"""
    typer.run(interactive_discord_fetch)
