/**
 * PROJECT DISCOVERY AND MANAGEMENT SYSTEM
 * ========================================
 *
 * This module manages project discovery for Claude CLI sessions.
 *
 * ## Architecture Overview
 *
 * **Claude Projects** (stored in ~/.claude/projects/)
 *    - Each project is a directory named with the project path encoded (/ replaced with -)
 *    - Contains .jsonl files with conversation history including 'cwd' field
 *    - Project metadata stored in ~/.claude/project-config.json
 *
 * ## Project Discovery Strategy
 *
 * 1. **Claude Projects Discovery**:
 *    - Scan ~/.claude/projects/ directory for Claude project folders
 *    - Extract actual project path from .jsonl files (cwd field)
 *    - Fall back to decoded directory name if no sessions exist
 *
 * 2. **Manual Project Addition**:
 *    - Users can manually add project paths via UI
 *    - Stored in ~/.claude/project-config.json with 'manuallyAdded' flag
 *
 * ## Error Handling
 *
 * - Missing ~/.claude directory is handled gracefully with automatic creation
 * - ENOENT errors are caught and handled without crashing
 * - Empty arrays returned when no projects/sessions exist
 *
 * ## Caching Strategy
 *
 * - Project directory extraction is cached to minimize file I/O
 * - Cache is cleared when project configuration changes
 * - Session data is fetched on-demand, not cached
 */

import { promises as fs } from 'fs';
import fsSync from 'fs';
import path from 'path';
import readline from 'readline';
import { getUserPaths } from './services/user-directories.js';

// Cache for extracted project directories
const projectDirectoryCache = new Map();

// Clear cache when needed (called when project files change)
function clearProjectDirectoryCache() {
  projectDirectoryCache.clear();
}

// Load project configuration file
async function loadProjectConfig(userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for loadProjectConfig');
  }
  const configPath = path.join(getUserPaths(userUuid).claudeDir, 'project-config.json');
  try {
    const configData = await fs.readFile(configPath, 'utf8');
    return JSON.parse(configData);
  } catch (error) {
    // Return empty config if file doesn't exist
    return {};
  }
}

// Save project configuration file
async function saveProjectConfig(config, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for saveProjectConfig');
  }
  const claudeDir = getUserPaths(userUuid).claudeDir;
  const configPath = path.join(claudeDir, 'project-config.json');

  // Ensure the .claude directory exists
  try {
    await fs.mkdir(claudeDir, { recursive: true });
  } catch (error) {
    if (error.code !== 'EEXIST') {
      throw error;
    }
  }

  await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
}

// Generate better display name from path
async function generateDisplayName(projectName, actualProjectDir = null) {
  // Use actual project directory if provided, otherwise decode from project name
  let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
  
  // Try to read package.json from the project path
  try {
    const packageJsonPath = path.join(projectPath, 'package.json');
    const packageData = await fs.readFile(packageJsonPath, 'utf8');
    const packageJson = JSON.parse(packageData);
    
    // Return the name from package.json if it exists
    if (packageJson.name) {
      return packageJson.name;
    }
  } catch (error) {
    // Fall back to path-based naming if package.json doesn't exist or can't be read
  }
  
  // If it starts with /, it's an absolute path
  if (projectPath.startsWith('/')) {
    const parts = projectPath.split('/').filter(Boolean);
    // Return only the last folder name
    return parts[parts.length - 1] || projectPath;
  }
  
  return projectPath;
}

