import { spawnSync } from 'node:child_process'
import { error as logError, log } from 'node:console'
import { existsSync } from 'node:fs'
import {
  chmod,
  mkdir,
  mkdtemp,
  readFile,
  rename,
  rm,
  writeFile,
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'

export const BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE =
  'this_dir="$(readlink -f "$(dirname "$0")")"'
export const FIXED_LINUXDEPLOY_APPRUN_DIR_LINE =
  'this_dir="$(dirname "$(readlink -f "$0")")"'
export const APPIMAGE_PLUGIN_WRAPPER_NAME = 'linuxdeploy-plugin-appimage.AppImage'
export const REAL_APPIMAGE_PLUGIN_NAME =
  'tolaria-real-linuxdeploy-plugin-appimage/linuxdeploy-plugin-appimage.AppImage'
export const APPIMAGE_FCITX_GTK3_IM_MODULE_PATH =
  'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so'
export const APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH =
  'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2'
export const DEFAULT_APPIMAGE_PLUGIN_URL =
  'https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage'

const WRAPPER_MARKER = 'Tolaria AppImage symlink launcher shim'
const REQUIRED_APPIMAGE_PATHS = [
  'AppRun',
  APPIMAGE_FCITX_GTK3_IM_MODULE_PATH,
  APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH,
]

export function tauriToolsCacheDir(env = process.env) {
  if (env.TOLARIA_TAURI_TOOLS_DIR) {
    return resolve(env.TOLARIA_TAURI_TOOLS_DIR)
  }

  if (env.XDG_CACHE_HOME) {
    return resolve(env.XDG_CACHE_HOME, 'tauri')
  }

  if (!env.HOME) {
    throw new Error('HOME or XDG_CACHE_HOME is required to locate the Tauri tools cache')
  }

  return resolve(env.HOME, '.cache', 'tauri')
}

export function patchAppRunText(text) {
  if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) {
    return {
      changed: true,
      text: text.replaceAll(
        BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE,
        FIXED_LINUXDEPLOY_APPRUN_DIR_LINE,
      ),
    }
  }

  return { changed: false, text }
}

export function assertSymlinkSafeAppRunText(text, label = 'AppRun') {
  if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) {
    throw new Error(`${label} still resolves dirname before following AppRun symlinks`)
  }

  if (!text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE)) {
    throw new Error(`${label} is missing the symlink-safe AppRun directory resolver`)
  }
}

export function appImagePluginWrapperSource({
  pluginUrl = DEFAULT_APPIMAGE_PLUGIN_URL,
} = {}) {
  return `#!/usr/bin/env bash
set -euo pipefail

# ${WRAPPER_MARKER}
PLUGIN_URL="\${TOLARIA_APPIMAGE_PLUGIN_URL:-${pluginUrl}}"
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
REAL_PLUGIN="\${TOLARIA_APPIMAGE_REAL_PLUGIN:-"$SCRIPT_DIR/${REAL_APPIMAGE_PLUGIN_NAME}"}"
FCITX_GTK3_IM_MODULE="\${TOLARIA_FCITX_GTK3_IM_MODULE:-/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so}"
FCITX_LIBRARY_DIR="\${TOLARIA_FCITX_LIBRARY_DIR:-/usr/lib/x86_64-linux-gnu}"

appdir_from_args() {
  local previous=""

  for arg in "$@"; do
    if [ "$previous" = "--appdir" ]; then
      printf '%s\\n' "$arg"
      return 0
    fi

    case "$arg" in
      --appdir=*)
        printf '%s\\n' "\${arg#--appdir=}"
        return 0
        ;;
    esac

    previous="$arg"
  done
}

download_real_plugin() {
  if [ -x "$REAL_PLUGIN" ]; then
    return 0
  fi

  local tmp_plugin="$REAL_PLUGIN.tmp.$$"
  rm -f "$tmp_plugin"
  mkdir -p "$(dirname -- "$REAL_PLUGIN")"

  if command -v curl >/dev/null 2>&1; then
    curl -fsSL -o "$tmp_plugin" "$PLUGIN_URL"
  elif command -v wget >/dev/null 2>&1; then
    wget -q -O "$tmp_plugin" "$PLUGIN_URL"
  else
    echo "curl or wget is required to fetch the real linuxdeploy AppImage output plugin" >&2
    return 1
  fi

  chmod +x "$tmp_plugin"
  mv "$tmp_plugin" "$REAL_PLUGIN"
}

patch_apprun() {
  local appdir="\${APPDIR:-}"

  if [ -z "$appdir" ]; then
    appdir="$(appdir_from_args "$@" || true)"
  fi

  if [ -z "$appdir" ] || [ ! -f "$appdir/AppRun" ]; then
    return 0
  fi

  python3 - "$appdir/AppRun" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
broken = 'this_dir="$(readlink -f "$(dirname "$0")")"'
fixed = 'this_dir="$(dirname "$(readlink -f "$0")")"'
text = path.read_text(encoding="utf-8")

if broken in text:
    path.write_text(text.replace(broken, fixed), encoding="utf-8")
    print(f"Patched linuxdeploy AppRun symlink resolution in {path}", file=sys.stderr)
elif fixed in text:
    pass
elif "autogenerated by linuxdeploy" in text and "AppRun.wrapped" in text:
    raise SystemExit(f"{path} is a linuxdeploy wrapper but does not contain the expected AppRun resolver")
PY
}

bundle_fcitx_gtk3_module() {
  local appdir="\${APPDIR:-}"

  if [ -z "$appdir" ]; then
    appdir="$(appdir_from_args "$@" || true)"
  fi

  if [ -z "$appdir" ] || [ ! -d "$appdir" ] || [ ! -f "$FCITX_GTK3_IM_MODULE" ]; then
    return 0
  fi

  local module_dest="$appdir/${APPIMAGE_FCITX_GTK3_IM_MODULE_PATH}"
  local library_dest_dir="$appdir/usr/lib/x86_64-linux-gnu"

  mkdir -p "$(dirname -- "$module_dest")" "$library_dest_dir"
  cp -a "$FCITX_GTK3_IM_MODULE" "$module_dest"

  local copied_library=0
  shopt -s nullglob
  for lib in "$FCITX_LIBRARY_DIR"/libFcitx5GClient.so* "$FCITX_LIBRARY_DIR"/libFcitx5Utils.so*; do
    cp -a "$lib" "$library_dest_dir/"
    copied_library=1
  done
  shopt -u nullglob

  if [ "$copied_library" -eq 0 ]; then
    echo "No fcitx GTK client libraries found in $FCITX_LIBRARY_DIR" >&2
  fi
}

download_real_plugin
patch_apprun "$@"
bundle_fcitx_gtk3_module "$@"
exec "$REAL_PLUGIN" "$@"
`
}

