#!/usr/bin/python

# Copyright (c) 2021, quidame <quidame@poivron.org>
# Copyright (c) 2013, Alexander Bulimov <lazywolf0@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
author:
  - Alexander Bulimov (@abulimov)
  - quidame (@quidame)
module: filesystem
short_description: Makes a filesystem
description:
  - This module creates a filesystem.
extends_documentation_fragment:
  - community.general._attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  state:
    description:
      - If O(state=present), the filesystem is created if it does not already exist, that is the default behaviour if O(state)
        is omitted.
      - If O(state=absent), filesystem signatures on O(dev) are wiped if it contains a filesystem (as known by C(blkid)).
      - When O(state=absent), all other options but O(dev) are ignored, and the module does not fail if the device O(dev)
        does not actually exist.
    type: str
    choices: [present, absent]
    default: present
    version_added: 1.3.0
  fstype:
    choices: [bcachefs, btrfs, ext2, ext3, ext4, ext4dev, f2fs, lvm, ocfs2, reiserfs, xfs, vfat, swap, ufs, gfs2]
    description:
      - Filesystem type to be created. This option is required with O(state=present) (or if O(state) is omitted).
      - V(ufs) support has been added in community.general 3.4.0.
      - V(bcachefs) support has been added in community.general 8.6.0.
      - V(gfs2) support has been added in community.general 13.1.0.
    type: str
    aliases: [type]
  dev:
    description:
      - Target path to block device (Linux) or character device (FreeBSD) or regular file (both).
      - When setting Linux-specific filesystem types on FreeBSD, this module only works when applying to regular files, also known as
        disk images.
      - Currently V(lvm) (Linux-only) and V(ufs) (FreeBSD-only) do not support a regular file as their target O(dev).
      - Support for character devices on FreeBSD has been added in community.general 3.4.0.
    type: path
    required: true
    aliases: [device]
  force:
    description:
      - If V(true), allows to create new filesystem on devices that already has filesystem.
    type: bool
    default: false
  resizefs:
    description:
      - If V(true), if the block device and filesystem size differ, grow the filesystem into the space.
      - >-
        Supported when O(fstype) is one of: V(bcachefs), V(btrfs), V(ext2), V(ext3), V(ext4), V(ext4dev), V(f2fs), V(lvm), V(xfs), V(ufs) and V(vfat).
        Attempts to resize other filesystem types fail.
      - XFS only grows if mounted. Currently, the module is based on commands from C(util-linux) package to perform operations,
        so resizing of XFS is not supported on FreeBSD systems.
      - VFAT is likely to fail if C(fatresize < 1.04).
      - Mutually exclusive with O(uuid).
    type: bool
    default: false
  opts:
    description:
      - List of options to be passed to C(mkfs) command.
    type: str
  uuid:
    description:
      - Set filesystem's UUID to the given value.
      - The UUID options specified in O(opts) take precedence over this value.
      - See xfs_admin(8) (C(xfs)), tune2fs(8) (C(ext2), C(ext3), C(ext4), C(ext4dev)) for possible values.
      - For O(fstype=lvm) the value is ignored, it resets the PV UUID if set.
      - Supported for O(fstype) being one of V(bcachefs), V(ext2), V(ext3), V(ext4), V(ext4dev), V(lvm), V(gfs2) or V(xfs).
      - This is B(not idempotent). Specifying this option always results in a change.
      - Mutually exclusive with O(resizefs).
    type: str
    version_added: 7.1.0
  label:
    description:
      - Set filesystem's label to the given value.
      - The label options specified in O(opts) take precedence over this value.
      - Supported for O(fstype) being one of V(gfs2). Support for other filesystems might be added later.
      - On GFS2 this sets the lock table with the C(-t) option of mkfs.gfs2(8). It has to be in the form V(CLUSTERNAME:LOCKSPACE).
    type: str
    version_added: 13.1.0
requirements:
  - Uses specific tools related to the O(fstype) for creating or resizing a filesystem (from packages e2fsprogs, xfsprogs,
    dosfstools, and so on).
  - Uses generic tools mostly related to the Operating System (Linux or FreeBSD) or available on both, as C(blkid).
  - On FreeBSD, either C(util-linux) or C(e2fsprogs) package is required.
