import readline from 'node:readline';
import { fingerprintApiKeys } from './api-key-fingerprint';
import { EventStream } from './event-stream';
import { logger } from './logger';
import { ProcessManager } from './process-manager';
import { SessionManager } from './session-manager';
import type {
  ApiKeys,
  PermissionReplyPayload,
  QuestionInfo,
  QuestionReplyPayload,
  ResumeSessionPayload,
  SidecarCommand,
  SidecarEvent,
  StartTaskPayload,
  Todo,
  UpdateMcpConfigPayload,
} from './types';

const SIDECAR_VERSION = '0.2.0';

// ============================================================================
// IPC Communication
// ============================================================================

function send(event: SidecarEvent): void {
  console.log(JSON.stringify(event));
}

function sendLog(level: 'debug' | 'info' | 'warn' | 'error', message: string): void {
  send({ type: 'log', payload: { level, message } });
}

// Wire logger to IPC so all logger.* calls also appear in the frontend debug panel
logger.setIpcEmitter(sendLog);

// ============================================================================
// State
// ============================================================================

let processManager: ProcessManager | null = null;
let eventStream: EventStream | null = null;
let sessionManager: SessionManager | null = null;
let currentDirectory: string | undefined;
let initializePromise: Promise<void> | null = null;
let appliedApiKeysFingerprint: string | undefined;

// ============================================================================
// Initialization
// ============================================================================

async function initialize(
  apiKeys?: ApiKeys,
  mcpServers?: Record<string, unknown>,
  modelId?: string,
  workingDirectory?: string
): Promise<void> {
  if (sessionManager) {
    // API keys are applied as env vars when the server process spawns, so a
    // key added or rotated after initialization needs a deliberate server
    // restart to take effect (technical review finding #14). Callers that
    // pass no keys (e.g. Copilot OAuth) express no opinion and skip the check.
    const keysChanged = apiKeys !== undefined && fingerprintApiKeys(apiKeys) !== appliedApiKeysFingerprint;

    if (keysChanged && sessionManager.activeSessionCount() === 0) {
      logger.info('API keys changed since initialization — restarting OpenCode server to apply them');
      initializePromise = (async () => {
        await teardown();
        await doInitialize(apiKeys, mcpServers, modelId, workingDirectory);
      })();
      try {
        await initializePromise;
      } finally {
        initializePromise = null;
      }
      return;
    }

    if (keysChanged) {
      logger.warn(
        'API keys changed but sessions are still active; restart deferred — the new keys will be applied when the next task starts while idle'
      );
    }

    // Fully initialized — but reconnect SSE if directory changed
    if (eventStream && workingDirectory && workingDirectory !== currentDirectory) {
      logger.info('Workspace directory changed, reconnecting SSE stream', { from: currentDirectory, to: workingDirectory });
      currentDirectory = workingDirectory;
      eventStream.reconnectWithDirectory(workingDirectory);
    }
    return;
  }

  // Another call is already initializing — wait for it
  if (initializePromise) {
    await initializePromise;
    return;
  }

  initializePromise = doInitialize(apiKeys, mcpServers, modelId, workingDirectory);
  try {
    await initializePromise;
  } finally {
    initializePromise = null;
  }
}