// Extract the actual project directory from JSONL sessions (with caching)
async function extractProjectDirectory(projectName, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for extractProjectDirectory');
  }
  // Check cache first
  const cacheKey = `${userUuid}:${projectName}`;
  if (projectDirectoryCache.has(cacheKey)) {
    return projectDirectoryCache.get(cacheKey);
  }

  // Check project config for originalPath (manually added projects via UI or platform)
  // This handles projects with dashes in their directory names correctly
  const config = await loadProjectConfig(userUuid);
  if (config[projectName]?.originalPath) {
    const originalPath = config[projectName].originalPath;
    projectDirectoryCache.set(cacheKey, originalPath);
    return originalPath;
  }

  const projectDir = path.join(getUserPaths(userUuid).claudeDir, 'projects', projectName);
  const cwdCounts = new Map();
  let latestTimestamp = 0;
  let latestCwd = null;
  let extractedPath;
  
  try {
    // Check if the project directory exists
    await fs.access(projectDir);
    
    const files = await fs.readdir(projectDir);
    const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
    
    if (jsonlFiles.length === 0) {
      // Fall back to decoded project name if no sessions
      extractedPath = projectName.replace(/-/g, '/');
    } else {
      // Process all JSONL files to collect cwd values
      for (const file of jsonlFiles) {
        const jsonlFile = path.join(projectDir, file);
        const fileStream = fsSync.createReadStream(jsonlFile);
        const rl = readline.createInterface({
          input: fileStream,
          crlfDelay: Infinity
        });
        
        for await (const line of rl) {
          if (line.trim()) {
            try {
              const entry = JSON.parse(line);
              
              if (entry.cwd) {
                // Count occurrences of each cwd
                cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
                
                // Track the most recent cwd
                const timestamp = new Date(entry.timestamp || 0).getTime();
                if (timestamp > latestTimestamp) {
                  latestTimestamp = timestamp;
                  latestCwd = entry.cwd;
                }
              }
            } catch (parseError) {
              // Skip malformed lines
            }
          }
        }
      }
      
      // Determine the best cwd to use
      if (cwdCounts.size === 0) {
        // No cwd found, fall back to decoded project name
        extractedPath = projectName.replace(/-/g, '/');
      } else if (cwdCounts.size === 1) {
        // Only one cwd, use it
        extractedPath = Array.from(cwdCounts.keys())[0];
      } else {
        // Multiple cwd values - prefer the most recent one if it has reasonable usage
        const mostRecentCount = cwdCounts.get(latestCwd) || 0;
        const maxCount = Math.max(...cwdCounts.values());
        
        // Use most recent if it has at least 25% of the max count
        if (mostRecentCount >= maxCount * 0.25) {
          extractedPath = latestCwd;
        } else {
          // Otherwise use the most frequently used cwd
          for (const [cwd, count] of cwdCounts.entries()) {
            if (count === maxCount) {
              extractedPath = cwd;
              break;
            }
          }
        }
        
        // Fallback (shouldn't reach here)
        if (!extractedPath) {
          extractedPath = latestCwd || projectName.replace(/-/g, '/');
        }
      }
    }
    
    // Cache the result
    projectDirectoryCache.set(cacheKey, extractedPath);

    return extractedPath;

  } catch (error) {
    // If the directory doesn't exist, just use the decoded project name
    if (error.code === 'ENOENT') {
      extractedPath = projectName.replace(/-/g, '/');
    } else {
      console.error(`Error extracting project directory for ${projectName}:`, error);
      // Fall back to decoded project name for other errors
      extractedPath = projectName.replace(/-/g, '/');
    }

    // Cache the fallback result too
    projectDirectoryCache.set(cacheKey, extractedPath);

    return extractedPath;
  }
}