notes:
  - Potential filesystems on O(dev) are checked using C(blkid). In case C(blkid) is unable to detect a filesystem (and in
    case C(fstyp) on FreeBSD is also unable to detect a filesystem), this filesystem is overwritten even if O(force=false).
  - On FreeBSD systems, both C(e2fsprogs) and C(util-linux) packages provide a C(blkid) command that is compatible with this
    module. However, these packages conflict with each other, and only the C(util-linux) package provides the command required
    to not fail when O(state=absent).
seealso:
  - module: community.general.filesize
  - module: ansible.posix.mount
  - name: xfs_admin(8) manpage for Linux
    description: Manual page of the GNU/Linux's xfs_admin implementation.
    link: https://man7.org/linux/man-pages/man8/xfs_admin.8.html
  - name: tune2fs(8) manpage for Linux
    description: Manual page of the GNU/Linux's tune2fs implementation.
    link: https://man7.org/linux/man-pages/man8/tune2fs.8.html
"""

EXAMPLES = r"""
- name: Create a ext2 filesystem on /dev/sdb1
  community.general.filesystem:
    fstype: ext2
    dev: /dev/sdb1

- name: Create a ext4 filesystem on /dev/sdb1 and check disk blocks
  community.general.filesystem:
    fstype: ext4
    dev: /dev/sdb1
    opts: -cc

- name: Blank filesystem signature on /dev/sdb1
  community.general.filesystem:
    dev: /dev/sdb1
    state: absent

- name: Create a filesystem on top of a regular file
  community.general.filesystem:
    dev: /path/to/disk.img
    fstype: vfat

- name: Reset an xfs filesystem UUID on /dev/sdb1
  community.general.filesystem:
    fstype: xfs
    dev: /dev/sdb1
    uuid: generate

- name: Reset an ext4 filesystem UUID on /dev/sdb1
  community.general.filesystem:
    fstype: ext4
    dev: /dev/sdb1
    uuid: random

- name: Reset an LVM filesystem (PV) UUID on /dev/sdc
  community.general.filesystem:
    fstype: lvm
    dev: /dev/sdc
    uuid: random