async function doInitialize(
  apiKeys?: ApiKeys,
  mcpServers?: Record<string, unknown>,
  modelId?: string,
  workingDirectory?: string
): Promise<void> {
  currentDirectory = workingDirectory;
  appliedApiKeysFingerprint = fingerprintApiKeys(apiKeys);

  // Start process manager — picks a random available port and generates a password
  processManager = new ProcessManager();
  await processManager.ensureServerRunning({ apiKeys, mcpServers, modelId });

  const port = processManager.getPort();
  const password = processManager.getPassword();
  logger.info(`OpenCode server bound to port ${port}`);

  // Start event stream with auth, scoped to the workspace directory
  eventStream = new EventStream({
    baseUrl: `http://127.0.0.1:${port}`,
    password,
    directory: workingDirectory,
  });

  // Initialize session manager with port and password (for dynamic system prompt with auth)
  sessionManager = new SessionManager(processManager.getClient(), eventStream, port, password);

  // Wire up session manager events to IPC
  sessionManager.on('started', (data: { taskId: string; sessionId: string }) => {
    send({
      type: 'task_started',
      taskId: data.taskId,
      payload: { taskId: data.taskId, sessionId: data.sessionId },
    });
  });

  sessionManager.on('progress', (data: { taskId: string; stage: string; message?: string }) => {
    send({
      type: 'task_progress',
      taskId: data.taskId,
      payload: {
        stage: data.stage as 'starting' | 'connecting' | 'configuring' | 'executing' | 'completing',
        message: data.message,
      },
    });
  });

  sessionManager.on(
    'message-partial',
    (data: { taskId: string; messageId: string; textSoFar: string; delta?: string; isStreaming: boolean }) => {
      send({
        type: 'task_message_partial',
        taskId: data.taskId,
        payload: {
          messageId: data.messageId,
          partId: 'text',
          textSoFar: data.textSoFar,
          delta: data.delta,
          isStreaming: data.isStreaming,
        },
      });
    }
  );

  sessionManager.on('message-complete', (data: { taskId: string; messageId: string; text: string }) => {
    send({
      type: 'task_message_complete',
      taskId: data.taskId,
      payload: { messageId: data.messageId, text: data.text },
    });
  });

  // Forward tool-use events as task_message so artifacts panel can track write_file calls.
  // The message is sent in OpenCode format (type: 'tool_use') which the frontend's
  // normalizeIncomingMessage handles via normalizeOpenCodeMessage.
  sessionManager.on('tool-use', (data: { taskId: string; messageId: string; part: Record<string, unknown> }) => {
    // Build OpenCode-format tool message for the frontend normalizer
    const toolMessage = {
      type: 'tool_use' as const,
      timestamp: ((data.part.time as Record<string, unknown> | undefined)?.start as number | undefined) ?? Date.now(),
      part: data.part,
    };
    send({
      type: 'task_message',
      taskId: data.taskId,
      payload: { message: toolMessage },
    } as unknown as SidecarEvent);
  });

  sessionManager.on(
    'permission-request',
    (data: {
      taskId: string;
      id: string;
      sessionId: string;
      permission: string;
      patterns: string[];
      metadata: Record<string, unknown>;
    }) => {
      send({
        type: 'permission_request',
        taskId: data.taskId,
        payload: {
          id: data.id,
          sessionId: data.sessionId,
          permission: data.permission,
          patterns: data.patterns,
          metadata: data.metadata,
        },
      });
    }
  );

  sessionManager.on('question-request', (data: { taskId: string; id: string; sessionId: string; questions: QuestionInfo[] }) => {
    send({
      type: 'question_request',
      taskId: data.taskId,
      payload: {
        id: data.id,
        sessionId: data.sessionId,
        questions: data.questions,
      },
    });
  });

  sessionManager.on('complete', (data: { taskId: string; sessionId: string; status: string }) => {
    send({
      type: 'task_complete',
      taskId: data.taskId,
      payload: {
        status: data.status as 'success' | 'error' | 'cancelled' | 'aborted',
        sessionId: data.sessionId,
      },
    });
  });

  sessionManager.on('error', (data: { taskId: string; error: string; sessionId?: string }) => {
    send({
      type: 'task_error',
      taskId: data.taskId,
      payload: { error: data.error, sessionId: data.sessionId },
    });
  });

  sessionManager.on('todo-updated', (data: { taskId: string; todos: Todo[] }) => {
    send({
      type: 'todo_updated',
      taskId: data.taskId,
      payload: { todos: data.todos },
    });
  });

  // Forward MCP tools changed SSE event to Tauri
  eventStream.on('mcp.tools.changed', (props: { server: string }) => {
    send({ type: 'mcp_tools_changed', payload: { server: props.server } });
  });

  // Handle event stream errors (connection drops, reconnects, etc.)
  eventStream.on('stream-error', (error: unknown) => {
    logger.warn('Event stream error (will reconnect)', error);
    sendLog('warn', 'SSE connection lost, reconnecting...');
  });

  // Connect event stream
  eventStream.connect();
}

// ============================================================================
// Command Handlers
// ============================================================================

async function handleStartTask(taskId: string, payload: StartTaskPayload): Promise<void> {
  try {
    await initialize(payload.apiKeys, payload.mcpServers, payload.modelId, payload.workingDirectory);

    if (!(sessionManager && processManager)) {
      throw new Error('Session manager not initialized');
    }

    // Update model config on disk for subsequent instance reloads.
    // This is a no-op when the server is freshly started (config already
    // written in startServer), but handles model changes between tasks
    // when the server is already running.
    processManager.updateModelConfig(payload.modelId);

    await sessionManager.startTask(payload);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    const stack = error instanceof Error ? error.stack : undefined;
    logger.error('Failed to start task', { taskId, error: message });
    send({
      type: 'task_error',
      taskId,
      payload: { error: message },
    });
  }
}

