import express from 'express';
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
import { spawn } from 'child_process';
import { v4 as uuidv4 } from 'uuid';
import cors from 'cors';
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
import { join, basename, extname } from 'path';
import multer from 'multer';
import session from 'express-session';
import { createClient } from 'redis';
import RedisStore from 'connect-redis';
import cookieParser from 'cookie-parser';

// Auth modules
import { authConfig, validateConfig } from './config/auth.js';
import { initializeOIDC } from './utils/oidc.js';
import { requireAuth, optionalAuth, authenticateWebSocket } from './middleware/auth.js';
import authRoutes from './routes/auth.js';

const app = express();

// Trust proxy - required for secure cookies behind reverse proxy (NPM)
app.set('trust proxy', 1);

// CORS configuration - allow credentials for cookies
app.use(cors({
  origin: authConfig.app.frontendUrl,
  credentials: true,
}));
app.use(express.json());
app.use(cookieParser());

// Session store reference (set after Redis connection)
let sessionStore = null;

const PORT = process.env.PORT || 3001;
const HOST = process.env.HOST || '0.0.0.0';
const DEBUG = process.env.DEBUG === 'true';
// Upload to /projects/.claude-uploads so Claude can access them via mounted volume
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/projects/.claude-uploads';
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

// Allowed file types
const ALLOWED_TYPES = {
  // Images
  'image/png': '.png',
  'image/jpeg': '.jpg',
  'image/gif': '.gif',
  'image/webp': '.webp',
  // Text/Code
  'text/plain': '.txt',
  'text/markdown': '.md',
  'text/csv': '.csv',
  'text/html': '.html',
  'text/css': '.css',
  'text/javascript': '.js',
  'application/json': '.json',
  'application/xml': '.xml',
  'text/xml': '.xml',
  'application/x-yaml': '.yaml',
  'text/yaml': '.yaml'
};

// Configure multer for file uploads
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const sessionId = req.params.sessionId || 'default';
    const sessionDir = join(UPLOAD_DIR, sessionId);
    if (!existsSync(sessionDir)) {
      mkdirSync(sessionDir, { recursive: true });
    }
    cb(null, sessionDir);
  },
  filename: (req, file, cb) => {
    // Preserve original filename with timestamp prefix to avoid collisions
    const timestamp = Date.now();
    const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
    cb(null, `${timestamp}-${safeName}`);
  }
});

const upload = multer({
  storage,
  limits: { fileSize: MAX_FILE_SIZE },
  fileFilter: (req, file, cb) => {
    if (ALLOWED_TYPES[file.mimetype]) {
      cb(null, true);
    } else {
      // Also allow common code file extensions
      const ext = extname(file.originalname).toLowerCase();
      const codeExtensions = ['.py', '.js', '.ts', '.jsx', '.tsx', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.sh', '.bash', '.zsh', '.yml', '.yaml', '.toml', '.ini', '.conf', '.md', '.txt', '.json', '.xml', '.html', '.css', '.scss', '.less', '.sql', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.lua', '.pl', '.pm'];
      if (codeExtensions.includes(ext)) {
        cb(null, true);
      } else {
        cb(new Error(`File type not allowed: ${file.mimetype} (${ext})`));
      }
    }
  }
});

// Load hosts configuration
const CONFIG_PATH = process.env.CONFIG_PATH || '/app/config/hosts.json';
let hostsConfig = { hosts: {}, defaults: { scanSubdirs: true, maxDepth: 1 } };

function loadConfig() {
  try {
    if (existsSync(CONFIG_PATH)) {
      hostsConfig = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
      console.log('Loaded hosts config:', Object.keys(hostsConfig.hosts));
    } else {
      console.log('No hosts.json found, using defaults');
    }
  } catch (err) {
    console.error('Error loading config:', err);
  }
}
loadConfig();

// Store active Claude sessions
const sessions = new Map();

// Control request counter for unique IDs
let controlRequestCounter = 0;
function generateRequestId() {
  return `req_${++controlRequestCounter}_${Date.now().toString(16)}`;
}

const server = createServer(app);
const wss = new WebSocketServer({ server });

// WebSocket heartbeat interval (30 seconds)
const WS_HEARTBEAT_INTERVAL = 30000;

// Scan directory for projects
function scanProjects(basePath, depth = 0, maxDepth = 1) {
  const projects = [];

  if (!existsSync(basePath)) return projects;

  try {
    const entries = readdirSync(basePath, { withFileTypes: true });
    for (const entry of entries) {
      if (entry.isDirectory() && !entry.name.startsWith('.')) {
        const fullPath = join(basePath, entry.name);
        projects.push({
          path: fullPath,
          name: entry.name,
          type: 'directory'
        });

        // Recurse if not at max depth
        if (depth < maxDepth - 1) {
          projects.push(...scanProjects(fullPath, depth + 1, maxDepth));
        }
      }
    }
  } catch (err) {
    console.error(`Error scanning ${basePath}:`, err.message);
  }

  return projects;
}

