import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import type {
  AutomationGuard,
  ClickResult,
  FocusGenshinResult,
  GdiCaptureResult,
  HelperOperationResponse,
  WindowBounds,
  RuntimeInfo,
  ScrollResult,
} from "../../src/types/global.js";

const INPUT_HELPER_SCRIPT = String.raw`
$ErrorActionPreference = "Stop"
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms

$signature = @"
[DllImport("user32.dll")]
public static extern bool SetProcessDPIAware();
[DllImport("shcore.dll")]
public static extern int SetProcessDpiAwareness(int value);
[DllImport("user32.dll")]
public static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll", SetLastError=true)]
public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll", SetLastError=true)]
public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll")]
public static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll", SetLastError=true)]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[StructLayout(LayoutKind.Sequential)]
public struct POINT { public int X; public int Y; }

[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }

[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT { public int dx; public int dy; public uint mouseData; public uint dwFlags; public uint time; public UIntPtr dwExtraInfo; }

[StructLayout(LayoutKind.Sequential)]
public struct INPUT { public int type; public MOUSEINPUT mi; }
"@
Add-Type -MemberDefinition $signature -Name InputHelper -Namespace Native
# Per-monitor DPI awareness (matches GenshinArtScanner's proven fix for the
# same symptom): the older SetProcessDPIAware() only applies a single,
# system-wide scale factor. On a mixed-DPI multi-monitor setup (e.g. Genshin
# on one display, this app's window on a differently-scaled second display),
# that single scale factor is wrong for whichever monitor didn't set it,
# silently shifting every SetCursorPos/click coordinate off-target even
# though cursor readback still matches what we asked for (both go through the
# same, wrong, virtualization layer). PROCESS_PER_MONITOR_DPI_AWARE = 2.
try {
  [Native.InputHelper]::SetProcessDpiAwareness(2) | Out-Null
} catch {
  [Native.InputHelper]::SetProcessDPIAware() | Out-Null
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# SizeOf must receive a struct instance: passing the type object throws in
# Windows PowerShell 5.1 (RuntimeType cannot be marshalled).
$inputSize = [Runtime.InteropServices.Marshal]::SizeOf((New-Object Native.InputHelper+INPUT))
$genshinHwnd = [IntPtr]::Zero

function Send-MouseInput {
  param([uint32]$flags, [int]$dx = 0, [int]$dy = 0, [long]$wheelData = 0)
  $mouseInput = New-Object Native.InputHelper+INPUT
  $mouseInput.type = 0
  $mouseInput.mi.dx = $dx
  $mouseInput.mi.dy = $dy
  if ($wheelData -lt 0) { $mouseInput.mi.mouseData = [uint32](4294967296 + $wheelData) } else { $mouseInput.mi.mouseData = [uint32]$wheelData }
  $mouseInput.mi.dwFlags = $flags
  return [Native.InputHelper]::SendInput(1, [Native.InputHelper+INPUT[]]@($mouseInput), $inputSize)
}

# Matches Inventory Kamera exactly (see docs/DECISIONS.md ADR-008): it moves
# with bare SetCursorPos, then clicks via the InputSimulator library's
# Mouse.LeftButtonClick(), which sends button-down and button-up as ONE
# SendInput call (two INPUT structs in the same array) - back-to-back with no
# artificial delay between them, unlike two separate SendInput calls with a
# Start-Sleep in between. Returns the number of injected events (2 = ok).
function Send-MouseClickBatch {
  $down = New-Object Native.InputHelper+INPUT
  $down.type = 0
  $down.mi.dwFlags = 0x0002
  $up = New-Object Native.InputHelper+INPUT
  $up.type = 0
  $up.mi.dwFlags = 0x0004
  return [Native.InputHelper]::SendInput(2, [Native.InputHelper+INPUT[]]@($down, $up), $inputSize)
}

function Get-CursorPoint {
  $pt = New-Object Native.InputHelper+POINT
  [Native.InputHelper]::GetCursorPos([ref]$pt) | Out-Null
  return $pt
}

function Get-ProcessNameFromHwnd {
  param([IntPtr]$hwnd)
  if ($hwnd -eq [IntPtr]::Zero) { return "" }
  $pidValue = [uint32]0
  [Native.InputHelper]::GetWindowThreadProcessId($hwnd, [ref]$pidValue) | Out-Null
  if ($pidValue -eq 0) { return "" }
  try {
    return (Get-Process -Id ([int]$pidValue) -ErrorAction Stop).ProcessName
  } catch {
    return ""
  }
}

function Get-CurrentProcessElevation {
  $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-ForegroundInfo {
  $hwnd = [Native.InputHelper]::GetForegroundWindow()
  return @{
    foregroundHwnd = $hwnd.ToInt64()
    foregroundProcess = Get-ProcessNameFromHwnd -hwnd $hwnd
  }
}

function Get-CursorState {
  $pt = New-Object Native.InputHelper+POINT
  [Native.InputHelper]::GetCursorPos([ref]$pt) | Out-Null
  # Only 0x8000 (key is held down right now). The 0x0001 "pressed since last
  # call" bit is unreliable and fires for ESC presses that happened long
  # before the scan (ESC is used constantly to navigate Genshin menus).
  $esc = ([Native.InputHelper]::GetAsyncKeyState(27) -band 0x8000) -ne 0
  $enter = ([Native.InputHelper]::GetAsyncKeyState(13) -band 0x8000) -ne 0
  $f9 = ([Native.InputHelper]::GetAsyncKeyState(120) -band 0x8000) -ne 0
  return @{ cursorX = $pt.X; cursorY = $pt.Y; escapePressed = $esc; enterPressed = $enter; f9Pressed = $f9 }
}

function Get-GenshinClientBounds {
  $hwnd = Find-GenshinWindow
  if ($hwnd -eq [IntPtr]::Zero) { return $null }

  $rect = New-Object Native.InputHelper+RECT
  if (-not [Native.InputHelper]::GetClientRect($hwnd, [ref]$rect)) { return $null }

  $topLeft = New-Object Native.InputHelper+POINT
  $topLeft.X = 0
  $topLeft.Y = 0
  if (-not [Native.InputHelper]::ClientToScreen($hwnd, [ref]$topLeft)) { return $null }

  $width = $rect.Right - $rect.Left
  $height = $rect.Bottom - $rect.Top
  if ($width -le 0 -or $height -le 0) { return $null }

  return @{
    Left = $topLeft.X
    Top = $topLeft.Y
    Width = $width
    Height = $height
  }
}

function Find-GenshinWindow {
  if ($script:genshinHwnd -ne [IntPtr]::Zero -and [Native.InputHelper]::IsWindow($script:genshinHwnd)) { return $script:genshinHwnd }
  $proc = Get-Process | Where-Object { $_.ProcessName -match 'GenshinImpact|YuanShen|Genshin' -and $_.MainWindowHandle -ne 0 } | Select-Object -First 1
  if ($proc) { $script:genshinHwnd = $proc.MainWindowHandle } else { $script:genshinHwnd = [IntPtr]::Zero }
  return $script:genshinHwnd
}

function Focus-GenshinWindow {
  $hwnd = Find-GenshinWindow
  $info = @{
    hwnd = $hwnd.ToInt64()
    focused = $false
    alreadyForeground = $false
    foregroundProcess = ""
    targetProcess = Get-ProcessNameFromHwnd -hwnd $hwnd
  }
  if ($hwnd -eq [IntPtr]::Zero) { return $info }

  $info.alreadyForeground = ([Native.InputHelper]::GetForegroundWindow() -eq $hwnd)
  if (-not $info.alreadyForeground) {
    [Native.InputHelper]::ShowWindowAsync($hwnd, 9) | Out-Null
    # A previous version tapped ALT (keybd_event) right before this call to
    # satisfy Windows' "who's allowed to change the foreground window"
    # eligibility check. That tap has a side effect in most Win32 apps: a
    # bare ALT press/release toggles menu-mnemonic navigation mode (verified
    # live - it left a real app's menu bar highlighted after just this call),
    # which then swallows the next several keyboard/mouse events as menu
    # navigation instead of routing them to the app - looking exactly like
    # "clicks/keys report success but do nothing". This app and Genshin run
    # at the same (elevated) integrity level, so plain SetForegroundWindow
    # already succeeds without the ALT tap - confirmed with a standalone
    # compiled test against a live target window.
    $info.setForegroundResult = [Native.InputHelper]::SetForegroundWindow($hwnd)
    Start-Sleep -Milliseconds 140
  }

  $foreground = [Native.InputHelper]::GetForegroundWindow()
  $info.focused = ($foreground -eq $hwnd)
  $info.foregroundProcess = Get-ProcessNameFromHwnd -hwnd $foreground
  return $info
}

while ($true) {
  $line = [Console]::In.ReadLine()
  if ($null -eq $line) { break }
  if ($line.Trim().Length -eq 0) { continue }
  $response = @{ id = ""; ok = $true }
  try {
    $cmd = $line | ConvertFrom-Json
    $response.id = "$($cmd.id)"
    switch ("$($cmd.op)") {
      "ping" {
        $response.pong = $true
      }
      "cursor" {
        $state = Get-CursorState
        $response.cursorX = $state.cursorX
        $response.cursorY = $state.cursorY
        $response.escapePressed = $state.escapePressed
        $response.enterPressed = $state.enterPressed
        $response.f9Pressed = $state.f9Pressed
      }
      "runtime" {
        $response.isElevated = Get-CurrentProcessElevation
        $hwnd = Find-GenshinWindow
        $foregroundInfo = Get-ForegroundInfo
        $response.genshinFound = ($hwnd -ne [IntPtr]::Zero)
        $response.genshinHwnd = $hwnd.ToInt64()
        $response.targetProcess = Get-ProcessNameFromHwnd -hwnd $hwnd
        $response.foregroundProcess = $foregroundInfo.foregroundProcess
        $response.foregroundHwnd = $foregroundInfo.foregroundHwnd
        $response.helperPid = $PID
      }
      "focus" {
        $focusInfo = Focus-GenshinWindow
        $response.focused = $focusInfo.focused
        $response.alreadyForeground = $focusInfo.alreadyForeground
        $response.foregroundProcess = $focusInfo.foregroundProcess
        $response.targetProcess = $focusInfo.targetProcess
        $response.genshinFound = ($focusInfo.hwnd -ne 0)
        $response.setForegroundResult = $focusInfo.setForegroundResult
      }
      "click" {
        $focusInfo = Focus-GenshinWindow
        if ($focusInfo.focused -and -not $focusInfo.alreadyForeground) {
          Start-Sleep -Milliseconds 120
        }
        $targetX = [int]$cmd.x
        $targetY = [int]$cmd.y
        # Matches Inventory Kamera's verified-working sequence exactly: bare
        # SetCursorPos immediately followed by a click, with NO extra move
        # event and NO artificial delay between moving and clicking - IK's
        # Navigation.Click(x, y) does SetCursor() then Click() back-to-back,
        # zero gap. Settling delays only happen after the click, in the scan
        # loop. Down+up are sent as one SendInput call (see
        # Send-MouseClickBatch), matching InputSimulator.Mouse.LeftButtonClick().
        [Native.InputHelper]::SetCursorPos($targetX, $targetY) | Out-Null
        $point = Get-CursorPoint
        $onTarget = (([Math]::Abs($targetX - $point.X) -le 2) -and ([Math]::Abs($targetY - $point.Y) -le 2))
        $clickEventsSent = 0
        if ($onTarget) {
          $clickEventsSent = Send-MouseClickBatch
        }
        $state = Get-CursorState
        $response.cursorX = $state.cursorX
        $response.cursorY = $state.cursorY
        $response.escapePressed = $state.escapePressed
        $response.enterPressed = $state.enterPressed
        $response.f9Pressed = $state.f9Pressed
        $response.moved = $onTarget
        $response.focused = $focusInfo.focused
        $response.alreadyForeground = $focusInfo.alreadyForeground
        $response.foregroundProcess = $focusInfo.foregroundProcess
        $response.targetProcess = $focusInfo.targetProcess
        $response.isElevated = Get-CurrentProcessElevation
        # Never report a click unless the cursor is verifiably on the target.
        # Real acceptance is proven later by the detail-panel fingerprint.
        $response.clicked = ($onTarget -and $clickEventsSent -ge 2)
        $response.inputBlocked = ($onTarget -and $clickEventsSent -lt 2)
      }
      "scroll" {
        $focusInfo = Focus-GenshinWindow
        if ($focusInfo.focused -and -not $focusInfo.alreadyForeground) {
          Start-Sleep -Milliseconds 120
        }
        if ($null -ne $cmd.x -and $null -ne $cmd.y) {
          [Native.InputHelper]::SetCursorPos([int]$cmd.x, [int]$cmd.y) | Out-Null
          Start-Sleep -Milliseconds 30
        }
        $point = Get-CursorPoint
        $response.cursorX = $point.X
        $response.cursorY = $point.Y
        $response.focused = $focusInfo.focused
        $response.foregroundProcess = $focusInfo.foregroundProcess
        $response.isElevated = Get-CurrentProcessElevation
        $notches = [int]$cmd.notches
        $stepDelta = 120
        if ($notches -lt 0) { $stepDelta = -120 }
        $count = [Math]::Abs($notches)
        if ($count -gt 60) { $count = 60 }
        $sentTotal = 0
        for ($i = 0; $i -lt $count; $i++) {
          $sentTotal += Send-MouseInput -flags 0x0800 -wheelData $stepDelta
          Start-Sleep -Milliseconds 45
        }
        $response.notchesSent = $sentTotal
        $response.inputBlocked = (($count -gt 0) -and ($sentTotal -eq 0))
      }
      "bounds" {
        $clientBounds = Get-GenshinClientBounds
        if ($null -eq $clientBounds) {
          $response.found = $false
        } else {
          $response.found = $true
          $response.left = $clientBounds.Left
          $response.top = $clientBounds.Top
          $response.width = $clientBounds.Width
          $response.height = $clientBounds.Height
        }
      }
      "capture" {
        $clientBounds = Get-GenshinClientBounds
        if ($null -eq $clientBounds) {
          $screenBounds = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
          $clientBounds = @{
            Left = $screenBounds.Left
            Top = $screenBounds.Top
            Width = $screenBounds.Width
            Height = $screenBounds.Height
          }
          $response.captureTarget = "primary-screen"
        } else {
          $response.captureTarget = "genshin-client"
        }
        $bitmap = New-Object System.Drawing.Bitmap $clientBounds.Width, $clientBounds.Height
        $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
        $graphics.CopyFromScreen($clientBounds.Left, $clientBounds.Top, 0, 0, $bitmap.Size)
        $capturePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "genshin-assistant-capture-" + [Guid]::NewGuid().ToString() + ".png")
        $bitmap.Save($capturePath, [System.Drawing.Imaging.ImageFormat]::Png)
        $graphics.Dispose()
        $bitmap.Dispose()
        $response.path = $capturePath
        $response.width = $clientBounds.Width
        $response.height = $clientBounds.Height
        $response.originX = $clientBounds.Left
        $response.originY = $clientBounds.Top
      }
      default {
        $response.ok = $false
        $response.error = "unknown op"
      }
    }
  } catch {
    $response.ok = $false
    $response.error = $_.Exception.Message
  }
  Write-Output (ConvertTo-Json $response -Compress)
}
`;