async function getProjects(userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for getProjects');
  }
  const claudeDir = path.join(getUserPaths(userUuid).claudeDir, 'projects');

  const config = await loadProjectConfig(userUuid);
  const projects = [];
  const existingProjects = new Set();

  try {
    // Check if the .claude/projects directory exists
    await fs.access(claudeDir);

    // First, get existing Claude projects from the file system
    const entries = await fs.readdir(claudeDir, { withFileTypes: true });

    for (const entry of entries) {
      if (entry.isDirectory()) {
        existingProjects.add(entry.name);
        const projectPath = path.join(claudeDir, entry.name);

        // Extract actual project directory from JSONL sessions
        const actualProjectDir = await extractProjectDirectory(entry.name, userUuid);

        // Get display name from config or generate one
        const customName = config[entry.name]?.displayName;
        const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
        const fullPath = actualProjectDir;

        const project = {
          name: entry.name,
          path: actualProjectDir,
          displayName: customName || autoDisplayName,
          fullPath: fullPath,
          isCustomName: !!customName,
          sessions: [],
          ...(config[entry.name]?.agentInfo ? { agentInfo: config[entry.name].agentInfo } : {})
        };

        // Try to get sessions for this project (just first 5 for performance)
        try {
          const sessionResult = await getSessions(entry.name, 5, 0, userUuid);
          project.sessions = sessionResult.sessions || [];
          project.sessionMeta = {
            hasMore: sessionResult.hasMore,
            total: sessionResult.total
          };
        } catch (e) {
          console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
        }

        projects.push(project);
      }
    }
  } catch (error) {
    // If the directory doesn't exist (ENOENT), that's okay - just continue with empty projects
    if (error.code !== 'ENOENT') {
      console.error('Error reading projects directory:', error);
    }
  }

  // Add manually configured projects that don't exist as folders yet
  // We need to check by actual path (not just encoded project name) to avoid duplicates
  // because Claude CLI may use a different encoding than addProjectManually
  const existingPaths = new Set(projects.map(p => p.fullPath));

  for (const [projectName, projectConfig] of Object.entries(config)) {
    if (projectConfig.manuallyAdded) {
      // Use the original path if available, otherwise extract from potential sessions
      let actualProjectDir = projectConfig.originalPath;

      if (!actualProjectDir) {
        try {
          actualProjectDir = await extractProjectDirectory(projectName, userUuid);
        } catch (error) {
          // Fall back to decoded project name
          actualProjectDir = projectName.replace(/-/g, '/');
        }
      }

      // Check if this project already exists by comparing actual paths
      // This prevents duplicates when Claude CLI creates a folder with a different encoded name
      if (existingPaths.has(actualProjectDir)) {
        continue;
      }

      // Also check if the encoded name already exists
      if (existingProjects.has(projectName)) {
        continue;
      }

      const project = {
        name: projectName,
        path: actualProjectDir,
        displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
        fullPath: actualProjectDir,
        isCustomName: !!projectConfig.displayName,
        isManuallyAdded: true,
        createdAt: projectConfig.createdAt || null,
        lastActivity: projectConfig.lastActivity || projectConfig.createdAt || null,
        sessions: [],
        ...(projectConfig.agentInfo ? { agentInfo: projectConfig.agentInfo } : {})
      };

      projects.push(project);
    }
  }

  return projects;
}

async function getSessions(projectName, limit = 5, offset = 0, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for getSessions');
  }
  const projectDir = path.join(getUserPaths(userUuid).claudeDir, 'projects', projectName);

  try {
    const files = await fs.readdir(projectDir);
    // agent-*.jsonl files contain session start data at this point. This needs to be revisited
    // periodically to make sure only accurate data is there and no new functionality is added there
    const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
    
    if (jsonlFiles.length === 0) {
      return { sessions: [], hasMore: false, total: 0 };
    }
    
    // Sort files by modification time (newest first)
    const filesWithStats = await Promise.all(
      jsonlFiles.map(async (file) => {
        const filePath = path.join(projectDir, file);
        const stats = await fs.stat(filePath);
        return { file, mtime: stats.mtime };
      })
    );
    filesWithStats.sort((a, b) => b.mtime - a.mtime);
    
    const allSessions = new Map();
    const allEntries = [];
    const uuidToSessionMap = new Map();
    
    // Collect all sessions and entries from all files
    for (const { file } of filesWithStats) {
      const jsonlFile = path.join(projectDir, file);
      const result = await parseJsonlSessions(jsonlFile);
      
      result.sessions.forEach(session => {
        if (!allSessions.has(session.id)) {
          allSessions.set(session.id, session);
        }
      });
      
      allEntries.push(...result.entries);
      
      // Early exit optimization for large projects
      if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
        break;
      }
    }
    
    // Build UUID-to-session mapping for timeline detection
    allEntries.forEach(entry => {
      if (entry.uuid && entry.sessionId) {
        uuidToSessionMap.set(entry.uuid, entry.sessionId);
      }
    });
    
    // Group sessions by first user message ID
    const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
    const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId

    // Find the first user message for each session
    allEntries.forEach(entry => {
      if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
        // This is a first user message in a session (parentUuid is null)
        const firstUserMsgId = entry.uuid;

        if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
          sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);

          const session = allSessions.get(entry.sessionId);
          if (session) {
            if (!sessionGroups.has(firstUserMsgId)) {
              sessionGroups.set(firstUserMsgId, {
                latestSession: session,
                allSessions: [session]
              });
            } else {
              const group = sessionGroups.get(firstUserMsgId);
              group.allSessions.push(session);

              // Update latest session if this one is more recent
              if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
                group.latestSession = session;
              }
            }
          }
        }
      }
    });

    // Collect all sessions that don't belong to any group (standalone sessions)
    const groupedSessionIds = new Set();
    sessionGroups.forEach(group => {
      group.allSessions.forEach(session => groupedSessionIds.add(session.id));
    });

    const standaloneSessionsArray = Array.from(allSessions.values())
      .filter(session => !groupedSessionIds.has(session.id));

    // Combine grouped sessions (only show latest from each group) + standalone sessions
    const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
      const session = { ...group.latestSession };
      // Add metadata about grouping
      if (group.allSessions.length > 1) {
        session.isGrouped = true;
        session.groupSize = group.allSessions.length;
        session.groupSessions = group.allSessions.map(s => s.id);
      }
      return session;
    });
    const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
      .filter(session => !session.summary.startsWith('{ "'))
      .sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));

    const total = visibleSessions.length;
    const paginatedSessions = visibleSessions.slice(offset, offset + limit);
    const hasMore = offset + limit < total;
    
    return {
      sessions: paginatedSessions,
      hasMore,
      total,
      offset,
      limit
    };
  } catch (error) {
    console.error(`Error reading sessions for project ${projectName}:`, error);
    return { sessions: [], hasMore: false, total: 0 };
  }
}

