import { glob } from "@synstack/glob";
import { json } from "@synstack/json";
import { MdDoc } from "@synstack/markdown";
import { type AnyPath, path } from "@synstack/path";
import { Pipeable } from "@synstack/pipe";
import { str } from "@synstack/str";
import { type Xml, xml } from "@synstack/xml";
import { yaml } from "@synstack/yaml";
import * as fsSync from "fs";
import * as fs from "fs/promises";
import type { ZodTypeDef as ZodTypeDefV3, ZodType as ZodTypeV3 } from "zod/v3";
import type { ZodType as ZodTypeV4 } from "zod/v4";
import { type Stringable } from "../../shared/src/ts.utils.ts";
import { FsDir } from "./dir.lib.ts";

// Union type to support both Zod v3 and v4 schemas
type ZodSchema<OUT = any, IN = any> =
  | ZodTypeV3<OUT, ZodTypeDefV3, IN>
  | ZodTypeV4<OUT, IN>;

type TextEncoding = Exclude<BufferEncoding, "buffer">;
type WriteMode = "preserve" | "overwrite";

export interface Base64Data {
  type: "base64";
  data: string;
  mimeType: string;
}

/**
 * A strongly-typed, chainable API for file system operations.
 * Provides methods for reading, writing, and manipulating files with support for multiple formats.
 *
 * @typeParam TEncoding - The text encoding to use for file operations (default: 'utf-8')
 * @typeParam TSchema - Optional Zod schema for validating JSON/YAML data
 *
 * ```typescript
 * import { file } from "@synstack/fs";
 *
 * // Create a file instance
 * const configFile = file("./config.json")
 *   .schema(ConfigSchema)
 *   .read.json();
 *
 * // Write text with different encodings
 * const logFile = file("./log.txt")
 *   .write.text("Hello World");
 * ```
 */
export class FsFile<
  TEncoding extends TextEncoding = "utf-8",
  TSchema extends ZodSchema | undefined = undefined,
