#!/usr/bin/env node
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import {
  runDevBaseImagePreflight,
  summarizeForRuntimeBase
} from "./src/dev-base-image-preflight.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const defaultRegistryPrefix =
  process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
const defaultBaseImage = process.env.HWLAB_DEV_RUNTIME_BASE_PARENT || process.env.HWLAB_DEV_BASE_IMAGE || null;

function parseArgs(argv) {
  const args = {
    registryPrefix: defaultRegistryPrefix,
    baseImage: defaultBaseImage,
    tag: null,
    build: true,
    push: false,
    help: false
  };

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--registry-prefix") args.registryPrefix = readOption(argv, ++index, arg);
    else if (arg === "--base-image") args.baseImage = readOption(argv, ++index, arg);
    else if (arg === "--tag") args.tag = readOption(argv, ++index, arg);
    else if (arg === "--dry-run" || arg === "--plan") args.build = false;
    else if (arg === "--push") args.push = true;
    else if (arg === "--help" || arg === "-h") args.help = true;
    else throw new Error(`unknown argument ${arg}`);
  }

  return args;
}

function readOption(argv, index, name) {
  const value = argv[index];
  if (!value || value.startsWith("--")) {
    throw new Error(`${name} requires a value`);
  }
  return value;
}

function usage() {
  return [
    "usage: node scripts/dev-runtime-base-image.mjs [--dry-run] [--push] [--base-image IMAGE] [--tag IMAGE]",
    "",
    "Build an optional D601-local HWLAB Node runtime base image with package dependencies preinstalled.",
    "",
    "options:",
    "  --registry-prefix PREFIX  default: 127.0.0.1:5000/hwlab",
    "  --base-image IMAGE        approved local parent image; default resolved by DEV base preflight",
    "  --tag IMAGE               output image tag; default: <registry>/hwlab-node-runtime-base:deps-<hash>",
    "  --dry-run                 print the build plan without building",
    "  --push                    push the resulting base image to the local/internal registry"
  ].join("\n");
}

function normalizeRegistryPrefix(prefix) {
  const normalized = prefix.replace(/\/+$/u, "");
  const [hostPort, ...pathParts] = normalized.split("/");
  const host = hostPort.split(":")[0].toLowerCase();
  assert.ok(hostPort && pathParts.length > 0, "registry prefix must include host and namespace");
  assert.ok(!/prod|production/iu.test(normalized), "registry prefix must not include prod");
  assert.ok(
    host === "localhost" ||
      host === "127.0.0.1" ||
      /^10\./u.test(host) ||
      /^192\.168\./u.test(host) ||
      /^172\.(1[6-9]|2\d|3[0-1])\./u.test(host) ||
      host.endsWith(".local") ||
      host.endsWith(".svc") ||
      host.endsWith(".cluster.local"),
    "registry prefix must target localhost or a private/internal host"
  );
  return normalized;
}

async function fileOrEmpty(relativePath) {
  try {
    return await readFile(path.join(repoRoot, relativePath), "utf8");
  } catch (error) {
    if (error?.code === "ENOENT") return "";
    throw error;
  }
}

async function dependencyHash() {
  const packageJson = await fileOrEmpty("package.json");
  const packageLockJson = await fileOrEmpty("package-lock.json");
  assert.ok(packageJson.trim(), "package.json is required");
  const hash = createHash("sha256");
  hash.update("package.json\0");
  hash.update(packageJson);
  hash.update("\0package-lock.json\0");
  hash.update(packageLockJson);
  return hash.digest("hex");
}

function dockerfile(parentImage, depsHash) {
  return [
    `FROM ${parentImage}`,
    "WORKDIR /opt/hwlab-node-runtime-base",
    `LABEL hwlab.pikastech.local/runtime-base-deps-hash=${depsHash}`,
    "COPY package.json ./package.json",
    "COPY package-lock.json* ./",
    "RUN if [ -f package-lock.json ]; then npm ci --omit=dev --ignore-scripts; else npm install --omit=dev --ignore-scripts; fi",
    ""
  ].join("\n");
}

function commandLine(command, args) {
  return [command, ...args].map((part) => (/\s/u.test(part) ? JSON.stringify(part) : part)).join(" ");
}

