import * as plugins from './plugins.js';
import * as interpreter from './interpreter.js';

import { SmartFile } from './classes.smartfile.js';

import * as memory from './memory.js';
import type { StreamFile } from './classes.streamfile.js';
/*===============================================================
============================ Checks =============================
===============================================================*/

/**
 *
 * @param filePath
 * @returns {boolean}
 */
export const fileExistsSync = (filePath): boolean => {
  let fileExistsBool: boolean = false;
  try {
    plugins.fsExtra.readFileSync(filePath);
    fileExistsBool = true;
  } catch (err) {
    fileExistsBool = false;
  }
  return fileExistsBool;
};

/**
 *
 * @param filePath
 * @returns {any}
 */
export const fileExists = async (filePath): Promise<boolean> => {
  const done = plugins.smartpromise.defer<boolean>();
  plugins.fs.access(filePath, 4, (err) => {
    err ? done.resolve(false) : done.resolve(true);
  });
  return done.promise;
};

/**
 * Checks if given path points to an existing directory
 */
export const isDirectory = (pathArg: string): boolean => {
  try {
    return plugins.fsExtra.statSync(pathArg).isDirectory();
  } catch (err) {
    return false;
  }
};

/**
 * Checks if given path points to an existing directory
 */
export const isDirectorySync = (pathArg: string): boolean => {
  try {
    return plugins.fsExtra.statSync(pathArg).isDirectory();
  } catch (err) {
    return false;
  }
};

/**
 * Checks if a given path points to an existing file
 */
export const isFile = (pathArg): boolean => {
  return plugins.fsExtra.statSync(pathArg).isFile();
};

/*===============================================================
============================ FS ACTIONS =========================
===============================================================*/

/**
 * copies a file or directory from A to B on the local disk
 */
export const copy = async (fromArg: string, toArg: string, optionsArg?: plugins.fsExtra.CopyOptions & { replaceTargetDir?: boolean }): Promise<void> => {
  if (optionsArg?.replaceTargetDir && isDirectory(fromArg) && isDirectory(toArg)) {
    await remove(toArg);
  }
  return await plugins.fsExtra.copy(fromArg, toArg, optionsArg as plugins.fsExtra.CopyOptions);
};

/**
 * copies a file or directory SYNCHRONOUSLY from A to B on the local disk
 */
export const copySync = (fromArg: string, toArg: string, optionsArg?: plugins.fsExtra.CopyOptionsSync & { replaceTargetDir?: boolean }): void => {
  if (optionsArg?.replaceTargetDir && isDirectory(fromArg) && isDirectory(toArg)) {
    removeSync(toArg);
  }
  return plugins.fsExtra.copySync(fromArg, toArg, optionsArg as plugins.fsExtra.CopyOptionsSync);
};

/**
 * ensures that a directory is in place
 */
export const ensureDir = async (dirPathArg: string) => {
  await plugins.fsExtra.ensureDir(dirPathArg);
};

/**
 * ensures that a directory is in place
 */
export const ensureDirSync = (dirPathArg: string) => {
  plugins.fsExtra.ensureDirSync(dirPathArg);
};

/**
 * ensure an empty directory
 * @executes ASYNC
 */
export const ensureEmptyDir = async (dirPathArg: string) => {
  await plugins.fsExtra.ensureDir(dirPathArg);
  await plugins.fsExtra.emptyDir(dirPathArg);
};

/**
 * ensure an empty directory
 * @executes SYNC
 */
export const ensureEmptyDirSync = (dirPathArg: string) => {
  plugins.fsExtra.ensureDirSync(dirPathArg);
  plugins.fsExtra.emptyDirSync(dirPathArg);
};

/**
 * ensures that a file is on disk
 * @param filePath the filePath to ensureDir
 * @param the fileContent to place into a new file in case it doesn't exist yet
 * @returns Promise<void>
 * @exec ASYNC
 */
