import { Readable } from 'node:stream';
import fsp from 'node:fs/promises';
import type { Stats } from 'node:fs';
import { constants } from 'node:fs';
import path from 'node:path';
import mime from 'mime';
import checkDiskSpace from 'check-disk-space';
import crc32 from 'cyclic-32';
import type { Resource as ResourceInterface, User } from 'nephele';
import {
  BadGatewayError,
  ForbiddenError,
  MethodNotSupportedError,
  ResourceExistsError,
  ResourceNotFoundError,
  ResourceTreeNotCompleteError,
  UnauthorizedError,
} from 'nephele';

import type Adapter from './Adapter.js';
import {
  userReadBit,
  userWriteBit,
  userExecuteBit,
  groupReadBit,
  groupWriteBit,
  groupExecuteBit,
  otherReadBit,
  otherWriteBit,
  otherExecuteBit,
} from './FileSystemBits.js';
import Properties from './Properties.js';
import Lock from './Lock.js';

export type MetaStorage = {
  props?: { [name: string]: any };
  locks?: {
    [token: string]: {
      username: string;
      date: number;
      timeout: number;
      scope: 'exclusive' | 'shared';
      depth: '0' | 'infinity';
      provisional: boolean;
      owner: any;
    };
  };
};

export default class Resource implements ResourceInterface {
  adapter: Adapter;
  baseUrl: URL;
  path: string;
  // Don't use this directly. Call isCollection() instead.
  private collection: boolean | undefined = undefined;
  private etag: string | undefined = undefined;
  private stats: Stats | undefined = undefined;

