import path from "path";
import fs from "fs";
import { fsFileRename } from "../helpers/fs-helpers.js";
import chalk from "chalk";

import Item, { FOError } from "../item.js";
import { TYPE_FILE } from "../constants.js";
import {
  buildFile,
  glob2regExp,
  registerFallback,
  registerFileRegExp,
  registerFolder,
  registerFolderInCache
} from "../register-file-types.js";
import Value from "../value.js";
import { parseFilename } from "../timestamp.js";
import { IconFailure, IconSuccess } from "../helpers/console-helpers.js";
import {
  reserveFreeIndex,
  initializeReservation,
  commitReservation,
  reservationGetAvailableIndex
} from "../reservations.js";
import { writeLine } from "../helpers/command-helper.js";
import { date2string } from "../helpers/time-helpers.js";

/**
 * @typedef {module:reservations.Flavor} Flavor
 */

/**
 * How does this work?
 *
 * - buildFile()
 * - loadData()
 *      will make the full "readonly" analysis
 *      will build up info (and values-problems)
 *      - initial (=current) and expected
 *
 * - runAllFixes()
 *      is only based on values
 *      the only "write" part of the process
 *      values{}.fixed() (expected -> current)
 *
 */
export default class File extends Item {
  static getType() {
    return TYPE_FILE;
  }

  static get C_ARCHIVES() {
    return "archives";
  }

  // ------------------------------------------
  //
  // Public properties
  //
  // ------------------------------------------

  /** @type {Value} */
  i_00_f_exists;

  /**
   * The full path to the file
   *
   * @type {Value}
   */
  i_f_path_full;

  /** @type {Value} */
  i_f_is_folder;

  /** @type {Value} */
  i_f_filename;

  /**
   * In the filename, the title part
   *
   * @type {Value}
   */
  i_f_title;

  /**
   * In the filename, the qualif filename part (without the [])
   *
   * @type {Value}
   */
  i_f_qualif;

  /**
   * In the filename, the timestamp part
   *   As it would appear in the filename,
   *   thus in "local time"
   *
   * @type {Value}
   */
  i_f_time;

  /**
   * ".blabla" (always have a dot: .blabla)
   * null mean the file has been deleted
   *
   * @type {Value}
   */
  i_f_extension;

  /** @type {fs.Stats} */
  stats = null;

