// node-pty ships its POSIX `spawn-helper` inside the published npm tarball with
// mode 0644 (no exec bit). node-pty `posix_spawnp`s that helper on macOS/Linux,
// so a non-executable helper fails every terminal spawn with
// `Error: posix_spawnp failed.`. Packaged builds are covered because
// stage-native-deps.mjs chmods the staged copy, but the dev flow
// (`npm run dev` → `electron .`) resolves node-pty straight from
// `node_modules/`, which nobody chmods. This restores the exec bits at runtime,
// best-effort, so both dev and any environment that stripped the bit keep
// working. Idempotent: files that are already executable are left untouched.

import {
  chmodSync as realChmodSync,
  existsSync as realExistsSync,
  readdirSync as realReaddirSync,
  statSync as realStatSync
} from 'node:fs'
import { join } from 'node:path'

const EXEC_BITS = 0o111

// Electron exposes module paths inside app.asar even when electron-builder has
// unpacked the native payload beside it. `stat` can read an archived path, but
// chmod cannot mutate it (ENOTDIR). Native node-pty helpers belong in the
// writable app.asar.unpacked tree; leave an already-unpacked path unchanged.
export function writableNodePtyRoot(nodePtyRoot: string): string {
  return nodePtyRoot.replace(/app\.asar(?!\.unpacked)/, 'app.asar.unpacked')
}

export interface SpawnHelperFs {
  existsSync(path: string): boolean
  readdirSync(path: string): string[]
  statSync(path: string): { mode: number }
  chmodSync(path: string, mode: number): void
}

export interface EnsureSpawnHelperResult {
  fixed: string[]
  errors: { path: string; error: string }[]
}

const defaultFs: SpawnHelperFs = {
  existsSync: realExistsSync,
  readdirSync: (path: string) => realReaddirSync(path),
  statSync: (path: string) => realStatSync(path),
  chmodSync: realChmodSync
}

// True when any of the owner/group/other execute bits are missing.
export function needsExecBit(mode: number): boolean {
  return (mode & EXEC_BITS) !== EXEC_BITS
}

// Preserve existing permission bits, adding execute for owner/group/other.
export function withExecBits(mode: number): number {
  return mode | EXEC_BITS
}

// Every place a `spawn-helper` can live under a node-pty package root: one per
// bundled prebuild (`prebuilds/<platform>-<arch>/`) plus a locally compiled
// `build/Release/` copy. Windows layouts have no spawn-helper, so the list is
// naturally empty there.
export function spawnHelperCandidates(
  nodePtyRoot: string,
  fs: Pick<SpawnHelperFs, 'existsSync' | 'readdirSync'> = defaultFs
): string[] {
  const candidates: string[] = []
  const prebuilds = join(nodePtyRoot, 'prebuilds')

  if (fs.existsSync(prebuilds)) {
    for (const entry of fs.readdirSync(prebuilds)) {
      candidates.push(join(prebuilds, entry, 'spawn-helper'))
    }
  }

  candidates.push(join(nodePtyRoot, 'build', 'Release', 'spawn-helper'))

  return candidates
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error)
}

// Best-effort: ensure every existing spawn-helper under `nodePtyRoot` is
// executable. Never throws — missing files are skipped, and chmod/stat failures
// are collected so the caller can log them without breaking terminal startup.
export function ensureSpawnHelperExecutable(
  nodePtyRoot: string,
  fs: SpawnHelperFs = defaultFs
): EnsureSpawnHelperResult {
  const result: EnsureSpawnHelperResult = { fixed: [], errors: [] }
  const writableRoot = writableNodePtyRoot(nodePtyRoot)

  for (const path of spawnHelperCandidates(writableRoot, fs)) {
    if (!fs.existsSync(path)) {
      continue
    }

    let mode: number

    try {
      mode = fs.statSync(path).mode
    } catch (error) {
      result.errors.push({ path, error: errorMessage(error) })

      continue
    }

    if (!needsExecBit(mode)) {
      continue
    }

    try {
      fs.chmodSync(path, withExecBits(mode))
      result.fixed.push(path)
    } catch (error) {
      result.errors.push({ path, error: errorMessage(error) })
    }
  }

  return result
}