export async function preparePluginWrapper({
  env = process.env,
  toolsDir = tauriToolsCacheDir(env),
} = {}) {
  await mkdir(toolsDir, { recursive: true })

  const wrapperPath = join(toolsDir, APPIMAGE_PLUGIN_WRAPPER_NAME)
  const realPluginPath = join(toolsDir, REAL_APPIMAGE_PLUGIN_NAME)

  if (existsSync(wrapperPath) && !existsSync(realPluginPath)) {
    const existing = await readFile(wrapperPath, 'utf8').catch(() => '')
    if (!existing.includes(WRAPPER_MARKER)) {
      await mkdir(dirname(realPluginPath), { recursive: true })
      await rename(wrapperPath, realPluginPath)
    }
  }

  await writeFile(wrapperPath, appImagePluginWrapperSource(), 'utf8')
  await chmod(wrapperPath, 0o755)

  return { realPluginPath, wrapperPath }
}

export async function validateAppRunFile(path) {
  const text = await readFile(path, 'utf8')
  assertSymlinkSafeAppRunText(text, path)
}

function extractAppImagePath(appImage, requiredPath, tempDir) {
  const result = spawnSync(appImage, ['--appimage-extract', requiredPath], {
    cwd: tempDir,
    encoding: 'utf8',
  })

  if (result.status === 0) {
    return
  }

  throw new Error(
    [
      `Failed to extract ${requiredPath} from ${appImage}`,
      result.stdout.trim(),
      result.stderr.trim(),
    ]
      .filter(Boolean)
      .join('\n'),
  )
}

function assertAppImagePathsExtracted(appImage, tempDir, requiredPaths) {
  for (const requiredPath of requiredPaths) {
    const extractedPath = join(tempDir, 'squashfs-root', requiredPath)
    if (!existsSync(extractedPath)) {
      throw new Error(`${appImage} is missing ${requiredPath}`)
    }
  }
}

async function validateExtractedAppImage(appImage, tempDir) {
  for (const requiredPath of REQUIRED_APPIMAGE_PATHS) {
    extractAppImagePath(appImage, requiredPath, tempDir)
  }

  await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun'))
  assertAppImagePathsExtracted(appImage, tempDir, REQUIRED_APPIMAGE_PATHS.slice(1))
}

export async function validateAppImages(paths) {
  if (paths.length === 0) {
    throw new Error('At least one AppImage path is required for launcher validation')
  }

  for (const appImage of paths.map((path) => resolve(path))) {
    const tempDir = await mkdtemp(join(tmpdir(), 'tolaria-appimage-'))
    try {
      await validateExtractedAppImage(appImage, tempDir)
    } finally {
      await rm(tempDir, { recursive: true, force: true })
    }
  }
}

async function preparePluginCommand() {
  const { wrapperPath, realPluginPath } = await preparePluginWrapper()
  log(`Prepared ${wrapperPath}`)
  log(`Real plugin cache: ${realPluginPath}`)
}

async function validateAppRunFilesCommand(paths) {
  for (const path of paths) {
    await validateAppRunFile(path)
    log(`Validated ${path}`)
  }
}

async function validateAppImagesCommand(paths) {
  await validateAppImages(paths)
  for (const path of paths) {
    log(`Validated AppImage launcher in ${path}`)
  }
}

const COMMANDS = new Map([
  ['prepare-plugin', preparePluginCommand],
  ['validate-apprun-file', validateAppRunFilesCommand],
  ['validate-appimages', validateAppImagesCommand],
])

function usage() {
  return 'Usage: node scripts/appimage-launcher-tools.mjs prepare-plugin | validate-apprun-file <AppRun...> | validate-appimages <AppImage...>'
}

async function main() {
  const [command, ...args] = process.argv.slice(2)
  const handler = COMMANDS.get(command)

  if (!handler) {
    throw new Error(usage())
  }

  await handler(args)
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main().catch((error) => {
    logError(error.message)
    process.exit(1)
  })
}