async function parseJsonlSessions(filePath) {
  const sessions = new Map();
  const entries = [];
  const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId

  try {
    const fileStream = fsSync.createReadStream(filePath);
    const rl = readline.createInterface({
      input: fileStream,
      crlfDelay: Infinity
    });

    for await (const line of rl) {
      if (line.trim()) {
        try {
          const entry = JSON.parse(line);
          entries.push(entry);

          // Handle summary entries that don't have sessionId yet
          if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
            pendingSummaries.set(entry.leafUuid, entry.summary);
          }

          if (entry.sessionId) {
            // Skip meta messages (e.g., skill/command loading messages)
            if (entry.isMeta) {
              continue;
            }

            // Check if this is an actual message (user or assistant)
            const isMessage = entry.type === 'user' || entry.type === 'assistant';

            if (!sessions.has(entry.sessionId)) {
              sessions.set(entry.sessionId, {
                id: entry.sessionId,
                summary: 'New Session',
                messageCount: 0,
                pendingTextMessage: false,
                lastActivity: new Date(),
                cwd: entry.cwd || '',
                lastUserMessage: null,
                lastAssistantMessage: null
              });
            }

            const session = sessions.get(entry.sessionId);

            // Apply pending summary if this entry has a parentUuid that matches a pending summary
            if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
              session.summary = pendingSummaries.get(entry.parentUuid);
            }

            // Update summary from summary entries with sessionId
            if (entry.type === 'summary' && entry.summary) {
              session.summary = entry.summary;
            }

            // Update summary from custom-title entries (user-set via renameSession SDK)
            if (entry.type === 'custom-title' && entry.customTitle) {
              session.summary = entry.customTitle;
            }

            // Track last user and assistant messages (skip system messages)
            if (entry.message?.role === 'user' && entry.message?.content) {
              const content = entry.message.content;

              // Extract text from array format if needed
              let textContent = content;
              if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
                textContent = content[0].text;
              }

              const isSystemMessage = typeof textContent === 'string' && (
                textContent.startsWith('<command-name>') ||
                textContent.startsWith('<command-message>') ||
                textContent.startsWith('<command-args>') ||
                textContent.startsWith('<local-command-stdout>') ||
                textContent.startsWith('<system-reminder>') ||
                textContent.startsWith('Caveat:') ||
                textContent.startsWith('This session is being continued from a previous') ||
                textContent.startsWith('Invalid API key') ||
                textContent.includes('{"subtasks":') || // Filter Task Master prompts
                textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
                textContent === 'Warmup' // Explicitly filter out "Warmup"
              );

              if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
                session.lastUserMessage = textContent;
              }
            } else if (entry.message?.role === 'assistant' && entry.message?.content) {
              // Skip API error messages using the isApiErrorMessage flag
              if (entry.isApiErrorMessage === true) {
                // Skip this message entirely
              } else {
                // Track last assistant text message
                let assistantText = null;

                if (Array.isArray(entry.message.content)) {
                  for (const part of entry.message.content) {
                    if (part.type === 'text' && part.text) {
                      assistantText = part.text;
                    }
                  }
                } else if (typeof entry.message.content === 'string') {
                  assistantText = entry.message.content;
                }

                // Additional filter for assistant messages with system content
                const isSystemAssistantMessage = typeof assistantText === 'string' && (
                  assistantText.startsWith('Invalid API key') ||
                  assistantText.includes('{"subtasks":') ||
                  assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
                );

                if (assistantText && !isSystemAssistantMessage) {
                  session.lastAssistantMessage = assistantText;
                }
              }
            }

            // Only count visible messages using stateful deferred-text approach:
            // - User text messages count immediately (after finalizing any pending assistant text)
            // - Assistant text messages are "deferred" – counted only if NOT followed by a tool_use
            //   (i.e., they are the final response, not transitional text before another tool call)
            // - Assistant tool_use entries count immediately (and discard any deferred text)
            // - thinking-only / tool_result-only / system messages are always skipped
            if (isMessage) {
              if (entry.type === 'user' && entry.message?.content) {
                const content = entry.message.content;
                let userShouldCount = true;
                if (Array.isArray(content)) {
                  if (content.length > 0 && content.every(p => p.type === 'tool_result')) {
                    userShouldCount = false;
                  } else {
                    const textPart = content.find(p => p.type === 'text');
                    const text = textPart?.text || '';
                    if (!text ||
                      text === 'Warmup' ||
                      text.startsWith('<command-name>') ||
                      text.startsWith('<command-message>') ||
                      text.startsWith('<command-args>') ||
                      text.startsWith('<local-command-stdout>') ||
                      text.startsWith('<system-reminder>') ||
                      text.startsWith('Caveat:') ||
                      text.startsWith('This session is being continued from a previous') ||
                      text.startsWith('Invalid API key') ||
                      text.includes('{"subtasks":') ||
                      text.includes('CRITICAL: You MUST respond with ONLY a JSON')
                    ) {
                      userShouldCount = false;
                    }
                  }
                } else if (typeof content === 'string') {
                  if (!content || content === 'Warmup' || content.startsWith('<')) {
                    userShouldCount = false;
                  }
                }
                if (userShouldCount) {
                  // Finalize any deferred assistant text (it was the final response before this user turn)
                  if (session.pendingTextMessage) {
                    session.messageCount++;
                    session.pendingTextMessage = false;
                  }
                  session.messageCount++;
                }
              } else if (entry.type === 'assistant') {
                if (entry.isApiErrorMessage === true) {
                  // skip API error messages
                } else if (Array.isArray(entry.message?.content)) {
                  const onlyThinking = entry.message.content.length > 0 &&
                    entry.message.content.every(p => p.type === 'thinking' || p.type === 'redacted_thinking');
                  const hasToolUse = entry.message.content.some(p => p.type === 'tool_use');
                  const hasText = entry.message.content.some(p => p.type === 'text');
                  if (onlyThinking) {
                    // skip thinking-only entries (not rendered in UI)
                  } else if (hasToolUse) {
                    // Tool call: discard any deferred text (it was transitional), count this tool
                    session.pendingTextMessage = false;
                    session.messageCount++;
                  } else if (hasText) {
                    // Text-only: defer counting — if next entry is tool_use this is transitional
                    session.pendingTextMessage = true;
                  }
                }
              }
            }

            if (entry.timestamp) {
              session.lastActivity = new Date(entry.timestamp);
            }
          }
        } catch (parseError) {
          // Skip malformed lines silently
        }
      }
    }

    // After processing all entries, set final summary based on last message if no summary exists
    for (const session of sessions.values()) {
      // Finalize any deferred assistant text (it was the last response in the session)
      if (session.pendingTextMessage) {
        session.messageCount++;
        session.pendingTextMessage = false;
      }
      if (session.summary === 'New Session') {
        // Prefer last user message, fall back to last assistant message
        const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
        if (lastMessage) {
          session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
        }
      }
    }

    // Filter out sessions that contain JSON responses (Task Master errors)
    const allSessions = Array.from(sessions.values());
    const filteredSessions = allSessions.filter(session => {
      const shouldFilter = session.summary.startsWith('{ "');
      if (shouldFilter) {
      }
      // Log a sample of summaries to debug
      if (Math.random() < 0.01) { // Log 1% of sessions
      }
      return !shouldFilter;
    });


    return {
      sessions: filteredSessions,
      entries: entries
    };

  } catch (error) {
    console.error('Error reading JSONL file:', error);
    return { sessions: [], entries: [] };
  }
}

