import chalk from 'chalk';

import readlinePromises from 'readline/promises';
import { Readable } from 'stream';
import {
  existsSync,
  createWriteStream,
} from 'fs';
import { finished } from 'stream/promises';
import {
  mkdir,
  rename,
  unlink,
  writeFile,
} from 'fs/promises';

import { log } from '../src/log.js';

/**
 * Validates a string as a MediaEntry id
 * @param {string} str gets a valid id for a MediaEntry; it cannot have symbols that are not valid in paths
 * @returns {string} the validated string
 */
const getValidId = (str) => (str || '').replace(/( |\/|\\)/gm,'_');

/**
 * Gets an array sort function based on the provided type
 * @param {string} type the sort function type
 * @returns array sort function
 */
const getSortFunction = (type) => {
  switch (type) {
    case 'date': {
      return (a, b) => {
        try {
          let dA = new Date(a.date || '');
          let dB = new Date(b.date || '');
          return dA.getTime() - dB.getTime();
        } catch (err) {
          log('Error parsing dates: ', 'error', 'cms-to-static::getSortFunction::date', err);
          return ('' + a.date).localeCompare(b.date || '');
        }
      };
    }
    case 'title':
    default: {
      return (a, b) => ('' + a.title || 'untitled').localeCompare(b.title || 'untitled');
    }
  }
}

/**
 * Saves the given url to the given destination
 * @param {string} url the url of the file to save
 * @param {string} destination the file destination
 */
const saveFileFromUrl = async (url, destination) => {
  if (existsSync(destination)) {
    log(`Deleting existing file ${chalk.italic(destination)}`, 'debug');
    await unlink(destination);
    log(`Deleted file ${chalk.italic(destination)}`, 'info');
  }

  log(`Downloading file ${url}`, 'debug');
  const res = await fetch(url);
  log(`Saving file ${chalk.italic(url)} to ${chalk.italic(destination)}`, 'debug');
  const fileStream = createWriteStream(destination, { flags: 'wx' });
  await finished(Readable.fromWeb(res.body).pipe(fileStream));
  log(`File ${chalk.italic(url)} saved to ${chalk.italic(destination)}`, 'info');
};

/**
 * Saves a media list's entries to the given destination and returns an updated list where the Strapi urls have been replaced
 * @param {MediaList} inVal the media list to save files from
 * @param {string} fetchUrl the Strapi API fetch url
 * @param {string} destination the parent destination where the media list is saving files/folders in
 * @param {string} destinationShort the shortened destination, where 'sites/{my-site}/' has been removed so that the file can be '/content/{file/path}'
 * @returns {Promise<MediaList>} the updated media list
 */
