import { spawn, spawnSync } from "node:child_process";
import { readdirSync, mkdirSync } from "node:fs";
import { mkdtemp, writeFile, readFile, copyFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, normalize } from "node:path";
import { createInterface } from "node:readline";
import { program } from "commander";
import fetchRetryWrapper from "fetch-retry";
import type { HealthcheckResponse } from "../src/pages/api/healthcheck";
import type { ExportStoryResponse } from "../src/pages/api/export-story/[...slug]";

function getRTFStyles(rtfSource: string) {
  const matches = rtfSource.matchAll(
    /\\s(?<styleNumber>\d+)(?:\\sbasedon\d+)?\\snext\d+(?<partialRTFStyle>(?:\\[a-z0-9]+ ?)+)(?: (?<styleName>[A-Z][a-zA-Z ]*));/g,
  );
  let hasMatches = false;
  const rtfStyles: Record<number | string, string> = {};
  for (const match of matches) {
    const { styleNumber, partialRTFStyle, styleName } = match.groups as {
      styleNumber: string;
      partialRTFStyle: string;
      styleName: string;
    };
    hasMatches = true;
    const rtfStyle = `\\s${styleNumber}${partialRTFStyle}`;
    rtfStyles[Number.parseInt(styleNumber)] = rtfStyle;
    rtfStyles[styleName] = rtfStyle;
  }
  if (!hasMatches) {
    throw new Error("Couldn't find valid RTF styles!");
  }
  return rtfStyles;
}

const fetchRetry = fetchRetryWrapper(global.fetch);

const isLibreOfficeRunning = async () =>
  new Promise<boolean>((res) => {
    spawn("ps", ["-ax"], { stdio: "pipe" });
    const lines = createInterface({ input: spawn("ps", ["-ax"], { stdio: "pipe" }).stdout });
    lines.on("line", (line) => {
      if (line.includes("libreoffice") && line.includes("--writer")) {
        res(true);
      }
    });
    lines.on("close", () => res(false));
  });