// Function to register API routes (called after session middleware is set up)
function registerApiRoutes() {

// REST endpoint to list hosts (protected)
app.get('/api/hosts', requireAuth, (req, res) => {
  const hosts = Object.entries(hostsConfig.hosts).map(([id, host]) => ({
    id,
    name: host.name,
    description: host.description,
    avatar: host.avatar,
    color: host.color,
    icon: host.icon,
    connectionType: host.connection.type,
    isLocal: host.connection.type === 'local'
  }));
  res.json({ hosts, defaultHost: hostsConfig.defaults?.host || 'neko' });
});

// REST endpoint to list projects for a host (protected)
app.get('/api/projects', requireAuth, (req, res) => {
  const hostId = req.query.host || hostsConfig.defaults?.host || 'neko';
  const host = hostsConfig.hosts[hostId];

  if (!host) {
    return res.status(404).json({ error: `Host '${hostId}' not found` });
  }

  // For SSH hosts, return the basePaths from config (can't scan remote directories)
  if (host.connection.type !== 'local') {
    const projects = host.basePaths.map(basePath => ({
      path: basePath,
      name: basename(basePath),
      type: 'base',
      isBase: true
    }));
    return res.json({
      projects,
      host: hostId,
      hostInfo: { name: host.name, color: host.color },
      message: 'SSH host - showing base paths only'
    });
  }

  const projects = [];
  const scanSubdirs = hostsConfig.defaults?.scanSubdirs ?? true;
  const maxDepth = hostsConfig.defaults?.maxDepth ?? 1;

  for (const basePath of host.basePaths) {
    // Add base path itself
    if (existsSync(basePath)) {
      projects.push({
        path: basePath,
        name: basename(basePath),
        type: 'base',
        isBase: true
      });

      // Scan subdirectories if enabled
      if (scanSubdirs) {
        projects.push(...scanProjects(basePath, 0, maxDepth));
      }
    }
  }

  res.json({ projects, host: hostId, hostInfo: { name: host.name, color: host.color } });
});

// Health check
app.get('/api/health', (req, res) => {
  res.json({
    status: 'ok',
    activeSessions: sessions.size,
    timestamp: new Date().toISOString()
  });
});

// Browse directories on a host (for directory picker)
app.get('/api/browse', requireAuth, async (req, res) => {
  const hostId = req.query.host || hostsConfig.defaults?.host || 'neko';
  const path = req.query.path || '~';
  const host = hostsConfig.hosts[hostId];

  if (!host) {
    return res.status(404).json({ error: `Host '${hostId}' not found` });
  }

  // For SSH hosts, execute ls command remotely
  if (host.connection.type === 'ssh') {
    const { host: sshHost, user, port = 22 } = host.connection;
    // Expand ~ to home directory, list only directories, format as JSON-friendly output
    const lsCmd = `cd ${path} 2>/dev/null && pwd && ls -1F 2>/dev/null | grep '/$' | sed 's/\\/$//'`;
    const sshCmd = `ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port} ${user}@${sshHost} "${lsCmd}"`;

    try {
      const { exec } = await import('child_process');
      const { promisify } = await import('util');
      const execAsync = promisify(exec);

      const { stdout, stderr } = await execAsync(sshCmd, { timeout: 10000 });
      const lines = stdout.trim().split('\n').filter(Boolean);
      const currentPath = lines[0] || path;
      const directories = lines.slice(1).map(name => ({
        name,
        path: currentPath === '/' ? `/${name}` : `${currentPath}/${name}`,
        type: 'directory'
      }));

      // Add parent directory if not at root
      if (currentPath !== '/') {
        const parentPath = currentPath.split('/').slice(0, -1).join('/') || '/';
        directories.unshift({
          name: '..',
          path: parentPath,
          type: 'parent'
        });
      }

      res.json({
        currentPath,
        directories,
        host: hostId
      });
    } catch (err) {
      console.error('Browse error:', err.message);
      res.status(500).json({ error: `Failed to browse: ${err.message}` });
    }
  } else {
    // Local browsing
    try {
      const resolvedPath = path === '~' ? process.env.HOME || '/home' : path;
      if (!existsSync(resolvedPath)) {
        return res.status(404).json({ error: `Path not found: ${resolvedPath}` });
      }

      const entries = readdirSync(resolvedPath);
      const directories = entries
        .filter(name => {
          try {
            return statSync(join(resolvedPath, name)).isDirectory() && !name.startsWith('.');
          } catch {
            return false;
          }
        })
        .map(name => ({
          name,
          path: join(resolvedPath, name),
          type: 'directory'
        }));

      // Add parent directory
      if (resolvedPath !== '/') {
        const parentPath = join(resolvedPath, '..');
        directories.unshift({
          name: '..',
          path: parentPath,
          type: 'parent'
        });
      }

      res.json({
        currentPath: resolvedPath,
        directories,
        host: hostId
      });
    } catch (err) {
      res.status(500).json({ error: `Failed to browse: ${err.message}` });
    }
  }
});

// File upload endpoint
app.post('/api/upload/:sessionId', requireAuth, upload.array('files', 5), async (req, res) => {
  try {
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ error: 'No files uploaded' });
    }

    const sessionId = req.params.sessionId;
    const session = sessions.get(sessionId);
    const isSSH = session?.host?.connection?.type === 'ssh';

    const uploadedFiles = [];

    for (const file of req.files) {
      // Convert container path to host path for Claude
      // /projects/.claude-uploads/... -> /home/sumdex/projects/.claude-uploads/...
      let hostPath = file.path.replace('/projects/', '/home/sumdex/projects/');

      // For SSH hosts, transfer file via SCP
      if (isSSH && session.host) {
        const { host: sshHost, user, port = 22 } = session.host.connection;
        const remotePath = `/tmp/.claude-uploads/${file.filename}`;

        try {
          // Create remote directory if needed
          const { execSync } = await import('child_process');
          execSync(`ssh -o StrictHostKeyChecking=no -p ${port} ${user}@${sshHost} "mkdir -p /tmp/.claude-uploads"`, {
            timeout: 10000
          });

          // Transfer file via SCP
          execSync(`scp -o StrictHostKeyChecking=no -P ${port} "${file.path}" ${user}@${sshHost}:"${remotePath}"`, {
            timeout: 60000  // 60s for large files
          });

          hostPath = remotePath;
          console.log(`[Upload] SCP transferred ${file.filename} to ${session.hostId}:${remotePath}`);
        } catch (scpErr) {
          console.error(`[Upload] SCP error for ${file.filename}:`, scpErr.message);
          // Fall back to local path (won't work but at least doesn't fail)
        }
      }

      uploadedFiles.push({
        originalName: file.originalname,
        savedName: file.filename,
        path: hostPath,
        containerPath: file.path,
        size: file.size,
        mimeType: file.mimetype,
        isImage: file.mimetype.startsWith('image/')
      });
    }

    console.log(`[Upload] Session ${sessionId}: ${uploadedFiles.length} files uploaded`);
    res.json({ files: uploadedFiles });
  } catch (err) {
    console.error('[Upload] Error:', err);
    res.status(500).json({ error: err.message });
  }
});

