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

import * as fs from 'node:fs';
import {
  debugLogger,
  enableKittyKeyboardProtocol,
  disableKittyKeyboardProtocol,
  enableModifyOtherKeys,
  disableModifyOtherKeys,
  enableBracketedPasteMode,
  disableBracketedPasteMode,
  disableMouseEvents,
} from '@google/gemini-cli-core';
import { parseColor } from '../themes/color-utils.js';

export type TerminalBackgroundColor = string | undefined;

const TERMINAL_CLEANUP_SEQUENCE =
  '\x1b[<u\x1b[>4;0m\x1b[?2004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l';

export function cleanupTerminalOnExit() {
  try {
    if (process.stdout?.fd !== undefined) {
      fs.writeSync(process.stdout.fd, TERMINAL_CLEANUP_SEQUENCE);
      return;
    }
  } catch (e) {
    debugLogger.warn('Failed to synchronously cleanup terminal modes:', e);
  }

  disableKittyKeyboardProtocol();
  disableModifyOtherKeys();
  disableBracketedPasteMode();
  disableMouseEvents();
}

export class TerminalCapabilityManager {
  private static instance: TerminalCapabilityManager | undefined;

  private static readonly KITTY_QUERY = '\x1b[?u';
  private static readonly OSC_11_QUERY = '\x1b]11;?\x1b\\';
  private static readonly TERMINAL_NAME_QUERY = '\x1b[>q';
  private static readonly DEVICE_ATTRIBUTES_QUERY = '\x1b[c';
  private static readonly MODIFY_OTHER_KEYS_QUERY = '\x1b[>4;?m';
  private static readonly HIDDEN_MODE = '\x1b[8m';
  private static readonly CLEAR_LINE_AND_RETURN = '\x1b[2K\r';
  private static readonly RESET_ATTRIBUTES = '\x1b[0m';

  /**
   * Triggers a terminal background color query.
   * @param stdout The stdout stream to write to.
   */
  static queryBackgroundColor(stdout: {
    write: (data: string) => void | boolean;
  }): void {
    stdout.write(TerminalCapabilityManager.OSC_11_QUERY);
  }

