const http = require('node:http');
const fs = require('fs');
const path = require('path');

async function fetch(url, method = "GET") {
  return new Promise((resolve) => {
    http.get(url, {method: method},(res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => resolve(data));
    }).on('error', () => resolve(""));
  });
}


async function saveImage(url, outputPath) {
  return new Promise((resolve, reject) => {
    const request = http.get(url, (response) => {
      if (response.statusCode !== 200) {
        reject(new Error(`Failed to download image. Status code: ${response.statusCode}`));
        return;
      }

      const writer = fs.createWriteStream(outputPath);
      response.pipe(writer);

      writer.on('finish', () => resolve(outputPath));
      writer.on('error', (err) => {
        fs.unlink(outputPath, () => reject(err));
      });
    });

    request.on('error', (err) => reject(err));
  });
}

async function findPort() {
  for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
    const result = await fetch("http://localhost:" + portToTest + "/ping");
    if (result === 'JoplinClipperServer') {
      return portToTest
    }
  }
  throw new Error("Joplin clipper server not found");
}

async function authenticate(host) {
  // Step 1: Call POST /auth to get the auth_token
  const authResponse = await fetch(host + "/auth", "POST");
  const { auth_token } = JSON.parse(authResponse);

  // Step 2: Poll GET /auth/check at regular intervals
  const checkAuthStatus = async () => {
    const checkResponse = await fetch(`${host}/auth/check?auth_token=${auth_token}`);
    const { status, token } = JSON.parse(checkResponse);

    if (status === "accepted") {
      console.log("Authentication successful.");
      return token;
    } else if (status === "rejected") {
      throw new Error("Authentication rejected by the user.");
    } else {
      console.log("Pending approval. Status:", status)
      return new Promise(resolve => setTimeout(() => resolve(checkAuthStatus()), 1000));
    }
  };

  return checkAuthStatus();
}

async function getNotebookNotes(apiToken, host) {

  const foldersResponse = await fetch(`${host}/folders?token=${apiToken}`);
  const folders = JSON.parse(foldersResponse);
  let id = null;
  for(let folder of folders.items) {
    if(folder.title === "Blog") {
      id = folder.id;
    }
  }

  if(id == null) {
    throw new Error("Blog notebook not found.");
  }

  const notesResponse = await fetch(`${host}/folders/${id}/notes?token=${apiToken}`);
  const notes = JSON.parse(notesResponse);

  let noteIds = [];
  for(let note of notes.items) {
    noteIds.push(note.id);
  }
  return noteIds

}

async function getNoteBody(apiToken, host, noteId) {
  const noteResponse = await fetch(`${host}/notes/${noteId}?token=${apiToken}&fields=body`);
  return JSON.parse(noteResponse).body;
}

async function getNoteResources(apiToken, host, noteId) {
  const resourcesResponse = await fetch(`${host}/notes/${noteId}/resources?token=${apiToken}&fields=id,mime`);
  let resources = [];
  for(let resource of JSON.parse(resourcesResponse).items) {
    resources.push([resource.id, resource.mime.replace(/^image\//, "")]);
  }
  return resources;
}

async function fixResourceLinks(resources, body) {
  for(let resource of resources) {
    // Replace markdown image links with ./ instead of :/
    body = body.replaceAll(":/" + resource[0], "./" + resource[0])
    // Add resource extension
    body = body.replaceAll(resource[0], resource[0] + "." + resource[1])
  }
  return body;
}

async function otherReplaces(body) {
  // Remove ogImage lines
  const ogImageRegex = /^\s*!\[ogImage\].*$/gm;
  body = body.replace(ogImageRegex, '');

  // Remove spoiler blocks
  const spoilerRegex = /^:\[\n(.*?)\n\n([\s\S]*?)\n\n\]:$/gm;
  return body.replace(spoilerRegex, '');
}


async function getNoteSlug(body) {
  let frontmatterSplt = body.split("---\n");
  if(frontmatterSplt.length < 3) {
    throw new Error("No frontmatter found in note.");
  }
  let frontmatter = frontmatterSplt[1];
  for(let line of frontmatter.split("\n")) {
    if(line.startsWith("slug: ")) {
      return line.split("slug: ")[1].trim();
    }
  }
  throw new Error("No slug found in frontmatter.");
}

async function saveNote(slug, noteBody) {
  const dirPath = path.join(__dirname, '../content/blog', slug);
  if (!fs.existsSync(dirPath)){
    fs.mkdirSync(dirPath, { recursive: true });
  }
  const filePath = path.join(dirPath, 'index.md');
  fs.writeFileSync(filePath, noteBody);
  console.log(`Note saved to ${filePath}`);
}

async function saveResources(resources, apiToken, host, slug) {
  for(let resource of resources) {
    const resID = resource[0];
    const extension = resource[1];
    const filePath = path.join(__dirname, '../content/blog', slug, resID + "." + extension);
    await saveImage(`${host}/resources/${resID}/file?token=${apiToken}&fields=data`, filePath)
    console.log(`Resource saved to ${filePath}`);
  }
}

async function clearBlogFolder() {
  const blogFolderPath = path.join(__dirname, '../content/blog');
  if (fs.existsSync(blogFolderPath)) {
    fs.rmSync(blogFolderPath, { recursive: true, force: true });
    console.log(`Cleared blog folder at ${blogFolderPath}`);
  }
}

async function main() {

  console.log("Finding JoplinClipperServer port...");
  let host = "http://localhost:" + await findPort();
  let apiToken = await authenticate(host);
  //let apiToken = "";
  let notes = await getNotebookNotes(apiToken, host);
  await clearBlogFolder();

  for(let noteId of notes) {

    let noteBody = await getNoteBody(apiToken, host, noteId);
    let slug = await getNoteSlug(noteBody);

    let resources = await getNoteResources(apiToken, host, noteId);

    noteBody = await fixResourceLinks(resources, noteBody);
    noteBody = await otherReplaces(noteBody);

    console.log("Saving note with slug:", slug, resources);
    await saveNote(slug, noteBody);
    await saveResources(resources, apiToken, host, slug)
  }
}

main().then();