/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {spawn} from 'node:child_process';
import fs from 'node:fs';
import net from 'node:net';

import type {CallToolResult} from '../third_party/index.js';
import {PipeTransport} from '../third_party/index.js';
import {getTempFilePath} from '../utils/files.js';
import {logger} from '../utils/logger.js';

import type {DaemonMessage, DaemonResponse} from './types.js';
import {
  DAEMON_SCRIPT_PATH,
  getSocketPath,
  getPidFilePath,
  isDaemonRunning,
} from './utils.js';

const FILE_TIMEOUT = 10_000;
const READY_CHECK_INTERVAL = 100;
const READY_CHECK_COMMAND_TIMEOUT = 1_000;

/**
 * Waits for a file to be created and populated (removed = false) or removed (removed = true).
 */
function waitForFile(filePath: string, removed = false) {
  return new Promise<void>((resolve, reject) => {
    const check = () => {
      const exists = fs.existsSync(filePath);
      if (removed) {
        return !exists;
      }
      if (!exists) {
        return false;
      }
      try {
        return fs.statSync(filePath).size > 0;
      } catch {
        return false;
      }
    };

    if (check()) {
      resolve();
      return;
    }

    const timer = setTimeout(() => {
      fs.unwatchFile(filePath);
      reject(
        new Error(
          `Timeout: file ${filePath} ${removed ? 'not removed' : 'not found'} within ${FILE_TIMEOUT}ms`,
        ),
      );
    }, FILE_TIMEOUT);

    fs.watchFile(filePath, {interval: 500}, () => {
      if (check()) {
        clearTimeout(timer);
        fs.unwatchFile(filePath);
        resolve();
      }
    });
  });
}

function delay(ms: number) {
  return new Promise<void>(resolve => {
    setTimeout(resolve, ms);
  });
}

async function waitForDaemonReady(sessionId: string) {
  const deadline = Date.now() + FILE_TIMEOUT;
  let lastError: unknown;

  while (Date.now() < deadline) {
    try {
      const response = await sendCommand(
        {method: 'status'},
        sessionId,
        READY_CHECK_COMMAND_TIMEOUT,
      );
      if (response.success) {
        return;
      }
      lastError = new Error(String(response.error));
    } catch (error) {
      lastError = error;
    }

    const timeLeft = deadline - Date.now();
    if (timeLeft > 0) {
      await delay(Math.min(READY_CHECK_INTERVAL, timeLeft));
    }
  }

  throw new Error(
    `Timeout: daemon not ready within ${FILE_TIMEOUT}ms`,
    lastError === undefined ? undefined : {cause: lastError},
  );
}

export async function startDaemon(mcpArgs: string[] = [], sessionId: string) {
  if (isDaemonRunning(sessionId)) {
    logger?.('Daemon is already running');
    await waitForDaemonReady(sessionId);
    return;
  }

  const pidFilePath = getPidFilePath(sessionId);

  if (fs.existsSync(pidFilePath)) {
    fs.unlinkSync(pidFilePath);
  }

  logger?.('Starting daemon...', ...mcpArgs);
  const child = spawn(process.execPath, [DAEMON_SCRIPT_PATH, ...mcpArgs], {
    detached: true,
    stdio: 'ignore',
    env: {...process.env, CHROME_DEVTOOLS_MCP_SESSION_ID: sessionId},
    cwd: process.cwd(),
    windowsHide: true,
  });
  child.unref();

  await waitForFile(pidFilePath);
  await waitForDaemonReady(sessionId);
}

const SEND_COMMAND_TIMEOUT = 60_000; // ms

/**
 * `sendCommand` opens a socket connection sends a single command and disconnects.
 */
export async function sendCommand(
  command: DaemonMessage,
  sessionId: string,
  timeout = SEND_COMMAND_TIMEOUT,
): Promise<DaemonResponse> {
  const socketPath = getSocketPath(sessionId);

  const socket = net.createConnection({
    path: socketPath,
  });

  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      socket.destroy();
      reject(new Error('Timeout waiting for daemon response'));
    }, timeout);

    const transport = new PipeTransport(socket, socket);
    transport.onmessage = async (message: string) => {
      clearTimeout(timer);
      logger?.('onmessage', message);
      resolve(JSON.parse(message));
    };
    socket.on('error', error => {
      clearTimeout(timer);
      logger?.('Socket error:', error);
      reject(error);
    });
    socket.on('close', () => {
      clearTimeout(timer);
      logger?.('Socket closed:');
      reject(new Error('Socket closed'));
    });
    logger?.('Sending message', command);
    transport.send(JSON.stringify(command));
  });
}

export async function stopDaemon(sessionId: string) {
  if (!isDaemonRunning(sessionId)) {
    logger?.('Daemon is not running');
    return;
  }

  const pidFilePath = getPidFilePath(sessionId);

  await sendCommand({method: 'stop'}, sessionId);

  await waitForFile(pidFilePath, /*removed=*/ true);
}

export async function handleResponse(
  response: CallToolResult,
  format: 'json' | 'md',
): Promise<string> {
  if (response.isError) {
    return JSON.stringify(response.content);
  }
  const chunks = [];
  const images: Array<{filePath: string; mimeType: string}> = [];
  for (const content of response.content) {
    if (content.type === 'text') {
      chunks.push(content.text);
    } else if (content.type === 'image') {
      const imageData = content.data;
      const mimeType = content.mimeType;
      let extension = '.png';
      switch (mimeType) {
        case 'image/jpg':
        case 'image/jpeg':
          extension = '.jpeg';
          break;
        case 'image/webp':
          extension = '.webp';
          break;
      }
      const data = Buffer.from(imageData, 'base64');
      const name = crypto.randomUUID();
      const filepath = await getTempFilePath(`${name}${extension}`);
      fs.writeFileSync(filepath, data);
      images.push({filePath: filepath, mimeType});
      chunks.push(`Saved to ${filepath}.`);
    } else {
      throw new Error('Not supported response content type');
    }
  }
  if (format === 'json') {
    if (response.structuredContent) {
      const structuredContent = {
        ...response.structuredContent,
        ...(images.length ? {images} : {}),
      };
      return JSON.stringify(structuredContent);
    }
    // Fall-through to text for backward compatibility.
  }
  return format === 'md' ? chunks.join(' ') : JSON.stringify(chunks);
}