"""

import os
import platform
import re
import stat

from ansible.module_utils.basic import AnsibleModule

from ansible_collections.community.general.plugins.module_utils._lvm import pvs_runner
from ansible_collections.community.general.plugins.module_utils._version import LooseVersion


class Device:
    def __init__(self, module, path):
        self.module = module
        self.path = path

    def size(self):
        """Return size in bytes of device. Returns int"""
        statinfo = os.stat(self.path)
        if stat.S_ISBLK(statinfo.st_mode):
            blockdev_cmd = self.module.get_bin_path("blockdev", required=True)
            dummy, out, dummy = self.module.run_command([blockdev_cmd, "--getsize64", self.path], check_rc=True)
            devsize_in_bytes = int(out)
        elif stat.S_ISCHR(statinfo.st_mode) and platform.system() == "FreeBSD":
            diskinfo_cmd = self.module.get_bin_path("diskinfo", required=True)
            dummy, out, dummy = self.module.run_command([diskinfo_cmd, self.path], check_rc=True)
            devsize_in_bytes = int(out.split()[2])
        elif os.path.isfile(self.path):
            devsize_in_bytes = os.path.getsize(self.path)
        else:
            self.module.fail_json(changed=False, msg=f"Target device not supported: {self}")

        return devsize_in_bytes

    def get_mountpoint(self):
        """Return (first) mountpoint of device. Returns None when not mounted."""
        cmd_findmnt = self.module.get_bin_path("findmnt", required=True)

        # find mountpoint
        rc, mountpoint, dummy = self.module.run_command(
            [cmd_findmnt, "--mtab", "--noheadings", "--output", "TARGET", "--source", self.path], check_rc=False
        )
        if rc != 0:
            mountpoint = None
        else:
            mountpoint = mountpoint.split("\n")[0]

        return mountpoint

    def __str__(self):
        return self.path


class Filesystem:
    MKFS: str | None = None
    MKFS_FORCE_FLAGS: list[str] | None = []
    MKFS_SET_UUID_OPTIONS: list[str] | None = None
    MKFS_SET_UUID_EXTRA_OPTIONS: list[str] | None = []
    MKFS_SET_LABEL_OPTIONS: list[str] | None = None
    INFO: str | None = None
    GROW: str | None = None
    GROW_SLACK: int = 0
    GROW_MAX_SPACE_FLAGS: list[str] | None = []
    GROW_MOUNTPOINT_ONLY = False
    CHANGE_UUID: str | None = None
    CHANGE_UUID_OPTION: str | None = None
    CHANGE_UUID_OPTION_HAS_ARG = True

    LANG_ENV = {"LANGUAGE": "C", "LC_ALL": "C"}

    def __init__(self, module):
        self.module = module

    @property
    def fstype(self):
        return type(self).__name__

    def get_fs_size(self, dev):
        """Return size in bytes of filesystem on device (integer).
        Should query the info with a per-fstype command that can access the
        device whenever it is mounted or not, and parse the command output.
        Parser must ensure to return an integer, or raise a ValueError.
        """
        raise NotImplementedError()

    def create(self, opts, dev, uuid=None, label=None):
        if self.module.check_mode:
            return

        if uuid and self.MKFS_SET_UUID_OPTIONS:
            if not (set(self.MKFS_SET_UUID_OPTIONS) & set(opts)):
                opts += [self.MKFS_SET_UUID_OPTIONS[0], uuid] + self.MKFS_SET_UUID_EXTRA_OPTIONS
        if label is not None and self.MKFS_SET_LABEL_OPTIONS:
            if not (set(self.MKFS_SET_LABEL_OPTIONS) & set(opts)):
                opts += [self.MKFS_SET_LABEL_OPTIONS[0], label]

        mkfs = self.module.get_bin_path(self.MKFS, required=True)
        cmd = [mkfs] + self.MKFS_FORCE_FLAGS + opts + [str(dev)]
        self.module.run_command(cmd, check_rc=True)
        if uuid and self.CHANGE_UUID and self.MKFS_SET_UUID_OPTIONS is None:
            self.change_uuid(new_uuid=uuid, dev=dev)

    def wipefs(self, dev):
        if self.module.check_mode:
            return

        # wipefs comes with util-linux package (as 'blockdev' & 'findmnt' above)
        # that is ported to FreeBSD. The use of dd as a portable fallback is
        # not doable here if it needs get_mountpoint() (to prevent corruption of
        # a mounted filesystem), since 'findmnt' is not available on FreeBSD,
        # even in util-linux port for this OS.
        wipefs = self.module.get_bin_path("wipefs", required=True)
        cmd = [wipefs, "--all", str(dev)]
        self.module.run_command(cmd, check_rc=True)

    def grow_cmd(self, target):
        """Build and return the resizefs commandline as list."""
        cmdline = [self.module.get_bin_path(self.GROW, required=True)]
        cmdline += self.GROW_MAX_SPACE_FLAGS + [target]
        return cmdline

    def grow(self, dev):
        """Get dev and fs size and compare. Returns stdout of used command."""
        devsize_in_bytes = dev.size()

        try:
            fssize_in_bytes = self.get_fs_size(dev)
        except NotImplementedError:
            self.module.fail_json(msg=f"module does not support resizing {self.fstype} filesystem yet")
        except ValueError as err:
            self.module.warn(f"unable to process {self.INFO} output '{err}'")
            self.module.fail_json(msg=f"unable to process {self.INFO} output for {dev}")

        if fssize_in_bytes + self.GROW_SLACK >= devsize_in_bytes:
            self.module.exit_json(changed=False, msg=f"{self.fstype} filesystem is using the whole device {dev}")
        elif self.module.check_mode:
            self.module.exit_json(changed=True, msg=f"resizing filesystem {self.fstype} on device {dev}")

        if self.GROW_MOUNTPOINT_ONLY:
            mountpoint = dev.get_mountpoint()
            if not mountpoint:
                self.module.fail_json(msg=f"{dev} needs to be mounted for {self.fstype} operations")
            grow_target = mountpoint
        else:
            grow_target = str(dev)

        dummy, out, dummy = self.module.run_command(self.grow_cmd(grow_target), check_rc=True)
        return out

    def change_uuid_cmd(self, new_uuid, target):
        """Build and return the UUID change command line as list."""
        cmdline = [self.module.get_bin_path(self.CHANGE_UUID, required=True)]
        if self.CHANGE_UUID_OPTION_HAS_ARG:
            cmdline += [self.CHANGE_UUID_OPTION, new_uuid, target]
        else:
            cmdline += [self.CHANGE_UUID_OPTION, target]
        return cmdline

    def change_uuid(self, new_uuid, dev):
        """Change filesystem UUID. Returns stdout of used command"""
        if self.module.check_mode:
            self.module.exit_json(change=True, msg=f"Changing {self.fstype} filesystem UUID on device {dev}")

        dummy, out, dummy = self.module.run_command(
            self.change_uuid_cmd(new_uuid=new_uuid, target=str(dev)), check_rc=True
        )
        return out


class Ext(Filesystem):
    MKFS_FORCE_FLAGS = ["-F"]
    MKFS_SET_UUID_OPTIONS = ["-U"]
    INFO = "tune2fs"
    GROW = "resize2fs"
    CHANGE_UUID = "tune2fs"
    CHANGE_UUID_OPTION = "-U"

    def get_fs_size(self, dev):
        """Get Block count and Block size and return their product."""
        cmd = self.module.get_bin_path(self.INFO, required=True)
        dummy, out, dummy = self.module.run_command([cmd, "-l", str(dev)], check_rc=True, environ_update=self.LANG_ENV)

        block_count = block_size = None
        for line in out.splitlines():
            if "Block count:" in line:
                block_count = int(line.split(":")[1].strip())
            elif "Block size:" in line:
                block_size = int(line.split(":")[1].strip())
            if None not in (block_size, block_count):
                break
        else:
            raise ValueError(repr(out))

        return block_size * block_count


class Ext2(Ext):
    MKFS = "mkfs.ext2"


class Ext3(Ext):
    MKFS = "mkfs.ext3"


class Ext4(Ext):
    MKFS = "mkfs.ext4"


class XFS(Filesystem):
    MKFS = "mkfs.xfs"
    MKFS_FORCE_FLAGS = ["-f"]
    INFO = "xfs_info"
    GROW = "xfs_growfs"
    # XFS (defaults with 4KiB blocksize) requires at least 64 block of free
    # space to add a new allocation group, avoid resizing (noop, but shown as
    # diff) if the difference between the filesystem and the device is less
    GROW_SLACK = 64 * 4096 - 1
    GROW_MOUNTPOINT_ONLY = True
    CHANGE_UUID = "xfs_admin"
    CHANGE_UUID_OPTION = "-U"

    def get_fs_size(self, dev):
        """Get bsize and blocks and return their product."""
        cmdline = [self.module.get_bin_path(self.INFO, required=True)]

        # Depending on the versions, xfs_info is able to get info from the
        # device, whenever it is mounted or not, or only if unmounted, or
        # only if mounted, or not at all. For any version until now, it is
        # able to query info from the mountpoint. So try it first, and use
        # device as the last resort: it may or may not work.
        mountpoint = dev.get_mountpoint()
        if mountpoint:
            cmdline += [mountpoint]
        else:
            cmdline += [str(dev)]
        dummy, out, dummy = self.module.run_command(cmdline, check_rc=True, environ_update=self.LANG_ENV)

        block_size = block_count = None
        for line in out.splitlines():
            col = line.split("=")
            if col[0].strip() == "data":
                if col[1].strip() == "bsize":
                    block_size = int(col[2].split()[0])
                if col[2].split()[1] == "blocks":
                    block_count = int(col[3].split(",")[0])
            if None not in (block_size, block_count):
                break
        else:
            raise ValueError(repr(out))

        return block_size * block_count


class Reiserfs(Filesystem):
    MKFS = "mkfs.reiserfs"
    MKFS_FORCE_FLAGS = ["-q"]


class Bcachefs(Filesystem):
    MKFS = "mkfs.bcachefs"
    MKFS_FORCE_FLAGS = ["--force"]
    MKFS_SET_UUID_OPTIONS = ["-U", "--uuid"]
    INFO = "bcachefs"
    GROW = "bcachefs"
    GROW_MAX_SPACE_FLAGS = ["device", "resize"]

    def get_fs_size(self, dev):
        """Return size in bytes of filesystem on device (integer)."""
        dummy, stdout, dummy = self.module.run_command(
            [self.module.get_bin_path(self.INFO), "show-super", str(dev)], check_rc=True
        )

        for line in stdout.splitlines():
            if "Size: " in line:
                parts = line.split()
                unit = parts[2]

                base = None
                exp = None

                units_2 = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
                units_10 = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]

                try:
                    exp = units_2.index(unit)
                    base = 1024
                except ValueError:
                    exp = units_10.index(unit)
                    base = 1000

                if exp == 0:
                    value = int(parts[1])
                else:
                    value = float(parts[1])

                if base is not None and exp is not None:
                    return int(value * pow(base, exp))

        raise ValueError(repr(stdout))


class Btrfs(Filesystem):
    MKFS = "mkfs.btrfs"
    INFO = "btrfs"
    GROW = "btrfs"
    GROW_MAX_SPACE_FLAGS = ["filesystem", "resize", "max"]
    GROW_MOUNTPOINT_ONLY = True

    def __init__(self, module):
        super().__init__(module)
        mkfs = self.module.get_bin_path(self.MKFS, required=True)
        dummy, stdout, stderr = self.module.run_command([mkfs, "--version"], check_rc=True)
        match = re.search(r" v([0-9.]+)", stdout)
        if not match:
            # v0.20-rc1 use stderr
            match = re.search(r" v([0-9.]+)", stderr)
        if match:
            # v0.20-rc1 doesn't have --force parameter added in following version v3.12
            if LooseVersion(match.group(1)) >= LooseVersion("3.12"):
                self.MKFS_FORCE_FLAGS = ["-f"]
        else:
            # assume version is greater or equal to 3.12
            self.MKFS_FORCE_FLAGS = ["-f"]
            self.module.warn(f"Unable to identify mkfs.btrfs version ({stdout!r}, {stderr!r})")

    def get_fs_size(self, dev):
        """Return size in bytes of filesystem on device (integer)."""
        mountpoint = dev.get_mountpoint()
        if not mountpoint:
            self.module.fail_json(msg=f"{dev} needs to be mounted for {self.fstype} operations")

        dummy, stdout, dummy = self.module.run_command(
            [self.module.get_bin_path(self.INFO), "filesystem", "usage", "-b", mountpoint], check_rc=True
        )
        for line in stdout.splitlines():
            if "Device size" in line:
                return int(line.split()[-1])
        raise ValueError(repr(stdout))


class Ocfs2(Filesystem):
    MKFS = "mkfs.ocfs2"
    MKFS_FORCE_FLAGS = ["-Fx"]


class F2fs(Filesystem):
    MKFS = "mkfs.f2fs"
    INFO = "dump.f2fs"
    GROW = "resize.f2fs"

    def __init__(self, module):
        super().__init__(module)
        mkfs = self.module.get_bin_path(self.MKFS, required=True)
        dummy, out, dummy = self.module.run_command([mkfs, os.devnull], check_rc=False, environ_update=self.LANG_ENV)
        # Looking for "	F2FS-tools: mkfs.f2fs Ver: 1.10.0 (2018-01-30)"
        # mkfs.f2fs displays version since v1.2.0
        match = re.search(r"F2FS-tools: mkfs.f2fs Ver: ([0-9.]+) \(", out)
        if match is not None:
            # Since 1.9.0, mkfs.f2fs check overwrite before make filesystem
            # before that version -f switch wasn't used
            if LooseVersion(match.group(1)) >= LooseVersion("1.9.0"):
                self.MKFS_FORCE_FLAGS = ["-f"]

    def get_fs_size(self, dev):
        """Get sector size and total FS sectors and return their product."""
        cmd = self.module.get_bin_path(self.INFO, required=True)
        dummy, out, dummy = self.module.run_command([cmd, str(dev)], check_rc=True, environ_update=self.LANG_ENV)
        sector_size = sector_count = None
        for line in out.splitlines():
            if "Info: sector size = " in line:
                # expected: 'Info: sector size = 512'
                sector_size = int(line.split()[4])
            elif "Info: total FS sectors = " in line:
                # expected: 'Info: total FS sectors = 102400 (50 MB)'
                sector_count = int(line.split()[5])
            if None not in (sector_size, sector_count):
                break
        else:
            raise ValueError(repr(out))

        return sector_size * sector_count


class VFAT(Filesystem):
    INFO = "fatresize"
    GROW = "fatresize"
    GROW_MAX_SPACE_FLAGS = ["-s", "max"]

    def __init__(self, module):
        super().__init__(module)
        if platform.system() == "FreeBSD":
            self.MKFS = "newfs_msdos"
        else:
            self.MKFS = "mkfs.vfat"

    def get_fs_size(self, dev):
        """Get and return size of filesystem, in bytes."""
        cmd = self.module.get_bin_path(self.INFO, required=True)
        dummy, out, dummy = self.module.run_command(
            [cmd, "--info", str(dev)], check_rc=True, environ_update=self.LANG_ENV
        )
        fssize = None
        for line in out.splitlines()[1:]:
            parts = line.split(":", 1)
            if len(parts) < 2:
                continue
            param, value = parts
            if param.strip() in ("Size", "Cur size"):
                fssize = int(value.strip())
                break
        else:
            raise ValueError(repr(out))

        return fssize


class LVM(Filesystem):
    MKFS = "pvcreate"
    MKFS_FORCE_FLAGS = ["-f"]
    MKFS_SET_UUID_OPTIONS = ["-u", "--uuid"]
    MKFS_SET_UUID_EXTRA_OPTIONS = ["--norestorefile"]
    INFO = "pvs"
    GROW = "pvresize"
    CHANGE_UUID = "pvchange"
    CHANGE_UUID_OPTION = "-u"
    CHANGE_UUID_OPTION_HAS_ARG = False

    def __init__(self, module):
        super().__init__(module)
        self._pvs = pvs_runner(module)

    def get_fs_size(self, dev):
        """Get and return PV size, in bytes."""
        with self._pvs("noheadings nosuffix units fields devices") as ctx:
            dummy, size, dummy = ctx.run(units="b", fields="pv_size", devices=[str(dev)])
        return int(size.strip())


class Swap(Filesystem):
    MKFS = "mkswap"
    MKFS_FORCE_FLAGS = ["-f"]


class UFS(Filesystem):
    MKFS = "newfs"
    INFO = "dumpfs"
    GROW = "growfs"
    GROW_MAX_SPACE_FLAGS = ["-y"]

    def get_fs_size(self, dev):
        """Get providersize and fragment size and return their product."""
        cmd = self.module.get_bin_path(self.INFO, required=True)
        dummy, out, dummy = self.module.run_command([cmd, str(dev)], check_rc=True, environ_update=self.LANG_ENV)

        fragmentsize = providersize = None
        for line in out.splitlines():
            if line.startswith("fsize"):
                fragmentsize = int(line.split()[1])
            elif "providersize" in line:
                providersize = int(line.split()[-1])
            if None not in (fragmentsize, providersize):
                break
        else:
            raise ValueError(repr(out))

        return fragmentsize * providersize


class GFS2(Filesystem):
    MKFS = "mkfs.gfs2"
    MKFS_FORCE_FLAGS = ["-O"]
    MKFS_SET_UUID_OPTIONS = ["-U"]
    MKFS_SET_LABEL_OPTIONS = ["-t"]


FILESYSTEMS = {
    "bcachefs": Bcachefs,
    "ext2": Ext2,
    "ext3": Ext3,
    "ext4": Ext4,
    "ext4dev": Ext4,
    "f2fs": F2fs,
    "reiserfs": Reiserfs,
    "xfs": XFS,
    "btrfs": Btrfs,
    "vfat": VFAT,
    "ocfs2": Ocfs2,
    "LVM2_member": LVM,
    "swap": Swap,
    "ufs": UFS,
    "gfs2": GFS2,
}


def main():
    friendly_names = {
        "lvm": "LVM2_member",
    }

    fstypes = set(FILESYSTEMS.keys()) - set(friendly_names.values()) | set(friendly_names.keys())

    # There is no "single command" to manipulate filesystems, so we map them all out and their options
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(type="str", default="present", choices=["present", "absent"]),
            fstype=dict(type="str", aliases=["type"], choices=list(fstypes)),
            dev=dict(type="path", required=True, aliases=["device"]),
            opts=dict(type="str"),
            force=dict(type="bool", default=False),
            resizefs=dict(type="bool", default=False),
            uuid=dict(type="str"),
            label=dict(type="str"),
        ),
        required_if=[("state", "present", ["fstype"])],
        mutually_exclusive=[
            ("resizefs", "uuid"),
        ],
        supports_check_mode=True,
    )

    state = module.params["state"]
    dev = module.params["dev"]
    fstype = module.params["fstype"]
    opts = module.params["opts"]
    force = module.params["force"]
    resizefs = module.params["resizefs"]
    uuid = module.params["uuid"]
    label = module.params["label"]

    mkfs_opts = []
    if opts is not None:
        mkfs_opts = opts.split()

    changed = False

    if not os.path.exists(dev):
        msg = f"Device {dev} not found."
        if state == "present":
            module.fail_json(msg=msg)
        else:
            module.exit_json(msg=msg)

    dev = Device(module, dev)

    # In case blkid/fstyp isn't able to identify an existing filesystem, device
    # is considered as empty, then this existing filesystem would be overwritten
    # even if force isn't enabled.
    cmd = module.get_bin_path("blkid", required=True)
    rc, raw_fs, err = module.run_command([cmd, "-c", os.devnull, "-o", "value", "-s", "TYPE", str(dev)])
    fs = raw_fs.strip()
    # BusyBox blkid may ignore -o/-s flags and output the full device info line,
    # e.g. '/dev/sda2: UUID="..." TYPE="btrfs"' instead of just 'btrfs'
    if fs and " " in fs:
        type_match = re.search(r'\bTYPE="?([^"\s]+)"?', raw_fs)
        fs = type_match.group(1) if type_match else ""
    if not fs and platform.system() == "FreeBSD":
        cmd = module.get_bin_path("fstyp", required=True)
        rc, raw_fs, err = module.run_command([cmd, str(dev)])
        fs = raw_fs.strip()

    if state == "present":
        if fstype in friendly_names:
            fstype = friendly_names[fstype]

        try:
            klass = FILESYSTEMS[fstype]
        except KeyError:
            module.fail_json(changed=False, msg=f"module does not support this filesystem ({fstype}) yet.")

        filesystem = klass(module)

        if uuid and not (filesystem.CHANGE_UUID or filesystem.MKFS_SET_UUID_OPTIONS):
            module.fail_json(
                changed=False, msg=f"module does not support UUID option for this filesystem ({fstype}) yet."
            )

        same_fs = fs and FILESYSTEMS.get(fs) == FILESYSTEMS[fstype]
        if same_fs and not resizefs and not uuid and not force:
            module.exit_json(changed=False)
        elif same_fs:
            if resizefs:
                if not filesystem.GROW:
                    module.fail_json(changed=False, msg=f"module does not support resizing {fstype} filesystem yet.")

                out = filesystem.grow(dev)

                module.exit_json(changed=True, msg=out)
            elif uuid:
                out = filesystem.change_uuid(new_uuid=uuid, dev=dev)

                module.exit_json(changed=True, msg=out)
        elif fs and not force:
            module.fail_json(msg=f"'{dev}' is already used as {fs}, use force=true to overwrite", rc=rc, err=err)

        # create fs
        filesystem.create(opts=mkfs_opts, dev=dev, uuid=uuid, label=label)
        changed = True

    elif fs:
        # wipe fs signatures
        filesystem = Filesystem(module)
        filesystem.wipefs(dev)
        changed = True

    module.exit_json(changed=changed)


if __name__ == "__main__":
    main()
