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

import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
import { debugLogger } from '../utils/debugLogger.js';
import { isSubpath, resolveToRealPath } from '../utils/paths.js';
import { isNodeError } from '../utils/errors.js';
import { type IdeInfo } from './detect-ide.js';

const logger = {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  debug: (...args: any[]) =>
    debugLogger.debug('[DEBUG] [IDEConnectionUtils]', ...args),
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  error: (...args: any[]) =>
    debugLogger.error('[ERROR] [IDEConnectionUtils]', ...args),
};

export type StdioConfig = {
  command: string;
  args: string[];
};

export type ConnectionConfig = {
  port?: string;
  authToken?: string;
  stdio?: StdioConfig;
};

export function validateWorkspacePath(
  ideWorkspacePath: string | undefined,
  cwd: string,
): { isValid: boolean; error?: string } {
  if (ideWorkspacePath === undefined) {
    return {
      isValid: false,
      error: `Failed to connect to IDE companion extension. Please ensure the extension is running. To install the extension, run /ide install.`,
    };
  }

  if (ideWorkspacePath === '') {
    return {
      isValid: false,
      error: `To use this feature, please open a workspace folder in your IDE and try again.`,
    };
  }

  const ideWorkspacePaths = ideWorkspacePath
    .split(path.delimiter)
    .map((p) => resolveToRealPath(p))
    .filter((e) => !!e);
  const realCwd = resolveToRealPath(cwd);
  const isWithinWorkspace = ideWorkspacePaths.some((workspacePath) =>
    isSubpath(workspacePath, realCwd),
  );

  if (!isWithinWorkspace) {
    return {
      isValid: false,
      error: `Directory mismatch. Gemini CLI is running in a different location than the open workspace in the IDE. Please run the CLI from one of the following directories: ${ideWorkspacePaths.join(
        ', ',
      )}`,
    };
  }
  return { isValid: true };
}

export function getPortFromEnv(): string | undefined {
  const port = process.env['GEMINI_CLI_IDE_SERVER_PORT'];
  if (!port) {
    return undefined;
  }
  return port;
}

export function getStdioConfigFromEnv(): StdioConfig | undefined {
  const command = process.env['GEMINI_CLI_IDE_SERVER_STDIO_COMMAND'];
  if (!command) {
    return undefined;
  }

  const argsStr = process.env['GEMINI_CLI_IDE_SERVER_STDIO_ARGS'];
  let args: string[] = [];
  if (argsStr) {
    try {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const parsedArgs = JSON.parse(argsStr);
      if (Array.isArray(parsedArgs)) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        args = parsedArgs;
      } else {
        logger.error(
          'GEMINI_CLI_IDE_SERVER_STDIO_ARGS must be a JSON array string.',
        );
      }
    } catch (e) {
      logger.error('Failed to parse GEMINI_CLI_IDE_SERVER_STDIO_ARGS:', e);
    }
  }

  return { command, args };
}

const IDE_SERVER_FILE_REGEX = /^gemini-ide-server-(\d+)-\d+\.json$/;

export async function getConnectionConfigFromFile(
  pid: number,
): Promise<
  (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo }) | undefined