// Get messages for a specific session with pagination support
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for getSessionMessages');
  }
  const projectDir = path.join(getUserPaths(userUuid).claudeDir, 'projects', projectName);

  try {
    const files = await fs.readdir(projectDir);
    // agent-*.jsonl files contain session start data at this point. This needs to be revisited
    // periodically to make sure only accurate data is there and no new functionality is added there
    const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
    
    if (jsonlFiles.length === 0) {
      return { messages: [], total: 0, hasMore: false };
    }
    
    const messages = [];
    
    // Process all JSONL files to find messages for this session
    for (const file of jsonlFiles) {
      const jsonlFile = path.join(projectDir, file);
      const fileStream = fsSync.createReadStream(jsonlFile);
      const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
      });
      
      for await (const line of rl) {
        if (line.trim()) {
          try {
            const entry = JSON.parse(line);
            if (entry.sessionId === sessionId) {
              messages.push(entry);
            }
          } catch (parseError) {
            console.warn('Error parsing line:', parseError.message);
          }
        }
      }
    }
    
    // Sort messages by timestamp
    const sortedMessages = messages.sort((a, b) => 
      new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
    );
    
    const total = sortedMessages.length;
    
    // If no limit is specified, return all messages (backward compatibility)
    if (limit === null) {
      return sortedMessages;
    }
    
    // Apply pagination - for recent messages, we need to slice from the end
    // offset 0 should give us the most recent messages
    const startIndex = Math.max(0, total - offset - limit);
    const endIndex = total - offset;
    const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
    const hasMore = startIndex > 0;
    
    return {
      messages: paginatedMessages,
      total,
      hasMore,
      offset,
      limit
    };
  } catch (error) {
    console.error(`Error reading messages for session ${sessionId}:`, error);
    return limit === null ? [] : { messages: [], total: 0, hasMore: false };
  }
}