  constructor({
    adapter,
    baseUrl,
    path: myPath,
    collection,
    stats,
  }: {
    adapter: Adapter;
    baseUrl: URL;
    path: string;
    collection?: boolean;
    stats?: Stats;
  }) {
    this.adapter = adapter;
    this.baseUrl = baseUrl;
    this.path = myPath.replace(
      new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`),
      '',
    );

    if (collection != null) {
      this.collection = collection;
    }

    if (stats) {
      this.stats = stats;
    }
  }

  get absolutePath() {
    return `${this.adapter.root}${path.sep}${this.path}`;
  }

  async getLocks() {
    const meta = await this.readMetadataFile();

    if (meta.locks == null) {
      return [];
    }

    return Object.entries(meta.locks).map(([token, entry]) => {
      const lock = new Lock({ resource: this, username: entry.username });

      lock.token = token;
      lock.date = new Date(entry.date);
      lock.timeout = entry.timeout;
      lock.scope = entry.scope;
      lock.depth = entry.depth;
      lock.provisional = entry.provisional;
      lock.owner = entry.owner;

      return lock;
    });
  }

  async getLocksByUser(user: User) {
    const meta = await this.readMetadataFile();

    if (meta.locks == null) {
      return [];
    }

    return Object.entries(meta.locks)
      .filter(([_token, entry]) => user.username === entry.username)
      .map(([token, entry]) => {
        const lock = new Lock({ resource: this, username: user.username });

        lock.token = token;
        lock.date = new Date(entry.date);
        lock.timeout = entry.timeout;
        lock.scope = entry.scope;
        lock.depth = entry.depth;
        lock.provisional = entry.provisional;
        lock.owner = entry.owner;

        return lock;
      });
  }

  async createLockForUser(user: User) {
    return new Lock({ resource: this, username: user.username });
  }

  async getProperties() {
    return new Properties({ resource: this });
  }

  async getStream(range?: { start: number; end: number }) {
    if (await this.isCollection()) {
      return Readable.from([]);
    }

    const handle = await fsp.open(this.absolutePath, 'r');

    const stream = handle.createReadStream(range ? range : undefined);
    stream.on('error', async () => {
      await handle.close();
    });
    stream.on('close', async () => {
      await handle.close();
    });

    return stream;
  }

  async setStream(input: Readable, user: User) {
    let exists = true;

    try {
      await fsp.access(path.dirname(this.absolutePath), constants.F_OK);
    } catch (e: any) {
      throw new ResourceTreeNotCompleteError(
        'One or more intermediate collections must be created before this resource.',
      );
    }

    if (await this.isCollection()) {
      throw new MethodNotSupportedError(
        'This resource is an existing collection.',
      );
    }

    try {
      await fsp.access(this.absolutePath, constants.W_OK);
    } catch (e: any) {
      exists = false;
    }

    if (!exists && user.uid != null) {
      await fsp.writeFile(this.absolutePath, Buffer.from([]));
      await fsp.chown(
        this.absolutePath,
        await this.adapter.getUid(user),
        await this.adapter.getGid(user),
      );
    }

    this.etag = undefined;

    const handle = await fsp.open(this.absolutePath, 'w');
    const stream = handle.createWriteStream();

    // Reset stats, since they are going to change.
    this.stats = undefined;

    input.pipe(stream);

    // Throttle throughput. Maybe add this as an option.

    // input.on('data', (chunk) => {
    //   if (!stream.write(chunk)) {
    //     input.pause();
    //     stream.once('drain', () => input.resume());
    //   } else {
    //     input.pause();
    //     setTimeout(() => input.resume(), 50);
    //   }
    // });

    // input.on('end', async () => {
    //   await stream.close();
    // });

    return await new Promise<void>((resolve, reject) => {
      stream.on('close', async () => {
        await handle.close();
        resolve();
      });

      stream.on('error', async (err) => {
        input.destroy(err);
        await handle.close();
        reject(err);
      });

      input.on('error', async (err) => {
        stream.destroy(err);
        await handle.close();
        reject(err);
      });
    });
  }

  async create(user: User) {
    if (await this.exists()) {
      throw new ResourceExistsError('A resource already exists here.');
    }

    try {
      await fsp.access(path.dirname(this.absolutePath), constants.F_OK);
    } catch (e: any) {
      throw new ResourceTreeNotCompleteError(
        'One or more intermediate collections must be created before this resource.',
      );
    }

    if (this.collection) {
      await fsp.mkdir(this.absolutePath);
    } else {
      await fsp.writeFile(this.absolutePath, Uint8Array.from([]));
    }

    if (user.uid != null) {
      await fsp.chown(
        this.absolutePath,
        await this.adapter.getUid(user),
        await this.adapter.getGid(user),
      );
    }
  }

  async delete(user: User) {
    if (!(await this.exists())) {
      throw new ResourceNotFoundError("This resource couldn't be found.");
    }

    try {
      await fsp.access(this.absolutePath, constants.W_OK);
    } catch (e: any) {
      throw new ForbiddenError('This resource cannot be deleted.');
    }

    const metaFilePath = await this.getMetadataFilePath();
    let metaFileExists = false;
    try {
      await fsp.access(metaFilePath, constants.F_OK);
      metaFileExists = true;
    } catch (e: any) {
      metaFileExists = false;
    }

    if (metaFileExists) {
      try {
        await fsp.access(metaFilePath, constants.W_OK);
      } catch (e: any) {
        throw new ForbiddenError('This resource cannot be deleted.');
      }
    }

    // We need the user and group IDs.
    const uid = await this.adapter.getUid(user);
    const gids = await this.adapter.getGids(user);

    if (user.uid != null) {
      // Check if the user can delete it.

      if (!this.stats) {
        this.stats = await this.adapter.stat(this.absolutePath);
      }

      if (
        !(
          this.stats.mode & otherWriteBit ||
          (this.stats.uid === uid && this.stats.mode & userWriteBit) ||
          (gids.includes(this.stats.gid) && this.stats.mode & groupWriteBit)
        )
      ) {
        throw new UnauthorizedError(
          'You do not have permission to delete this resource.',
        );
      }
    }

    if (metaFileExists) {
      await fsp.unlink(metaFilePath);
    }

    if (await this.isCollection()) {
      await this.deleteOrphanedMetadataFiles();
      await fsp.rmdir(this.absolutePath);
    } else {
      await fsp.unlink(this.absolutePath);
    }

    // Reset stats.
    this.stats = undefined;
  }

  async copy(destination: URL, baseUrl: URL, user: User) {
    const destinationPath = this.adapter.urlToAbsolutePath(
      destination,
      baseUrl,
    );

    if (destinationPath == null) {
      throw new BadGatewayError(
        'The destination URL is not under the namespace of this server.',
      );
    }

    if (
      this.absolutePath === destinationPath ||
      ((await this.isCollection()) &&
        destinationPath.startsWith(
          this.absolutePath.replace(
            new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`),
            () => path.sep,
          ),
        ))
    ) {
      throw new ForbiddenError(
        'The destination cannot be the same as or contained within the source.',
      );
    }

    try {
      await fsp.access(path.dirname(destinationPath), constants.F_OK);
    } catch (e: any) {
      throw new ResourceTreeNotCompleteError(
        'One or more intermediate collections must be created before this resource.',
      );
    }

    // We need the user and group IDs.
    const uid = await this.adapter.getUid(user);
    const gids = await this.adapter.getGids(user);

    if (user.uid != null) {
      // Check if the user can put it in the destination.
      const dstats = await this.adapter.stat(path.dirname(destinationPath));

      if (
        !(
          dstats.mode & otherReadBit ||
          (dstats.uid === uid && dstats.mode & userReadBit) ||
          (gids.includes(dstats.gid) && dstats.mode & groupReadBit)
        )
      ) {
        throw new UnauthorizedError(
          'You do not have permission to access the destination.',
        );
      }

      if (
        !(
          dstats.mode & otherWriteBit ||
          (dstats.uid === uid && dstats.mode & userWriteBit) ||
          (gids.includes(dstats.gid) && dstats.mode & groupWriteBit)
        )
      ) {
        throw new UnauthorizedError(
          'You do not have permission to write to the destination.',
        );
      }
    }

    let metaFilePath: string | undefined = undefined;
    if (await this.isCollection()) {
      try {
        const stat = await this.adapter.stat(destinationPath);
        if (stat.isDirectory()) {
          const metaFilePath = `${destinationPath}${path.sep}.nephelemeta`;
          const contents = await fsp.readdir(destinationPath);

          if (
            contents.length > 1 ||
            (contents.length === 1 && contents[0] !== metaFilePath)
          ) {
            throw new Error('Directory not empty.');
          }

          try {
            if (
              this.adapter.properties !== 'meta-files' &&
              this.adapter.locks !== 'meta-files'
            ) {
              throw new Error('Ignored error. Bypass metadata file deletion.');
            }
            await fsp.unlink(metaFilePath);
          } catch (e: any) {
            // Ignore errors deleting possible non-existent file.
          }
          await fsp.rmdir(destinationPath);
        } else {
          await fsp.unlink(destinationPath);
        }
      } catch (e: any) {
        // Ignore errors stat-ing a possible non-existent directory and deleting
        // a possibly non-empty directory.
      }
      try {
        await fsp.mkdir(destinationPath);
      } catch (e: any) {
        // We don't care if the function failed just because it's a directory
        // that already exists.
        const stat = await this.adapter.stat(destinationPath);
        if (!stat.isDirectory()) {
          throw e;
        }
      }
      try {
        if (
          this.adapter.properties !== 'meta-files' &&
          this.adapter.locks !== 'meta-files'
        ) {
          throw new Error('Ignored error. Bypass metadata file deletion.');
        }

        metaFilePath = `${destinationPath}${path.sep}.nephelemeta`;

        try {
          await fsp.unlink(metaFilePath);
        } catch (e: any) {
          // Ignore errors deleting a possibly non-existent file.
        }

        const meta = await this.readMetadataFile();
        meta.locks = {};
        await this.saveMetadataFile(meta, destinationPath, metaFilePath);
      } catch (e: any) {
        // Ignore errors while copying metadata files.
        metaFilePath = undefined;
      }
    } else {
      await fsp.copyFile(this.absolutePath, destinationPath);
      try {
        if (
          this.adapter.properties !== 'meta-files' &&
          this.adapter.locks !== 'meta-files'
        ) {
          throw new Error('Ignored error. Bypass metadata file deletion.');
        }

        const dirname = path.dirname(destinationPath);
        const basename = path.basename(destinationPath);
        metaFilePath = `${dirname}${path.sep}${basename}.nephelemeta`;

        try {
          await fsp.unlink(metaFilePath);
        } catch (e: any) {
          // Ignore errors deleting a possibly non-existent file.
        }

        const meta = await this.readMetadataFile();
        meta.locks = {};
        await this.saveMetadataFile(meta, destinationPath, metaFilePath);
      } catch (e: any) {
        // Ignore errors while copying metadata files.
        metaFilePath = undefined;
      }
    }

    if (user.uid != null) {
      const uid = await this.adapter.getUid(user);
      const gid = await this.adapter.getGid(user);

      // Set owner info.
      await fsp.chown(destinationPath, uid, gid);
      if (!this.stats) {
        this.stats = await this.adapter.stat(this.absolutePath);
      }
      // Set permissions.
      await fsp.chmod(destinationPath, this.stats.mode % 0o1000);

      if (metaFilePath != null) {
        try {
          await fsp.chown(metaFilePath, uid, gid);
          await fsp.chmod(metaFilePath, this.stats.mode % 0o1000);
        } catch (e: any) {
          // Ignore errors chown/chmod a possibly non-existent file.
        }
      }
    }

    if (!this.stats) {
      this.stats = await this.adapter.stat(this.absolutePath);
    }

    // Copy mode.
    try {
      await fsp.chmod(destinationPath, this.stats.mode);
    } catch (e: any) {
      // Ignore errors copying mode.
    }

    // Copy dates.
    try {
      await fsp.utimes(destinationPath, this.stats.atime, this.stats.mtime);
    } catch (e: any) {
      // Ignore errors copying dates.
    }

    return;
  }

  async move(destination: URL, baseUrl: URL, user: User) {
    if (await this.isCollection()) {
      throw new Error('Move called on a collection resource.');
    }

    const destinationPath = this.adapter.urlToAbsolutePath(
      destination,
      baseUrl,
    );

    if (destinationPath == null) {
      throw new BadGatewayError(
        'The destination URL is not under the namespace of this server.',
      );
    }

    if (
      this.absolutePath === destinationPath ||
      ((await this.isCollection()) &&
        destinationPath.startsWith(
          this.absolutePath.replace(
            new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`),
            () => path.sep,
          ),
        ))
    ) {
      throw new ForbiddenError(
        'The destination cannot be the same as or contained within the source.',
      );
    }

    try {
      await fsp.access(path.dirname(destinationPath), constants.F_OK);
    } catch (e: any) {
      throw new ResourceTreeNotCompleteError(
        'One or more intermediate collections must be created before this resource.',
      );
    }

    // We need the user and group IDs.
    const uid = await this.adapter.getUid(user);
    const gids = await this.adapter.getGids(user);

    if (user.uid != null) {
      // Check if the user can move it.
      if (!this.stats) {
        this.stats = await this.adapter.stat(this.absolutePath);
      }

      if (
        !(
          this.stats.mode & otherWriteBit ||
          (this.stats.uid === uid && this.stats.mode & userWriteBit) ||
          (gids.includes(this.stats.gid) && this.stats.mode & groupWriteBit)
        )
      ) {
        throw new UnauthorizedError(
          'You do not have permission to move this resource.',
        );
      }

      // Check if the user can put it in the destination.
      const dstats = await this.adapter.stat(path.dirname(destinationPath));

      if (
        !(
          dstats.mode & otherReadBit ||
          (dstats.uid === uid && dstats.mode & userReadBit) ||
          (gids.includes(dstats.gid) && dstats.mode & groupReadBit)
        )
      ) {
        throw new UnauthorizedError(
          'You do not have permission to access the destination.',
        );
      }

      if (
        !(
          dstats.mode & otherWriteBit ||
          (dstats.uid === uid && dstats.mode & userWriteBit) ||
          (gids.includes(dstats.gid) && dstats.mode & groupWriteBit)
        )
      ) {
        throw new UnauthorizedError(
          'You do not have permission to write to the destination.',
        );
      }
    }

    const metaFilePath = await this.getMetadataFilePath();
    const meta = await this.readMetadataFile();
    await fsp.rename(this.absolutePath, destinationPath);
    try {
      if (
        this.adapter.properties !== 'meta-files' &&
        this.adapter.locks !== 'meta-files'
      ) {
        throw new Error('Ignored error. Bypass metadata file deletion.');
      }
      const dirname = path.dirname(destinationPath);
      const basename = path.basename(destinationPath);
      const destMetaFilePath = `${dirname}${path.sep}${basename}.nephelemeta`;
      try {
        await fsp.unlink(destMetaFilePath);
      } catch (e: any) {
        // Ignore errors deleting a possibly non-existent file.
      }

      meta.locks = {};
      await this.saveMetadataFile(meta, destinationPath, destMetaFilePath);

      try {
        await fsp.unlink(metaFilePath);
      } catch (e: any) {
        // Ignore errors deleting a possibly non-existent file.
      }
    } catch (e: any) {
      // Ignore errors while moving metadata files.
    }

    // Reset stats.
    this.stats = undefined;
  }

  async getLength() {
    if (await this.isCollection()) {
      return 0;
    }

    if (!this.stats) {
      this.stats = await this.adapter.stat(this.absolutePath);
    }

    return this.stats.size;
  }

  async getEtag() {
    if (this.etag != null) {
      return this.etag;
    }

    if (!this.stats) {
      this.stats = await this.adapter.stat(this.absolutePath);
    }

    let etag: string;
    if (
      (await this.isCollection()) ||
      this.stats.size > this.adapter.contentEtagMaxBytes
    ) {
      etag = crc32
        .c(
          Buffer.from(
            `size: ${this.stats.size}; birthtime: ${this.stats.birthtimeMs}; mtime: ${this.stats.mtimeMs}`,
            'utf8',
          ),
        )
        .toString(16);
    } else {
      // Check if we can open the file.
      try {
        const handle = await fsp.open(this.absolutePath, 'r');
        await handle.close();
      } catch (e: any) {
        throw new Error('Resource is not accessible.');
      }
      try {
        etag = await new Promise(async (resolve, reject) => {
          const stream = (await this.getStream()).pipe(
            crc32.createHash({ seed: 0, table: crc32.TABLE.CASTAGNOLI }),
          );
          stream.on('error', reject);
          stream.on('data', (buffer: Buffer) => {
            resolve(buffer.toString('hex'));
          });
        });
      } catch (e: any) {
        throw new Error('Etag could not be calculated.');
      }
    }

    this.etag = etag;

    return this.etag;
  }

  async getMediaType() {
    if (await this.isCollection()) {
      return null;
    }

    const mediaType = mime.getType(path.basename(this.absolutePath));
    if (!mediaType) {
      return 'application/octet-stream';
    } else if (Array.isArray(mediaType)) {
      return typeof mediaType[0] === 'string'
        ? mediaType[0]
        : 'application/octet-stream';
    } else if (typeof mediaType === 'string') {
      return mediaType;
    } else {
      return 'application/octet-stream';
    }
  }

  async getCanonicalName() {
    return path.basename(this.path);
  }

  async getCanonicalPath() {
    if (await this.isCollection()) {
      return this.path.replace(
        new RegExp(`${path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}?$`),
        () => path.sep,
      );
    }
    return this.path;
  }

  async getCanonicalUrl() {
    return new URL(
      (await this.getCanonicalPath())
        .split(path.sep)
        .map(encodeURIComponent)
        .join('/')
        .replace(/^\//, () => ''),
      this.baseUrl,
    );
  }

  async isCollection() {
    if (this.collection != null) {
      return this.collection;
    }

    try {
      if (!this.stats) {
        this.stats = await this.adapter.stat(this.absolutePath);
      }
      this.collection = this.stats.isDirectory();
      return this.collection;
    } catch (e: any) {
      return false;
    }
  }

  async getInternalMembers(user: User) {
    if (!(await this.isCollection())) {
      throw new MethodNotSupportedError('This is not a collection.');
    }

    // We need the user and group IDs.
    const uid = await this.adapter.getUid(user);
    const gids = await this.adapter.getGids(user);

    if (user.uid != null) {
      // Check if the user can list its contents.
      if (!this.stats) {
        this.stats = await this.adapter.stat(this.absolutePath);
      }

      if (
        !(
          this.stats.mode & otherExecuteBit ||
          (this.stats.uid === uid && this.stats.mode & userExecuteBit) ||
          (gids.includes(this.stats.gid) && this.stats.mode & groupExecuteBit)
        )
      ) {
        throw new UnauthorizedError(
          "You do not have permission to list this collection's members.",
        );
      }
    }

    const listing = await fsp.readdir(this.absolutePath, {
      withFileTypes: true,
    });
    const resources: Resource[] = [];

    for (let dir of listing) {
      if (
        (this.adapter.properties === 'meta-files' ||
          this.adapter.locks === 'meta-files') &&
        dir.name.endsWith('.nephelemeta')
      ) {
        continue;
      }

      try {
        const isDir = dir.isDirectory();
        const isFile = dir.isFile();
        const isLink = dir.isSymbolicLink();

        // This adapter only supports directories, files, and symlinks.
        if (!isDir && !isFile && !isLink) {
          continue;
        }

        const filepath = `${this.path}${path.sep}${dir.name}`;

        if (isLink && this.adapter.followLinks) {
          const stats = await this.adapter.stat(
            `${this.absolutePath}${path.sep}${dir.name}`,
          );
          resources.push(
            new Resource({
              path: filepath,
              baseUrl: this.baseUrl,
              adapter: this.adapter,
              collection: stats.isDirectory(),
              stats,
            }),
          );
        } else {
          resources.push(
            new Resource({
              path: filepath,
              baseUrl: this.baseUrl,
              adapter: this.adapter,
              collection: isDir,
            }),
          );
        }
      } catch (e: any) {
        continue;
      }
    }

    return resources;
  }

  async exists() {
    if (this.stats && this.stats.birthtime != null) {
      return true;
    }

    try {
      await fsp.access(this.absolutePath, constants.F_OK);
    } catch (e: any) {
      return false;
    }

    return true;
  }

  async getStats() {
    if (!this.stats) {
      this.stats = await this.adapter.stat(this.absolutePath);
    }
    return this.stats;
  }

  async setMode(mode: number) {
    await fsp.chmod(this.absolutePath, mode);
    await fsp.chmod(await this.getMetadataFilePath(), mode);
  }

  async getFreeSpace() {
    const directory = (await this.isCollection())
      ? this.absolutePath
      : path.dirname(this.absolutePath);
    return (await checkDiskSpace(directory)).free;
  }

  async getTotalSpace() {
    const directory = (await this.isCollection())
      ? this.absolutePath
      : path.dirname(this.absolutePath);
    return (await checkDiskSpace(directory)).size;
  }

  async getMetadataFilePath() {
    if (await this.isCollection()) {
      return `${this.absolutePath}${path.sep}.nephelemeta`;
    } else {
      const dirname = path.dirname(this.absolutePath);
      const basename = path.basename(this.absolutePath);
      return `${dirname}${path.sep}${basename}.nephelemeta`;
    }
  }

  async readMetadataFile() {
    const filepath = await this.getMetadataFilePath();
    let meta: MetaStorage = {};

    if (
      this.adapter.properties !== 'meta-files' &&
      this.adapter.locks !== 'meta-files'
    ) {
      return meta;
    }

    try {
      meta = JSON.parse((await fsp.readFile(filepath)).toString());
    } catch (e: any) {
      if (e.code !== 'ENOENT' && e.code !== 'ENOTDIR') {
        throw e;
      }
    }

    return meta;
  }

  async saveMetadataFile(
    meta: MetaStorage,
    filePath?: string,
    metaFilePath?: string,
  ) {
    const saveProperties = this.adapter.properties === 'meta-files';
    const saveLocks = this.adapter.locks === 'meta-files';
    if (!saveProperties && !saveLocks) {
      return;
    }
    if (!saveProperties) {
      delete meta.props;
    }
    if (!saveLocks) {
      delete meta.locks;
    }

    if (!metaFilePath) {
      metaFilePath = await this.getMetadataFilePath();
    }
    let exists = true;

    try {
      await fsp.access(path.dirname(metaFilePath), constants.F_OK);
    } catch (e: any) {
      throw new ResourceTreeNotCompleteError(
        'One or more intermediate collections must be created before this resource.',
      );
    }

    try {
      await fsp.access(metaFilePath, constants.F_OK);
    } catch (e: any) {
      exists = false;
    }

    if (
      (meta.props == null || Object.keys(meta.props).length === 0) &&
      (meta.locks == null || Object.keys(meta.locks).length === 0)
    ) {
      if (exists) {
        // Delete metadata file, since it should now be empty.
        await fsp.unlink(metaFilePath);
      }
    } else {
      await fsp.writeFile(metaFilePath, JSON.stringify(meta, null, 2));

      try {
        const stat = filePath
          ? await this.adapter.stat(filePath)
          : await this.getStats();
        await fsp.chown(metaFilePath, stat.uid, stat.gid);
        await fsp.chmod(metaFilePath, stat.mode % 0o1000);
      } catch (e: any) {
        // Ignore errors on setting ownership of meta file.
      }
    }
  }

  async deleteOrphanedMetadataFiles() {
    if (!(await this.isCollection())) {
      throw new MethodNotSupportedError('This is not a collection.');
    }

    if (
      this.adapter.properties !== 'meta-files' &&
      this.adapter.locks !== 'meta-files'
    ) {
      return;
    }

    const listing = await fsp.readdir(this.absolutePath);
    const files: Set<string> = new Set();
    const metaFiles: Set<string> = new Set();

    for (let name of listing) {
      if (name === '.nephelemeta') {
        continue;
      }

      if (name.endsWith('.nephelemeta')) {
        metaFiles.add(name);
      } else {
        files.add(name);
      }
    }

    for (let name of files) {
      metaFiles.delete(`${name}.nephelemeta`);
    }

    const orphans = Array.from(metaFiles);

    for (let name of orphans) {
      const orphanPath = `${this.absolutePath}${path.sep}${name}`;
      await fsp.unlink(orphanPath);
    }
  }
}