export const ensureFile = async (filePathArg, initFileStringArg): Promise<void> => {
  ensureFileSync(filePathArg, initFileStringArg);
};

/**
 * ensures that a file is on disk
 * @param filePath the filePath to ensureDir
 * @param the fileContent to place into a new file in case it doesn't exist yet
 * @returns Promise<void>
 * @exec SYNC
 */
export const ensureFileSync = (filePathArg: string, initFileStringArg: string): void => {
  if (fileExistsSync(filePathArg)) {
    return null;
  } else {
    memory.toFsSync(initFileStringArg, filePathArg);
  }
};

/**
 * removes a file or folder from local disk
 */
export const remove = async (pathArg: string): Promise<void> => {
  await plugins.fsExtra.remove(pathArg);
};

/**
 * removes a file SYNCHRONOUSLY from local disk
 */
export const removeSync = (pathArg: string): void => {
  plugins.fsExtra.removeSync(pathArg);
};

/**
 * removes an array of filePaths from disk
 */
export const removeMany = async (filePathArrayArg: string[]) => {
  const promiseArray: Array<Promise<void>> = [];
  for (const filePath of filePathArrayArg) {
    promiseArray.push(remove(filePath));
  }
  await Promise.all(promiseArray);
};

/**
 * like removeFilePathArray but SYNCHRONOUSLY
 */
export const removeManySync = (filePathArrayArg: string[]): void => {
  for (const filePath of filePathArrayArg) {
    removeSync(filePath);
  }
};

/*===============================================================
============================ Write/Read =========================
===============================================================*/

/**
 * reads a file content to an object
 * good for JSON, YAML, TOML, etc.
 * @param filePathArg
 * @param fileTypeArg
 * @returns {any}
 */
export const toObjectSync = (filePathArg, fileTypeArg?) => {
  const fileString = plugins.fsExtra.readFileSync(filePathArg, 'utf8');
  let fileType;
  fileTypeArg ? (fileType = fileTypeArg) : (fileType = interpreter.filetype(filePathArg));
  try {
    return interpreter.objectFile(fileString, fileType);
  } catch (err) {
    err.message = `Failed to read file at ${filePathArg}` + err.message;
    throw err;
  }
};

/**
 * reads a file content to a String
 */
export const toStringSync = (filePath: string): string => {
  const encoding = plugins.smartmime.getEncodingForPathSync(filePath);
  let fileString: string | Buffer = plugins.fsExtra.readFileSync(filePath, encoding);
  if (Buffer.isBuffer(fileString)) {
    fileString = fileString.toString('binary');
  }
  return fileString;
};

export const toBuffer = async (filePath: string): Promise<Buffer> => {
  return plugins.fsExtra.readFile(filePath);
};

export const toBufferSync = (filePath: string): Buffer => {
  return plugins.fsExtra.readFileSync(filePath);
};

/**
 * Creates a Readable Stream from a file path.
 * @param filePath The path to the file.
 * @returns {fs.ReadStream}
 */
export const toReadStream = (filePath: string): plugins.fs.ReadStream => {
  if (!fileExistsSync(filePath)) {
    throw new Error(`File does not exist at path: ${filePath}`);
  }
  return plugins.fsExtra.createReadStream(filePath);
};

export const fileTreeToHash = async (dirPathArg: string, miniMatchFilter: string) => {
  const fileTreeObject = await fileTreeToObject(dirPathArg, miniMatchFilter);
  let combinedString = '';
  for (const smartfile of fileTreeObject) {
    combinedString += await smartfile.getHash();
  }
  const hash = await plugins.smarthash.sha256FromString(combinedString);
  return hash;
};

/**
 * creates a smartfile array from a directory
 * @param dirPathArg the directory to start from
 * @param miniMatchFilter a minimatch filter of what files to include
 */