class InputHelperClient {
  private child: ChildProcessWithoutNullStreams | null = null;
  private pending = new Map<string, { resolve: (value: HelperOperationResponse) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>();
  private buffer = "";
  private nextId = 1;
  private starting: Promise<void> | null = null;
  private disposed = false;

  constructor(private readonly scriptUserDataPath: string) {}

  private async ensureStarted() {
    if (this.child) return;
    if (this.disposed) throw new Error("Input helper disposed");
    if (!this.starting) {
      this.starting = this.start().finally(() => {
        this.starting = null;
      });
    }
    await this.starting;
  }

  private async start() {
    const scriptPath = path.join(this.scriptUserDataPath, "input-helper.ps1");
    await fs.mkdir(path.dirname(scriptPath), { recursive: true });
    await fs.writeFile(scriptPath, INPUT_HELPER_SCRIPT, "utf8");

    const child = spawn("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath], {
      windowsHide: true,
      stdio: ["pipe", "pipe", "pipe"],
    });
    child.stdout.setEncoding("utf8");
    child.stdout.on("data", (chunk: string) => this.handleStdout(chunk));
    child.stderr.setEncoding("utf8");
    child.stderr.on("data", () => undefined);
    child.on("exit", () => {
      this.child = null;
      this.buffer = "";
      for (const entry of this.pending.values()) {
        clearTimeout(entry.timer);
        entry.reject(new Error("Input helper exited"));
      }
      this.pending.clear();
    });
    this.child = child;

    // First request compiles the Win32 interop; give it extra time.
    await this.send("ping", {}, 20000);
  }

