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

import { debugLogger } from '@google/gemini-cli-core';
import clipboardy from 'clipboardy';
import type { SlashCommand } from '../commands/types.js';
import fs from 'node:fs';
import type { Writable } from 'node:stream';
import type { Settings } from '../../config/settingsSchema.js';
import { AT_COMMAND_PATH_REGEX_SOURCE } from '../hooks/atCommandProcessor.js';

// Pre-compiled regex for detecting @<path> patterns consistent with parseAllAtCommands.
// Uses the same AT_COMMAND_PATH_REGEX_SOURCE so that isAtCommand is true whenever
// parseAllAtCommands would find at least one atPath part.
const AT_COMMAND_DETECT_REGEX = new RegExp(
  `(?<!\\\\)@${AT_COMMAND_PATH_REGEX_SOURCE}`,
);

/**
 * Checks if a query string potentially represents an '@' command.
 * Returns true if the query contains any '@<path>' pattern that would be
 * recognised by the @ command processor, regardless of what character
 * precedes the '@' sign. This ensures that prompts written in an external
 * editor (where '@' may follow punctuation like ':' or '(') are correctly
 * identified and their referenced files pre-loaded before the query is sent
 * to the model.
 *
 * @param query The input query string.
 * @returns True if the query looks like an '@' command, false otherwise.
 */
export const isAtCommand = (query: string): boolean =>
  AT_COMMAND_DETECT_REGEX.test(query);

/**
 * Checks if a query string potentially represents an '/' command.
 * It triggers if the query starts with '/' but excludes code comments like '//' and '/*'.
 *
 * @param query The input query string.
 * @returns True if the query looks like an '/' command, false otherwise.
 */
export const isSlashCommand = (query: string): boolean => {
  if (!query.startsWith('/')) {
    return false;
  }

  // Exclude line comments that start with '//'
  if (query.startsWith('//')) {
    return false;
  }

  // Exclude block comments that start with '/*'
  if (query.startsWith('/*')) {
    return false;
  }

  return true;
};

const ESC = '\u001B';
const BEL = '\u0007';
const ST = '\u001B\\';

const MAX_OSC52_SEQUENCE_BYTES = 100_000;
const OSC52_HEADER = `${ESC}]52;c;`;
const OSC52_FOOTER = BEL;
const MAX_OSC52_BODY_B64_BYTES =
  MAX_OSC52_SEQUENCE_BYTES -
  Buffer.byteLength(OSC52_HEADER) -
  Buffer.byteLength(OSC52_FOOTER);
const MAX_OSC52_DATA_BYTES = Math.floor(MAX_OSC52_BODY_B64_BYTES / 4) * 3;

// Conservative chunk size for GNU screen DCS passthrough.
const SCREEN_DCS_CHUNK_SIZE = 240;

type TtyTarget = { stream: Writable; closeAfter: boolean } | null;

const pickTty = (): Promise<TtyTarget> =>
  new Promise((resolve) => {
    // /dev/tty is only available on Unix-like systems (Linux, macOS, BSD, etc.)
    if (process.platform !== 'win32') {
      // Prefer the controlling TTY to avoid interleaving escape sequences with piped stdout.
      try {
        const devTty = fs.createWriteStream('/dev/tty');

        // Safety timeout: if /dev/tty doesn't respond quickly, fallback to avoid hanging.
        const timeout = setTimeout(() => {
          // Remove listeners to prevent them from firing after timeout.
          devTty.removeAllListeners('open');
          devTty.removeAllListeners('error');
          devTty.destroy();
          resolve(getStdioTty());
        }, 100);

        // If we can't open it (e.g. sandbox), we'll get an error.
        // We wait for 'open' to confirm it's usable, or 'error' to fallback.
        // If it opens, we resolve with the stream.
        devTty.once('open', () => {
          clearTimeout(timeout);
          devTty.removeAllListeners('error');
          // Prevent future unhandled 'error' events from crashing the process
          devTty.on('error', () => {});
          resolve({ stream: devTty, closeAfter: true });
        });

        // If it errors immediately (or quickly), we fallback.
        devTty.once('error', () => {
          clearTimeout(timeout);
          devTty.removeAllListeners('open');
          resolve(getStdioTty());
        });
        return;
      } catch {
        // fall through - synchronous failure
      }
    }

    resolve(getStdioTty());
  });

const getStdioTty = (): TtyTarget => {
  // On Windows, prioritize stdout to prevent shell-specific formatting (e.g., PowerShell's
  // red stderr) from corrupting the raw escape sequence payload.
  if (process.platform === 'win32') {
    if (process.stdout?.isTTY)
      return { stream: process.stdout, closeAfter: false };
    if (process.stderr?.isTTY)
      return { stream: process.stderr, closeAfter: false };
    return null;
  }

  // On non-Windows platforms, prioritize stderr to avoid polluting stdout,
  // preserving it for potential redirection or piping.
  if (process.stderr?.isTTY)
    return { stream: process.stderr, closeAfter: false };
  if (process.stdout?.isTTY)
    return { stream: process.stdout, closeAfter: false };
  return null;
};

const inTmux = (): boolean =>
  Boolean(
    process.env['TMUX'] || (process.env['TERM'] ?? '').startsWith('tmux'),
  );

const inScreen = (): boolean =>
  Boolean(
    process.env['STY'] || (process.env['TERM'] ?? '').startsWith('screen'),
  );

