#!/usr/bin/env node
import { spawn } from "node:child_process";
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import { stdin as input, stdout as output } from "node:process";
import { createInterface } from "node:readline/promises";
import { fileURLToPath } from "node:url";
import { Client } from "basic-ftp";

const cwd = process.cwd();
const args = parseArgs(process.argv.slice(2));
const target = args.target ?? "local";
const sourceDir = resolve(cwd, "dist");
const localConfigFile = resolve(cwd, ".orderly-publish.local.json");
const hydrateScript = fileURLToPath(new URL("./orderly-hydrate-static-pages.mjs", import.meta.url));
const buildCategoryScript = fileURLToPath(new URL("./orderly-build-category-pages.mjs", import.meta.url));

if (target !== "local" && target !== "ftp") {
  throw new Error(`Unsupported publish target "${target}". Use --target local or --target ftp.`);
}

if (!args["skip-build"]) {
  if (!args["no-category-pages"]) {
    await runNode([buildCategoryScript, ...passthroughCategoryBuildArgs(process.argv.slice(2))]);
  } else {
    await runNpm(["run", "build"]);
  }
}

if (!existsSync(sourceDir)) {
  throw new Error("Build output directory does not exist: dist");
}

if (args.hydrate && args["no-category-pages"]) {
  await runNode([hydrateScript, ...passthroughHydrateArgs(process.argv.slice(2))]);
}

if (target === "local") {
  publishLocal(args.dir ?? args.out ?? "published-site");
} else {
  await publishFtp();
}

function publishLocal(outputDirectory) {
  const destination = resolve(cwd, outputDirectory);
  if (destination === sourceDir) {
    throw new Error("Local publish directory cannot be the source directory.");
  }
  rmSync(destination, { recursive: true, force: true });
  mkdirSync(dirname(destination), { recursive: true });
  cpSync(sourceDir, destination, { recursive: true });
  console.log(`Published static site from dist to ${relative(cwd, destination)}.`);
}

async function publishFtp() {
  const saved = loadLocalConfig();
  const configured = args.configure ? {} : saved.ftp ?? {};
  const ftp = await resolveFtpSettings(configured);
  saveLocalConfig({ ...saved, ftp });

  const client = new Client();
  try {
    const connection = ftpConnection(ftp.serverUrl);
    await client.access({
      host: connection.host,
      port: connection.port,
      user: ftp.username,
      password: ftp.password,
      secure: connection.secure
    });
    await client.ensureDir(ftp.folder);
    await client.uploadFromDir(sourceDir);
    console.log(`Published static site from dist to ${ftp.serverUrl}${ftp.folder}.`);
  } finally {
    client.close();
  }
}

async function resolveFtpSettings(configured) {
  const serverUrl = await promptText({
    label: "FTP server URL",
    value: args["server-url"] ?? configured.serverUrl,
    required: true
  });
  const folder = await promptText({
    label: "Remote folder",
    value: args.folder ?? configured.folder,
    required: true
  });
  const username = await promptText({
    label: "Username",
    value: args.username ?? configured.username,
    required: true
  });
  const password = await promptPassword(args.password ?? configured.password);
  return {
    serverUrl,
    folder: normalizeRemoteFolder(folder),
    username,
    password
  };
}

async function promptText({ label, value, required }) {
  if (value && !args.configure) {
    return value;
  }
  if (!input.isTTY) {
    if (value) {
      return value;
    }
    throw new Error(`${label} is required. Pass it as an option or run in an interactive terminal.`);
  }
  const rl = createInterface({ input, output });
  try {
    const suffix = value ? ` [${value}]` : "";
    const answer = (await rl.question(`${label}${suffix}: `)).trim();
    const resolved = answer || value;
    if (required && !resolved) {
      throw new Error(`${label} is required.`);
    }
    return resolved;
  } finally {
    rl.close();
  }
}

async function promptPassword(value) {
  if (value && !args.configure) {
    return value;
  }
  if (!input.isTTY) {
    if (value) {
      return value;
    }
    throw new Error("Password is required. Pass it as --password or run in an interactive terminal.");
  }
  const suffix = value ? " [saved, press Enter to keep]" : "";
  const answer = await promptHidden(`Password${suffix}: `);
  const resolved = answer || value;
  if (!resolved) {
    throw new Error("Password is required.");
  }
  return resolved;
}