const saveVariantsToDestination = async (inVal, fetchUrl, destination, destinationShort) => {
  const rootUrl = fetchUrl.replace(/((http)|(https)):\/\//, '').split('/')[0];
  const outVal = JSON.parse(JSON.stringify(inVal));
  // Loop over each entry in the media list
  await Promise.all(Object.keys(outVal.entries).map(async entryId => {
    const entryPath = `${destination}/${entryId}`;
    if (!existsSync(entryPath)) {
      log(`Creating directory at ${chalk.italic(entryPath)}`, 'debug');
      await mkdir(entryPath);
      log(`Created directory ${chalk.italic(entryPath)}`, 'info');
    }

    // Loop over each variant in the media entry
    await Promise.all(outVal.entries[entryId].variants.map(async (variant, index) => {
      // save the main file and update in the variant info
      const filePath = `${entryPath}/${variant.url.replace('/uploads/', '')}`;
      saveFileFromUrl(`http://${rootUrl}${variant.url}`, filePath);
      outVal.entries[entryId].variants[index].url = `/content/${destinationShort}/${entryId}/${variant.url.replace('/uploads/', '')}`;

      // save the thumbnail and update in the variant info
      const thumbnailPath = `${entryPath}/${variant.thumbnailUrl.replace('/uploads/', '')}`;
      saveFileFromUrl(`http://${rootUrl}${variant.thumbnailUrl}`, thumbnailPath);
      outVal.entries[entryId].variants[index].thumbnailUrl = `/content/${destinationShort}/${entryId}/${variant.thumbnailUrl.replace('/uploads/', '')}`;

      if (true) {
        await writeFile(`${entryPath}/info.json`, JSON.stringify(outVal.entries[entryId], null, 2));
        outVal.entries[entryId] = `/content/${destinationShort}/${entryId}/info.json`;
      }
    }));
  }));

  return new Promise(resolve => resolve(outVal));
};

/**
 * Maps Strapi API response to a mackenzii media list
 * @param {StrapiMediaList} inVal the Strapi API response
 * @returns {Promise<MediaList>} the media list mapped from the Strapi API
 */
const mapStrapiResponseToMackenzii = async (inVal) => {
  const { data } = inVal;
  const outVal = {};

  // handle list basic fields
  outVal.title = data.title;
  
  // handle list entries
  outVal.entries = {};
  data.entries.forEach(entry => {
    // give it an id
    const id = `${getValidId(entry.date)}_${getValidId(entry.title)}_${getValidId(entry.documentId)}`;

    // copy all values at first...
    outVal.entries[id] = {...entry};

    // ...then delete strapi fields (as there are more to copy than delete)
    delete outVal.entries[id].id;
    delete outVal.entries[id].documentId;
    delete outVal.entries[id].createdAt;
    delete outVal.entries[id].updatedAt;
    delete outVal.entries[id].publishedAt;
    delete outVal.entries[id].locale;

    // ...then handle variants
    outVal.entries[id].variants = entry.variants.map(variant => ({
      alternativeText: variant.alternativeText,
      caption: variant.caption,
      // the images will currently be mapped to strapi, to be handled with file grabber
      url: variant.url,
      thumbnailUrl: variant.formats['thumbnail'].url, // TBD: non-image assets
    }));

    // ...also post-process tags into an array
    outVal.entries[id].tags = (outVal.entries[id].tags || '').split(/,| |;/).filter(val => val !== '');
  });

  return new Promise(resolve => resolve(outVal));
};

/**
 * Sorts the entries in the list based on the provided sort method
 * @param {StrapiMediaList} inVal the media list to sort
 * @param {[string, string]} sortMethod the sort method; first string is the field to sort by, the second string is the order (asc/desc)
 * @returns {Promise<StrapiMediaList>} the updated media list
 */
const sortVariants = async (inVal, sortMethod) => {
  let outVal;

  if (sortMethod.length > 0) {
    outVal = JSON.parse(JSON.stringify(inVal));
    outVal.data.entries = outVal.data.entries.sort(getSortFunction(sortMethod[0]));
    if (sortMethod[1] === 'desc') {
      outVal.data.entries = outVal.data.entries.reverse();
    }
  } else {
    outVal = inVal;
  }

  return new Promise(resolve => resolve(outVal));
};

(async () => {
  console.log('NOTE: This is currently only built to support gallery lists. Article lists and other content are not supported');
  const rl = readlinePromises.createInterface(process.stdin, process.stdout);
  let fetchUrl = '';
  try {
    fetchUrl = await rl.question('Enter the URL to fetch from: ');
  } catch (err) {
    log('There was an error: ', 'error', 'cms-to-static::main::Q1', err);
  } finally {
    if (!fetchUrl) {
      log('An invalid URL was given. Quitting...', 'log', 'cms-to-static::main::Q1');
      rl.close();
      return;
    } else if (!fetchUrl.includes('?')) {
      fetchUrl += '?populate[0]=entries&populate[1]=entries.variants'
    }
  }

  let sort = '';
  let sortSplit = [];
  try {
    sort = await rl.question('Enter entry sorting method (valid options are "date" and "title" without quotes followed by a colon and either "asc" or "desc" without quotes): ');
  } catch (err) {
    log('There was an error: ', 'error', 'cms-to-static::main::Q2', err);
  } finally {
    if (sort !== '') {
      sortSplit = sort.split(/\.|\,|\:|\ |\-/);
      if (
        sortSplit.length < 2
        || !['date','title'].includes(sortSplit[0])
        || !['asc', 'desc'].includes(sortSplit[1])
      ) {
        log(`Provided function ${sortSplit[0]} or order ${sortSplit[1]} not valid. Defaulting to title:asc`, 'error', 'cms-to-static::main::Q2');
        sortSplit = ['title', 'asc'];
      }
    }
  }

  let destination = '';
  try {
    destination = await rl.question('Enter the directory to save the response\n(note: this should be a path relative to "sites";\n{your/input} will become sites/{your/input}): ');
    destination = 'sites/' + destination;
  } catch (err) {
    log('There was an error: ', 'error', 'cms-to-static::main::Q3', err);
  } finally {
    rl.close();
    if (!destination) {
      log('An invalid path was given. Quitting...', 'log', 'cms-to-static::main::Q3');
      return;
    } else {
      if (existsSync(destination)) {
        const split = destination.split('/');
        await rename(destination, `${split.slice(0, -1).join('/')}/${split[split.length - 1]}_${Date.now()}`);
      }
      await mkdir(destination, { recursive: true });
    }
  }

  let res = {};
  try {
    res = await (await fetch(fetchUrl, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    })).json();
  } catch (err) {
    log('There was an error: ', 'error', 'cms-to-static::main', err);
  }

  log('Writing...', 'log', 'cms-to-static::main::writing');
  try {
    await writeFile(`${destination}/in.json`, JSON.stringify(res, null, 2));

    sortVariants(res, sortSplit)
      .then(list => mapStrapiResponseToMackenzii(list))
      .then(list => saveVariantsToDestination(list, fetchUrl, destination, destination.replace('sites/', '').split('/').slice(1).join('/')))
      .then(list => writeFile(`${destination}/out.json`, JSON.stringify(list, null, 2)));
    log('Done!', 'log', 'cms-to-static::main::writing');
  } catch (err) {
    log('There was an error: ', 'error', 'cms-to-static::main::writing', err);
  }
})();