import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { stat } from "node:fs/promises";
import { createRequire } from "node:module";
import { readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";

import { resolvePathFromUserInput, resolveRunxWorkspaceBase } from "../cli-config.js";
import { parseToolManifestJson, validateToolManifest } from "../cli-parser/index.js";
import { errorMessage } from "../cli-util.js";

import {
  hashToolSource,
  isPlainRecord,
  safeReadDir,
  sha256Stable,
  toProjectPath,
  writeJsonFile,
} from "../authoring-utils.js";
import { readCliDependencyVersion } from "../metadata.js";
import { statusIcon, theme } from "../ui.js";

const require = createRequire(import.meta.url);
const tscPath = require.resolve("typescript/bin/tsc");
const toolkitVersion = readCliDependencyVersion("@runxhq/authoring");

export interface ToolCommandArgs {
  readonly toolAction?: "build";
  readonly toolPath?: string;
  readonly toolAll: boolean;
}

export interface ToolBuildReport {
  readonly schema: "runx.tool.build.v1";
  readonly status: "success" | "failure";
  readonly built: readonly {
    readonly path: string;
    readonly manifest: string;
    readonly source_hash: string;
    readonly schema_hash: string;
  }[];
  readonly errors: readonly string[];
}

export async function handleToolBuildCommand(parsed: ToolCommandArgs, env: NodeJS.ProcessEnv): Promise<ToolBuildReport> {
  const root = resolveRunxWorkspaceBase(env);
  await ensureAuthoringRuntimeFresh(root);
  const toolDirs = parsed.toolAll
    ? await discoverToolDirectories(root)
    : [resolvePathFromUserInput(parsed.toolPath ?? "", env)];
  const built: {
    readonly path: string;
    readonly manifest: string;
    readonly source_hash: string;
    readonly schema_hash: string;
  }[] = [];
  const errors: string[] = [];
  for (const toolDir of toolDirs) {
    try {
      const result = await buildToolManifest(root, toolDir);
      built.push(result);
    } catch (error) {
      errors.push(`${toProjectPath(root, toolDir)}: ${errorMessage(error)}`);
    }
  }
  return {
    schema: "runx.tool.build.v1",
    status: errors.length > 0 ? "failure" : "success",
    built,
    errors,
  };
}

export function renderToolCommandResult(result: ToolBuildReport, env: NodeJS.ProcessEnv = process.env): string {
  const t = theme(process.stdout, env);
  const count = result.built.length;
  const lines = [
    "",
    `  ${statusIcon(result.status, t)}  ${t.bold}tool build${t.reset}  ${t.dim}${count} tool(s)${t.reset}`,
  ];
  for (const error of result.errors) {
    lines.push(`  ${t.red}${error}${t.reset}`);
  }
  lines.push("");
  return lines.join("\n");
}

async function buildToolManifest(root: string, toolDir: string): Promise<ToolBuildReport["built"][number]> {
  const manifestPath = path.join(toolDir, "manifest.json");
  const authored = await loadAuthoredToolDefinition(root, toolDir);
  if (!existsSync(manifestPath) && !authored) {
    throw new Error("missing manifest.json");
  }
  const raw = authored ?? JSON.parse(await readFile(manifestPath, "utf8")) as unknown;
  if (!isPlainRecord(raw)) {
    throw new Error("manifest.json must be an object.");
  }
  if (authored) {
    await writeAuthoredToolShim(toolDir);
  }
  const sourceHash = await hashToolSource(toolDir);
  const output = isPlainRecord(raw.output)
    ? raw.output
    : normalizeToolOutput(raw);
  const schemaHash = sha256Stable({
    ...("inputs" in raw ? { inputs: raw.inputs } : {}),
    output,
    ...(isPlainRecord(raw.runx) && raw.runx.artifacts !== undefined ? { artifacts: raw.runx.artifacts } : {}),
  });
  const normalized = {
    schema: "runx.tool.manifest.v1",
    ...raw,
    runtime: isPlainRecord(raw.runtime)
      ? raw.runtime
      : {
          command: isPlainRecord(raw.source) ? raw.source.command ?? "node" : "node",
          args: isPlainRecord(raw.source) ? raw.source.args ?? ["./run.mjs"] : ["./run.mjs"],
        },
    output,
    source_hash: sourceHash,
    schema_hash: schemaHash,
    toolkit_version: toolkitVersion,
  };
  validateToolManifest(parseToolManifestJson(JSON.stringify(normalized)));
  await writeJsonFile(manifestPath, normalized);
  return {
    path: toProjectPath(root, toolDir),
    manifest: toProjectPath(root, manifestPath),
    source_hash: sourceHash,
    schema_hash: schemaHash,
  };
}

async function ensureAuthoringRuntimeFresh(root: string): Promise<void> {
  const authoringSource = path.join(root, "packages", "authoring", "src", "index.ts");
  const authoringDist = path.join(root, "packages", "authoring", "dist", "index.js");
  if (!existsSync(authoringSource)) {
    return;
  }
  const sourceStat = await stat(authoringSource);
  const distStat = existsSync(authoringDist) ? await stat(authoringDist) : undefined;
  if (distStat && distStat.mtimeMs >= sourceStat.mtimeMs) {
    return;
  }
  const result = spawnSync(process.execPath, [
    tscPath,
    "--module", "NodeNext",
    "--moduleResolution", "NodeNext",
    "--target", "ES2022",
    "--declaration",
    "--outDir", "packages/authoring/dist",
    "--rootDir", "packages/authoring/src",
    "packages/authoring/src/index.ts",
  ], {
    cwd: root,
    encoding: "utf8",
    env: process.env,
    shell: false,
  });
  if (result.error) {
    throw result.error;
  }
  if (result.status !== 0) {
    throw new Error(result.stderr || result.stdout || "tsc failed while refreshing @runxhq/authoring.");
  }
}

async function loadAuthoredToolDefinition(root: string, toolDir: string): Promise<Readonly<Record<string, unknown>> | undefined> {
  const sourcePath = path.join(toolDir, "src", "index.ts");
  if (!existsSync(sourcePath)) {
    return undefined;
  }
  let importPath = sourcePath;
  let cleanupPath: string | undefined;
  try {
    const rewritten = await rewriteAuthoredSourceImport(root, sourcePath);
    importPath = rewritten.path;
    cleanupPath = rewritten.cleanupPath;
    const imported = await import(`${pathToFileURL(importPath).href}?runx_build=${Date.now()}`);
    const tool = imported.default;
    if (!isPlainRecord(tool) || typeof tool.name !== "string") {
      return undefined;
    }
    const output = isPlainRecord(tool.output) ? tool.output : undefined;
    const wrapAs = typeof output?.wrap_as === "string" ? output.wrap_as : undefined;
    const namedEmits = isPlainRecord(output?.named_emits) ? output.named_emits : undefined;
    return {
      name: tool.name,
      version: typeof tool.version === "string" ? tool.version : undefined,
      description: typeof tool.description === "string" ? tool.description : undefined,
      source: isPlainRecord(tool.source)
        ? tool.source
        : {
            type: "cli-tool",
            command: "node",
            args: ["./run.mjs"],
          },
      inputs: serializeAuthoringInputs(isPlainRecord(tool.inputs) ? tool.inputs : {}),
      output: output ? output : undefined,
      scopes: Array.isArray(tool.scopes) ? tool.scopes.filter((scope): scope is string => typeof scope === "string") : [],
      runx: wrapAs || namedEmits
        ? {
            artifacts: {
              ...(wrapAs ? { wrap_as: wrapAs } : {}),
              ...(namedEmits ? { named_emits: namedEmits } : {}),
            },
          }
        : undefined,
    };
  } catch {
    return undefined;
  } finally {
    if (cleanupPath) {
      await rm(cleanupPath, { force: true });
    }
  }
}

async function rewriteAuthoredSourceImport(
  root: string,
  sourcePath: string,
): Promise<{ readonly path: string; readonly cleanupPath?: string }> {
  const authoringSourcePath = path.join(root, "packages", "authoring", "src", "index.ts");
  if (!existsSync(authoringSourcePath)) {
    return { path: sourcePath };
  }
  const source = await readFile(sourcePath, "utf8");
  if (!source.includes("@runxhq/authoring")) {
    return { path: sourcePath };
  }
  const relativeAuthoringPath = path.relative(path.dirname(sourcePath), authoringSourcePath).split(path.sep).join("/");
  const authoringSpecifier = relativeAuthoringPath.startsWith(".") ? relativeAuthoringPath : `./${relativeAuthoringPath}`;
  const rewritten = source.replaceAll(`"@runxhq/authoring"`, `"${authoringSpecifier}"`)
    .replaceAll(`'@runxhq/authoring'`, `'${authoringSpecifier}'`);
  const tempPath = path.join(
    path.dirname(sourcePath),
    `.runx-build-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.ts`,
  );
  await writeFile(tempPath, rewritten);
  return {
    path: tempPath,
    cleanupPath: tempPath,
  };
}

function serializeAuthoringInputs(inputs: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> {
  return Object.fromEntries(
    Object.entries(inputs).map(([name, parser]) => {
      const manifest = isPlainRecord(parser) && isPlainRecord(parser.manifest)
        ? parser.manifest
        : { type: "json", required: !(isPlainRecord(parser) && parser.optional === true) };
      return [name, manifest];
    }),
  );
}

async function writeAuthoredToolShim(toolDir: string): Promise<void> {
  await writeFile(
    path.join(toolDir, "run.mjs"),
    [
      "#!/usr/bin/env node",
      "import fs from \"node:fs\";",
      "import path from \"node:path\";",
      "import { fileURLToPath, pathToFileURL } from \"node:url\";",
      "",
      "const here = path.dirname(fileURLToPath(import.meta.url));",
      "const packageRoot = path.resolve(here, \"../../../\");",
      "const relativeToolDir = path.relative(packageRoot, here);",
      "const sourceEntry = path.join(here, \"src\", \"index.ts\");",
      "const distEntry = path.join(packageRoot, \"dist\", relativeToolDir, \"src\", \"index.js\");",
      "const entry = fs.existsSync(distEntry)",
      "  ? distEntry",
      "  : sourceEntry;",
      "const tool = (await import(pathToFileURL(entry).href)).default;",
      "await tool.main();",
      "",
    ].join("\n"),
  );
}

function normalizeToolOutput(raw: Readonly<Record<string, unknown>>): Readonly<Record<string, unknown>> {
  const runx = isPlainRecord(raw.runx) ? raw.runx : undefined;
  const artifacts = isPlainRecord(runx?.artifacts) ? runx.artifacts : undefined;
  if (typeof artifacts?.wrap_as === "string") {
    return { wrap_as: artifacts.wrap_as };
  }
  if (isPlainRecord(artifacts?.named_emits)) {
    return { named_emits: artifacts.named_emits };
  }
  return {};
}

export async function discoverToolDirectories(root: string): Promise<readonly string[]> {
  const toolsRoot = path.join(root, "tools");
  const directories: string[] = [];
  for (const namespaceEntry of await safeReadDir(toolsRoot)) {
    if (!namespaceEntry.isDirectory()) continue;
    for (const toolEntry of await safeReadDir(path.join(toolsRoot, namespaceEntry.name))) {
      if (!toolEntry.isDirectory()) {
        continue;
      }
      const toolDir = path.join(toolsRoot, namespaceEntry.name, toolEntry.name);
      if (existsSync(path.join(toolDir, "manifest.json")) || existsSync(path.join(toolDir, "src", "index.ts"))) {
        directories.push(toolDir);
      }
    }
  }
  return directories.sort();
}

export function resolveToolDirFromRef(root: string, ref: string): string | undefined {
  const parts = ref.split(".").filter(Boolean);
  if (parts.length < 2) return undefined;
  const candidate = path.join(root, "tools", ...parts);
  return existsSync(path.join(candidate, "manifest.json")) ? candidate : undefined;
}