function promptHidden(question) {
  return new Promise((resolve, reject) => {
    let value = "";
    const wasRaw = input.isRaw;

    function cleanup() {
      input.off("data", onData);
      if (input.isTTY) {
        input.setRawMode(wasRaw);
      }
    }

    function onData(buffer) {
      const text = buffer.toString("utf8");
      for (const character of text) {
        if (character === "\u0003") {
          cleanup();
          output.write("\n");
          reject(new Error("Cancelled."));
          return;
        }
        if (character === "\r" || character === "\n") {
          cleanup();
          output.write("\n");
          resolve(value);
          return;
        }
        if (character === "\u007f") {
          if (value.length > 0) {
            value = value.slice(0, -1);
            output.write("\b \b");
          }
          continue;
        }
        if (character >= " ") {
          value += character;
          output.write("*");
        }
      }
    }

    output.write(question);
    input.setRawMode(true);
    input.resume();
    input.on("data", onData);
  });
}

function loadLocalConfig() {
  if (!existsSync(localConfigFile)) {
    return {};
  }
  return JSON.parse(readFileSync(localConfigFile, "utf8"));
}

function saveLocalConfig(config) {
  mkdirSync(dirname(localConfigFile), { recursive: true });
  writeFileSync(localConfigFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
  chmodSync(localConfigFile, 0o600);
  console.log(`Saved FTP publish settings to ${relative(cwd, localConfigFile)}.`);
}

function ftpConnection(serverUrl) {
  const url = new URL(serverUrl.includes("://") ? serverUrl : `ftp://${serverUrl}`);
  if (url.protocol !== "ftp:" && url.protocol !== "ftps:") {
    throw new Error("FTP server URL must use ftp:// or ftps://.");
  }
  return {
    host: url.hostname,
    port: url.port ? Number(url.port) : undefined,
    secure: url.protocol === "ftps:"
  };
}

function normalizeRemoteFolder(folder) {
  const normalized = folder.trim().replace(/\\/g, "/").replace(/\/+$/g, "");
  if (!normalized || normalized === "/") {
    throw new Error("Remote folder must not be empty or the FTP root.");
  }
  return normalized.startsWith("/") ? normalized : `/${normalized}`;
}

function parseArgs(values) {
  const parsed = {};
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index];
    if (!value.startsWith("--")) {
      continue;
    }
    const [name, inlineValue] = value.slice(2).split("=", 2);
    if (inlineValue !== undefined) {
      parsed[name] = inlineValue;
    } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
      parsed[name] = values[index + 1];
      index += 1;
    } else {
      parsed[name] = true;
    }
  }
  return parsed;
}

function runNpm(args, env = {}) {
  return new Promise((resolve, reject) => {
    const command = process.platform === "win32" ? "npm.cmd" : "npm";
    const child = spawn(command, args, {
      env: { ...process.env, ...env },
      stdio: "inherit"
    });
    child.on("error", reject);
    child.on("exit", (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`npm ${args.join(" ")} exited with code ${code}.`));
      }
    });
  });
}

function runNode(args) {
  return new Promise((resolve, reject) => {
    const child = spawn(process.execPath, args, {
      env: process.env,
      stdio: "inherit"
    });
    child.on("error", reject);
    child.on("exit", (code) => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error(`${process.execPath} ${args.join(" ")} exited with code ${code}.`));
      }
    });
  });
}

function passthroughHydrateArgs(values) {
  const result = [];
  const blocked = new Set([
    "target",
    "dir",
    "out",
    "skip-build",
    "category-pages",
    "no-category-pages",
    "hydrate",
    "server-url",
    "folder",
    "username",
    "password",
    "configure"
  ]);
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index];
    const name = value.startsWith("--") ? value.slice(2).split("=", 1)[0] : "";
    if (blocked.has(name)) {
      if (!value.includes("=") && values[index + 1] && !values[index + 1].startsWith("--")) {
        index += 1;
      }
      continue;
    }
    result.push(value);
  }
  return result;
}

function passthroughCategoryBuildArgs(values) {
  const result = [];
  const blocked = new Set([
    "target",
    "dir",
    "out",
    "skip-build",
    "category-pages",
    "no-category-pages",
    "hydrate",
    "server-url",
    "folder",
    "username",
    "password",
    "configure"
  ]);
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index];
    const name = value.startsWith("--") ? value.slice(2).split("=", 1)[0] : "";
    if (blocked.has(name)) {
      if (!value.includes("=") && values[index + 1] && !values[index + 1].startsWith("--")) {
        index += 1;
      }
      continue;
    }
    result.push(value);
  }
  return result;
}
