#!/usr/bin/env node

import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { env } from "node:process";
import {
  apiJson,
  ensureEvidence,
  evidencePaths,
  loadEnvFiles,
  resetAndAuthLocalUser,
  writeResult,
} from "./lib/langbot-e2e.mjs";

const caseId = env.LBS_CASE_ID || "install-qa-plugin-smoke";
const paths = evidencePaths(caseId);
await loadEnvFiles();
await ensureEvidence(paths);

const backendUrl = env.LANGBOT_BACKEND_URL || "";
const user = env.LANGBOT_E2E_LOGIN_USER || "";
const password = env.LANGBOT_E2E_LOGIN_PASSWORD || "LangBotE2ELocalPass!2026";
const packagePath = resolve(
  env.LANGBOT_E2E_PLUGIN_PACKAGE
    || env.LANGBOT_QA_PLUGIN_SMOKE_PACKAGE
    || "skills/langbot-testing/fixtures/plugins/qa-plugin-smoke/dist/qa-plugin-smoke-0.1.0.lbpkg",
);
const expectedPluginId = env.LANGBOT_E2E_EXPECTED_PLUGIN_ID || "qa/plugin-smoke";
const expectedTool = env.LANGBOT_E2E_EXPECTED_TOOL || (expectedPluginId === "qa/plugin-smoke" ? "qa_plugin_echo" : "");
const expectedRunnerId = env.LANGBOT_E2E_EXPECTED_RUNNER_ID || "";

const result = {
  source: "automation",
  case_id: caseId,
  run_id: paths.runId,
  status: "fail",
  reason: "",
  backend_url: backendUrl,
  package_path: packagePath,
  package_preview: null,
  task_id: null,
  task: null,
  plugin_present_before: false,
  plugin_present_after: false,
  tool_names: [],
  runner_ids: [],
  evidence: {
    automation_result_json: paths.automationResultJson,
    result_json: paths.resultJson,
  },
  evidence_collected: ["api_diagnostic", "filesystem"],
};

try {
  if (!backendUrl) throw new Error("LANGBOT_BACKEND_URL is not configured.");
  if (!user) throw new Error("LANGBOT_E2E_LOGIN_USER is required.");
  const bytes = await readFile(packagePath);

  const auth = await resetAndAuthLocalUser({ backendUrl, user, password });
  result.package_preview = await previewPackage(backendUrl, auth.token, bytes, packagePath);
  const metadata = result.package_preview.metadata || {};
  if (`${metadata.author}/${metadata.name}` !== expectedPluginId) {
    throw new Error(`Fixture package metadata is ${metadata.author}/${metadata.name}, expected ${expectedPluginId}.`);
  }
  result.plugin_present_before = await hasPlugin(backendUrl, auth.token);

  if (!result.plugin_present_before) {
    const form = new FormData();
    form.set("file", new Blob([bytes]), packagePath.split("/").pop());
    const response = await fetch(`${backendUrl.replace(/\/$/, "")}/api/v1/plugins/install/local`, {
      method: "POST",
      headers: { Authorization: `Bearer ${auth.token}` },
      body: form,
    });
    const json = await response.json().catch(() => ({}));
    if (response.status >= 400 || json.code !== 0) {
      throw new Error(json.msg || `Plugin install request failed with HTTP ${response.status}.`);
    }
    result.task_id = json.data?.task_id ?? null;
    if (!result.task_id) throw new Error("Plugin install response did not include task_id.");
    result.task = await waitForTask(backendUrl, auth.token, result.task_id);
    if (!isTaskComplete(result.task)) {
      throw new Error(`Plugin install task did not complete successfully: ${JSON.stringify(result.task)}`);
    }
  }

  await sleep(1000);
  result.plugin_present_after = await hasPlugin(backendUrl, auth.token);
  if (!result.plugin_present_after) throw new Error(`${expectedPluginId} is not listed by /api/v1/plugins after install.`);
  if (expectedTool) {
    result.tool_names = await listToolNames(backendUrl, auth.token);
    if (!result.tool_names.includes(expectedTool)) {
      throw new Error(`${expectedTool} is not listed by /api/v1/tools after install.`);
    }
  }
  if (expectedRunnerId) {
    result.runner_ids = await listRunnerIds(backendUrl, auth.token);
    if (!result.runner_ids.includes(expectedRunnerId)) {
      throw new Error(`${expectedRunnerId} is not listed by /api/v1/pipelines/_/metadata after install.`);
    }
  }

  result.status = "pass";
  result.reason = `${expectedPluginId} is installed.`;
} catch (error) {
  result.status = "fail";
  result.reason = error.message;
} finally {
  await writeResult(paths, result);
  console.log(JSON.stringify(result, null, 2));
}