  constructor(givenFilePath) {
    const absoluteGivenFilePath = path.resolve(givenFilePath);

    // Calculate the parent
    let parent = null;
    let parentDir = path.dirname(absoluteGivenFilePath);
    if (parentDir != "/") {
      parent = buildFile(parentDir);
    }

    /**
     *
     *  Title: filepath
     *  Set the parent
     *
     */
    super(path.basename(absoluteGivenFilePath), parent);

    /**
     *
     * Handle folder and existence
     *
     * Require: none
     * Fix: require I_FULL_PATH
     *
     * Note: throwIfNoEntry -> will return null if file does not exists
     *
     */
    this.stats = fs.statSync(absoluteGivenFilePath, { throwIfNoEntry: false });
    let isFolder = this.stats?.isDirectory() ?? null;
    let exists = isFolder !== null;

    //
    // We need to register the file as early as possible
    // and we do that here, already too late
    // since the parent is already created
    //
    if (isFolder) {
      registerFolderInCache(this, absoluteGivenFilePath);
    }

    this.i_f_is_folder = new Value(isFolder);
    this.i_00_f_exists = new Value(exists)
      .expect(true, "File: must exists")
      .withFix(async (v) => {
        const vIsFolder = this.i_f_is_folder;
        if (v.current && !v.expected) {
          if (vIsFolder.current) {
            await fs.promises.rmdir(this.i_f_path_full.current);
            v.fixed();
          } else {
            await fs.promises.unlink(this.i_f_path_full.current);
            v.fixed();
          }
          // When deleting a file, all values are de-facto fixed
          this.getAllValues().forEach((v) => v.fixed());
        }

        if (!v.current && v.expected) {
          if (vIsFolder.expected) {
            await fs.promises.mkdir(this.i_f_path_full.expected, {
              recursive: true
            });
            v.fixed();
            vIsFolder.fixed();
          }
        }
      });

    /**
     * Filename of the file (with extension)
     *
     * Modified only by 'rename'
     *    Particularities are calculated based on filename.
     *    Expected is updated based on particularities.
     *
     * Require: i_00_f_exists
     */
    const parsed = parseFilename(path.parse(absoluteGivenFilePath).name);

    this.i_f_time = new Value(parsed.time);
    this.i_f_title = new Value(parsed.title);
    this.i_f_qualif = new Value(parsed.qualif); // Contain the "index", but updated by i_f_path_full
    this.i_f_extension = new Value(path.parse(absoluteGivenFilePath).ext);

    this.i_f_filename = new Value(
      path.parse(absoluteGivenFilePath).base
    ).withExpectedCalculated(() => {
      // The initial name is the parsed data
      // If we are a folder, we can not change the name
      if (this.i_f_is_folder.expected) {
        return path.parse(absoluteGivenFilePath).base;
      }

      let canonicalFilename = "";
      if (this.i_f_time.expected) {
        canonicalFilename = (canonicalFilename + this.i_f_time.expected).trim();
      }
      if (this.i_f_title.expected) {
        canonicalFilename = (
          canonicalFilename +
          " " +
          this.i_f_title.expected
        ).trim();
      }
      if (this.i_f_qualif.expected) {
        if (canonicalFilename == "") {
          // Avoid having a name like "[blabla]"
          canonicalFilename = this.i_f_qualif.expected;
        } else {
          canonicalFilename = (
            canonicalFilename +
            " [" +
            this.i_f_qualif["expected"] +
            "]"
          ).trim();
        }
      }
      if (this.i_f_extension?.["expected"]) {
        canonicalFilename = (
          canonicalFilename + this.i_f_extension["expected"]
        ).trim();
      }

      return canonicalFilename;
    }, [this.i_f_time, this.i_f_title, this.i_f_qualif, this.i_f_extension]);

    /**
     *
     * Full path of the file
     *
     * Require: i_parent, File._F_FILENAME
     *
     */
    this.i_f_path_full = new Value(
      path.join(
        this.i_parent.initial?.i_f_path_full.initial ?? "/",
        this.i_f_filename.initial
      )
    )
      .withExpectedCalculated(
        () =>
          path.join(
            this.i_parent.expected?.i_f_path_full.expected ?? "/",
            this.i_f_filename.expected
          ),
        [this.i_parent, this.i_f_filename]
      )
      .withFix(
        async (v) => {
          if (this.i_f_is_folder?.expected) {
            throw new FOError("Could not rename a folder");
          }

          // If we don't expect file to exists at the end, we don't change it...
          if (this.i_00_f_exists?.expected) {
            /**
             * First step: calculate the new index
             *
             *    - get a free index in knownFiles
             */
            await fsFileRename(v.current, v.expected);
            commitReservation(this);
          }

          v.fixed();
        },
        [
          this.i_parent,
          this.i_f_filename,
          this.i_f_time,
          this.i_f_title,
          this.i_f_qualif,
          this.i_f_extension
        ]
      )
      .onExpectedChanged(
        (_value) => {
          //
          // Difficulty: by changing the File.i_f_qualif, we will trigger this again.
          // To avoid cycles, we check for changes.
          //
          const newQualif = reservationGetAvailableIndex(this);
          if (newQualif != this.i_f_qualif.expected) {
            this.i_f_qualif.expect(newQualif);
          }
          reserveFreeIndex(this);
        },
        // We don't trigger imediately, initializeReservation will do it for us
        false
      );

    // Need File.i_f_path_full and other informations
    initializeReservation(this);
  }

  get currentFilePath() {
    return this.i_f_path_full.current;
  }

  // ------------------------------------------
  //
  // Protected methods
  //
  // TO BE USED BY SPECIALIZED FILES
  //
  // ------------------------------------------------