export const fileTreeToObject = async (dirPathArg: string, miniMatchFilter: string) => {
  // handle absolute miniMatchFilter
  let dirPath: string;
  if (plugins.path.isAbsolute(miniMatchFilter)) {
    dirPath = '/';
  } else {
    dirPath = plugins.smartpath.transform.toAbsolute(dirPathArg) as string;
  }

  const fileTree = await listFileTree(dirPath, miniMatchFilter);
  const smartfileArray: SmartFile[] = [];
  for (const filePath of fileTree) {
    const readPath = ((): string => {
      if (!plugins.path.isAbsolute(filePath)) {
        return plugins.path.join(dirPath, filePath);
      } else {
        return filePath;
      }
    })();
    const fileBuffer = plugins.fs.readFileSync(readPath);

    // push a read file as Smartfile
    smartfileArray.push(
      new SmartFile({
        contentBuffer: fileBuffer,
        base: dirPath,
        path: filePath,
      })
    );
  }
  return smartfileArray;
};

/**
 * lists Folders in a directory on local disk
 * @returns Promise with an array that contains the folder names
 */
export const listFolders = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
  return listFoldersSync(pathArg, regexFilter);
};

/**
 * lists Folders SYNCHRONOUSLY in a directory on local disk
 * @returns an array with the folder names as strings
 */
export const listFoldersSync = (pathArg: string, regexFilter?: RegExp): string[] => {
  let folderArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
    return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isDirectory();
  });
  if (regexFilter) {
    folderArray = folderArray.filter((fileItem) => {
      return regexFilter.test(fileItem);
    });
  }
  return folderArray;
};

/**
 * lists Files in a directory on local disk
 * @returns Promise
 */
export const listFiles = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
  return listFilesSync(pathArg, regexFilter);
};

/**
 * lists Files SYNCHRONOUSLY in a directory on local disk
 * @returns an array with the folder names as strings
 */
export const listFilesSync = (pathArg: string, regexFilter?: RegExp): string[] => {
  let fileArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
    return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
  });
  if (regexFilter) {
    fileArray = fileArray.filter((fileItem) => {
      return regexFilter.test(fileItem);
    });
  }
  return fileArray;
};

/**
 * lists all items (folders AND files) in a directory on local disk
 * @returns Promise<string[]>
 */
export const listAllItems = async (pathArg: string, regexFilter?: RegExp): Promise<string[]> => {
  return listAllItemsSync(pathArg, regexFilter);
};

/**
 * lists all items (folders AND files) in a directory on local disk
 * @returns an array with the folder names as strings
 * @executes SYNC
 */
export const listAllItemsSync = (pathArg: string, regexFilter?: RegExp): string[] => {
  let allItmesArray = plugins.fsExtra.readdirSync(pathArg).filter((file) => {
    return plugins.fsExtra.statSync(plugins.path.join(pathArg, file)).isFile();
  });
  if (regexFilter) {
    allItmesArray = allItmesArray.filter((fileItem) => {
      return regexFilter.test(fileItem);
    });
  }
  return allItmesArray;
};

/**
 * lists a file tree using a miniMatch filter
 * note: if the miniMatch Filter is an absolute path, the cwdArg will be omitted
 * @returns Promise<string[]> string array with the absolute paths of all matching files
 */
export const listFileTree = async (
  dirPathArg: string,
  miniMatchFilter: string,
  absolutePathsBool: boolean = false
): Promise<string[]> => {
  // handle absolute miniMatchFilter
  let dirPath: string;
  if (plugins.path.isAbsolute(miniMatchFilter)) {
    dirPath = '/';
  } else {
    dirPath = dirPathArg;
  }

  const options = {
    cwd: dirPath,
    nodir: true,
    dot: true,
  };

  // Fix inconsistent **/* glob behavior across systems
  // Some glob implementations don't include root-level files when using **/*
  // To ensure consistent behavior, we expand **/* patterns to include both root and nested files
  let patterns: string[];
  if (miniMatchFilter.startsWith('**/')) {
    // Extract the part after **/ (e.g., "*.ts" from "**/*.ts")
    const rootPattern = miniMatchFilter.substring(3);
    // Use both the root pattern and the original pattern to ensure we catch everything
    patterns = [rootPattern, miniMatchFilter];
  } else {
    patterns = [miniMatchFilter];
  }

  // Collect results from all patterns
  const allFiles = new Set<string>();
  for (const pattern of patterns) {
    const files = await plugins.glob.glob(pattern, options);
    files.forEach(file => allFiles.add(file));
  }

  let fileList = Array.from(allFiles).sort();
  
  if (absolutePathsBool) {
    fileList = fileList.map((filePath) => {
      return plugins.path.resolve(plugins.path.join(dirPath, filePath));
    });
  }

  return fileList;
};