process.exit(result.status === "pass" ? 0 : 1);

async function hasPlugin(backendUrl, token) {
  const response = await apiJson(backendUrl, "/api/v1/plugins", { token });
  const plugins = response.json.data?.plugins || [];
  return plugins.some((plugin) => {
    const metadata = plugin.manifest?.manifest?.metadata || plugin.manifest?.metadata || plugin.metadata || {};
    return `${metadata.author}/${metadata.name}` === expectedPluginId;
  });
}

async function previewPackage(backendUrl, token, bytes, packagePath) {
  const form = new FormData();
  form.set("file", new Blob([bytes]), packagePath.split("/").pop());
  const response = await fetch(`${backendUrl.replace(/\/$/, "")}/api/v1/plugins/install/local/preview`, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}` },
    body: form,
  });
  const json = await response.json().catch(() => ({}));
  if (response.status >= 400 || json.code !== 0) {
    throw new Error(json.msg || `Plugin package preview failed with HTTP ${response.status}.`);
  }
  return {
    metadata: json.data?.metadata || {},
    component_types: json.data?.component_types || [],
    file_count: json.data?.file_count ?? null,
  };
}

async function listToolNames(backendUrl, token) {
  const response = await apiJson(backendUrl, "/api/v1/tools", { token });
  return (response.json.data?.tools || [])
    .map((tool) => tool.name || tool.tool_name || tool.function?.name || "")
    .filter(Boolean)
    .sort();
}

async function listRunnerIds(backendUrl, token) {
  const response = await apiJson(backendUrl, "/api/v1/pipelines/_/metadata", { token });
  const configs = response.json.data?.configs || [];
  return configs
    .flatMap((section) => section.stages || [])
    .flatMap((stage) => stage.config || [])
    .filter((item) => item.name === "id")
    .flatMap((item) => item.options || [])
    .map((option) => option.name || option.value || option.id || "")
    .filter(Boolean)
    .sort();
}

async function waitForTask(backendUrl, token, taskId) {
  const deadline = Date.now() + Number(env.LANGBOT_PLUGIN_INSTALL_TIMEOUT_MS || 120000);
  let last = null;
  while (Date.now() < deadline) {
    const response = await apiJson(backendUrl, `/api/v1/system/tasks/${encodeURIComponent(taskId)}`, { token });
    last = response.json.data || response.json;
    if (isTaskComplete(last) || isTaskFailed(last)) return last;
    await sleep(1000);
  }
  return last;
}

function isTaskComplete(task) {
  const status = String(task?.status || task?.state || "").toLowerCase();
  const runtimeStatus = String(task?.runtime?.status || task?.runtime?.state || "").toLowerCase();
  return ["done", "completed", "success", "succeeded", "finished"].includes(status)
    || ["done", "completed", "success", "succeeded", "finished"].includes(runtimeStatus)
    || task?.done === true
    || task?.completed === true
    || (task?.runtime?.done === true && !task?.runtime?.exception);
}

function isTaskFailed(task) {
  const status = String(task?.status || task?.state || "").toLowerCase();
  const runtimeStatus = String(task?.runtime?.status || task?.runtime?.state || "").toLowerCase();
  return ["failed", "error", "cancelled", "canceled"].includes(status)
    || ["failed", "error", "cancelled", "canceled"].includes(runtimeStatus)
    || task?.failed === true
    || Boolean(task?.error)
    || Boolean(task?.runtime?.exception);
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