const isSSH = (): boolean =>
  Boolean(
    process.env['SSH_TTY'] ||
      process.env['SSH_CONNECTION'] ||
      process.env['SSH_CLIENT'],
  );

const isWSL = (): boolean =>
  Boolean(
    process.env['WSL_DISTRO_NAME'] ||
      process.env['WSLENV'] ||
      process.env['WSL_INTEROP'],
  );

const isWindowsTerminal = (): boolean =>
  process.platform === 'win32' && Boolean(process.env['WT_SESSION']);

const isDumbTerm = (): boolean => (process.env['TERM'] ?? '') === 'dumb';

const shouldUseOsc52 = (tty: TtyTarget, settings?: Settings): boolean =>
  Boolean(tty) &&
  !isDumbTerm() &&
  (settings?.experimental?.useOSC52Copy ||
    isSSH() ||
    isWSL() ||
    isWindowsTerminal());

const safeUtf8Truncate = (buf: Buffer, maxBytes: number): Buffer => {
  if (buf.length <= maxBytes) return buf;
  let end = maxBytes;
  // Back up to the start of a UTF-8 code point if we cut through a continuation byte (10xxxxxx).
  while (end > 0 && (buf[end - 1] & 0b1100_0000) === 0b1000_0000) end--;
  return buf.subarray(0, end);
};

const buildOsc52 = (text: string): string => {
  const raw = Buffer.from(text, 'utf8');
  const safe = safeUtf8Truncate(raw, MAX_OSC52_DATA_BYTES);
  const b64 = safe.toString('base64');
  return `${OSC52_HEADER}${b64}${OSC52_FOOTER}`;
};

const wrapForTmux = (seq: string): string => {
  // Double ESC bytes in payload without a control-character regex.
  const doubledEsc = seq.split(ESC).join(ESC + ESC);
  return `${ESC}Ptmux;${doubledEsc}${ST}`;
};

const wrapForScreen = (seq: string): string => {
  let out = '';
  for (let i = 0; i < seq.length; i += SCREEN_DCS_CHUNK_SIZE) {
    out += `${ESC}P${seq.slice(i, i + SCREEN_DCS_CHUNK_SIZE)}${ST}`;
  }
  return out;
};

const writeAll = (stream: Writable, data: string): Promise<void> =>
  new Promise<void>((resolve, reject) => {
    // On Windows, writing directly to the underlying file descriptor bypasses
    // application-level stream interception (e.g., by the Ink UI framework).
    // This ensures the raw OSC-52 escape sequence reaches the terminal host uncorrupted.
    // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
    const fd = (stream as unknown as { fd?: number }).fd;
    if (
      process.platform === 'win32' &&
      typeof fd === 'number' &&
      (stream === process.stdout || stream === process.stderr)
    ) {
      try {
        fs.writeSync(fd, data);
        resolve();
        return;
      } catch (e) {
        debugLogger.warn(
          'Direct write to TTY failed, falling back to stream write',
          e,
        );
      }
    }

    const onError = (err: unknown) => {
      cleanup();
      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      reject(err as Error);
    };
    const onDrain = () => {
      cleanup();
      resolve();
    };
    const cleanup = () => {
      stream.off('error', onError);
      stream.off('drain', onDrain);
      // Writable.write() handlers may not emit 'drain' if the first write succeeded.
    };
    stream.once('error', onError);
    if (stream.write(data)) {
      cleanup();
      resolve();
    } else {
      stream.once('drain', onDrain);
    }
  });

// Copies a string snippet to the clipboard with robust OSC-52 support.
export const copyToClipboard = async (
  text: string,
  settings?: Settings,
): Promise<void> => {
  if (!text) return;

  const tty = await pickTty();

  if (shouldUseOsc52(tty, settings)) {
    const osc = buildOsc52(text);
    const payload = inTmux()
      ? wrapForTmux(osc)
      : inScreen()
        ? wrapForScreen(osc)
        : osc;

    await writeAll(tty!.stream, payload);

    if (tty!.closeAfter) {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      (tty!.stream as fs.WriteStream).end();
    }
    return;
  }

  // Local / non-TTY fallback
  await clipboardy.write(text);
};

export const getUrlOpenCommand = (): string => {
  // --- Determine the OS-specific command to open URLs ---
  let openCmd: string;
  switch (process.platform) {
    case 'darwin':
      openCmd = 'open';
      break;
    case 'win32':
      openCmd = 'start';
      break;
    case 'linux':
      openCmd = 'xdg-open';
      break;
    default:
      // Default to xdg-open, which appears to be supported for the less popular operating systems.
      openCmd = 'xdg-open';
      debugLogger.warn(
        `Unknown platform: ${process.platform}. Attempting to open URLs with: ${openCmd}.`,
      );
      break;
  }
  return openCmd;
};

/**
 * Determines if a slash command should auto-execute when selected.
 *
 * All built-in commands have autoExecute explicitly set to true or false.
 * Custom commands (.toml files) and extension commands without this flag
 * will default to false (safe default - won't auto-execute).
 *
 * @param command The slash command to check
 * @returns true if the command should auto-execute on Enter
 */
export function isAutoExecutableCommand(
  command: SlashCommand | undefined | null,
): boolean {
  if (!command) {
    return false;
  }

  // Simply return the autoExecute flag value, defaulting to false if undefined
  return command.autoExecute ?? false;
}