> {
  // For backwards compatibility
  try {
    const portFile = path.join(
      os.tmpdir(),
      'gemini',
      'ide',
      `gemini-ide-server-${pid}.json`,
    );
    const portFileContents = await fs.promises.readFile(portFile, 'utf8');
    const parsed: unknown = JSON.parse(portFileContents);
    type ConfigType = ConnectionConfig & {
      workspacePath?: string;
      ideInfo?: IdeInfo;
    };
    const isConfig = (val: unknown): val is ConfigType =>
      typeof val === 'object' && val !== null;
    if (isConfig(parsed)) {
      return parsed;
    }
    throw new Error('Invalid connection config format');
  } catch {
    // For newer extension versions, the file name matches the pattern
    // /^gemini-ide-server-${pid}-\d+\.json$/. If multiple IDE
    // windows are open, multiple files matching the pattern are expected to
    // exist.
  }

  const portFileDir = path.join(os.tmpdir(), 'gemini', 'ide');
  let portFiles;
  try {
    portFiles = await fs.promises.readdir(portFileDir);
  } catch (e) {
    logger.debug('Failed to read IDE connection directory:', e);
    return undefined;
  }

  if (!portFiles) {
    return undefined;
  }

  const matchingFiles = portFiles.filter((file) =>
    IDE_SERVER_FILE_REGEX.test(file),
  );

  if (matchingFiles.length === 0) {
    return undefined;
  }

  sortConnectionFiles(matchingFiles, pid);

  let fileContents: string[];
  try {
    fileContents = await Promise.all(
      matchingFiles.map((file) =>
        fs.promises.readFile(path.join(portFileDir, file), 'utf8'),
      ),
    );
  } catch (e) {
    logger.debug('Failed to read IDE connection config file(s):', e);
    return undefined;
  }
  const parsedContents = fileContents.map(
    (
      content,
    ):
      | (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
      | undefined => {
      try {
        const parsed: unknown = JSON.parse(content);
        type ConfigType = ConnectionConfig & {
          workspacePath?: string;
          ideInfo?: IdeInfo;
        };
        const isConfig = (val: unknown): val is ConfigType =>
          typeof val === 'object' && val !== null;
        if (isConfig(parsed)) {
          return parsed;
        }
        return undefined;
      } catch (e) {
        logger.debug('Failed to parse JSON from config file: ', e);
        return undefined;
      }
    },
  );

  const validWorkspaces = parsedContents.filter(
    (
      content,
    ): content is ConnectionConfig & {
      workspacePath?: string;
      ideInfo?: IdeInfo;
    } => {
      if (!content) {
        return false;
      }
      const { isValid } = validateWorkspacePath(
        content.workspacePath,
        process.cwd(),
      );
      return isValid;
    },
  );

  if (validWorkspaces.length === 0) {
    return undefined;
  }

  if (validWorkspaces.length === 1) {
    const selected = validWorkspaces[0];
    const fileIndex = parsedContents.indexOf(selected);
    if (fileIndex !== -1) {
      logger.debug(`Selected IDE connection file: ${matchingFiles[fileIndex]}`);
    }
    return selected;
  }

  const portFromEnv = getPortFromEnv();
  if (portFromEnv) {
    const matchingPortIndex = validWorkspaces.findIndex(
      (content) => String(content.port) === portFromEnv,
    );
    if (matchingPortIndex !== -1) {
      const selected = validWorkspaces[matchingPortIndex];
      const fileIndex = parsedContents.indexOf(selected);
      if (fileIndex !== -1) {
        logger.debug(
          `Selected IDE connection file (matched port from env): ${matchingFiles[fileIndex]}`,
        );
      }
      return selected;
    }
  }

  const selected = validWorkspaces[0];
  const fileIndex = parsedContents.indexOf(selected);
  if (fileIndex !== -1) {
    logger.debug(
      `Selected first valid IDE connection file: ${matchingFiles[fileIndex]}`,
    );
  }
  return selected;
}

// Sort files to prioritize the one matching the target pid,
// then by whether the process is still alive, then by newest (largest PID).
function sortConnectionFiles(files: string[], targetPid: number) {
  files.sort((a, b) => {
    const aMatch = a.match(IDE_SERVER_FILE_REGEX);
    const bMatch = b.match(IDE_SERVER_FILE_REGEX);
    const aPid = aMatch ? parseInt(aMatch[1], 10) : 0;
    const bPid = bMatch ? parseInt(bMatch[1], 10) : 0;

    if (aPid === targetPid && bPid !== targetPid) {
      return -1;
    }
    if (bPid === targetPid && aPid !== targetPid) {
      return 1;
    }

    const aIsAlive = isPidAlive(aPid);
    const bIsAlive = isPidAlive(bPid);

    if (aIsAlive && !bIsAlive) {
      return -1;
    }
    if (bIsAlive && !aIsAlive) {
      return 1;
    }

    // Newest PIDs first as a heuristic
    return bPid - aPid;
  });
}

function isPidAlive(pid: number): boolean {
  if (pid <= 0) {
    return false;
  }
  // Assume the process is alive since checking would introduce significant overhead.
  if (os.platform() === 'win32') {
    return true;
  }
  try {
    process.kill(pid, 0);
    return true;
  } catch (e) {
    return isNodeError(e) && e.code === 'EPERM';
  }
}

export async function createProxyAwareFetch(ideServerHost: string) {
  // ignore proxy for the IDE server host to allow connecting to the ide mcp server
  const existingNoProxy = process.env['NO_PROXY'] || '';
  const agent = new EnvHttpProxyAgent({
    noProxy: [existingNoProxy, ideServerHost].filter(Boolean).join(','),
  });
  return async (url: string | URL, init?: RequestInit): Promise<Response> => {
    const fetchOptions: RequestInit & { dispatcher?: unknown } = {
      ...init,
      dispatcher: agent,
    };
    // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
    const options = fetchOptions as unknown as import('undici').RequestInit;
    try {
      const response = await undiciFetch(url, options);
      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      return new Response(response.body as ReadableStream<unknown> | null, {
        status: response.status,
        statusText: response.statusText,
        headers: [...response.headers.entries()],
      });
    } catch (error) {
      const urlString = typeof url === 'string' ? url : url.href;
      logger.error(`IDE fetch failed for ${urlString}`, error);
      throw error;
    }
  };
}

export function getIdeServerHost() {
  let host: string;
  host = '127.0.0.1';
  if (isInContainer()) {
    // when ssh-connection (e.g. remote-ssh) or devcontainer setup:
    // --> host must be '127.0.0.1' to have cli companion working
    if (!isSshConnected() && !isDevContainer()) {
      host = 'host.docker.internal';
    }
  }
  logger.debug(`[getIdeServerHost] Mapping IdeServerHost to '${host}'`);
  return host;
}

function isInContainer() {
  return fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
}

function isSshConnected() {
  return !!process.env['SSH_CONNECTION'];
}

function isDevContainer() {
  return !!(
    process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] ||
    process.env['REMOTE_CONTAINERS']
  );
}
