#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import * as readline from "node:readline";
import { createInterface } from "node:readline/promises";
import {
  ensureTargetDir,
  fetchTemplateRepos,
  installTemplateRepo,
  parseArgs,
  resolveTemplateRepo,
  selectTemplateRepo,
  usage,
} from "../lib/scaffold.mjs";

function clearMenu(lines) {
  if (lines <= 0) {
    return;
  }
  readline.moveCursor(process.stdout, 0, -lines);
  readline.clearScreenDown(process.stdout);
}

function renderRepoSelect(label, repos, cursor) {
  const lines = [`${label} Use ↑/↓ and Enter.`];
  for (let index = 0; index < repos.length; index += 1) {
    const pointer = index === cursor ? ">" : " ";
    const summary = repos[index].description ? ` - ${repos[index].description}` : "";
    lines.push(`${pointer} ${repos[index].name}${summary}`);
  }
  process.stdout.write(`${lines.join("\n")}\n`);
  return lines.length;
}

function renderChoiceSelect(label, choices, cursor) {
  const lines = [`${label} Use ↑/↓ and Enter.`];
  for (let index = 0; index < choices.length; index += 1) {
    const pointer = index === cursor ? ">" : " ";
    const summary = choices[index].description ? ` - ${choices[index].description}` : "";
    lines.push(`${pointer} ${choices[index].label}${summary}`);
  }
  process.stdout.write(`${lines.join("\n")}\n`);
  return lines.length;
}

async function runArrowMenu(render, onInput) {
  const stdin = process.stdin;
  const stdout = process.stdout;
  const previousRawMode = typeof stdin.setRawMode === "function" ? stdin.isRaw : undefined;

  if (typeof stdin.setRawMode === "function") {
    stdin.setRawMode(true);
  }
  stdin.resume();
  stdin.setEncoding("utf8");

  return new Promise((resolve, reject) => {
    let renderedLines = render();

    const cleanup = () => {
      stdin.removeListener("data", handleData);
      if (typeof stdin.setRawMode === "function") {
        stdin.setRawMode(Boolean(previousRawMode));
      }
      stdout.write("\n");
    };

    const rerender = () => {
      clearMenu(renderedLines);
      renderedLines = render();
    };

    const handleData = (chunk) => {
      try {
        const result = onInput(chunk, rerender);
        if (result !== undefined) {
          clearMenu(renderedLines);
          cleanup();
          resolve(result);
        }
      } catch (error) {
        clearMenu(renderedLines);
        cleanup();
        reject(error);
      }
    };

    stdin.on("data", handleData);
  });
}

async function promptForTemplateRepo(rl, repos, owner) {
  if (process.stdin.isTTY && process.stdout.isTTY) {
    console.log("");
    let cursor = 0;
    return runArrowMenu(
      () => renderRepoSelect(`Public template repos from ${owner}:`, repos, cursor),
      (chunk, rerender) => {
        if (chunk === "\u0003") {
          throw new Error("Prompt cancelled");
        }
        if (chunk === "\r" || chunk === "\n") {
          return repos[cursor].name;
        }
        if (chunk === "\u001b[A") {
          cursor = (cursor - 1 + repos.length) % repos.length;
          rerender();
        } else if (chunk === "\u001b[B") {
          cursor = (cursor + 1) % repos.length;
          rerender();
        }
        return undefined;
      },
    );
  }

  console.log("");
  console.log(`Public template repos from ${owner}:`);
  repos.forEach((repo, index) => {
    const summary = repo.description ? ` - ${repo.description}` : "";
    console.log(`  ${index + 1}. ${repo.name}${summary}`);
  });
  console.log("");

  while (true) {
    const answer = await rl.question("Select template repo (number or repo name): ");
    try {
      return selectTemplateRepo(answer, repos).name;
    } catch (error) {
      console.log(error.message);
    }
  }
}

async function promptForChoice(rl, label, choices) {
  if (process.stdin.isTTY && process.stdout.isTTY) {
    console.log("");
    let cursor = 0;
    return runArrowMenu(
      () => renderChoiceSelect(label, choices, cursor),
      (chunk, rerender) => {
        if (chunk === "\u0003") {
          throw new Error("Prompt cancelled");
        }
        if (chunk === "\r" || chunk === "\n") {
          return choices[cursor].value;
        }
        if (chunk === "\u001b[A") {
          cursor = (cursor - 1 + choices.length) % choices.length;
          rerender();
        } else if (chunk === "\u001b[B") {
          cursor = (cursor + 1) % choices.length;
          rerender();
        }
        return undefined;
      },
    );
  }

  console.log("");
  console.log(label);
  choices.forEach((choice, index) => {
    const summary = choice.description ? ` - ${choice.description}` : "";
    console.log(`  ${index + 1}. ${choice.label}${summary}`);
  });
  console.log("");

  while (true) {
    const answer = (await rl.question("Select option: ")).trim();
    if (/^\d+$/.test(answer)) {
      const index = Number.parseInt(answer, 10) - 1;
      if (index >= 0 && index < choices.length) {
        return choices[index].value;
      }
    }
    const exact = choices.find((choice) => choice.label === answer || choice.value === answer);
    if (exact) {
      return exact.value;
    }
    console.log(`Invalid selection: ${answer || "(empty)"}`);
  }
}