> extends Pipeable<FsFile<TEncoding, TSchema>, AnyPath> {
  private readonly _path: AnyPath;
  private readonly _encoding: TEncoding;
  private readonly _schema?: TSchema;

  /**
   * Create a new FsFile instance from a path, a list of paths to be resolved, or an existing FsFile instance.
   * The resulting path will be an absolute path.
   *
   * @param paths - A path or an existing FsFile instance
   * @returns A new FsFile instance with UTF-8 encoding
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const relativeFile = file("./relative/path.txt");
   * const existingFile = file(file("/path/to/existing.txt"));
   * ```
   */
  public static from(this: void, arg: FsFile | AnyPath) {
    if (arg instanceof FsFile) return arg;
    return new FsFile<"utf-8", undefined>(path.resolve(arg), "utf-8");
  }

  private constructor(path: AnyPath, encoding?: TEncoding, schema?: TSchema) {
    super();
    this._path = path;
    this._encoding = encoding ?? ("utf-8" as TEncoding);
    this._schema = schema ?? undefined;
  }

  /**
   * Provide a validation schema for the file content. To be used with:
   * - `.read.json`
   * - `.write.json`
   * - `.read.yaml`
   * - `.write.yaml`
   */
  /**
   * Provide a validation schema for JSON/YAML operations.
   * The schema will be used to validate data when reading or writing JSON/YAML files.
   *
   * @typeParam NewSchema - The Zod schema type for validation
   * @param schema - A Zod schema to validate JSON/YAML data
   * @returns A new FsFile instance with the schema attached
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   * import { z } from "zod";
   *
   * const ConfigSchema = z.object({
   *   port: z.number(),
   *   host: z.string()
   * });
   *
   * const config = await file("config.json")
   *   .schema(ConfigSchema)
   *   .read.json();
   * // config is typed as { port: number, host: string }
   * ```
   */
  public schema<NewSchema extends ZodSchema>(schema: NewSchema) {
    return new FsFile<TEncoding, NewSchema>(this._path, this._encoding, schema);
  }

  /**
   * Get the path of the file.
   *
   * @returns The absolute path of the file
   */
  public valueOf(): AnyPath {
    return this._path;
  }

  /**
   * Get the current instance of the file.
   * Used for type compatibility with Pipeable.
   *
   * @returns The current FsFile instance
   */
  public instanceOf(): FsFile<TEncoding, TSchema> {
    return this;
  }

  // #region sub actions

  /**
   * Access the read operations for the file.
   * Provides methods for reading file contents in various formats.
   *
   * @returns An FsFileRead instance with methods for reading the file
   *
   * ```typescript
   * const content = await file("data.txt").read.text();
   * const json = await file("config.json").read.json();
   * const yaml = await file("config.yml").read.yaml();
   * ```
   */
  public get read() {
    return new FsFileRead<TEncoding, TSchema>(
      this._path,
      this._encoding,
      this._schema,
    );
  }

  /**
   * Access the write operations for the file.
   * Provides methods for writing content to the file in various formats.
   *
   * @returns An FsFileWrite instance with methods for writing to the file
   *
   * ```typescript
   * await file("data.txt").write.text("Hello");
   * await file("config.json").write.json({ hello: "world" });
   * await file("config.yml").write.yaml({ config: true });
   * ```
   */
  public get write() {
    return new FsFileWrite<TEncoding, TSchema>(
      this._path,
      this._encoding,
      "overwrite",
      this._schema,
    );
  }

  // #endregion

  // #region sync

  /**
   * Get the absolute path of the file.
   *
   * @returns The absolute path as a string
   */
  public get path() {
    return this._path;
  }

  /**
   * Get the absolute path of the directory containing the file.
   *
   * @returns The directory path as a string
   */
  public dirPath() {
    return path.dirname(this._path);
  }

  /**
   * Get an FsDir instance representing the directory containing the file.
   *
   * @returns An FsDir instance for the parent directory
   *
   * ```typescript
   * const file = file("/path/to/file.txt");
   * const parentDir = file.dir(); // FsDir for "/path/to"
   * ```
   */
  public dir() {
    return FsDir.cwd(this.dirPath());
  }

  /**
   * Get the name of the file including its extension.
   *
   * @returns The file name with extension
   *
   * ```typescript
   * const file = file("/path/to/document.txt");
   * console.log(file.fileName()); // "document.txt"
   * ```
   */
  public fileName() {
    return path.filename(this._path);
  }

  /**
   * Get the extension of the file.
   *
   * @returns The file extension including the dot (e.g., ".txt")
   *
   * ```typescript
   * const file = file("/path/to/document.txt");
   * console.log(file.fileExtension()); // ".txt"
   * ```
   */
  public fileExtension() {
    return path.fileExtension(this._path);
  }

  /**
   * Get the name of the file without its extension.
   *
   * @returns The file name without extension
   *
   * ```typescript
   * const file = file("/path/to/document.txt");
   * console.log(file.fileNameWithoutExtension()); // "document"
   * ```
   */
  public fileNameWithoutExtension() {
    return path.filenameWithoutExtension(this._path);
  }

  /**
   * Get the MIME type of the file based on its extension.
   *
   * @returns The MIME type string or null if it cannot be determined
   *
   * ```typescript
   * const file = file("/path/to/image.png");
   * console.log(file.mimeType()); // "image/png"
   * ```
   */
  public mimeType() {
    return path.mimeType(this._path);
  }

  /**
   * Create a new FsFile instance with a path relative to this file's directory.
   *
   * @param relativePath - The relative path from this file's directory
   * @returns A new FsFile instance for the target path
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const sourceFile = file("/path/to/source.txt");
   * const targetFile = sourceFile.toFile("../output/target.txt");
   * // targetFile.path === "/path/output/target.txt"
   * ```
   */
  public toFile(relativePath: string) {
    const newPath = path.resolve(this.dirPath(), relativePath);
    return new FsFile(newPath);
  }

  /**
   * Create a new FsDir instance with a path relative to this file's directory.
   *
   * @param relativePath - The relative path from this file's directory
   * @returns A new FsDir instance for the target directory
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const sourceFile = file("/path/to/source.txt");
   * const outputDir = sourceFile.toDir("../output");
   * // outputDir.path === "/path/output"
   * ```
   */
  public toDir(relativePath: string) {
    const newPath = path.join(this.dirPath(), relativePath);
    return FsDir.cwd(newPath);
  }

  /**
   * Get the relative path from another file to this file
   *
   * ```ts
   * import { file } from "@synstack/fs";
   *
   * const file1 = file("/path/to/file1.txt");
   * const file2 = file("/path/to-other/file2.txt");
   *
   * console.log(file1.relativePathFrom(file2)); // ../to/file1.txt
   * ```
   */
  public relativePathFrom(dirOrFile: FsDir | FsFile): string {
    if (dirOrFile instanceof FsFile)
      return this.relativePathFrom(dirOrFile.dir());
    return path.relative(dirOrFile.path, this.path);
  }

  /**
   * Get the relative path to go from this file to another
   *
   * ```ts
   * import { file } from "@synstack/fs";
   *
   * const file1 = file("/path/to/file1.txt");
   * const file2 = file("/path/to-other/file2.txt");
   *
   * console.log(file1.relativePathTo(file2)); // ../to-other/file2.txt
   * ```
   */
  public relativePathTo(dirOrFileOrPath: FsDir | FsFile): string {
    return path.relative(this.dirPath(), dirOrFileOrPath.path);
  }

  /**
   * Check if the file is located within the specified directory.
   *
   * @param dirOrPath - The directory or path to check against
   * @returns True if the file is in the directory, false otherwise
   *
   * ```typescript
   * import { file, dir } from "@synstack/fs";
   *
   * const sourceFile = file("/path/to/file.txt");
   * console.log(sourceFile.isInDir(dir("/path"))); // true
   * console.log(sourceFile.isInDir(dir("/other"))); // false
   * ```
   */
  public isInDir(dirOrPath: AnyPath | FsDir) {
    return path.isInPath(dirOrPath.valueOf(), this._path.valueOf());
  }

  /**
   * Delete the file from the file system.
   * If the file doesn't exist, the operation is silently ignored.
   *
   * @returns A promise that resolves when the file is deleted
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const tempFile = file("./temp.txt");
   * await tempFile.write.text("temporary content");
   * await tempFile.remove(); // File is deleted
   * ```
   */
  public async remove(): Promise<void> {
    await fs.rm(this._path, { recursive: true }).catch((e) => {
      if (e.code === "ENOENT") return;
      throw e;
    });
  }

  /**
   * @deprecated Use {@link remove} instead.
   */
  public rm(): Promise<void> {
    return this.remove();
  }

  /**
   * Delete the file from the file system synchronously.
   * If the file doesn't exist, the operation is silently ignored.
   *
   * @synchronous
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const tempFile = file("./temp.txt");
   * tempFile.write.textSync("temporary content");
   * tempFile.removeSync(); // File is deleted immediately
   * ```
   */
  public removeSync(): void {
    try {
      fsSync.rmSync(this._path, { recursive: false });
    } catch (error: any) {
      if (error.code === "ENOENT") return;
      throw error;
    }
  }

  /**
   * @deprecated Use {@link removeSync} instead.
   */
  public rmSync(): void {
    this.removeSync();
  }

  /**
   * Move the file to a new location.
   *
   * @param newPath - The new path for the file or an existing FsFile instance
   * @returns A promise that resolves the new file
   */
  public async move(newPath: FsFile | AnyPath): Promise<FsFile> {
    const newFile = FsFile.from(newPath);
    await fs.rename(this._path, newFile.path);
    return newFile;
  }

  /**
   * Move the file to a new location synchronously.
   *
   * @param newPath - The new path for the file or an existing FsFile instance
   * @returns The new file
   */
  public moveSync(newPath: FsFile | AnyPath): FsFile {
    const newFile = FsFile.from(newPath);
    fsSync.renameSync(this._path, newFile.path);
    return newFile;
  }

  /**
   * Check if the file exists in the file system.
   *
   * @returns A promise that resolves to true if the file exists, false otherwise
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const configFile = file("./config.json");
   * if (await configFile.exists()) {
   *   const config = await configFile.read.json();
   * }
   * ```
   */
  public async exists(): Promise<boolean> {
    return fs
      .access(this._path, fs.constants.F_OK)
      .then(() => true)
      .catch(() => false);
  }

  /**
   * Check if the file exists in the file system synchronously.
   *
   * @synchronous
   * @returns True if the file exists, false otherwise
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const configFile = file("./config.json");
   * if (configFile.existsSync()) {
   *   const config = configFile.read.jsonSync();
   * }
   * ```
   */
  public existsSync(): boolean {
    try {
      fsSync.accessSync(this._path);
      return true;
    } catch (error: any) {
      if (error.code === "ENOENT") return false;
      throw error;
    }
  }

  /**
   * Get the creation date of the file.
   *
   * @returns A promise that resolves to the file's creation date
   * @throws If the file doesn't exist or cannot be accessed
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const sourceFile = file("./source.txt");
   * const created = await sourceFile.creationDate();
   * console.log(`File created on: ${created.toISOString()}`);
   * ```
   */
  public async creationDate(): Promise<Date> {
    const fileStats = await fs.stat(this._path);
    return fileStats.birthtime;
  }

  /**
   * Get the creation date of the file synchronously.
   *
   * @synchronous
   * @returns The file's creation date
   * @throws If the file doesn't exist or cannot be accessed
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const sourceFile = file("./source.txt");
   * const created = sourceFile.creationDateSync();
   * console.log(`File created on: ${created.toISOString()}`);
   * ```
   */
  public creationDateSync(): Date {
    const stats = fsSync.statSync(this._path);
    return stats.birthtime;
  }

  /**
   * Check if the file path matches any of the provided glob patterns.
   *
   * @param globs - One or more glob patterns to match against, either as separate arguments or an array
   * @returns True if the file matches any pattern, false otherwise
   *
   * ```typescript
   * import { file } from "@synstack/fs";
   *
   * const sourceFile = file("./src/components/Button.tsx");
   * console.log(sourceFile.matchesGlobs("**\/*.tsx")); // true
   * console.log(sourceFile.matchesGlobs(["*.css", "*.html"])); // false
   * console.log(sourceFile.matchesGlobs("**\/*.ts", "**\/*.tsx")); // true
   * ```
   */
  public matchesGlobs(...globs: Array<string> | [Array<string>]) {
    return glob.matches(this._path, ...globs);
  }

  /**
   * Capture parts of the file path using a glob pattern
   *
   * ```ts
   * import { file } from "@synstack/fs";
   *
   * const myFile = file("/my-domain/my-sub-domain/features/feature-name.controller.ts");
   * const res = myFile.globCapture("/(*)/(*)/features/(*).controller.ts");
   * if (!res) throw new Error("File doesn't match glob pattern");
   * console.log(res[1]); // my-domain
   * console.log(res[2]); // my-sub-domain
   * console.log(res[3]); // feature-name.controller.ts
   * ```
   */
  public globCapture(globPattern: string) {
    return glob.capture(globPattern, this._path);
  }
}