async function handleResumeSession(taskId: string, payload: ResumeSessionPayload): Promise<void> {
  try {
    await initialize(payload.apiKeys, payload.mcpServers, payload.modelId, payload.workingDirectory);

    if (!(sessionManager && processManager)) {
      throw new Error('Session manager not initialized');
    }

    // Update model config on disk (same rationale as handleStartTask)
    processManager.updateModelConfig(payload.modelId);

    await sessionManager.resumeSession(payload);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to resume session', { taskId, error: message });
    send({
      type: 'task_error',
      taskId,
      payload: { error: message },
    });
  }
}

async function handleCancelTask(_taskId: string): Promise<void> {
  sendLog('info', 'Cancel not supported in server mode, use abort_session instead');
}

async function handleUpdateMcpConfig(payload: UpdateMcpConfigPayload): Promise<void> {
  try {
    if (!processManager) {
      throw new Error('Process manager not initialized');
    }

    // Write to config files on disk so the next server start picks up MCP servers.
    // OpenCode does NOT dynamically initialize MCP servers from PATCH /config;
    // it only reads them at startup.
    processManager.updateMcpConfig(payload.mcpServers);

    // Also send PATCH /config to update the in-memory config (model, permissions, etc.)
    const client = processManager.getClient();
    await client.updateConfig({ mcp: payload.mcpServers }, payload.workingDirectory);
    logger.info('MCP config updated', { serverCount: Object.keys(payload.mcpServers).length });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to update MCP config', { error: message });
    send({
      type: 'error',
      payload: { message: `Failed to update MCP config: ${message}` },
    });
  }
}

async function handleAbortSession(taskId: string, sessionId: string): Promise<void> {
  try {
    if (!sessionManager) {
      throw new Error('Session manager not initialized');
    }

    await sessionManager.abortSession(taskId, sessionId);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to abort session', { taskId, sessionId, error: message });
    send({
      type: 'task_error',
      taskId,
      payload: { error: message },
    });
  }
}

async function handlePermissionReply(taskId: string, payload: PermissionReplyPayload): Promise<void> {
  try {
    if (!sessionManager) {
      throw new Error('Session manager not initialized');
    }

    await sessionManager.replyToPermission(taskId, payload.requestId, payload.reply, payload.message);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to reply to permission', { taskId, error: message });
  }
}

async function handleQuestionReply(taskId: string, payload: QuestionReplyPayload): Promise<void> {
  try {
    if (!sessionManager) {
      throw new Error('Session manager not initialized');
    }

    await sessionManager.replyToQuestion(taskId, payload.requestId, payload.answers);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to reply to question', { taskId, error: message });
    send({
      type: 'task_error',
      taskId,
      payload: { error: `Failed to reply to question: ${message}` },
    });
  }
}

async function handleCopilotOAuthAuthorize(enterpriseUrl?: string): Promise<void> {
  try {
    await initialize();

    if (!processManager) {
      throw new Error('Process manager not initialized');
    }

    const client = processManager.getClient();
    const providerID = enterpriseUrl ? 'github-copilot-enterprise' : 'github-copilot';

    if (enterpriseUrl) {
      await client.updateConfig({
        provider: {
          'github-copilot-enterprise': { url: enterpriseUrl },
        },
      });
    }

    const result = await client.oauthAuthorize(providerID, 0);

    send({
      type: 'copilot_oauth_result',
      payload: {
        url: result.url,
        method: (result.method || 'code') as 'auto' | 'code',
        instructions: result.instructions,
      },
    } as SidecarEvent);

    // The authorize call stores a pending auth in the server's memory.
    // Calling callback with method:"auto" blocks until the user completes
    // the device flow on GitHub, then exchanges the code for a token and
    // persists it via Auth.set(). This is the correct completion signal.
    const callbackAsync = async () => {
      try {
        await client.oauthCallback(providerID, 0);

        send({
          type: 'copilot_oauth_complete',
          payload: { connected: true },
        } as SidecarEvent);
      } catch (e) {
        const errMsg = e instanceof Error ? e.message : String(e);
        logger.error('Copilot OAuth callback failed', { error: errMsg });

        send({
          type: 'copilot_oauth_complete',
          payload: { connected: false, error: errMsg },
        } as SidecarEvent);
      }
    };

    callbackAsync();
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Copilot OAuth authorize failed', { error: message });
    send({
      type: 'copilot_oauth_complete',
      payload: { connected: false, error: message },
    } as SidecarEvent);
  }
}