/**
 * Watches for file stability before resolving the promise.
 * Ensures that the directory/file exists before setting up the watcher.
 *
 * **New behavior**: If the given path is a directory, this function will:
 *  1. Wait for that directory to exist (creating a timeout if needed).
 *  2. Watch the directory until at least one file appears.
 *  3. Then wait for the first file in the directory to stabilize before resolving.
 *
 * @param fileOrDirPathArg The path of the file or directory to monitor.
 * @param timeoutMs The maximum time to wait for the file to stabilize (in milliseconds). Default is 60 seconds.
 * @returns A promise that resolves when the target is stable or rejects on timeout/error.
 */
export const waitForFileToBeReady = async (
  fileOrDirPathArg: string,
  timeoutMs: number = 60000
): Promise<void> => {
  const startTime = Date.now();

  /**
   * Ensure that a path (file or directory) exists. If it doesn't yet exist,
   * wait until it does (or time out).
   * @param pathToCheck The file or directory path to check.
   */
  const ensurePathExists = async (pathToCheck: string): Promise<void> => {
    while (true) {
      try {
        await plugins.smartpromise.fromCallback((cb) =>
          plugins.fs.access(pathToCheck, plugins.fs.constants.F_OK, cb)
        );
        return;
      } catch (err: any) {
        if (err.code !== 'ENOENT') {
          throw err; // Propagate unexpected errors
        }
        if (Date.now() - startTime > timeoutMs) {
          throw new Error(`Timeout waiting for path to exist: ${pathToCheck}`);
        }
        await plugins.smartdelay.delayFor(500);
      }
    }
  };

  /**
   * Checks if a file (not directory) is stable by comparing sizes
   * across successive checks.
   * @param filePathArg The path of the file to check.
   * @returns A promise that resolves once the file stops changing.
   */
  const waitForSingleFileToBeStable = async (filePathArg: string): Promise<void> => {
    let lastFileSize = -1;
    let fileIsStable = false;

    // We'll create a helper for repeated stats-checking logic
    const checkFileStability = async () => {
      try {
        const stats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
          plugins.fs.stat(filePathArg, cb)
        );
        if (stats.isDirectory()) {
          // If it unexpectedly turns out to be a directory here, throw
          throw new Error(`Expected a file but found a directory: ${filePathArg}`);
        }
        if (stats.size === lastFileSize) {
          fileIsStable = true;
        } else {
          lastFileSize = stats.size;
          fileIsStable = false;
        }
      } catch (err: any) {
        // Ignore only if file not found
        if (err.code !== 'ENOENT') {
          throw err;
        }
      }
    };

    // Ensure file exists first
    await ensurePathExists(filePathArg);

    // Set up a watcher on the file itself
    const fileWatcher = plugins.fs.watch(filePathArg, { persistent: true }, async () => {
      if (!fileIsStable) {
        await checkFileStability();
      }
    });

    try {
      // Poll until stable or timeout
      while (!fileIsStable) {
        if (Date.now() - startTime > timeoutMs) {
          throw new Error(`Timeout waiting for file to stabilize: ${filePathArg}`);
        }
        await checkFileStability();
        if (!fileIsStable) {
          await plugins.smartdelay.delayFor(1000);
        }
      }
    } finally {
      fileWatcher.close();
    }
  };

  /**
   * Main logic: check if we have a directory or file at fileOrDirPathArg.
   * If directory, wait for first file in the directory to appear and stabilize.
   * If file, do the old single-file wait logic.
   */
  const statsForGivenPath = await (async () => {
    try {
      await ensurePathExists(fileOrDirPathArg);
      return await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
        plugins.fs.stat(fileOrDirPathArg, cb)
      );
    } catch (err) {
      // If there's an error (including timeout), just rethrow
      throw err;
    }
  })();

  if (!statsForGivenPath.isDirectory()) {
    // It's a file – just do the single-file stability wait
    await waitForSingleFileToBeStable(fileOrDirPathArg);
    return;
  }

  // Otherwise, it's a directory. Wait for the first file inside to appear and be stable
  const dirPath = fileOrDirPathArg;

  // Helper to find the first file in the directory if it exists
  const getFirstFileInDirectory = async (): Promise<string | null> => {
    const entries = await plugins.smartpromise.fromCallback<string[]>((cb) =>
      plugins.fs.readdir(dirPath, cb)
    );
    // We only want actual files, not subdirectories
    for (const entry of entries) {
      const entryPath = plugins.path.join(dirPath, entry);
      const entryStats = await plugins.smartpromise.fromCallback<plugins.fs.Stats>((cb) =>
        plugins.fs.stat(entryPath, cb)
      );
      if (entryStats.isFile()) {
        return entryPath;
      }
    }
    return null;
  };

  // Wait for a file to appear in this directory
  let firstFilePath = await getFirstFileInDirectory();
  if (!firstFilePath) {
    // Set up a watcher on the directory to see if a file appears
    const directoryWatcher = plugins.fs.watch(dirPath, { persistent: true });
    try {
      // We'll poll for the existence of a file in that directory
      while (!firstFilePath) {
        if (Date.now() - startTime > timeoutMs) {
          throw new Error(`Timeout waiting for a file to appear in directory: ${dirPath}`);
        }
        firstFilePath = await getFirstFileInDirectory();
        if (!firstFilePath) {
          await plugins.smartdelay.delayFor(1000);
        }
      }
    } finally {
      directoryWatcher.close();
    }
  }

  // Now that we have a file path, wait for that file to stabilize
  await waitForSingleFileToBeStable(firstFilePath);
};