function runWithInput(command, args, input) {
  const startedAt = Date.now();
  return new Promise((resolve) => {
    const child = spawn(command, args, {
      cwd: repoRoot,
      env: process.env,
      stdio: ["pipe", "pipe", "pipe"]
    });
    let stdout = "";
    let stderr = "";
    child.stdin.end(input);
    child.stdout.on("data", (chunk) => {
      stdout += chunk;
    });
    child.stderr.on("data", (chunk) => {
      stderr += chunk;
    });
    child.on("error", (error) => {
      resolve({
        command: commandLine(command, args),
        code: 127,
        stdout,
        stderr: error.message,
        durationMs: Date.now() - startedAt
      });
    });
    child.on("close", (code) => {
      resolve({
        command: commandLine(command, args),
        code: code ?? 1,
        stdout,
        stderr,
        durationMs: Date.now() - startedAt
      });
    });
  });
}

function run(command, args) {
  const startedAt = Date.now();
  return new Promise((resolve) => {
    const child = spawn(command, args, {
      cwd: repoRoot,
      env: process.env,
      stdio: ["ignore", "pipe", "pipe"]
    });
    let stdout = "";
    let stderr = "";
    child.stdout.on("data", (chunk) => {
      stdout += chunk;
    });
    child.stderr.on("data", (chunk) => {
      stderr += chunk;
    });
    child.on("error", (error) => {
      resolve({
        command: commandLine(command, args),
        code: 127,
        stdout,
        stderr: error.message,
        durationMs: Date.now() - startedAt
      });
    });
    child.on("close", (code) => {
      resolve({
        command: commandLine(command, args),
        code: code ?? 1,
        stdout,
        stderr,
        durationMs: Date.now() - startedAt
      });
    });
  });
}

function tailText(value, maxLength = 2500) {
  const text = value.trim();
  if (text.length <= maxLength) return text;
  return text.slice(text.length - maxLength);
}

function inspectDigest(pushOutput) {
  const matches = [...pushOutput.matchAll(/digest:\s+(sha256:[a-f0-9]{64})/giu)];
  return matches.length ? matches[matches.length - 1][1] : null;
}

async function main() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    console.log(usage());
    return;
  }

  args.registryPrefix = normalizeRegistryPrefix(args.registryPrefix);
  const depsHash = await dependencyHash();
  const image = args.tag ?? `${args.registryPrefix}/hwlab-node-runtime-base:deps-${depsHash.slice(0, 12)}`;
  const baseImagePreflight = await runDevBaseImagePreflight({
    env: args.baseImage ? { ...process.env, HWLAB_DEV_BASE_IMAGE: args.baseImage } : process.env
  });
  const parentImage = baseImagePreflight.localTag;
  const plan = {
    status: args.build ? "planned" : "dry-run",
    image,
    parentImage,
    depsHash,
    push: args.push,
    baseImagePreflight: summarizeForRuntimeBase(baseImagePreflight)
  };

  if (!baseImagePreflight.publishUsable) {
    console.log(JSON.stringify({
      ...plan,
      status: "blocked",
      blockers: baseImagePreflight.blockers,
      nextSteps: baseImagePreflight.nextSteps
    }, null, 2));
    process.exitCode = 2;
    return;
  }

  if (!args.build) {
    console.log(JSON.stringify(plan, null, 2));
    return;
  }

  const build = await runWithInput(
    "docker",
    ["build", "--pull=false", "-t", image, "-f", "-", "."],
    dockerfile(parentImage, depsHash)
  );
  if (build.code !== 0) {
    console.log(JSON.stringify({
      ...plan,
      status: "build_failed",
      command: build.command,
      durationMs: build.durationMs,
      logTail: tailText(`${build.stdout}\n${build.stderr}`)
    }, null, 2));
    process.exitCode = 1;
    return;
  }

  const inspect = await run("docker", ["image", "inspect", "--format", "{{.Id}}", image]);
  let push = null;
  if (args.push) {
    const pushResult = await run("docker", ["push", image]);
    const digest = pushResult.code === 0 ? inspectDigest(`${pushResult.stdout}\n${pushResult.stderr}`) : null;
    push = {
      status: pushResult.code === 0 && digest ? "published" : "blocked",
      command: pushResult.command,
      code: pushResult.code,
      durationMs: pushResult.durationMs,
      digest,
      logTail: tailText(`${pushResult.stdout}\n${pushResult.stderr}`, 1200)
    };
  }

  const status = push ? push.status : "built";
  console.log(JSON.stringify({
    ...plan,
    status,
    imageId: inspect.code === 0 ? inspect.stdout.trim() : null,
    build: {
      command: build.command,
      durationMs: build.durationMs,
      logTail: tailText(`${build.stdout}\n${build.stderr}`, 1200)
    },
    push
  }, null, 2));
  process.exitCode = status === "blocked" ? 2 : 0;
}

main().catch((error) => {
  console.error(JSON.stringify({
    status: "failed",
    error: error instanceof Error ? error.message : String(error)
  }, null, 2));
  process.exitCode = 1;
});