  // Kitty keyboard flags: CSI ? flags u
  // eslint-disable-next-line no-control-regex
  private static readonly KITTY_REGEX = /\x1b\[\?(\d+)u/;
  // Terminal Name/Version response: DCS > | text ST (or BEL)
  // eslint-disable-next-line no-control-regex
  private static readonly TERMINAL_NAME_REGEX = /\x1bP>\|(.+?)(\x1b\\|\x07)/;
  // Primary Device Attributes: CSI ? ID ; ... c
  // eslint-disable-next-line no-control-regex
  private static readonly DEVICE_ATTRIBUTES_REGEX = /\x1b\[\?(\d+)(;\d+)*c/;
  // OSC 11 response: OSC 11 ; rgb:rrrr/gggg/bbbb ST (or BEL)
  static readonly OSC_11_REGEX =
    // eslint-disable-next-line no-control-regex
    /\x1b\]11;rgb:([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})\/([0-9a-fA-F]{1,4})(\x1b\\|\x07)/;
  // modifyOtherKeys response: CSI > 4 ; level m
  // eslint-disable-next-line no-control-regex
  private static readonly MODIFY_OTHER_KEYS_REGEX = /\x1b\[>4;(\d+)m/;

  private detectionComplete = false;
  private terminalBackgroundColor: TerminalBackgroundColor;
  private kittySupported = false;
  private kittyEnabled = false;
  private modifyOtherKeysSupported = false;
  private terminalName: string | undefined;

  private constructor() {}

  static getInstance(): TerminalCapabilityManager {
    if (!this.instance) {
      this.instance = new TerminalCapabilityManager();
    }
    return this.instance;
  }

  static resetInstanceForTesting(): void {
    this.instance = undefined;
  }

  /**
   * Detects terminal capabilities (Kitty protocol support, terminal name,
   * background color).
   * This should be called once at app startup.
   */
  async detectCapabilities(): Promise<void> {
    if (this.detectionComplete) return;

    if (!process.stdin.isTTY || !process.stdout.isTTY) {
      this.detectionComplete = true;
      return;
    }

    process.off('exit', cleanupTerminalOnExit);
    process.off('SIGTERM', cleanupTerminalOnExit);
    process.off('SIGINT', cleanupTerminalOnExit);
    process.on('exit', cleanupTerminalOnExit);
    process.on('SIGTERM', cleanupTerminalOnExit);
    process.on('SIGINT', cleanupTerminalOnExit);

    return new Promise((resolve) => {
      const originalRawMode = process.stdin.isRaw;
      if (!originalRawMode) {
        process.stdin.setRawMode(true);
      }

      let buffer = '';
      let kittyKeyboardReceived = false;
      let terminalNameReceived = false;
      let deviceAttributesReceived = false;
      let bgReceived = false;
      let modifyOtherKeysReceived = false;
      // eslint-disable-next-line prefer-const
      let timeoutId: NodeJS.Timeout;

      const cleanup = () => {
        if (timeoutId) {
          clearTimeout(timeoutId);
        }
        process.stdin.removeListener('data', onData);
        if (!originalRawMode) {
          process.stdin.setRawMode(false);
        }
        this.detectionComplete = true;
        resolve();
      };

      // A somewhat long timeout is acceptable as all terminals should respond
      // to the device attributes query used as a sentinel.
      timeoutId = setTimeout(cleanup, 1000);

      const onData = (data: Buffer) => {
        buffer += data.toString();

        // Check OSC 11
        if (!bgReceived) {
          const match = buffer.match(TerminalCapabilityManager.OSC_11_REGEX);
          if (match) {
            bgReceived = true;
            this.terminalBackgroundColor = parseColor(
              match[1],
              match[2],
              match[3],
            );

            // Heuristic: tmux 3.5+ may report #ffffff when it doesn't know the
            // actual host terminal color (e.g. over mosh). We ignore this specific
            // fallback value to prevent blinding the user with a light theme in a
            // likely dark terminal.
            if (this.terminalBackgroundColor === '#ffffff' && this.isTmux()) {
              debugLogger.log(
                'Ignored #ffffff background in tmux (common false positive over mosh).',
              );
              this.terminalBackgroundColor = undefined;
            } else {
              debugLogger.log(
                `Detected terminal background color: ${this.terminalBackgroundColor}`,
              );
            }
          }
        }

        if (
          !kittyKeyboardReceived &&
          TerminalCapabilityManager.KITTY_REGEX.test(buffer)
        ) {
          kittyKeyboardReceived = true;
          this.kittySupported = true;
        }

        // check for modifyOtherKeys support
        if (!modifyOtherKeysReceived) {
          const match = buffer.match(
            TerminalCapabilityManager.MODIFY_OTHER_KEYS_REGEX,
          );
          if (match) {
            modifyOtherKeysReceived = true;
            const level = parseInt(match[1], 10);
            this.modifyOtherKeysSupported = level >= 2;
            debugLogger.log(
              `Detected modifyOtherKeys support: ${this.modifyOtherKeysSupported} (level ${level})`,
            );
          }
        }

        // Check for Terminal Name/Version response.
        if (!terminalNameReceived) {
          const match = buffer.match(
            TerminalCapabilityManager.TERMINAL_NAME_REGEX,
          );
          if (match) {
            terminalNameReceived = true;
            this.terminalName = match[1];

            debugLogger.log(`Detected terminal name: ${this.terminalName}`);
          }
        }

        // We use the Primary Device Attributes response as a sentinel to know
        // that the terminal has processed all our queries. Since we send it
        // last, receiving it means we can stop waiting.
        if (!deviceAttributesReceived) {
          const match = buffer.match(
            TerminalCapabilityManager.DEVICE_ATTRIBUTES_REGEX,
          );
          if (match) {
            deviceAttributesReceived = true;
            cleanup();
          }
        }
      };

      process.stdin.on('data', onData);

      try {
        fs.writeSync(
          process.stdout.fd,
          // Use hidden mode to prevent potential "m" character from being printed
          // to the terminal during startup when querying for modifyOtherKeys.
          // This can happen on some terminals that might echo the query or
          // malform the response. We hide the output, send queries, then
          // immediately clear the line and reset attributes.
          TerminalCapabilityManager.HIDDEN_MODE +
            TerminalCapabilityManager.KITTY_QUERY +
            TerminalCapabilityManager.OSC_11_QUERY +
            TerminalCapabilityManager.TERMINAL_NAME_QUERY +
            TerminalCapabilityManager.MODIFY_OTHER_KEYS_QUERY +
            TerminalCapabilityManager.DEVICE_ATTRIBUTES_QUERY +
            TerminalCapabilityManager.CLEAR_LINE_AND_RETURN +
            TerminalCapabilityManager.RESET_ATTRIBUTES,
        );
      } catch (e) {
        debugLogger.warn('Failed to write terminal capability queries:', e);
        cleanup();
      }
    });
  }

  enableSupportedModes() {
    try {
      if (this.kittySupported) {
        debugLogger.log('Enabling Kitty keyboard protocol');
        enableKittyKeyboardProtocol();
        this.kittyEnabled = true;
      } else if (this.modifyOtherKeysSupported) {
        debugLogger.log('Enabling modifyOtherKeys');
        enableModifyOtherKeys();
      }
      // Always enable bracketed paste since it'll be ignored if unsupported.
      enableBracketedPasteMode();
    } catch (e) {
      debugLogger.warn('Failed to enable keyboard protocols:', e);
    }
  }

  getTerminalBackgroundColor(): TerminalBackgroundColor {
    return this.terminalBackgroundColor;
  }

  getTerminalName(): string | undefined {
    return this.terminalName;
  }

  isKittyProtocolEnabled(): boolean {
    return this.kittyEnabled;
  }

  isGhosttyTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
    const termProgram = env['TERM_PROGRAM']?.toLowerCase();
    const term = env['TERM']?.toLowerCase();
    const name = this.getTerminalName()?.toLowerCase();

    return !!(
      name?.includes('ghostty') ||
      termProgram?.includes('ghostty') ||
      term?.includes('ghostty')
    );
  }

  isTmux(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!env['TMUX'];
  }

  isScreen(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!env['STY'];
  }

  isITerm2(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!(
      this.getTerminalName()?.toLowerCase().includes('iterm') ||
      env['TERM_PROGRAM']?.toLowerCase().includes('iterm')
    );
  }

  isAlacritty(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!(
      this.getTerminalName()?.toLowerCase().includes('alacritty') ||
      env['ALACRITTY_WINDOW_ID'] ||
      env['TERM']?.toLowerCase().includes('alacritty')
    );
  }

  isAppleTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!(
      this.getTerminalName()?.toLowerCase().includes('apple_terminal') ||
      env['TERM_PROGRAM']?.toLowerCase().includes('apple_terminal')
    );
  }

  isVSCodeTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!env['TERM_PROGRAM']?.toLowerCase().includes('vscode');
  }

  isWindowsTerminal(env: NodeJS.ProcessEnv = process.env): boolean {
    return !!env['WT_SESSION'];
  }
}

export const terminalCapabilityManager =
  TerminalCapabilityManager.getInstance();