async function exportStory(slug: string, options: { outputDir: string }) {
  /* Check that LibreOffice is not running */
  if (await isLibreOfficeRunning()) {
    console.error("ERROR: LibreOffice cannot be open while this command is running!");
    process.exit(1);
  }
  /* Check that outputDir is valid */
  const outputDir = normalize(options.outputDir);
  let files: string[];
  try {
    files = readdirSync(outputDir);
  } catch {
    files = [];
    console.log(`Created directory at ${mkdirSync(outputDir, { recursive: true })}`);
  }
  if (files.length > 0) {
    console.error(`ERROR: Directory ${outputDir} is not empty!`);
    process.exit(1);
  }

  /* Spawn Astro dev server */
  console.log("Starting Astro development server...");
  const devServerProcess = spawn("./node_modules/.bin/astro", ["dev"], { stdio: "pipe" });
  let astroURL: string | null = null;
  try {
    astroURL = await new Promise<string>((resolve, reject) => {
      const localServerRegex = /Local\s+(http:\/\/\S+)/;
      const lines = createInterface({ input: devServerProcess.stdout });
      lines.on("line", (line) => {
        const match = localServerRegex.exec(line);
        if (match && match[1]) {
          resolve(match[1]);
        }
      });
      lines.on("close", reject);
    });
    console.log(`Astro listening on ${astroURL}`);
    const response = await fetchRetry(new URL(`api/healthcheck`, astroURL), { retries: 5, retryDelay: 2000 });
    if (!response.ok) {
      throw new Error(response.statusText);
    }
    const healthcheck: HealthcheckResponse = await response.json();
    if (healthcheck.isAlive !== true) {
      throw new Error(JSON.stringify(healthcheck));
    }
  } catch {
    console.error("ERROR: Astro dev server didn't respond in time!");
    if (!devServerProcess.kill("SIGTERM")) {
      console.error("WARNING: Unable to shut down Astro dev server!");
    }
    process.exit(1);
  }

  /* Get data (story, thumbnail, descriptions) from Astro development server */
  let storyText = "";
  try {
    console.log("Getting data from Astro...");
    const response = await fetch(new URL(`api/export-story/${slug}`, astroURL));
    if (!response.ok) {
      throw new Error(`Failed to reach export-story API (status code ${response.status})`);
    }
    const data: ExportStoryResponse = await response.json();
    // Process response fields in parallel
    await Promise.all(
      [
        // Story
        (async () => {
          storyText = data.story;
          await writeFile(join(outputDir, `${slug}.txt`), storyText);
        })(),
        // Descriptions
        Object.entries(data.description).map(
          async ([filename, description]) => await writeFile(join(outputDir, filename), description),
        ),
        // Thumbnail
        (async () => {
          if (data.thumbnail) {
            if (data.thumbnail.startsWith("/@fs/")) {
              const thumbnailPath = data.thumbnail
                .replace(/^\/@fs/, "")
                .replace(/\?(&?[a-z][a-zA-Z0-9_-]+=[a-zA-Z0-9_-]*)*$/, "");
              await copyFile(thumbnailPath, join(outputDir, `thumbnail${thumbnailPath.match(/\.[^.]+$/)![0]}`));
            } else {
              const thumbnail = await fetchRetry(data.thumbnail, { retries: 2, retryDelay: 10000 });
              if (!thumbnail.ok) {
                throw new Error("Failed to get thumbnail");
              }
              const thumbnailExt = thumbnail.headers.get("Content-Type")!.startsWith("image/png") ? "png" : "jpg";
              await writeFile(join(outputDir, `thumbnail.${thumbnailExt}`), Buffer.from(await thumbnail.arrayBuffer()));
            }
          }
        })(),
      ].flat(),
    );
  } finally {
    if (devServerProcess) {
      console.log("Shutting down the Astro development server...");
      if (!devServerProcess.kill("SIGTERM")) {
        console.error("WARNING: Unable to shut down Astro dev server!");
      }
    }
  }

  /* Parse story into output formats */
  console.log("Parsing story into output formats...");
  // Process output files in parallel
  await Promise.all([
    // ${slug}.md
    writeFile(join(outputDir, `${slug}.md`), storyText.replaceAll(/=(?==)/g, "= ").replaceAll("*", "\\*")),
    // ${slug}.rtf
    (async () => {
      const tempDir = await mkdtemp(join(tmpdir(), "export-story-"));
      await writeFile(join(tempDir, "temp.txt"), storyText.replaceAll(/\n\n+/g, "\n"));
      spawnSync(
        "libreoffice",
        ["--convert-to", "rtf:Rich Text Format", "--outdir", tempDir, join(tempDir, "temp.txt")],
        {
          stdio: "ignore",
        },
      );
      const rtfText = await readFile(join(tempDir, "temp.rtf"), "utf-8");
      const rtfStyles = getRTFStyles(rtfText);
      if (!rtfStyles["Preformatted Text"]) {
        console.warn(`Missing RTF style "Preformatted Text"! Skipping RTF file generation.`);
      } else if (!rtfStyles["Normal"]) {
        console.warn(`Missing RTF style "Normal"! Skipping RTF file generation.`);
      } else {
        await writeFile(
          join(outputDir, `${slug}.rtf`),
          rtfText.replaceAll(rtfStyles["Preformatted Text"], rtfStyles["Normal"]),
        );
      }
    })(),
  ]);
  console.log("Success!");
  process.exit(0);
}

await program
  .name("export-story")
  .description("Generate and export formatted upload files for a story")
  .argument("<story-slug>", `Slug portion of the story's URL (eg. "the-lost-of-the-marshes/chapter-1")`)
  .option("-o, --output-dir <directory>", `Empty or inexistent directory path to export files to`)
  .action(exportStory)
  .parseAsync();
