let fs = require("fs");
let path = require("path");
let mkdirp = require("mkdirp");
import defaultsDeep from "lodash.defaultsdeep";
import isString from "lodash.isstring";
import isEmpty from "lodash.isempty";
import isFunction from "lodash.isfunction";
import {
  IArgdownRequest,
  IArgdownResponse,
  IArgdownLogger,
  ArgdownPluginError
} from "@argdown/core";
import {
  IAsyncArgdownPlugin,
  IAsyncRequestHandler
} from "../IAsyncArgdownPlugin";

export interface IFileNameProvider {
  (
    settings: ISaveAsSettings,
    request: IArgdownRequest,
    response: IArgdownResponse
  ): string;
}
export interface ISaveAsSettings {
  dataKey?: string;
  extension?: string;
  outputDir?: string;
  isRequestData?: boolean;
  fileName?: string | IFileNameProvider;
}
declare module "@argdown/core" {
  interface IArgdownRequest {
    saveAs?: ISaveAsSettings;
  }
  interface IArgdownResponse {
    outputPath?: string;
  }
}
export class SaveAsFilePlugin implements IAsyncArgdownPlugin {
  name = "SaveAsFilePlugin";
  defaults: ISaveAsSettings;
  constructor(config: ISaveAsSettings) {
    this.defaults = defaultsDeep({}, config, {
      dataKey: "test",
      extension: ".txt",
      outputDir: "./output"
    });
  }
  // there can be several instances of this plugin in the same ArgdownApplication
  // Because of this, we can not add the instance default settings to the request object as in other plugins
  // Instead we have to add it each time the getSettings method is called to avoid keeping request specific state
  getSettings(request: IArgdownRequest): ISaveAsSettings {
    let settings = request.saveAs || {};
    settings = defaultsDeep({}, settings, this.defaults);
    return settings;
  }
  runAsync: IAsyncRequestHandler = async (request, response, logger) => {
    const settings = this.getSettings(request);
    if (!settings.dataKey) {
      throw new ArgdownPluginError(
        this.name,
        "missing-dataKey-setting",
        "No data key."
      );
    }
    let fileContent: string | Buffer | undefined = !settings.isRequestData
      ? (<any>response)[settings.dataKey!]
      : (<any>request)[settings.dataKey!];
    if (fileContent === undefined) {
      throw new ArgdownPluginError(
        this.name,
        "missing-content",
        "No content to save."
      );
    }
    if (!isString(fileContent) && !Buffer.isBuffer(fileContent)) {
      throw new ArgdownPluginError(
        this.name,
        "invalid-content",
        "Content is neither a string nor a buffer."
      );
    }
    if (isEmpty(fileContent)) {
      throw new ArgdownPluginError(
        this.name,
        "missing-content",
        "Data field is empty string."
      );
    }
    const dataSettings =
      !settings.isRequestData && settings.dataKey
        ? (<any>request)[settings.dataKey]
        : null;
    let fileName = "default";
    let outputDir = settings.outputDir;
    if (request.outputPath) {
      outputDir = path.dirname(request.outputPath);
    } else if (dataSettings && dataSettings.outputDir) {
      outputDir = path.dirname(dataSettings.outputDir);
    }
    if (request.outputPath) {
      fileName = this.getFileName(request.outputPath);
    } else if (isFunction(settings.fileName)) {
      fileName = settings.fileName.call(this, settings, request, response);
    } else if (isString(settings.fileName)) {
      fileName = settings.fileName;
    } else if (request.inputPath) {
      fileName = this.getFileName(request.inputPath);
    }
    if (request.outputSuffix) {
      fileName = fileName + request.outputSuffix;
    }
    if (dataSettings && dataSettings.outputDir) {
      outputDir = dataSettings.outputDir;
    }
    if (outputDir && settings.extension) {
      response.outputPath = await this.saveAsFile(
        fileContent,
        outputDir,
        fileName,
        settings.extension,
        logger
      );
    }
  };
  getFileName(file: string): string {
    let extension = path.extname(file);
    return path.basename(file, extension);
  }
  async saveAsFile(
    data: string | Buffer,
    outputDir: string,
    fileName: string,
    extension: string,
    logger: IArgdownLogger
  ) {
    let absoluteOutputDir = path.resolve(process.cwd(), outputDir);
    const outputPath = path.resolve(absoluteOutputDir, fileName + extension);
    await mkdirp(absoluteOutputDir);
    await new Promise<void>((resolve, reject) => {
      fs.writeFile(outputPath, data, function(err: Error) {
        if (err) {
          reject(err);
        }
        logger.log(
          "verbose",
          "Saved " + absoluteOutputDir + "/" + fileName + extension
        );
        resolve();
      });
    });
    return outputPath;
  }
}
