#!/usr/bin/env node

import { existsSync, readFileSync, writeFileSync, appendFileSync, copyFileSync, renameSync, unlinkSync, readdirSync } from "fs";
import { homedir, tmpdir } from "os";
import { dirname, join, basename } from "path";
import { applyEdits, modify, parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";

const isCI = process.env.CI === "true" || process.env.CONTINUOUS_INTEGRATION === "true";

const TIMEOUT_MS = 30000;
const timeoutId = setTimeout(() => {
  console.log("⚠️  preuninstall timeout - exiting gracefully");
  process.exit(0);
}, TIMEOUT_MS);

process.on("SIGINT", () => process.exit(0));
process.on("SIGTERM", () => process.exit(0));

// Log to file for debugging
const LOG_FILE = join(tmpdir(), "opencode-orchestrator.log");
function log(message: string, data?: unknown): void {
  try {
    const timestamp = new Date().toISOString();
    const entry = `[${timestamp}] [preuninstall] ${message} ${data ? JSON.stringify(data) : ""}\n`;
    appendFileSync(LOG_FILE, entry);
  } catch { /* ignore */ }
}

interface NodeError extends Error {
  code?: string;
}

function formatError(err: unknown, context: string): string {
  if (err instanceof Error) {
    const nodeErr = err as NodeError;
    if (nodeErr.code === "EACCES" || nodeErr.code === "EPERM") {
      return `Permission denied: Cannot ${context}. Try running as administrator.`;
    }
    if (nodeErr.code === "ENOENT") {
      return `File not found while trying to ${context}.`;
    }
    if (err instanceof SyntaxError) {
      return `JSON syntax error while trying to ${context}: ${err.message}.`;
    }
    if (nodeErr.code === "EIO") {
      return `File lock error: Cannot ${context}. Please close OpenCode and try again.`;
    }
    if (nodeErr.code === "ENOSPC") {
      return `Disk full: Cannot ${context}. Free up disk space and try again.`;
    }
    if (nodeErr.code === "EROFS") {
      return `Read-only filesystem: Cannot ${context}.`;
    }
    return `Failed to ${context}: ${err.message}`;
  }
  return `Failed to ${context}: ${String(err)}`;
}

const PLUGIN_NAME = "opencode-orchestrator";
type PluginOptions = Record<string, unknown>;
type PluginTuple = [string, PluginOptions];
type PluginEntry = string | PluginTuple;

function isRecord(value: unknown): value is PluginOptions {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function isPluginTuple(value: unknown): value is PluginTuple {
  return Array.isArray(value)
    && value.length === 2
    && typeof value[0] === "string"
    && isRecord(value[1]);
}

function isPluginEntry(value: unknown): value is PluginEntry {
  return typeof value === "string" || isPluginTuple(value);
}

function getPluginName(entry: PluginEntry): string {
  return typeof entry === "string" ? entry : entry[0];
}

function isOurPluginEntry(entry: unknown): boolean {
  if (!isPluginEntry(entry)) return false;
  const pluginName = getPluginName(entry);
  return pluginName === PLUGIN_NAME || pluginName.startsWith(`${PLUGIN_NAME}@`);
}

function getConfigFileCandidates(configDir: string): string[] {
  return [join(configDir, "opencode.jsonc"), join(configDir, "opencode.json")];
}

function resolveConfigFile(configDir: string): string {
  for (const candidate of getConfigFileCandidates(configDir)) {
    if (existsSync(candidate)) {
      return candidate;
    }
  }
  return getConfigFileCandidates(configDir)[0];
}

function parseConfigContent(rawContent: string): { config?: Record<string, any>; parseError?: string } {
  const errors: ParseError[] = [];
  const config = parseJsonc(rawContent, errors, {
    allowTrailingComma: true,
    disallowComments: false,
  });

  if (errors.length > 0) {
    const [firstError] = errors;
    const line = rawContent.slice(0, firstError.offset).split("\n").length;
    const column = firstError.offset - rawContent.lastIndexOf("\n", firstError.offset - 1);
    return {
      parseError: `${printParseErrorCode(firstError.error)} at line ${line}, column ${column}`,
    };
  }

  if (typeof config !== "object" || config === null || Array.isArray(config)) {
    return {
      parseError: "Root config must be a JSON object",
    };
  }

  return { config: config as Record<string, any> };
}

/**
 * Get all possible config directories for OpenCode.
 * On Windows, OpenCode may use either:
 * - %APPDATA%/opencode (native Windows)
 * - ~/.config/opencode (Git Bash, WSL, MSYS2)
 */
/**
 * Detect if running inside WSL2 and return the Windows-side OpenCode config path.
 * Mirrors the same logic in postinstall.ts — must stay in sync.
 */
function detectWSLWindowsConfigDir(): string | null {
  try {
    const isWSL = process.env.WSL_DISTRO_NAME || process.env.WSLENV;
    if (!isWSL) {
      try {
        const procVersion = readFileSync("/proc/version", "utf-8");
        if (!/microsoft|WSL/i.test(procVersion)) return null;
      } catch {
        return null;
      }
    }

    const windowsUser = process.env.WINDOWS_USERNAME || process.env.USERNAME;
    const candidates: string[] = [];

    const userDir = "/mnt/c/Users";
    if (existsSync(userDir)) {
      try {
        const users = readdirSync(userDir);
        for (const user of users) {
          if (["Public", "Default", "Default User", "All Users", "desktop.ini"]
            .includes(user) || user.startsWith(".")) continue;
          const candidate = join(userDir, user, "AppData", "Roaming", "opencode");
          candidates.push(candidate);
        }
      } catch { /* ignore */ }
    }

    if (windowsUser) {
      const preferred = `/mnt/c/Users/${windowsUser}/AppData/Roaming/opencode`;
      if (candidates.includes(preferred)) return preferred;
    }

    for (const c of candidates) {
      if (existsSync(c)) return c;
    }

    return candidates[0] || null;
  } catch {
    return null;
  }
}

function getConfigPaths(): string[] {
  const paths: string[] = [];

  if (process.env.XDG_CONFIG_HOME) {
    paths.push(join(process.env.XDG_CONFIG_HOME, "opencode"));
  }

  if (process.platform === "win32") {
    const appDataPath =
      process.env.APPDATA || join(homedir(), "AppData", "Roaming");
    paths.push(join(appDataPath, "opencode"));

    const dotConfigPath = join(homedir(), ".config", "opencode");
    if (!paths.includes(dotConfigPath)) {
      paths.push(dotConfigPath);
    }
  } else {
    paths.push(join(homedir(), ".config", "opencode"));

    // WSL2: also remove from the Windows-side config directory
    const wslWindowsConfig = detectWSLWindowsConfigDir();
    if (wslWindowsConfig && !paths.includes(wslWindowsConfig)) {
      log("Detected WSL2 - also checking Windows config path", { wslWindowsConfig });
      paths.push(wslWindowsConfig);
    }
  }

  return [...new Set(paths)];
}

/**
 * Validate JSON config structure
 */
function validateConfig(config: any): boolean {
  try {
    // Must be an object
    if (typeof config !== "object" || config === null) {
      return false;
    }

    // If plugin field exists, must be an array
    if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
      return false;
    }

    // Plugin entries may be bare names or [name, options] tuples.
    if (config.plugin) {
      for (const entry of config.plugin) {
        if (!isPluginEntry(entry)) {
          return false;
        }
      }
    }

    return true;
  } catch {
    return false;
  }
}

/**
 * Create backup of existing config file
 */
function createBackup(configFile: string): string | null {
  try {
    if (!existsSync(configFile)) {
      return null;
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    const backupFile = `${configFile}.backup.${timestamp}`;
    copyFileSync(configFile, backupFile);
    log("Backup created", { backupFile });
    return backupFile;
  } catch (error) {
    log("Failed to create backup", { error: String(error) });
    return null;
  }
}

/**
 * Atomic file write: write to temp file, then rename
 */
function atomicWriteJSON(filePath: string, data: any, originalContent?: string): void {
  const tempFile = `${filePath}.tmp.${Date.now()}`;
  try {
    let output = JSON.stringify(data, null, 2) + "\n";
    if (filePath.endsWith(".jsonc") && originalContent !== undefined) {
      const source = originalContent.trim() ? originalContent : "{}";
      const edits = modify(source, ["plugin"], data.plugin, {
        formattingOptions: { tabSize: 2, insertSpaces: true },
      });
      output = applyEdits(source, edits);
      if (!output.endsWith("\n")) output += "\n";
    }

    writeFileSync(tempFile, output, { mode: 0o644 });

    // Atomic rename (OS-level atomic operation)
    renameSync(tempFile, filePath);
    log("Atomic write successful", { filePath });
  } catch (error) {
    // Cleanup temp file on failure
    try {
      if (existsSync(tempFile)) {
        unlinkSync(tempFile);
      }
    } catch { /* ignore */ }
    throw error;
  }
}

/**
 * Remove plugin from a single config file with rollback support
 */
function removeFromConfig(configDir: string): { success: boolean; backupFile: string | null } {
  const configFile = resolveConfigFile(configDir);
  let backupFile: string | null = null;
  let originalContent: string | undefined;

  try {
    if (!existsSync(configFile)) {
      log("Config file does not exist", { configFile });
      return { success: false, backupFile: null };
    }

    // Read existing config
    const content = readFileSync(configFile, "utf-8");
    originalContent = content;
    const trimmedContent = content.trim();
    if (!trimmedContent) {
      log("Empty config file", { configFile });
      return { success: false, backupFile };
    }

    const parsed = parseConfigContent(trimmedContent);
    if (parsed.parseError) {
      log("Failed to parse config, skipping", { error: parsed.parseError, configFile });
      console.log(`⚠️  Corrupted config detected. Backup saved: ${backupFile}`);
      return { success: false, backupFile };
    }
    const config = parsed.config ?? {};

    // Validate config structure
    if (!validateConfig(config)) {
      log("Invalid config structure, skipping", { config, configFile });
      return { success: false, backupFile };
    }

    if (!config.plugin || !Array.isArray(config.plugin)) {
      log("No plugin array found", { configFile });
      return { success: false, backupFile };
    }

    const originalLength = config.plugin.length;
    const originalPlugins = [...config.plugin];

    config.plugin = config.plugin.filter((entry: unknown) => !isOurPluginEntry(entry));

    if (config.plugin.length === originalLength) {
      log("Plugin not found in config", { configFile });
      return { success: false, backupFile };
    }

    // Back up only when a real mutation is about to happen.
    backupFile = createBackup(configFile);

    const removedCount = originalLength - config.plugin.length;
    log("Removing plugin from config", {
      plugin: PLUGIN_NAME,
      removedCount,
      originalPlugins,
      newPlugins: config.plugin,
      configFile
    });

    // Atomic write (temp file + rename)
    atomicWriteJSON(configFile, config, originalContent);

    // Verify write succeeded
    try {
      const verifyContent = readFileSync(configFile, "utf-8");
      const verifyParsed = parseConfigContent(verifyContent);
      if (verifyParsed.parseError || !verifyParsed.config) {
        throw new Error(`Verification parse failed: ${verifyParsed.parseError ?? "unknown parse error"}`);
      }
      const verifyConfig = verifyParsed.config;

      const stillHasPlugin = verifyConfig.plugin?.some((entry: unknown) => isOurPluginEntry(entry));

      if (stillHasPlugin) {
        throw new Error("Verification failed: plugin still present after removal");
      }
    } catch (verifyError) {
      log("Write verification failed, rolling back", { error: String(verifyError) });
      // Rollback: restore from backup
      if (backupFile && existsSync(backupFile)) {
        copyFileSync(backupFile, configFile);
        console.log(`⚠️  Write verification failed. Restored from backup.`);
      }
      throw verifyError;
    }

    log("Plugin removed successfully", { configFile, removedCount });
    return { success: true, backupFile };
  } catch (error) {
    log("Removal failed", { error: String(error), configFile });

    // Rollback: restore from backup
    if (backupFile && existsSync(backupFile)) {
      try {
        copyFileSync(backupFile, configFile);
        log("Rolled back to backup", { backupFile });
        console.log(`⚠️  Removal failed. Restored from backup: ${backupFile}`);
      } catch (rollbackError) {
        log("Rollback failed", { error: String(rollbackError) });
      }
    }

    return { success: false, backupFile };
  }
}

/**
 * Clean up old backup files (keep only last 5)
 */
function cleanupOldBackups(configFile: string): void {
  try {
    const configDir = dirname(configFile);
    const configBase = basename(configFile);
    const files = readdirSync(configDir);
    const backupFiles = files
      .filter((f: string) => f.startsWith(`${configBase}.backup.`))
      .sort()
      .reverse();

    // Keep only last 5 backups
    for (let i = 5; i < backupFiles.length; i++) {
      const backupPath = join(configDir, backupFiles[i]);
      try {
        unlinkSync(backupPath);
        log("Deleted old backup", { file: backupFiles[i] });
      } catch { /* ignore */ }
    }
  } catch { /* ignore */ }
}

try {
  console.log("🧹 OpenCode Orchestrator - Uninstalling...");
  if (isCI) log("Running in CI mode");
  log("Uninstallation started", { platform: process.platform, node: process.version });

  if (isCI) {
    console.log("ℹ️  CI environment detected. Skipping automatic plugin cleanup.");
    log("Skipping automatic plugin cleanup in CI");
    clearTimeout(timeoutId);
    process.exit(0);
  }

  const configPaths = getConfigPaths();
  log("Config paths to check", configPaths);

  let removed = false;
  let backupCreated: string | null = null;

  for (const configDir of configPaths) {
    const configFile = resolveConfigFile(configDir);

    const result = removeFromConfig(configDir);
    if (result.success) {
      console.log(`✅ Plugin removed: ${configFile}`);
      if (result.backupFile) {
        console.log(`   Backup created: ${result.backupFile}`);
        backupCreated = result.backupFile;
      }
      removed = true;

      // Cleanup old backups
      cleanupOldBackups(configFile);
    } else if (result.backupFile) {
      backupCreated = result.backupFile;
    }
  }

  if (!removed) {
    console.log("✅ Plugin was not registered. Nothing to clean up.");
    log("Plugin was not registered");
  }

  console.log("");
  log("Uninstallation completed", { removed, backupCreated });
} catch (error) {
  log("Uninstallation error", { error: String(error) });
  console.error("❌ " + formatError(error, "clean up config"));
  console.log(`   Check logs: ${LOG_FILE}`);
  process.exit(0); // Don't fail npm uninstall
} finally {
  clearTimeout(timeoutId);
}