// Rename a project's display name
async function renameProject(projectName, newDisplayName, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for renameProject');
  }
  const config = await loadProjectConfig(userUuid);

  if (!newDisplayName || newDisplayName.trim() === '') {
    // Remove only the displayName, preserve manuallyAdded/originalPath/createdAt/lastActivity
    if (config[projectName]) {
      const { displayName, ...rest } = config[projectName];
      if (Object.keys(rest).length === 0) {
        delete config[projectName];
      } else {
        config[projectName] = rest;
      }
    }
  } else {
    // Merge displayName into existing entry to preserve manuallyAdded/originalPath/createdAt/lastActivity
    config[projectName] = {
      ...config[projectName],
      displayName: newDisplayName.trim()
    };
  }

  await saveProjectConfig(config, userUuid);
  return true;
}

// Delete a session from a project
async function deleteSession(projectName, sessionId, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for deleteSession');
  }
  const projectDir = path.join(getUserPaths(userUuid).claudeDir, 'projects', projectName);

  try {
    const files = await fs.readdir(projectDir);
    const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));

    if (jsonlFiles.length === 0) {
      throw new Error('No session files found for this project');
    }

    // Check all JSONL files to find which one contains the session
    for (const file of jsonlFiles) {
      const jsonlFile = path.join(projectDir, file);
      const content = await fs.readFile(jsonlFile, 'utf8');
      const lines = content.split('\n').filter(line => line.trim());

      // Check if this file contains the session
      const hasSession = lines.some(line => {
        try {
          const data = JSON.parse(line);
          return data.sessionId === sessionId;
        } catch {
          return false;
        }
      });

      if (hasSession) {
        // Filter out all entries for this session
        const filteredLines = lines.filter(line => {
          try {
            const data = JSON.parse(line);
            return data.sessionId !== sessionId;
          } catch {
            return true; // Keep malformed lines
          }
        });

        // Write back the filtered content
        await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
        return true;
      }
    }

    throw new Error(`Session ${sessionId} not found in any files`);
  } catch (error) {
    console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
    throw error;
  }
}

// Check if a project is empty (has no sessions)
async function isProjectEmpty(projectName, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for isProjectEmpty');
  }
  try {
    const sessionsResult = await getSessions(projectName, 1, 0, userUuid);
    return sessionsResult.total === 0;
  } catch (error) {
    console.error(`Error checking if project ${projectName} is empty:`, error);
    return false;
  }
}