async function handleCopilotGetModels(): Promise<void> {
  try {
    if (!processManager) {
      throw new Error('Process manager not initialized');
    }

    const client = processManager.getClient();
    const providers = await client.listProviders();

    const copilotProvider = providers.all?.find((p: { id: string }) => p.id === 'github-copilot' || p.id === 'github-copilot-enterprise');

    if (!(copilotProvider && copilotProvider.models)) {
      send({
        type: 'copilot_models_result',
        payload: { success: false, error: 'No Copilot provider found or no models available' },
      } as SidecarEvent);
      return;
    }

    const prefix = copilotProvider.id;
    const models = Object.entries(copilotProvider.models).map(([modelId, meta]) => ({
      id: `${prefix}/${modelId}`,
      name: (meta as { name?: string }).name || modelId,
    }));

    send({
      type: 'copilot_models_result',
      payload: { success: true, models },
    } as SidecarEvent);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to get Copilot models', { error: message });
    send({
      type: 'copilot_models_result',
      payload: { success: false, error: message },
    } as SidecarEvent);
  }
}

async function handleCopilotDisconnect(): Promise<void> {
  try {
    if (!processManager) {
      throw new Error('Process manager not initialized');
    }

    const client = processManager.getClient();
    try {
      await client.deleteAuth('github-copilot');
    } catch {
      // May not exist
    }
    try {
      await client.deleteAuth('github-copilot-enterprise');
    } catch {
      // May not exist
    }

    send({
      type: 'copilot_oauth_complete',
      payload: { connected: false },
    } as SidecarEvent);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to disconnect Copilot', { error: message });
    send({
      type: 'error',
      payload: { message: `Failed to disconnect Copilot: ${message}` },
    });
  }
}

async function handleGetMcpStatus(): Promise<void> {
  try {
    if (!processManager) {
      send({ type: 'mcp_status', payload: { servers: {} } });
      return;
    }

    const client = processManager.getClient();
    const servers = await client.getMcpStatus(currentDirectory);
    send({ type: 'mcp_status', payload: { servers } });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to get MCP status', { error: message });
    send({ type: 'mcp_status', payload: { servers: {} } });
  }
}

async function handleGetMcpTools(): Promise<void> {
  try {
    if (!processManager) {
      send({ type: 'mcp_tools', payload: { toolIds: [] } });
      return;
    }

    const client = processManager.getClient();
    const toolIds = await client.getToolIds(currentDirectory);
    send({ type: 'mcp_tools', payload: { toolIds } });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to get MCP tools', { error: message });
    send({ type: 'mcp_tools', payload: { toolIds: [] } });
  }
}

async function handleConnectMcpServer(name: string): Promise<void> {
  try {
    if (!processManager) {
      throw new Error('Process manager not initialized');
    }

    const client = processManager.getClient();
    await client.connectMcpServer(name, currentDirectory);
    // Refresh status after connect
    const servers = await client.getMcpStatus(currentDirectory);
    send({ type: 'mcp_status', payload: { servers } });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to connect MCP server', { name, error: message });
    send({ type: 'error', payload: { message: `Failed to connect MCP server "${name}": ${message}` } });
  }
}

async function handleDisconnectMcpServer(name: string): Promise<void> {
  try {
    if (!processManager) {
      throw new Error('Process manager not initialized');
    }

    const client = processManager.getClient();
    await client.disconnectMcpServer(name, currentDirectory);
    // Refresh status after disconnect
    const servers = await client.getMcpStatus(currentDirectory);
    send({ type: 'mcp_status', payload: { servers } });
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error('Failed to disconnect MCP server', { name, error: message });
    send({ type: 'error', payload: { message: `Failed to disconnect MCP server "${name}": ${message}` } });
  }
}