  private handleStdout(chunk: string) {
    this.buffer += chunk;
    let newlineIndex = this.buffer.indexOf("\n");
    while (newlineIndex >= 0) {
      const line = this.buffer.slice(0, newlineIndex).trim();
      this.buffer = this.buffer.slice(newlineIndex + 1);
      newlineIndex = this.buffer.indexOf("\n");
      if (!line.startsWith("{")) continue;
      try {
        const message = JSON.parse(line) as HelperOperationResponse;
        const entry = this.pending.get(String(message.id));
        if (!entry) continue;
        this.pending.delete(String(message.id));
        clearTimeout(entry.timer);
        if (message.ok) entry.resolve(message);
        else entry.reject(new Error(message.error || "Input helper command failed"));
      } catch {
        // Ignore non-JSON noise on stdout.
      }
    }
  }

  private send(op: string, params: Record<string, unknown>, timeoutMs: number) {
    return new Promise<HelperOperationResponse>((resolve, reject) => {
      const child = this.child;
      if (!child?.stdin.writable) {
        reject(new Error("Input helper is not running"));
        return;
      }
      const id = String(this.nextId++);
      const timer = setTimeout(() => {
        this.pending.delete(id);
        reject(new Error(`Input helper timed out on ${op}`));
      }, timeoutMs);
      this.pending.set(id, { resolve, reject, timer });
      child.stdin.write(`${JSON.stringify({ id, op, ...params })}\n`);
    });
  }