function directoryHasUserFiles(targetDir) {
  const resolved = path.resolve(process.cwd(), targetDir);
  if (!fs.existsSync(resolved)) {
    return false;
  }

  const entries = fs
    .readdirSync(resolved, { withFileTypes: true })
    .filter((entry) => entry.name !== ".git");
  return entries.length > 0;
}

function printPlannedRun(options) {
  console.log("");
  console.log("Plan:");
  console.log(`  Project directory: ${path.resolve(process.cwd(), options.targetDir)}`);
  console.log(`  Template repo: ${options.template}`);
  console.log(`  GitHub owner: ${options.owner}`);
  console.log(`  Allow non-empty directory: ${options.force ? "yes" : "no"}`);
  console.log(`  Run install/bootstrap: ${options.skipBootstrap ? "no" : "yes"}`);
}

async function promptForMissing(options, repos) {
  if (options.yes && (!options.targetDir || !options.template)) {
    throw new Error("`--yes` requires both target directory and template repo.");
  }

  if (options.targetDir && options.template) {
    return options;
  }

  const rl = createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  try {
    while (!options.targetDir) {
      const answer = await rl.question("Project directory: ");
      options.targetDir = answer.trim();
      if (!options.targetDir) {
        console.log("Project directory cannot be empty.");
      }
    }

    if (!options.template) {
      options.template = await promptForTemplateRepo(rl, repos, options.owner);
    }

    if (!options.force && directoryHasUserFiles(options.targetDir)) {
      const allowOverwrite = await promptForChoice(rl, "Target directory is not empty.", [
        { label: "Continue", value: true, description: "Scaffold into the existing directory" },
        { label: "Cancel", value: false, description: "Stop without changing files" },
      ]);
      if (!allowOverwrite) {
        throw new Error("Prompt cancelled");
      }
      options.force = true;
    }

    if (!options.yes) {
      printPlannedRun(options);
      const proceed = await promptForChoice(rl, "Run scaffold now?", [
        { label: "Proceed", value: true, description: "Clone, install, and bootstrap now" },
        { label: "Cancel", value: false, description: "Stop before running commands" },
      ]);
      if (!proceed) {
        throw new Error("Prompt cancelled");
      }
    }
  } finally {
    rl.close();
  }

  return options;
}

function printSuccess(result) {
  console.log("");
  console.log(`Initialized template repo: ${result.templateRepo}`);
  console.log(`Destination: ${result.destinationRoot}`);
  if (result.executedSteps.length > 0) {
    console.log("Completed:");
    for (const step of result.executedSteps) {
      console.log(`  ${step}`);
    }
  }
  if (result.nextSteps.length > 0) {
    console.log("Next steps:");
    for (const step of result.nextSteps) {
      console.log(`  ${step}`);
    }
  }
}

async function main() {
  let options;
  try {
    options = parseArgs(process.argv.slice(2));
  } catch (error) {
    console.error(error.message);
    console.error("");
    console.error(usage());
    process.exitCode = 1;
    return;
  }

  if (options.command === "help") {
    console.log(usage());
    return;
  }

  if (options.command !== "init") {
    if (!options.targetDir) {
      options.targetDir = options.command;
      options.command = "init";
    } else {
      console.error(`Unsupported command: ${options.command}`);
      console.error("");
      console.error(usage());
      process.exitCode = 1;
      return;
    }
  }

  try {
    const repos = await fetchTemplateRepos({ owner: options.owner });
    if (repos.length === 0) {
      throw new Error(`No public template-* repos found for ${options.owner}.`);
    }

    options = await promptForMissing(options, repos);
    const selectedRepo = resolveTemplateRepo(options.template, repos);
    const destinationRoot = ensureTargetDir(options.targetDir, { force: options.force });
    const result = installTemplateRepo({
      destinationRoot: path.resolve(destinationRoot),
      templateRepo: selectedRepo,
      skipBootstrap: options.skipBootstrap,
    });
    printSuccess(result);
  } catch (error) {
    console.error(error.message);
    process.exitCode = 1;
  }
}

await main();