// Error handler for multer
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB` });
    }
    return res.status(400).json({ error: err.message });
  }
  if (err) {
    return res.status(400).json({ error: err.message });
  }
  next();
});

// Parse history content into messages
function parseHistoryContent(content) {
  const lines = content.split('\n').filter(l => l.trim());
  const messages = [];

  for (const line of lines) {
    try {
      const event = JSON.parse(line);

      // Parse user messages
      if (event.type === 'user' && event.message?.content) {
        const textContent = event.message.content
          .filter(c => c.type === 'text')
          .map(c => c.text)
          .join('');
        if (textContent && !event.tool_use_result) {
          messages.push({
            type: 'user',
            content: textContent,
            timestamp: event.timestamp || Date.now()
          });
        }
      }

      // Parse assistant messages
      if (event.type === 'assistant' && event.message?.content) {
        for (const block of event.message.content) {
          if (block.type === 'text' && block.text) {
            messages.push({
              type: 'assistant',
              content: block.text,
              timestamp: event.timestamp || Date.now()
            });
          } else if (block.type === 'tool_use') {
            messages.push({
              type: 'tool_use',
              tool: block.name,
              input: block.input,
              toolUseId: block.id,
              timestamp: event.timestamp || Date.now()
            });
          }
        }
      }

      // Parse tool results
      if (event.type === 'user' && event.tool_use_result) {
        messages.push({
          type: 'tool_result',
          content: event.tool_use_result.content,
          toolUseId: event.tool_use_result.tool_use_id,
          isError: event.tool_use_result.is_error || false,
          timestamp: event.timestamp || Date.now()
        });
      }
    } catch (e) {
      // Skip invalid JSON lines
    }
  }

  return messages;
}

// Get session history for a project (supports SSH hosts)
app.get('/api/history/:project', requireAuth, async (req, res) => {
  console.log(`[History] Request for project: ${req.params.project}, host: ${req.query.host}`);
  try {
    const projectPath = decodeURIComponent(req.params.project);
    const hostId = req.query.host;
    const host = hostId ? hostsConfig.hosts[hostId] : null;
    const isSSH = host?.connection?.type === 'ssh';
    console.log(`[History] Resolved - projectPath: ${projectPath}, hostId: ${hostId}, isSSH: ${isSSH}`);

    // Convert project path to Claude's folder naming convention
    const projectFolder = projectPath.replace(/\//g, '-');

    if (isSSH) {
      // Load history via SSH
      const { host: sshHost, user, port = 22 } = host.connection;
      const sshTarget = `${user}@${sshHost}`;
      const historyDir = `~/.claude/projects/${projectFolder}`;

      // Find latest non-empty session file via SSH (skip agent files and empty files)
      // Using a simpler approach: find non-empty files with find command
      const findCmd = `find ${historyDir} -maxdepth 1 -name '*.jsonl' ! -name 'agent-*' -size +0 -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2-`;

      const { execSync } = await import('child_process');
      try {
        const latestFile = execSync(`ssh -T -o StrictHostKeyChecking=no -p ${port} ${sshTarget} "${findCmd}"`, {
          encoding: 'utf-8',
          timeout: 10000
        }).trim();

        if (!latestFile) {
          return res.json({ messages: [], sessionId: null });
        }

        // Read the last 2000 lines (to handle large history files)
        const content = execSync(`ssh -T -o StrictHostKeyChecking=no -p ${port} ${sshTarget} "tail -n 2000 '${latestFile}'"`, {
          encoding: 'utf-8',
          timeout: 30000,
          maxBuffer: 50 * 1024 * 1024  // 50MB buffer
        });

        const sessionId = basename(latestFile).replace('.jsonl', '');
        const messages = parseHistoryContent(content);

        console.log(`[History] SSH - Returning ${messages.length} messages from session ${sessionId}`);
        return res.json({ messages, sessionId, source: 'ssh' });
      } catch (sshErr) {
        console.error('SSH history fetch error:', sshErr.message);
        return res.json({ messages: [], sessionId: null, error: sshErr.message });
      }
    }

    // Local history
    const historyDir = `/home/node/.claude/projects/${projectFolder}`;

    if (!existsSync(historyDir)) {
      return res.json({ messages: [], sessionId: null });
    }

    // Find the most recent non-agent, non-empty session file
    const files = readdirSync(historyDir)
      .filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'))
      .map(f => ({
        name: f,
        path: join(historyDir, f),
        mtime: statSync(join(historyDir, f)).mtime,
        size: statSync(join(historyDir, f)).size
      }))
      .filter(f => f.size > 0)  // Skip empty files
      .sort((a, b) => b.mtime - a.mtime);

    if (files.length === 0) {
      return res.json({ messages: [], sessionId: null });
    }

    const latestFile = files[0];
    const sessionId = latestFile.name.replace('.jsonl', '');
    const content = readFileSync(latestFile.path, 'utf-8');
    const messages = parseHistoryContent(content);

    res.json({ messages, sessionId });
  } catch (err) {
    console.error('Error reading history:', err);
    res.status(500).json({ error: err.message });
  }
});

} // End of registerApiRoutes function

wss.on('connection', async (ws, req) => {
  const sessionId = uuidv4();
  console.log(`[${sessionId}] New WebSocket connection`);

  // Track connection health
  ws.isAlive = true;

  // Heartbeat to keep connection alive through proxies
  const heartbeatInterval = setInterval(() => {
    if (ws.readyState === ws.OPEN) {
      ws.ping();
    }
  }, WS_HEARTBEAT_INTERVAL);

  ws.on('pong', () => {
    ws.isAlive = true;
  });

  // Authenticate WebSocket connection
  let wsUser = null;
  if (authConfig.app.authEnabled && sessionStore) {
    wsUser = await authenticateWebSocket(req, sessionStore);
    if (!wsUser) {
      console.log(`[${sessionId}] WebSocket authentication failed - closing connection`);
      ws.close(1008, 'Unauthorized');
      return;
    }
    console.log(`[${sessionId}] WebSocket authenticated as: ${wsUser.email}`);
  } else {
    // Auth disabled - use anonymous user
    wsUser = { id: 'anonymous', email: 'anonymous@local', groups: ['agent-admin'], isAdmin: true };
  }

  let claudeProcess = null;
  let currentProject = null;
  let currentHostId = null;  // Track current host for restart
  let isInitialized = false;
  let currentPermissionMode = 'default';
  let savedPermissionMode = 'default';  // Store mode before plan mode switch
  let inPlanMode = false;  // Track if we're in plan mode (to require approval for ExitPlanMode)
  const pendingControlRequests = new Map();
  const pendingPermissionRequests = new Map();  // Track tool permission requests

  // Cleanup stale pending requests (TTL: 30 seconds)
  const REQUEST_TTL = 30000;
  const cleanupInterval = setInterval(() => {
    const now = Date.now();
    for (const [id, req] of pendingControlRequests) {
      if (req.createdAt && now - req.createdAt > REQUEST_TTL) {
        pendingControlRequests.delete(id);
      }
    }
    for (const [id, req] of pendingPermissionRequests) {
      if (req.createdAt && now - req.createdAt > REQUEST_TTL) {
        pendingPermissionRequests.delete(id);
      }
    }
  }, 60000);

  // Helper to set permission mode via control protocol (needs claudeProcess)
  const setPermissionModeViaControl = (mode) => {
    if (!claudeProcess) return;
    const modeRequestId = generateRequestId();
    const modeRequest = {
      type: 'control_request',
      request_id: modeRequestId,
      request: {
        subtype: 'set_permission_mode',
        mode: mode
      }
    };
    console.log(`[${sessionId}] Auto-switching permission mode to: ${mode}`);
    claudeProcess.stdin.write(JSON.stringify(modeRequest) + '\n');
    pendingControlRequests.set(modeRequestId, { type: 'set_permission_mode', mode, createdAt: Date.now() });
  };

  const sendToClient = (type, data) => {
    if (ws.readyState === ws.OPEN) {
      try {
        ws.send(JSON.stringify({ type, ...data, timestamp: Date.now() }));
      } catch (err) {
        console.error(`[${sessionId}] WebSocket send failed:`, err.message);
      }
    }
  };

  const startClaudeSession = (projectPath, resume = true, hostId = null) => {
    if (claudeProcess) {
      console.log(`[${sessionId}] Killing existing Claude process`);
      claudeProcess.kill();
    }

    currentProject = projectPath;
    currentHostId = hostId;  // Save for potential restart

    // Get host config
    const host = hostId ? hostsConfig.hosts[hostId] : null;
    const isSSH = host?.connection?.type === 'ssh';

    console.log(`[${sessionId}] Starting Claude in: ${projectPath} (resume: ${resume}, host: ${hostId || 'local'}, ssh: ${isSSH})`);

    const claudeArgs = [
      '-p',
      '--output-format', 'stream-json',
      '--input-format', 'stream-json',
      '--include-partial-messages',
      '--verbose'
      // Note: No --dangerously-skip-permissions - we handle permissions via control protocol
    ];

    // Add continue flag to resume most recent conversation
    if (resume) {
      claudeArgs.push('--continue');
    }

    if (isSSH) {
      // SSH execution
      const { host: sshHost, user, port = 22 } = host.connection;
      const sshTarget = `${user}@${sshHost}`;

      // Use claudePath from config if specified, otherwise default to 'claude'
      const claudeBin = host.claudePath || 'claude';

      // Build the remote command with PATH setup for non-login shells
      const remoteCmd = `export PATH="$HOME/.local/bin:$PATH" && cd ${projectPath} && ${claudeBin} ${claudeArgs.join(' ')}`;

      console.log(`[${sessionId}] SSH to ${sshTarget}:${port} - ${remoteCmd}`);

      claudeProcess = spawn('ssh', [
        '-T',                     // Disable TTY (needed for JSON streaming)
        '-o', 'StrictHostKeyChecking=no',
        '-o', 'BatchMode=no',
        '-p', String(port),
        sshTarget,
        remoteCmd
      ], {
        stdio: ['pipe', 'pipe', 'pipe'],
        env: { ...process.env }
      });
    } else {
      // Local execution
      claudeProcess = spawn('claude', claudeArgs, {
        stdio: ['pipe', 'pipe', 'pipe'],
        cwd: projectPath || '/projects',
        env: { ...process.env }
      });
    }

    sessions.set(sessionId, { process: claudeProcess, project: projectPath, host: host, hostId: hostId, user: wsUser });

    sendToClient('session_started', {
      sessionId,
      project: projectPath
    });

    // Send control protocol initialization after process starts
    const initializeControlProtocol = () => {
      const requestId = generateRequestId();
      const initRequest = {
        type: 'control_request',
        request_id: requestId,
        request: {
          subtype: 'initialize',
          hooks: null
        }
      };
      console.log(`[${sessionId}] Sending control initialization`);
      claudeProcess.stdin.write(JSON.stringify(initRequest) + '\n');
      pendingControlRequests.set(requestId, { type: 'initialize', createdAt: Date.now() });
    };

    // Small delay to ensure process is ready
    setTimeout(initializeControlProtocol, 100);

    // Handle stdout (JSON events)
    let pendingLine = '';
    claudeProcess.stdout.on('data', (data) => {
      const chunk = data.toString();
      if (DEBUG) console.log(`[${sessionId}] stdout chunk:`, chunk.substring(0, 200));

      const parts = (pendingLine + chunk).split('\n');
      pendingLine = parts.pop() || '';

      for (const line of parts) {
        if (line.trim()) {
          if (DEBUG) console.log(`[${sessionId}] Processing line:`, line.substring(0, 100));
          try {
            const event = JSON.parse(line);
            if (DEBUG) console.log(`[${sessionId}] Event type:`, event.type);

            // Handle control protocol requests (permission prompts from CLI)
            if (event.type === 'control_request') {
              const request = event.request;
              const requestId = event.request_id;

              if (request?.subtype === 'can_use_tool') {
                if (DEBUG) console.log(`[${sessionId}] Permission request for tool: ${request.tool_name}`);

                // Special handling for ExitPlanMode - always send to UI for approval
                const isPlanApproval = request.tool_name === 'ExitPlanMode';

                // Track plan mode state for ExitPlanMode
                if (isPlanApproval && !inPlanMode) {
                  savedPermissionMode = currentPermissionMode;
                  inPlanMode = true;
                  if (DEBUG) console.log(`[${sessionId}] Entering plan mode, saved mode: ${savedPermissionMode}`);
                }

                // Store the request so we know which tool is being approved/denied
                pendingPermissionRequests.set(requestId, {
                  toolName: request.tool_name,
                  toolInput: request.input,
                  isPlanApproval,
                  createdAt: Date.now()
                });

                // Send to frontend for user approval
                sendToClient('permission_request', {
                  requestId,
                  toolName: request.tool_name,
                  toolInput: request.input,
                  permissionSuggestions: request.permission_suggestions,
                  blockedPath: request.blocked_path,
                  isPlanApproval  // Flag for special UI treatment
                });
              }
              continue;
            }

            // Handle control protocol responses
            if (event.type === 'control_response') {
              const response = event.response;
              const requestId = response?.request_id;
              const pending = pendingControlRequests.get(requestId);

              if (pending) {
                pendingControlRequests.delete(requestId);

                if (response.subtype === 'success') {
                  if (pending.type === 'initialize') {
                    isInitialized = true;
                    console.log(`[${sessionId}] Control protocol initialized`);
                    // Send available commands/models to frontend
                    sendToClient('control_initialized', {
                      commands: response.response?.commands,
                      models: response.response?.models,
                      account: response.response?.account
                    });
                  } else if (pending.type === 'set_permission_mode') {
                    currentPermissionMode = pending.mode;
                    console.log(`[${sessionId}] Permission mode changed to: ${currentPermissionMode}`);
                    sendToClient('permission_mode_changed', {
                      mode: currentPermissionMode
                    });
                  }
                } else if (response.subtype === 'error') {
                  console.error(`[${sessionId}] Control request error:`, response.error);
                  sendToClient('control_error', {
                    requestId,
                    error: response.error
                  });
                }
              }
              // Don't forward control responses as regular events
              continue;
            }

            // Debug: log all tool_use blocks in assistant messages
            if (event.type === 'assistant' && event.message?.content) {
              for (const block of event.message.content) {
                if (block.type === 'tool_use') {
                  if (DEBUG) console.log(`[${sessionId}] Tool use detected: ${block.name}`);
                }
              }
            }
            // Note: ExitPlanMode is now handled exclusively via Control Protocol (can_use_tool)
            // to avoid tool_result/control_response confusion and message compaction issues

            // Debug: Write all events to file for context analysis
            const debugLogPath = '/tmp/claude-events-debug.jsonl';
            const debugEntry = {
              timestamp: new Date().toISOString(),
              sessionId,
              event
            };
            try {
              appendFileSync(debugLogPath, JSON.stringify(debugEntry) + '\n');
            } catch (e) {
              // Ignore write errors
            }

            sendToClient('claude_event', { event });
          } catch (e) {
            // Non-JSON output, send as raw
            if (DEBUG) console.log(`[${sessionId}] Raw output:`, line.substring(0, 100));
            sendToClient('raw_output', { content: line });
          }
        }
      }
    });

    // Handle stderr
    claudeProcess.stderr.on('data', async (data) => {
      const content = data.toString();
      // Always log stderr for SSH connections (exit code 255 debugging)
      if (DEBUG || isSSH) console.log(`[${sessionId}] stderr:`, content);

      // Debug: Write stderr to file for context analysis
      const debugLogPath = '/tmp/claude-events-debug.jsonl';
      const debugEntry = {
        timestamp: new Date().toISOString(),
        sessionId,
        type: 'stderr',
        content
      };
      try {
        const fs = await import('fs');
        fs.appendFileSync(debugLogPath, JSON.stringify(debugEntry) + '\n');
      } catch (e) {
        // Ignore write errors
      }

      sendToClient('stderr', { content });
    });

    claudeProcess.on('close', (code) => {
      console.log(`[${sessionId}] Claude process exited with code ${code}`);
      sendToClient('session_ended', { code });
      sessions.delete(sessionId);
      claudeProcess = null;
    });

    claudeProcess.on('error', (err) => {
      console.error(`[${sessionId}] Claude process error:`, err);
      sendToClient('error', { message: err.message });
    });
  };

  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message.toString());
      if (DEBUG) console.log(`[${sessionId}] Received:`, data.type);

      switch (data.type) {
        case 'start_session':
          startClaudeSession(data.project || '/projects', data.resume !== false, data.host || null);
          break;

        case 'user_message':
          if (!claudeProcess) {
            sendToClient('error', { message: 'No active Claude session' });
            return;
          }

          const payload = {
            type: 'user',
            message: {
              role: 'user',
              content: [{ type: 'text', text: data.message }]
            }
          };

          if (DEBUG) console.log(`[${sessionId}] Sending to Claude:`, data.message.substring(0, 50) + '...');
          claudeProcess.stdin.write(JSON.stringify(payload) + '\n');
          break;

        case 'stop_session':
          if (claudeProcess) {
            claudeProcess.kill();
            claudeProcess = null;
          }
          break;

        case 'stop_generation':
          // Kill the process and restart with --continue to resume session
          if (claudeProcess) {
            console.log(`[${sessionId}] Stop generation: killing process and restarting`);

            // Save current state for restart
            const restartProject = currentProject;
            const restartHost = currentHostId;
            const restartPermissionMode = currentPermissionMode;

            // Kill the process
            claudeProcess.kill('SIGKILL');
            claudeProcess = null;
            isInitialized = false;

            // Notify frontend
            sendToClient('generation_stopped', {
              message: 'Generation stopped, reconnecting...',
              timestamp: Date.now()
            });

            // Restart after a short delay
            setTimeout(() => {
              console.log(`[${sessionId}] Restarting session with --continue`);
              startClaudeSession(restartProject, true, restartHost);

              // Restore permission mode after initialization
              savedPermissionMode = restartPermissionMode;
            }, 500);
          } else {
            sendToClient('generation_stopped', {
              message: 'No active process',
              timestamp: Date.now()
            });
          }
          break;

        case 'set_permission_mode':
          if (!claudeProcess) {
            sendToClient('error', { message: 'No active Claude session' });
            return;
          }
          if (!isInitialized) {
            sendToClient('error', { message: 'Control protocol not yet initialized' });
            return;
          }

          // Don't allow mode change while in plan mode - plan mode takes priority
          if (inPlanMode) {
            console.log(`[${sessionId}] Ignoring set_permission_mode while in plan mode (saving ${data.mode} for later)`);
            // Save what the user wanted so we can restore it after plan approval
            savedPermissionMode = data.mode;
            // Notify frontend that we're still in plan mode
            sendToClient('permission_mode', { mode: 'plan', reason: 'plan_mode_active' });
            return;
          }

          const mode = data.mode;
          const validModes = ['default', 'acceptEdits', 'plan', 'bypassPermissions'];
          if (!validModes.includes(mode)) {
            sendToClient('error', { message: `Invalid permission mode: ${mode}. Valid: ${validModes.join(', ')}` });
            return;
          }

          const modeRequestId = generateRequestId();
          const modeRequest = {
            type: 'control_request',
            request_id: modeRequestId,
            request: {
              subtype: 'set_permission_mode',
              mode: mode
            }
          };

          console.log(`[${sessionId}] Setting permission mode to: ${mode}`);
          claudeProcess.stdin.write(JSON.stringify(modeRequest) + '\n');
          pendingControlRequests.set(modeRequestId, { type: 'set_permission_mode', mode, createdAt: Date.now() });
          break;

        case 'get_permission_mode':
          sendToClient('permission_mode', { mode: currentPermissionMode });
          break;

        case 'permission_response':
          if (!claudeProcess) {
            sendToClient('error', { message: 'No active Claude session' });
            return;
          }

          const pendingPerm = pendingPermissionRequests.get(data.requestId);
          if (pendingPerm) {
            pendingPermissionRequests.delete(data.requestId);
          }

          // All permission responses (including ExitPlanMode/plan approval) use control_response
          // This avoids tool_result issues when message history is compacted
          const permResponse = {
            type: 'control_response',
            response: {
              subtype: 'success',
              request_id: data.requestId,
              response: data.allow
                ? { behavior: 'allow', updated_input: data.updatedInput || null }
                : { behavior: 'deny', message: data.message || 'User denied permission' }
            }
          };

          console.log(`[${sessionId}] Sending permission response: ${data.allow ? 'allow' : 'deny'} for ${data.requestId}`);
          claudeProcess.stdin.write(JSON.stringify(permResponse) + '\n');

          // Handle plan mode state updates for ExitPlanMode
          if (pendingPerm?.isPlanApproval) {
            if (data.allow) {
              const modeToRestore = savedPermissionMode || 'default';
              console.log(`[${sessionId}] ExitPlanMode approved - restoring mode to '${modeToRestore}'`);
              inPlanMode = false;
              savedPermissionMode = null;

              // Restore previous permission mode after a small delay
              setTimeout(() => {
                setPermissionModeViaControl(modeToRestore);
              }, 100);

              sendToClient('plan_mode_exited', { approved: true });
            } else {
              console.log(`[${sessionId}] ExitPlanMode rejected`);
              sendToClient('plan_mode_exited', { approved: false });
            }
          }
          break;

        // Note: plan_approval case removed - now handled via permission_response with isPlanApproval flag

        default:
          console.log(`[${sessionId}] Unknown message type:`, data.type);
      }
    } catch (e) {
      console.error(`[${sessionId}] Error processing message:`, e);
      sendToClient('error', { message: e.message });
    }
  });

  ws.on('close', () => {
    console.log(`[${sessionId}] WebSocket closed`);
    clearInterval(heartbeatInterval);
    clearInterval(cleanupInterval);
    if (claudeProcess) {
      claudeProcess.kill();
      sessions.delete(sessionId);
    }
  });

  ws.on('error', (err) => {
    console.error(`[${sessionId}] WebSocket error:`, err);
  });
});

// Initialize and start server
async function startServer() {
  // Validate auth config
  if (!validateConfig()) {
    console.error('[Server] Auth configuration invalid');
    if (authConfig.app.authEnabled) {
      process.exit(1);
    }
  }

  // Initialize Redis and session store
  if (authConfig.app.authEnabled) {
    try {
      console.log('[Server] Connecting to Redis...');
      const redisClient = createClient({ url: authConfig.redis.url });
      redisClient.on('error', err => console.error('[Redis] Error:', err));
      await redisClient.connect();
      console.log('[Server] Redis connected');

      // Create session store
      sessionStore = new RedisStore({ client: redisClient });

      // Configure session middleware
      app.use(session({
        store: sessionStore,
        name: authConfig.session.name,
        secret: authConfig.session.secret,
        resave: false,
        saveUninitialized: false,
        cookie: {
          httpOnly: true,
          secure: authConfig.session.secure,
          sameSite: 'lax',
          maxAge: authConfig.session.maxAge,
          domain: authConfig.session.domain,
        },
      }));

      // Initialize OIDC client
      console.log('[Server] Initializing OIDC...');
      await initializeOIDC();
      console.log('[Server] OIDC initialized');

      // Mount auth routes
      app.use('/auth', authRoutes);
      console.log('[Server] Auth routes mounted at /auth');

      // Register API routes (after session middleware is set up)
      registerApiRoutes();
      console.log('[Server] API routes registered');
    } catch (error) {
      console.error('[Server] Failed to initialize auth:', error);
      process.exit(1);
    }
  } else {
    console.log('[Server] Authentication disabled');
    // Register API routes (no session middleware needed when auth is disabled)
    registerApiRoutes();
    console.log('[Server] API routes registered');
  }

  // Start listening
  server.listen(PORT, HOST, () => {
    console.log(`Claude Web UI Backend running on http://${HOST}:${PORT}`);
    console.log(`WebSocket available at ws://${HOST}:${PORT}`);
    console.log(`Authentication: ${authConfig.app.authEnabled ? 'ENABLED' : 'DISABLED'}`);
  });
}

// Start the server
startServer().catch(err => {
  console.error('[Server] Fatal error:', err);
  process.exit(1);
});