  request(op: string, params: Record<string, unknown> = {}, timeoutMs = 8000) {
    return this.ensureStarted().then(() => this.send(op, params, timeoutMs));
  }

  dispose() {
    this.disposed = true;
    this.child?.kill();
    this.child = null;
  }
}

export interface InputHelperService {
  getRuntimeInfo(): Promise<RuntimeInfo>;
  focusGenshinWindow(): Promise<FocusGenshinResult>;
  focusGenshinForScanStart(): Promise<FocusGenshinResult>;
  getGenshinWindowBounds(): Promise<WindowBounds | null>;
  clickScreen(x: number, y: number): Promise<ClickResult>;
  scrollScreen(notches: number, anchorX?: number, anchorY?: number): Promise<ScrollResult>;
  getAutomationGuard(): Promise<AutomationGuard>;
  capturePrimaryScreenViaGdi(): Promise<GdiCaptureResult>;
  dispose(): void;
}

export function createInputHelperService(options: { userDataPath: string }): InputHelperService {
  const inputHelper = new InputHelperClient(options.userDataPath);

  async function request(op: string, params: Record<string, unknown> = {}, timeoutMs = 8000) {
    return inputHelper.request(op, params, timeoutMs);
  }

  async function getRuntimeInfo() {
    const result = await request("runtime", {}, 4000);
    return {
      ok: true,
      isElevated: Boolean(result.isElevated),
      platform: process.platform,
      genshinFound: Boolean(result.genshinFound),
      genshinHwnd: typeof result.genshinHwnd === "number" ? result.genshinHwnd : undefined,
      targetProcess: typeof result.targetProcess === "string" ? result.targetProcess : undefined,
      foregroundProcess: typeof result.foregroundProcess === "string" ? result.foregroundProcess : undefined,
      foregroundHwnd: typeof result.foregroundHwnd === "number" ? result.foregroundHwnd : undefined,
      helperPid: typeof result.helperPid === "number" ? result.helperPid : undefined,
    };
  }

  async function focusGenshinWindow() {
    const result = await request("focus", {}, 6000);
    return {
      focused: Boolean(result.focused),
      alreadyForeground: Boolean(result.alreadyForeground),
      genshinFound: Boolean(result.genshinFound),
      foregroundProcess: typeof result.foregroundProcess === "string" ? result.foregroundProcess : undefined,
      targetProcess: typeof result.targetProcess === "string" ? result.targetProcess : undefined,
      setForegroundResult: typeof result.setForegroundResult === "boolean" ? result.setForegroundResult : undefined,
    };
  }

  async function focusGenshinForScanStart() {
    let result = await focusGenshinWindow();
    if (result.focused) return result;
    await new Promise((resolve) => setTimeout(resolve, 700));
    result = await focusGenshinWindow();
    return result;
  }

  async function getGenshinWindowBounds() {
    const result = await request("bounds", {}, 4000);
    if (!result.found) return null;
    return {
      x: Number(result.left),
      y: Number(result.top),
      width: Number(result.width),
      height: Number(result.height),
    };
  }

  async function clickScreen(x: number, y: number) {
    const result = (await request("click", { x: Math.round(x), y: Math.round(y) }, 8000)) as HelperOperationResponse;
    return {
      ok: true,
      x: Math.round(x),
      y: Math.round(y),
      cursorX: typeof result.cursorX === "number" ? result.cursorX : undefined,
      cursorY: typeof result.cursorY === "number" ? result.cursorY : undefined,
      escapePressed: Boolean(result.escapePressed),
      enterPressed: Boolean(result.enterPressed),
      f9Pressed: Boolean(result.f9Pressed),
      moved: Boolean(result.moved),
      clicked: Boolean(result.clicked),
      inputBlocked: Boolean(result.inputBlocked),
      focused: Boolean(result.focused),
      alreadyForeground: Boolean(result.alreadyForeground),
      foregroundProcess: typeof result.foregroundProcess === "string" ? result.foregroundProcess : undefined,
      targetProcess: typeof result.targetProcess === "string" ? result.targetProcess : undefined,
      isElevated: typeof result.isElevated === "boolean" ? result.isElevated : undefined,
    };
  }

  async function scrollScreen(notches: number, anchorX?: number, anchorY?: number) {
    const safeNotches = Math.max(-60, Math.min(60, Math.round(notches)));
    const params: Record<string, unknown> = { notches: safeNotches };
    if (typeof anchorX === "number" && typeof anchorY === "number") {
      params.x = Math.round(anchorX);
      params.y = Math.round(anchorY);
    }
    const result = (await request("scroll", params, 8000 + Math.abs(safeNotches) * 80)) as HelperOperationResponse;
    return {
      ok: true,
      notchesSent: Number(result.notchesSent ?? 0),
      inputBlocked: Boolean(result.inputBlocked),
      isElevated: typeof result.isElevated === "boolean" ? result.isElevated : undefined,
    };
  }

  async function getAutomationGuard() {
    const result = await request("cursor", {}, 4000);
    return {
      ok: true,
      cursorX: typeof result.cursorX === "number" ? result.cursorX : undefined,
      cursorY: typeof result.cursorY === "number" ? result.cursorY : undefined,
      escapePressed: Boolean(result.escapePressed),
      enterPressed: Boolean(result.enterPressed),
      f9Pressed: Boolean(result.f9Pressed),
    };
  }

  async function capturePrimaryScreenViaGdi() {
    const result = await request("capture", {}, 15000);
    const capturePath = String(result.path);
    const buffer = await fs.readFile(capturePath);
    await fs.unlink(capturePath).catch(() => undefined);
    const captureTargetRaw = typeof result.captureTarget === "string" ? result.captureTarget : "";
    const captureTarget: "primary-screen" | "genshin-client" = captureTargetRaw === "primary-screen" || captureTargetRaw === "genshin-client"
      ? captureTargetRaw
      : "primary-screen";

    return {
      dataUrl: `data:image/png;base64,${buffer.toString("base64")}`,
      width: Number(result.width),
      height: Number(result.height),
      originX: Number(result.originX),
      originY: Number(result.originY),
      captureTarget,
    };
  }

  return {
    getRuntimeInfo,
    focusGenshinWindow,
    focusGenshinForScanStart,
    getGenshinWindowBounds,
    clickScreen,
    scrollScreen,
    getAutomationGuard,
    capturePrimaryScreenViaGdi,
    dispose: () => inputHelper.dispose(),
  };
}
