import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { spawnSync } from 'node:child_process';
import type { InputControllerLike } from './action_executor.js';

/**
 * 将热键字符串（如 'ctrl+v' / 'enter' / 'ctrl+shift+esc'）
 * 转换为 nut.js Key 枚举数组（按下顺序；释放时逆序）。
 *
 * @param combo - 热键组合字符串，多个键用 '+' 分隔
 * @param Key   - nut.js 的 Key 枚举对象（运行时注入，方便测试 mock）
 * @returns 对应 Key 枚举值的数组
 * @throws 遇到无法识别的键名时抛出 'unknown-hotkey-part: <名称>'
 */
function parseHotkey(combo: string, Key: any): any[] {
  const parts = combo
    .toLowerCase()
    .split('+')
    .map(s => s.trim());

  // 常用修饰键 + 功能键的名称映射
  const map: Record<string, any> = {
    ctrl: Key.LeftControl,
    control: Key.LeftControl,
    shift: Key.LeftShift,
    alt: Key.LeftAlt,
    win: Key.LeftSuper,
    meta: Key.LeftSuper,
    enter: Key.Enter,
    tab: Key.Tab,
    escape: Key.Escape,
    esc: Key.Escape,
    space: Key.Space,
    backspace: Key.Backspace,
    delete: Key.Delete,
    up: Key.Up,
    down: Key.Down,
    left: Key.Left,
    right: Key.Right,
  };

  return parts.map(p => {
    // 已在映射表中的键名
    if (map[p] !== undefined) return map[p];

    // 单字符按键：'a'..'z' / '0'..'9'
    if (p.length === 1) {
      const upper = p.toUpperCase();
      if (Key[upper] !== undefined) return Key[upper];
    }

    // F1..F12 功能键
    if (/^f([1-9]|1[0-2])$/.test(p)) {
      const fKey = 'F' + p.slice(1);
      if (Key[fKey] !== undefined) return Key[fKey];
    }

    throw new Error('unknown-hotkey-part: ' + p);
  });
}

/**
 * 鼠标 + 键盘 + 剪切板控制器（仅支持 Windows）。
 *
 * 实现说明：
 *  - click / hotkey：依赖 @nut-tree-fork/nut-js（原生模块，延迟 require）
 *  - writeClipboard：使用 child_process.spawnSync('clip.exe', stdin = UTF-16 LE + BOM)
 *      理由：clipboardy v5 为 ESM-only，CJS require 报 ERR_PACKAGE_PATH_NOT_EXPORTED；
 *            clip.exe 是 Windows 内置工具，从 stdin 读取并写入剪切板。
 *      中文支持：clip.exe 需要 UTF-16 LE 编码，并在字节流开头加 BOM（0xFF 0xFE）。
 *  - typeViaClipboard：先 writeClipboard，再模拟 ctrl+v 粘贴
 *
 * 非 win32 平台会抛出 'unsupported-platform'。
 * 单元测试通过 jest.mock 同时 mock child_process 和 nut.js，无需真实 Windows 环境。
 */
@Provide()
@Scope(ScopeEnum.Singleton)
export class InputController implements InputControllerLike {
  /** 缓存已加载的 nut.js 模块，避免重复 require */
  private nut: {
    mouse: any;
    keyboard: any;
    Key: any;
    Point: any;
  } | null = null;

  /**
   * 惰性加载 nut.js，仅在 win32 平台上执行。
   * 测试环境下 jest.mock('@nut-tree-fork/nut-js') 已在模块解析阶段注册，
   * require 会直接返回 mock 对象。
   */
  private ensureNut() {
    if (this.nut) return this.nut;

    if (process.platform !== 'win32') {
      throw new Error('unsupported-platform');
    }

    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const nutjs = require('@nut-tree-fork/nut-js');
    this.nut = {
      mouse: nutjs.mouse,
      keyboard: nutjs.keyboard,
      Key: nutjs.Key,
      Point: nutjs.Point,
    };
    return this.nut;
  }

  /**
   * 将鼠标移动到 (x, y) 并执行左键单击。
   *
   * @param x - 屏幕横坐标（物理像素）
   * @param y - 屏幕纵坐标（物理像素）
   */
  async click(x: number, y: number): Promise<void> {
    const { mouse, Point } = this.ensureNut()!;
    await mouse.move([new Point(x, y)]);
    await mouse.leftClick();
  }

  /**
   * 按下并释放热键组合。
   * 按下顺序：从左到右；释放顺序：从右到左（逆序），符合操作系统预期行为。
   *
   * @param combo - 热键字符串，例如 'ctrl+v'、'ctrl+shift+esc'、'enter'
   */
  async hotkey(combo: string): Promise<void> {
    const { keyboard, Key } = this.ensureNut()!;
    const keys = parseHotkey(combo, Key);

    // 按顺序按下
    for (const k of keys) {
      await keyboard.pressKey(k);
    }
    // 逆序释放
    for (let i = keys.length - 1; i >= 0; i--) {
      await keyboard.releaseKey(keys[i]);
    }
  }

  /**
   * 将文本写入 Windows 剪切板（同步）。
   *
   * 编码方案：UTF-16 LE + BOM（0xFF 0xFE）
   *  - clip.exe 要求 UTF-16 LE 才能正确处理中文等非 ASCII 字符
   *  - BOM 用于告知 clip.exe 文件编码
   *
   * @param text - 要写入剪切板的文本
   * @throws 当 clip.exe 返回非零退出码时抛出错误
   */
  writeClipboard(text: string): void {
    // 构造 UTF-16 LE + BOM 缓冲区
    const bom = Buffer.from([0xff, 0xfe]);
    const content = Buffer.from(text, 'utf16le');
    const buf = Buffer.concat([bom, content]);

    const result = spawnSync('clip.exe', [], { input: buf });

    if (result.status !== 0) {
      throw new Error(
        'clip.exe failed: ' + (result.stderr?.toString() || 'unknown'),
      );
    }
  }

  /**
   * 通过剪切板粘贴方式输入文本。
   * 先将文本写入剪切板，再模拟 ctrl+v 触发粘贴。
   * 适用于无法直接用键盘输入 Unicode 文本的场景（如中文输入法绕过）。
   *
   * @param text - 要输入的文本
   */
  async typeViaClipboard(text: string): Promise<void> {
    this.writeClipboard(text);
    await this.hotkey('ctrl+v');
  }
}