  /**
   * [Tool for specialized classes]
   *
   * Run the analysis on this element and generate infos
   *
   * Flow:
   *   - call super.analyse() => initialize the above layers
   *   - analisysAddInfo with the information from the layer
   *   - set the expected values from the layer and the above layers
   *
   * @returns {Promise<File>} resolved as analysis is done
   */
  async loadData() {
    // Lowercase extension
    const currentExtension = this.i_f_extension.current;
    if (currentExtension.toLowerCase() != currentExtension) {
      this.i_f_extension.expect(
        currentExtension.toLowerCase(),
        "File: To lower case"
      );
    }

    // Parse the qualif filename to see if it is a timestamp too
    // and take it as the source of thruth if applicable
    // TODO(timestamp): this should move into timestamp ?
    if (this.i_f_qualif.current) {
      const ts2 = parseFilename(this.i_f_qualif.current).time;
      if (ts2) {
        this.i_f_time.expect(ts2, "parse the qualif instead of the timestamp");
      }
    }

    if (
      this.i_f_qualif.expected &&
      this.i_f_title.expected == this.i_f_qualif.expected
    ) {
      // 'remove duplicate title/original'
      this.i_f_qualif.expect("", "Original is a duplicate of the title");
    }

    return this;
  }

  // ------------------------------------------
  //
  // Private methods
  //
  // ------------------------------------------

  /**
   * @override
   */
  modifiedValuesKeys() {
    if (this.i_00_f_exists.current === false) {
      // The file does not exists anyway
      return ["i_00_f_exists"];
    }

    return super.modifiedValuesKeys();
  }

  /**
   *
   * @param {object} opts to set what to print
   * @property {boolean} includesActions? - if true, print also correct values
   * @property {boolean} includesInfos? - if true, print also untouched values
   * @property {boolean} includesCalculated? - if true, print also untouched values
   * @property {Array<string>} includes - to be included anyway
   * @property {Array<string>} excludes - to be excluded anyway
   * @returns {this} for chaining
   */
  printDetails(opts = {}) {
    const f0 = this.getTopParent();

    const keyList = new Set([
      ...(opts.includes ?? []),
      ...this.unsolvedValuesKeys()
    ]);

    if (opts.includesActions) {
      this.modifiedValuesKeys().forEach((k) => keyList.add(k));
    }

    if (opts.includesInfos) {
      this.getAllValuesKeys().forEach((k) => keyList.add(k));
    }

    for (const k of opts.excludes ?? []) {
      keyList.delete(k);
    }

    const ttyWidth = process.stdout.columns ?? 80;

    const p = (v, w) => (v + "").substring(0, w).padEnd(w);
    const wK = Math.round((18 / 100) * ttyWidth);
    const wV = Math.round((30 / 100) * ttyWidth);

    const strs = Array.from(keyList)
      .filter((k) => k in this)
      .filter((k) => opts.includesCalculated || this[k].isCalculated())
      .sort()
      .map((k) => {
        /** @type {Value} */
        const v = this[k];
        if (v.isDone()) {
          if (v.isModified()) {
            return `${IconSuccess} ${p(k, wK)} (${p(this[k].initial, wV)} → ${p(
              this[k].expected,
              wV
            )}) ${this[k].messages.join(" / ")}`;
          } else {
            return `${IconSuccess} ${p(k, wK)} (${p(this[k].initial, wV)} ✓ ${p(
              "",
              wV
            )}) ${this[k].messages.join(" / ")}`;
          }
        } else {
          return chalk.red(
            `${IconFailure} ${p(k, wK)} (👁 ${p(
              this[k].current,
              wV - 2
            )} ⮀ 🏁 ${p(this[k].expected, wV - 2)}) ${this[k].messages.join(
              " / "
            )}`
          );
        }
      })
      .map((s) => `   ${s}`);

    if (strs.length > 0) {
      writeLine(
        chalk.yellow(
          `${path.relative(
            f0.i_parent.initial?.i_f_path_full.initial ?? "",
            this.i_f_path_full.initial
          )}`
        )
      );
      writeLine(strs.join("\n"));
    }

    return this;
  }

  getMTime() {
    return date2string(this.stats.mtime);
  }
}

registerFileRegExp([glob2regExp("*.pdf"), glob2regExp("*.doc*")], File);
registerFolder(File);

export class FileFallback extends File {}

registerFallback(FileFallback);