/**
 * writes string or Smartfile to disk.
 * @param fileArg
 * @param fileNameArg
 * @param fileBaseArg
 */
export let toFs = async (
  fileContentArg: string | Buffer | SmartFile | StreamFile,
  filePathArg: string,
  optionsArg: {
    respectRelative?: boolean;
  } = {}
) => {
  const done = plugins.smartpromise.defer();

  // check args
  if (!fileContentArg || !filePathArg) {
    throw new Error('expected valid arguments');
  }

  // prepare actual write action
  let fileContent: string | Buffer;
  let fileEncoding: 'utf8' | 'binary' = 'utf8';
  let filePath: string = filePathArg;

  // handle Smartfile
  if (fileContentArg instanceof SmartFile) {
    fileContent = fileContentArg.contentBuffer;
    // handle options
    if (optionsArg.respectRelative) {
      filePath = plugins.path.join(filePath, fileContentArg.path);
    }
  } else if (Buffer.isBuffer(fileContentArg)) {
    fileContent = fileContentArg;
    fileEncoding = 'binary';
  } else if (typeof fileContentArg === 'string') {
    fileContent = fileContentArg;
  } else {
    throw new Error('fileContent is neither string nor Smartfile');
  }
  await ensureDir(plugins.path.parse(filePath).dir);
  plugins.fsExtra.writeFile(filePath, fileContent, { encoding: fileEncoding }, done.resolve);
  return await done.promise;
};

export const stat = async (filePathArg: string) => {
  return plugins.fsPromises.stat(filePathArg);
};