class FsFileRead<
  ENCODING extends TextEncoding = "utf-8",
  SCHEMA extends ZodSchema | undefined = undefined,
> {
  private readonly _path: AnyPath;
  private readonly _encoding: ENCODING;
  private readonly _schema?: SCHEMA;

  public constructor(path: AnyPath, encoding: ENCODING, schema?: SCHEMA) {
    this._path = path;
    this._encoding = encoding;
    this._schema = schema;
  }

  public get path() {
    return this._path;
  }

  // #region sync

  /**
   * Read the file contents as a string.
   *
   * @returns A promise that resolves to the file contents as a string
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * const content = await file("data.txt").read.text();
   * console.log(content); // "Hello, World!"
   * ```
   */
  public async text() {
    return fs.readFile(this._path, this._encoding);
  }

  /**
   * Read the file contents as a string synchronously.
   *
   * @synchronous
   * @returns The file contents as a string
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * const content = file("data.txt").read.textSync();
   * console.log(content); // "Hello, World!"
   * ```
   */
  public textSync() {
    return fsSync.readFileSync(this._path, this._encoding);
  }

  /**
   * Read the file contents and return a chainable string instance.
   * Used for further manipulation of the content using @synstack/str methods.
   *
   * @returns A promise that resolves to a chainable string instance
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * const content = await file("data.txt").read.str();
   * const lines = content
   *   .split("\n")
   *   .filter((line) => line.trim().length > 0);
   * ```
   */
  public async str() {
    return this.text().then(str);
  }

  /**
   * Read the file contents and return a chainable string instance synchronously.
   * Used for further manipulation of the content using @synstack/str methods.
   *
   * @synchronous
   * @returns A chainable string instance
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * const content = file("data.txt").read.strSync();
   * const lines = content
   *   .split("\n")
   *   .filter((line) => line.trim().length > 0);
   * ```
   */
  public strSync() {
    return str(this.textSync());
  }

  /**
   * Read and parse the file contents as JSON.
   * If a schema is provided, the parsed data will be validated against it.
   *
   * @returns A promise that resolves to the parsed JSON data
   * @throws If the file doesn't exist, cannot be read, or contains invalid JSON
   * @throws If schema validation fails when a schema is provided
   *
   * ```typescript
   * interface Config {
   *   port: number;
   *   host: string;
   * }
   *
   * const config = await file("config.json")
   *   .schema(ConfigSchema)
   *   .read.json();
   * // config is automatically typed as the schema's output type
   * ```
   */
  public json<
    OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,
  >(): Promise<OUT> {
    return this.text().then((t) =>
      json.deserialize(t, { schema: this._schema }),
    );
  }

  /**
   * Read and parse the file contents as JSON synchronously.
   * If a schema is provided, the parsed data will be validated against it.
   *
   * @synchronous
   * @returns The parsed JSON data
   * @throws If the file doesn't exist, cannot be read, or contains invalid JSON
   * @throws If schema validation fails when a schema is provided
   *
   * ```typescript
   * const config = file("config.json")
   *   .schema(ConfigSchema)
   *   .read.jsonSync();
   * // config is automatically typed as the schema's output type
   * ```
   */
  public jsonSync<
    OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,
  >(): OUT {
    return json.deserialize(this.textSync(), {
      schema: this._schema,
    });
  }

  /**
   * Read and parse the file contents as YAML.
   * If a schema is provided, the parsed data will be validated against it.
   *
   * @typeParam T - The type of the parsed YAML data
   * @returns A promise that resolves to the parsed YAML data
   * @throws If the file doesn't exist, cannot be read, or contains invalid YAML
   * @throws If schema validation fails when a schema is provided
   *
   * ```typescript
   * interface Config {
   *   environment: string;
   *   settings: Record<string, unknown>;
   * }
   *
   * const config = await file("config.yml")
   *   .schema(ConfigSchema)
   *   .read.yaml();
   * // config is automatically typed as the schema's output type
   * ```
   */
  public yaml<
    OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,
  >(): Promise<OUT> {
    return this.text().then((t) =>
      yaml.deserialize(t, { schema: this._schema }),
    );
  }

  /**
   * Read and parse the file contents as YAML synchronously.
   * If a schema is provided, the parsed data will be validated against it.
   *
   * @typeParam T - The type of the parsed YAML data
   * @synchronous
   * @returns The parsed YAML data
   * @throws If the file doesn't exist, cannot be read, or contains invalid YAML
   * @throws If schema validation fails when a schema is provided
   *
   * ```typescript
   * const config = file("config.yml")
   *   .schema(ConfigSchema)
   *   .read.yamlSync();
   * // config is automatically typed as the schema's output type
   * ```
   */
  public yamlSync<
    OUT = SCHEMA extends ZodSchema<infer O> ? O : unknown,
  >(): OUT {
    return yaml.deserialize(this.textSync(), {
      schema: this._schema,
    });
  }

  /**
   * Read and parse the file contents as XML using @synstack/xml.
   * This parser is specifically designed for LLM-related XML processing.
   *
   * @typeParam T - The type of the parsed XML nodes array, must extend Array<Xml.Node>
   * @returns A promise that resolves to the parsed XML nodes
   * @throws If the file doesn't exist, cannot be read, or contains invalid XML
   * @see {@link https://github.com/pAIrprogio/synscript/tree/main/packages/xml|@synstack/xml documentation}
   *
   * ```typescript
   * interface XmlNode {
   *   tag: string;
   *   attributes: Record<string, string>;
   *   children: Array<XmlNode>;
   * }
   *
   * const nodes = await file("data.xml").read.xml<XmlNode[]>();
   * console.log(nodes[0].tag); // "root"
   * console.log(nodes[0].attributes.id); // "main"
   * ```
   *
   * @remarks
   * - Uses a non-spec-compliant XML parser tailored for LLM use cases
   * - Optimized for simple XML structures commonly used in LLM responses
   * - Does not support all XML features (see documentation for details)
   */
  public async xml<T extends Array<Xml.Node>>(): Promise<T> {
    return this.text().then((content) => xml.parse<T>(content));
  }

  /**
   * Read and parse the file contents as XML synchronously using @synstack/xml.
   * This parser is specifically designed for LLM-related XML processing.
   *
   * @typeParam T - The type of the parsed XML nodes array, must extend Array<Xml.Node>
   * @synchronous
   * @returns The parsed XML nodes
   * @throws If the file doesn't exist, cannot be read, or contains invalid XML
   * @see {@link https://github.com/pAIrprogio/synscript/tree/main/packages/xml|@synstack/xml documentation}
   *
   * ```typescript
   * const nodes = file("data.xml").read.xmlSync<XmlNode[]>();
   * console.log(nodes[0].tag); // "root"
   * console.log(nodes[0].attributes.id); // "main"
   * ```
   *
   * @remarks
   * - Uses a non-spec-compliant XML parser tailored for LLM use cases
   * - Optimized for simple XML structures commonly used in LLM responses
   * - Does not support all XML features (see documentation for details)
   */
  public xmlSync<T extends Array<Xml.Node>>(): T {
    return xml.parse<T>(this.textSync());
  }

  /**
   * Read the file contents and encode them as a base64 string.
   * Useful for handling binary data or preparing content for data URLs.
   *
   * @returns A promise that resolves to the file contents as a base64-encoded string
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * // Read and encode an image file
   * const imageBase64 = await file("image.png").read.base64();
   * console.log(imageBase64); // "iVBORw0KGgoAAAANSUhEUgAA..."
   *
   * // Create a data URL for use in HTML/CSS
   * const dataUrl = `data:image/png;base64,${imageBase64}`;
   * ```
   */
  public async base64(): Promise<string> {
    return fs.readFile(this._path, "base64");
  }

  /**
   * Read the file contents and encode them as a base64 string synchronously.
   * Useful for handling binary data or preparing content for data URLs.
   *
   * @synchronous
   * @returns The file contents as a base64-encoded string
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * const imageBase64 = file("image.png").read.base64Sync();
   * const dataUrl = `data:image/png;base64,${imageBase64}`;
   * ```
   */
  public base64Sync(): string {
    return fsSync.readFileSync(this._path, "base64");
  }

  /**
   * Read the file contents and create a synstack-compatible Base64Data object.
   * This format includes MIME type information along with the base64-encoded data.
   *
   * @param defaultMimeType - The MIME type to use if it cannot be determined from the file extension
   * @returns A promise that resolves to a Base64Data object containing the encoded content and MIME type
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * // Read an image with automatic MIME type detection
   * const imageData = await file("image.png").read.base64Data();
   * console.log(imageData);
   * // {
   * //   type: "base64",
   * //   data: "iVBORw0KGgoAAAANSUhEUgAA...",
   * //   mimeType: "image/png"
   * // }
   *
   * // Specify a custom MIME type for a binary file
   * const data = await file("custom.bin")
   *   .read.base64Data("application/custom");
   * ```
   */
  public async base64Data(
    defaultMimeType: string = "application/octet-stream",
  ): Promise<Base64Data> {
    return {
      type: "base64",
      data: await this.base64(),
      mimeType: defaultMimeType,
    } satisfies Base64Data;
  }

  /**
   * Read the file contents and create a synstack-compatible Base64Data object synchronously.
   * This format includes MIME type information along with the base64-encoded data.
   *
   * @param defaultMimeType - The MIME type to use if it cannot be determined from the file extension
   * @synchronous
   * @returns A Base64Data object containing the encoded content and MIME type
   * @throws If the file doesn't exist or cannot be read
   *
   * ```typescript
   * const imageData = file("image.png").read.base64DataSync();
   * console.log(imageData);
   * // {
   * //   type: "base64",
   * //   data: "iVBORw0KGgoAAAANSUhEUgAA...",
   * //   mimeType: "image/png"
   * // }
   * ```
   */
  public base64DataSync(
    defaultMimeType: string = "application/octet-stream",
  ): Base64Data {
    return {
      type: "base64",
      data: this.base64Sync(),
      mimeType: defaultMimeType,
    } satisfies Base64Data;
  }

  /**
   * Read the file contents and parse them as a markdown document.
   * If a schema is provided, the header data will be validated before returning.
   *
   * @returns A promise that resolves to the markdown document
   * @throws If the file doesn't exist, cannot be read, or contains invalid markdown
   * @throws If schema validation fails when a schema is provided
   */
  public md<DATA_SHAPE = SCHEMA extends ZodSchema<infer O> ? O : unknown>() {
    return this.text().then((t) =>
      MdDoc.withOptions<DATA_SHAPE>({
        schema: this._schema,
      }).fromString(t),
    );
  }

  /**
   * Read the file contents and parse them as a markdown document synchronously.
   * If a schema is provided, the header data will be validated before returning.
   *
   * @synchronous
   * @returns The markdown document
   * @throws If the file doesn't exist, cannot be read, or contains invalid markdown
   * @throws If schema validation fails when a schema is provided
   */
  public mdSync<
    DATA_SHAPE = SCHEMA extends ZodSchema<infer O> ? O : unknown,
  >() {
    return MdDoc.withOptions<DATA_SHAPE>({
      schema: this._schema,
    }).fromString(this.textSync());
  }
}

