import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import packageJson from "../../package.json";
import type {
  EcosystemPackage,
  EcosystemPackageState,
  EcosystemRecipe,
  EcosystemRunnerPreset,
} from "../lib/ecosystemContract";
import { npmSpawnCommand } from "./platformCommands";

export type PackageDefinition = {
  id: string;
  name: string;
  npmName: string;
  capability: string;
  description: string;
  workflow: string;
  primary: boolean;
};

type NpmResult = {
  ok: boolean;
  stdout: string;
};

export const ECOSYSTEM_PACKAGES: PackageDefinition[] = [
  {
    id: "inspector",
    name: "Agent Inspector",
    npmName: "@tonyclaw/agent-inspector",
    capability: "Observe",
    description: "Captures proxy traffic, sessions, turns, tools, providers, and raw bodies.",
    workflow: "Keep every agent run visible and reloadable.",
    primary: true,
  },
  {
    id: "mcp",
    name: "Inspector MCP",
    npmName: "@tonyclaw/agent-inspector-mcp",
    capability: "Connect",
    description: "Exposes sessions, logs, providers, runs, and evidence to MCP clients.",
    workflow: "Let Codex, OpenCode, MiMo Code, and other tools query Inspector as memory.",
    primary: true,
  },
  {
    id: "eval-harness",
    name: "Eval Harness",
    npmName: "@tonyclaw/eval-harness",
    capability: "Evaluate",
    description:
      "Runs MCP-aware agent evaluations, runner presets, regressions, and model bakeoffs.",
    workflow: "Promote Inspector evidence into repeatable evaluation batches.",
    primary: false,
  },
];

export const RUNNER_PRESETS: EcosystemRunnerPreset[] = [
  {
    id: "opencode",
    name: "OpenCode",
    validatedVersion: "1.17.11",
    binary: "opencode",
    modelFlag: "--model",
    variantFlag: "--variant",
    tools: ["opencode", "node"],
    logDir: "opencode",
    skillsDir: ".opencode/skills",
    agentsDir: ".opencode/agents",
  },
  {
    id: "mimo",
    name: "MiMo Code",
    validatedVersion: "0.1.4",
    binary: "mimo",
    modelFlag: "--model",
    variantFlag: "--agent",
    tools: ["mimo", "node"],
    logDir: "mimocode",
    skillsDir: ".mimocode/skills",
    agentsDir: ".mimocode/agents",
  },
  {
    id: "codex",
    name: "Codex CLI",
    validatedVersion: "0.142.5",
    binary: "codex",
    modelFlag: "--model",
    variantFlag: null,
    tools: ["codex"],
    logDir: "codex",
    skillsDir: ".codex/skills",
    agentsDir: ".codex/agents",
  },
  {
    id: "claude",
    name: "Claude Code",
    validatedVersion: "2.1.195",
    binary: "claude",
    modelFlag: "--model",
    variantFlag: null,
    tools: ["claude"],
    logDir: "claude",
    skillsDir: ".claude/skills",
    agentsDir: ".claude/agents",
  },
];

export const LAB_RECIPES: EcosystemRecipe[] = [
  {
    id: "inspector-mcp-smoke",
    title: "Inspector MCP smoke",
    stage: "Validate",
    packageId: "eval-harness",
    description: "Check that the running Inspector exposes the MCP tools eval-harness expects.",
    command: "npx @tonyclaw/eval-harness check-inspector-mcp",
    requiresSession: false,
    runnable: true,
  },
  {
    id: "session-evidence-smoke",
    title: "Session evidence smoke",
    stage: "Capture",
    packageId: "eval-harness",
    description:
      "Create a lightweight Inspector evidence run from the latest captured logs or selected session.",
    command:
      'npx @tonyclaw/eval-harness inspector-smoke --title "TonyClaw Lab smoke" --latest-log-limit 5',
    requiresSession: false,
    runnable: true,
  },
  {
    id: "runner-presets-json",
    title: "Runner presets JSON",
    stage: "Connect",
    packageId: "eval-harness",
    description: "Export runner adapter baselines as machine-readable JSON for CI or docs.",
    command: "npx @tonyclaw/eval-harness runner-presets --format json",
    requiresSession: false,
    runnable: true,
  },
  {
    id: "local-smoke-batch",
    title: "Local smoke batch",
    stage: "Evaluate",
    packageId: "eval-harness",
    description:
      "Run one short local Jenkins-like evaluation using explicit project/work directories.",
    command:
      "npx @tonyclaw/eval-harness run-once --smoke --project-source <project> --work-source <work> --count 1 --parallel 1",
    requiresSession: false,
    runnable: false,
  },
];