async function handleCheckServer(): Promise<void> {
  try {
    if (!processManager) {
      send({
        type: 'server_status',
        payload: { running: false },
      });
      return;
    }

    const client = processManager.getClient();
    const health = await client.health();
    send({
      type: 'server_status',
      payload: {
        running: true,
        port: processManager.getPort(),
        version: health.version,
      },
    });
  } catch {
    send({
      type: 'server_status',
      payload: { running: false },
    });
  }
}

// ============================================================================
// Message Router
// ============================================================================

async function handleMessage(msg: SidecarCommand): Promise<void> {
  logger.debug('Received command', msg);

  switch (msg.type) {
    case 'start_task':
      await handleStartTask(msg.taskId, msg.payload);
      break;

    case 'resume_session':
      await handleResumeSession(msg.taskId, msg.payload);
      break;

    case 'cancel_task':
      await handleCancelTask(msg.taskId);
      break;

    case 'abort_session':
      await handleAbortSession(msg.taskId, msg.sessionId);
      break;

    case 'send_permission_reply':
      await handlePermissionReply(msg.taskId, msg.payload);
      break;

    case 'send_question_reply':
      await handleQuestionReply(msg.taskId, msg.payload);
      break;

    case 'ping':
      send({ type: 'pong', payload: { timestamp: Date.now() } });
      break;

    case 'check_server':
      await handleCheckServer();
      break;

    case 'update_mcp_config':
      await handleUpdateMcpConfig(msg.payload);
      break;

    case 'get_mcp_status':
      await handleGetMcpStatus();
      break;

    case 'get_mcp_tools':
      await handleGetMcpTools();
      break;

    case 'connect_mcp_server':
      await handleConnectMcpServer(msg.payload.name);
      break;

    case 'disconnect_mcp_server':
      await handleDisconnectMcpServer(msg.payload.name);
      break;

    case 'copilot_oauth_authorize':
      await handleCopilotOAuthAuthorize(msg.enterpriseUrl);
      break;

    case 'copilot_get_models':
      await handleCopilotGetModels();
      break;

    case 'copilot_disconnect':
      await handleCopilotDisconnect();
      break;

    case 'get_session_todos': {
      try {
        if (!processManager) throw new Error('Not initialized');
        const client = processManager.getClient();
        const todos = await client.getSessionTodos(msg.sessionId);
        send({ type: 'todo_updated', taskId: msg.taskId, payload: { todos } });
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        logger.error('Failed to get session todos', { error: message });
      }
      break;
    }

    case 'shutdown':
      // Graceful shutdown: stop OpenCode server, then exit
      logger.info('Received shutdown command, cleaning up...');
      try {
        await cleanup();
      } catch {
        // Best-effort cleanup
      }
      process.exit(0);
      break;

    default:
      logger.warn('Unknown command type', msg);
  }
}

// ============================================================================
// Cleanup
// ============================================================================

/** Tear down the session manager, event stream, and OpenCode server process. */
async function teardown(): Promise<void> {
  if (sessionManager) {
    sessionManager.dispose();
    sessionManager = null;
  }

  if (eventStream) {
    eventStream.disconnect();
    eventStream = null;
  }

  if (processManager) {
    await processManager.stopServer();
    processManager = null;
  }
}

async function cleanup(): Promise<void> {
  logger.info('Cleaning up...');
  await teardown();
  logger.close();
}

// ============================================================================
// Main
// ============================================================================

async function main(): Promise<void> {
  logger.startSession();
  logger.info(`Sidecar-OpenCode v${SIDECAR_VERSION} starting...`);

  // Set up stdin reader
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    terminal: false,
  });

  rl.on('line', async (line: string) => {
    try {
      const msg = JSON.parse(line) as SidecarCommand;
      await handleMessage(msg);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      logger.error('Failed to parse command', { line, error: message });
      send({
        type: 'error',
        payload: { message: `Failed to parse command: ${message}` },
      });
    }
  });

  rl.on('close', () => {
    logger.info('stdin closed, cleaning up...');
    cleanup().then(() => process.exit(0));
  });

  // Handle signals
  process.on('SIGINT', async () => {
    await cleanup();
    process.exit(0);
  });

  process.on('SIGTERM', async () => {
    await cleanup();
    process.exit(0);
  });

  // Send ready event
  send({
    type: 'ready',
    payload: {
      version: SIDECAR_VERSION,
      serverAvailable: false, // Will be true after first task starts
    },
  });
}

main().catch((error) => {
  logger.error('Fatal error', error);
  process.exit(1);
});