// Todo: Passing absolute paths will break the cache, find a way to fix this
class FsFileWrite<
  TEncoding extends TextEncoding,
  TSchema extends ZodSchema | undefined = undefined,
> {
  private readonly _path: AnyPath;
  private readonly _encoding: TEncoding;
  private readonly _schema?: TSchema;
  private readonly _mode: WriteMode;

  public constructor(
    path: AnyPath,
    encoding: TEncoding,
    mode: WriteMode = "overwrite",
    schema?: TSchema,
  ) {
    this._path = path;
    this._encoding = encoding;
    this._schema = schema;
    this._mode = mode;
  }

  /**
   * Set the write mode of the file
   * @argument preserve: If the file already exists, it will be left untouched
   * @argument overwrite: If the file already exists, it will be overwritten
   */
  public mode<NewWriteMode extends WriteMode>(writeMode: NewWriteMode) {
    return new FsFileWrite(this._path, this._encoding, writeMode, this._schema);
  }

  /**
   * Write text content to a file asynchronously.
   * Creates parent directories if they don't exist.
   * Respects the write mode (overwrite/preserve) setting.
   *
   * @param content - The content to write, will be converted to string using toString()
   * @returns A promise that resolves when the write operation is complete
   * @throws If the write operation fails or if parent directory creation fails
   */
  public async text(content: Stringable): Promise<void> {
    if (this._mode === "preserve" && (await FsFile.from(this._path).exists()))
      return;
    const dirname = path.dirname(this._path);
    await fs.mkdir(dirname, { recursive: true });
    await fs.writeFile(this._path, content.toString(), this._encoding);
  }

  /**
   * Write text content to a file synchronously.
   * Creates parent directories if they don't exist.
   * Respects the write mode (overwrite/preserve) setting.
   *
   * @param content - The content to write, will be converted to string using toString()
   * @synchronous
   * @throws If the write operation fails or if parent directory creation fails
   */
  public textSync(content: Stringable): void {
    if (this._mode === "preserve" && FsFile.from(this._path).existsSync())
      return;
    const dirname = path.dirname(this._path);
    fsSync.mkdirSync(dirname, { recursive: true });
    fsSync.writeFileSync(this._path, content.toString(), this._encoding);
  }

  /**
   * Write data as JSON to a file asynchronously.
   * The data will be serialized using JSON.stringify.
   * If a schema is provided, the data will be validated before writing.
   *
   * @typeParam T - The type of data being written
   * @param data - The data to write, must be JSON-serializable
   * @returns A promise that resolves when the write operation is complete
   * @throws If schema validation fails or if the write operation fails
   */
  public async json<T>(
    data: TSchema extends ZodSchema<infer O> ? O : T,
  ): Promise<void> {
    return this.text(json.serialize(data, { schema: this._schema }));
  }

  // Todo: add mergeJson

  /**
   * Write data as formatted JSON to a file asynchronously.
   * The data will be serialized using JSON.stringify with pretty printing.
   * If a schema is provided, the data will be validated before writing.
   *
   * @typeParam T - The type of data being written
   * @param data - The data to write, must be JSON-serializable
   * @returns A promise that resolves when the write operation is complete
   * @throws If schema validation fails or if the write operation fails
   */
  public async prettyJson<T>(
    data: TSchema extends ZodSchema<infer O> ? O : T,
  ): Promise<void> {
    return this.text(
      json.serialize(data, { schema: this._schema, pretty: true }) + "\n",
    );
  }

  /**
   * Write data as JSON to a file synchronously.
   * The data will be serialized using JSON.stringify.
   * If a schema is provided, the data will be validated before writing.
   *
   * @typeParam T - The type of data being written
   * @param data - The data to write, must be JSON-serializable
   * @synchronous
   * @throws If schema validation fails or if the write operation fails
   */
  public jsonSync<T>(data: TSchema extends ZodSchema<infer O> ? O : T): void {
    return this.textSync(json.serialize(data, { schema: this._schema }));
  }

  /**
   * Write data as formatted JSON to a file synchronously.
   * The data will be serialized using JSON.stringify with pretty printing.
   * If a schema is provided, the data will be validated before writing.
   *
   * @typeParam T - The type of data being written
   * @param data - The data to write, must be JSON-serializable
   * @synchronous
   * @throws If schema validation fails or if the write operation fails
   */
  public prettyJsonSync<T = unknown>(
    data: TSchema extends ZodSchema<infer O> ? O : T,
  ): void {
    return this.textSync(
      json.serialize(data, { schema: this._schema, pretty: true }) + "\n",
    );
  }

  /**
   * Write data as YAML to a file asynchronously.
   * The data will be serialized using YAML.stringify.
   * If a schema is provided, the data will be validated before writing.
   *
   * @typeParam T - The type of data being written
   * @param data - The data to write, must be YAML-serializable
   * @throws If schema validation fails or if the write operation fails
   */
  public async yaml<T = unknown>(
    data: TSchema extends ZodSchema<infer O> ? O : T,
  ): Promise<void> {
    return this.text(yaml.serialize(data, { schema: this._schema }));
  }

  /**
   * Write data as YAML to a file synchronously.
   * The data will be serialized using YAML.stringify.
   * If a schema is provided, the data will be validated before writing.
   *
   * @typeParam T - The type of data being written
   * @param data - The data to write, must be YAML-serializable
   * @synchronous
   * @throws If schema validation fails or if the write operation fails
   */
  public yamlSync<T = unknown>(
    data: TSchema extends ZodSchema<infer O> ? O : T,
  ): void {
    return this.textSync(yaml.serialize(data, { schema: this._schema }));
  }

  /**
   * Write base64-encoded data to a file asynchronously.
   * Creates parent directories if they don't exist.
   * Respects the write mode (overwrite/preserve) setting.
   *
   * @param data - The base64-encoded string to write
   * @throws If the write operation fails or if parent directory creation fails
   */
  public async base64(data: Stringable): Promise<void> {
    if (this._mode === "preserve" && (await FsFile.from(this._path).exists()))
      return;
    const dirname = path.dirname(this._path);
    await fs.mkdir(dirname, { recursive: true });
    await fs.writeFile(this._path, data.toString(), "base64");
  }

  /**
   * Write base64-encoded data to a file synchronously.
   * Creates parent directories if they don't exist.
   * Respects the write mode (overwrite/preserve) setting.
   *
   * @param data - The base64-encoded string to write
   * @synchronous
   * @throws If the write operation fails or if parent directory creation fails
   */
  public base64Sync(data: Stringable): void {
    if (this._mode === "preserve" && FsFile.from(this._path).existsSync())
      return;
    const dirname = path.dirname(this._path);
    fsSync.mkdirSync(dirname, { recursive: true });
    return fsSync.writeFileSync(this._path, data.toString(), "base64");
  }

  /**
   * Write a markdown document to a file asynchronously.
   * The markdown document will be serialized using MdDoc.toMd.
   * If a schema is provided, the data will be validated before writing.
   *
   * @param data - The markdown document to write
   * @throws If schema validation fails or if the write operation fails
   */
  public md(data: MdDoc) {
    return this.text(data.toMd());
  }

  /**
   * Write a markdown document to a file synchronously.
   * The markdown document will be serialized using MdDoc.toMd.
   * If a schema is provided, the data will be validated before writing.
   *
   * @param data - The markdown document to write
   * @throws If schema validation fails or if the write operation fails
   * @synchronous
   */
  public mdSync(data: MdDoc) {
    return this.textSync(data.toMd());
  }
}

/**
 * Create a new FsFile instance from a path, a list of paths to be resolved, or an existing FsFile instance.
 * The resulting path will be an absolute path.
 *
 * @param args - One or more path segments to join into a file path, or an existing FsFile instance
 * @returns A new FsFile instance with UTF-8 encoding
 *
 * ```typescript
 * import { file } from "@synstack/fs";
 *
 * const relativeFile = file("./relative/path.txt");
 * const existingFile = file(file("/path/to/existing.txt"));
 * ```
 */
export const file = FsFile.from;