const NPM_TIMEOUT_MS = 4000;

function runNpm(args: readonly string[]): Promise<NpmResult> {
  return new Promise((resolve) => {
    const command = npmSpawnCommand(args);
    try {
      execFile(
        command.command,
        command.args,
        { timeout: NPM_TIMEOUT_MS, windowsHide: true },
        (error, stdout) => {
          resolve({
            ok: error === null,
            stdout: String(stdout).trim(),
          });
        },
      );
    } catch {
      resolve({ ok: false, stdout: "" });
    }
  });
}

async function readGlobalNpmRoot(): Promise<string | null> {
  const result = await runNpm(["root", "-g"]);
  if (!result.ok || result.stdout.length === 0) return null;
  return result.stdout;
}

function readGlobalNpmRootFallback(): string | null {
  switch (process.platform) {
    case "win32": {
      const appData = process.env["APPDATA"];
      if (appData === undefined || appData.length === 0) return null;
      return join(appData, "npm", "node_modules");
    }
    case "darwin":
    case "linux":
    case "aix":
    case "freebsd":
    case "openbsd":
    case "sunos": {
      const prefix = process.env["PREFIX"];
      if (prefix !== undefined && prefix.length > 0) return join(prefix, "lib", "node_modules");
      return "/usr/local/lib/node_modules";
    }
    case "android":
    case "cygwin":
    case "haiku":
    case "netbsd":
      return null;
  }

  return null;
}

function packageJsonPath(globalRoot: string, npmName: string): string {
  const parts = npmName.split("/");
  return join(globalRoot, ...parts, "package.json");
}

function readJsonStringField(raw: unknown, field: string): string | null {
  if (typeof raw !== "object" || raw === null) return null;
  const descriptor = Object.getOwnPropertyDescriptor(raw, field);
  if (descriptor === undefined) return null;
  return typeof descriptor.value === "string" ? descriptor.value : null;
}

async function readInstalledVersion(
  globalRoot: string | null,
  npmName: string,
): Promise<string | null> {
  if (npmName === packageJson.name) return packageJson.version;
  if (globalRoot === null) return null;

  try {
    const raw = await readFile(packageJsonPath(globalRoot, npmName), "utf8");
    return readJsonStringField(JSON.parse(raw), "version");
  } catch {
    return null;
  }
}

async function readLatestVersion(npmName: string): Promise<string | null> {
  const result = await runNpm(["view", npmName, "version", "--json"]);
  if (!result.ok || result.stdout.length === 0) return null;

  try {
    const parsed: unknown = JSON.parse(result.stdout);
    return typeof parsed === "string" && parsed.length > 0 ? parsed : null;
  } catch {
    return result.stdout.length > 0 ? result.stdout : null;
  }
}

function packageState(
  installedVersion: string | null,
  latestVersion: string | null,
): EcosystemPackageState {
  if (installedVersion !== null && latestVersion !== null && installedVersion !== latestVersion) {
    return "update-available";
  }
  if (installedVersion !== null) return "installed";
  if (latestVersion !== null) return "available";
  return "planned";
}

async function describePackage(
  definition: PackageDefinition,
  globalRoot: string | null,
): Promise<EcosystemPackage> {
  const [installedVersion, latestVersion] = await Promise.all([
    readInstalledVersion(globalRoot, definition.npmName),
    readLatestVersion(definition.npmName),
  ]);
  const state = packageState(installedVersion, latestVersion);

  return {
    ...definition,
    installCommand: `npm install -g ${definition.npmName}`,
    installedVersion,
    latestVersion,
    state,
  };
}

export async function listEcosystemPackages(): Promise<EcosystemPackage[]> {
  const globalRoot = (await readGlobalNpmRoot()) ?? readGlobalNpmRootFallback();
  return Promise.all(
    ECOSYSTEM_PACKAGES.map((definition) => describePackage(definition, globalRoot)),
  );
}

export function findEcosystemPackage(packageId: string): PackageDefinition | null {
  return ECOSYSTEM_PACKAGES.find((definition) => definition.id === packageId) ?? null;
}

export function findEcosystemRecipe(recipeId: string): EcosystemRecipe | null {
  return LAB_RECIPES.find((recipe) => recipe.id === recipeId) ?? null;
}