// Delete a project (sessions and optionally the actual project folder)
async function deleteProject(projectName, userUuid, options = {}) {
  if (!userUuid) {
    throw new Error('userUuid is required for deleteProject');
  }
  const { deleteFolder = false } = options;
  const sessionDir = path.join(getUserPaths(userUuid).claudeDir, 'projects', projectName);

  try {
    // If deleteFolder requested, resolve the actual path BEFORE removing config
    let actualProjectDir = null;
    if (deleteFolder) {
      actualProjectDir = await extractProjectDirectory(projectName, userUuid);
    }

    // Also resolve the actual path to clean up .claude.json project-scoped MCPs
    const resolvedProjectDir = actualProjectDir || await extractProjectDirectory(projectName, userUuid).catch(() => null);

    // Remove the Claude sessions directory
    try {
      await fs.rm(sessionDir, { recursive: true, force: true });
    } catch (err) {
      if (err.code !== 'ENOENT') {
        throw err;
      }
    }

    // Remove from project config
    const config = await loadProjectConfig(userUuid);
    delete config[projectName];
    await saveProjectConfig(config, userUuid);

    // Clean up project-scoped MCP servers from .claude.json
    if (resolvedProjectDir) {
      try {
        const claudeJsonPath = path.join(getUserPaths(userUuid).claudeDir, '.claude.json');
        const claudeConfig = JSON.parse(await fs.readFile(claudeJsonPath, 'utf-8'));
        if (claudeConfig.projects?.[resolvedProjectDir]) {
          delete claudeConfig.projects[resolvedProjectDir];
          await fs.writeFile(claudeJsonPath, JSON.stringify(claudeConfig, null, 2), 'utf-8');
        }
      } catch (err) {
        // Non-fatal: log and continue
        console.error(`[deleteProject] Failed to clean up .claude.json for ${resolvedProjectDir}:`, err.message);
      }
    }

    // Optionally delete the actual project folder
    if (deleteFolder && actualProjectDir) {
      try {
        await fs.rm(actualProjectDir, { recursive: true, force: true });
      } catch (err) {
        if (err.code !== 'ENOENT') {
          console.error(`Error deleting project folder ${actualProjectDir}:`, err);
          // Non-fatal: sessions already deleted, log and continue
        }
      }
    }

    return true;
  } catch (error) {
    console.error(`Error deleting project ${projectName}:`, error);
    throw error;
  }
}

// Add a project manually to the config (without creating folders)
async function addProjectManually(projectPath, displayName = null, userUuid) {
  if (!userUuid) {
    throw new Error('userUuid is required for addProjectManually');
  }
  const absolutePath = path.resolve(projectPath);

  try {
    // Check if the path exists
    await fs.access(absolutePath);
  } catch (error) {
    throw new Error(`Path does not exist: ${absolutePath}`);
  }

  // Generate project name (encode path for use as directory name)
  const projectName = absolutePath.replace(/\//g, '-');

  // Check if project already exists in config
  const config = await loadProjectConfig(userUuid);

  if (config[projectName]) {
    throw new Error(`Project already configured for path: ${absolutePath}`);
  }

  // Allow adding projects even if the directory exists - this enables tracking
  // existing Claude Code projects in the UI

  const createdAt = new Date().toISOString();

  // Add to config as manually added project
  config[projectName] = {
    manuallyAdded: true,
    originalPath: absolutePath,
    createdAt,
    lastActivity: createdAt
  };

  if (displayName) {
    config[projectName].displayName = displayName;
  }

  await saveProjectConfig(config, userUuid);


  return {
    name: projectName,
    path: absolutePath,
    fullPath: absolutePath,
    displayName: displayName || await generateDisplayName(projectName, absolutePath),
    isManuallyAdded: true,
    createdAt,
    lastActivity: createdAt,
    sessions: []
  };
}

// Update lastActivity for a project by its absolute path (used by Gemini/Codex shell sessions)
async function updateProjectLastActivity(projectPath, userUuid) {
  if (!userUuid || !projectPath) return;
  try {
    const absolutePath = path.resolve(projectPath);
    const projectName = absolutePath.replace(/\//g, '-');
    const config = await loadProjectConfig(userUuid);
    if (config[projectName]) {
      config[projectName].lastActivity = new Date().toISOString();
      await saveProjectConfig(config, userUuid);
    }
  } catch (err) {
    // Non-fatal - just log
    console.warn('[WARN] Failed to update project lastActivity:', err.message);
  }
}

export {
  getProjects,
  getSessions,
  getSessionMessages,
  parseJsonlSessions,
  renameProject,
  deleteSession,
  isProjectEmpty,
  deleteProject,
  addProjectManually,
  loadProjectConfig,
  saveProjectConfig,
  extractProjectDirectory,
  clearProjectDirectoryCache,
  updateProjectLastActivity
};
