BEGIN { push( @INC, ".." ); }
use WebminCore;
init_config();
foreign_require( "mount", "mount-lib.pl" );
foreign_require( "fdisk", "fdisk-lib.pl" );

#---------------------------------------------------------------------
# Helper: Cache mount info
# Returns a hash reference of device => mount point and an arrayref
# containing mount entries.
sub get_all_mount_points_cached {
    my %mount_info;
    my @mount_list = mount::list_mounted();
    foreach my $m (@mount_list) {
        $mount_info{ $m->[0] } = $m->[1];
    }
    my $swapinfo = backquote_command("swapinfo -k 2>/dev/null");
    foreach my $line ( split( /\n/, $swapinfo ) ) {
        if ( $line =~ /^(\/dev\/\S+)\s+\d+\s+\d+\s+\d+/ ) {
            $mount_info{$1} = "swap";
        }
    }

    # ZFS, GEOM, glabel, geli  remain the same as the original.
    if ( has_command("zpool") ) {
        my $zpool_out    = backquote_command("zpool status 2>/dev/null");
        my $current_pool = "";
        foreach my $line ( split( /\n/, $zpool_out ) ) {
            if ( $line =~ /^\s*pool:\s+(\S+)/ ) {
                $current_pool = $1;
            }
            elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
                my $dev = $1;
                $mount_info{$dev} = "ZFS pool: $current_pool";
            }
        }
    }
    if ( has_command("geom") ) {

        # gmirror
        my $gmirror_out    = backquote_command("gmirror status 2>/dev/null");
        my $current_mirror = "";
        foreach my $line ( split( /\n/, $gmirror_out ) ) {
            if ( $line =~ /^(\S+):/ ) {
                $current_mirror = $1;
            }
            elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
                my $dev = $1;
                $mount_info{$dev} = "gmirror: $current_mirror";
            }
        }

        # gstripe
        my $gstripe_out    = backquote_command("gstripe status 2>/dev/null");
        my $current_stripe = "";
        foreach my $line ( split( /\n/, $gstripe_out ) ) {
            if ( $line =~ /^(\S+):/ ) {
                $current_stripe = $1;
            }
            elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
                my $dev = $1;
                $mount_info{$dev} = "gstripe: $current_stripe";
            }
        }

        # graid
        my $graid_out    = backquote_command("graid status 2>/dev/null");
        my $current_raid = "";
        foreach my $line ( split( /\n/, $graid_out ) ) {
            if ( $line =~ /^(\S+):/ ) {
                $current_raid = $1;
            }
            elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
                my $dev = $1;
                $mount_info{$dev} = "graid: $current_raid";
            }
        }
    }
    if ( has_command("glabel") ) {
        my $glabel_out = backquote_command("glabel status 2>/dev/null");
        foreach my $line ( split( /\n/, $glabel_out ) ) {
            if ( $line =~ /^\s*(\S+)\s+(\S+)\s+(\S+)/ ) {
                my $label = $1;
                my $dev   = $3;
                if ( $dev =~ /^\/dev\// ) {
                    $mount_info{$dev} = "glabel: $label";
                }
            }
        }
    }
    if ( has_command("geli") ) {
        my $geli_out = backquote_command("geli status 2>/dev/null");
        foreach my $line ( split( /\n/, $geli_out ) ) {
            if ( $line =~ /^(\/dev\/\S+)\s+/ ) {
                my $dev = $1;
                $mount_info{$dev} = "geli encrypted";
            }
        }
    }
    return ( \%mount_info, \@mount_list );
}

#---------------------------------------------------------------------
# Helper: Get file statistics for a device (cached per device)
sub get_dev_stat {
    my ($dev) = @_;
    if ( -e $dev ) {
        my @st = stat($dev);
        if (@st) {
            my $size   = $st[7];
            my $blocks = int( $size / 512 );
            return ( $size, $blocks );
        }
    }
    return ( undef, undef );
}

#---------------------------------------------------------------------
# is_boot_partition()
# Accepts a partition hash and an optional mount list to avoid
# re-calling mount::list_mounted()
sub is_boot_partition {
    my ( $part, $mount_list_ref ) = @_;
    return 1
      if ( $part->{'type'} eq 'freebsd-boot' or $part->{'type'} eq 'efi' );
    return 1 if ( $part->{'active'} );
    my @mounts = $mount_list_ref ? @$mount_list_ref : mount::list_mounted();
    foreach my $m (@mounts) {
        if ( $m->[1] eq '/boot' and $m->[0] eq $part->{'device'} ) {
            return 1;
        }
    }
    if ( $part->{'type'} eq 'freebsd-zfs' ) {
        my $out = backquote_command("zpool get bootfs 2>/dev/null");
        if ( $out =~ /\s+bootfs\s+\S+\/boot\s+/ ) {
            my $pool_out = backquote_command("zpool status 2>/dev/null");
            if ( $pool_out =~ /\Q$part->{'device'}\E/ ) {
                return 1;
            }
        }
    }
    return 0;
}

#---------------------------------------------------------------------
# list_disks_partitions()
# Returns a list of all disks, slices and partitions (optimized)
sub list_disks_partitions {
    my @results;
    my %dev_stat_cache;    # cache stat info per /dev device
    my @disk_devices;

    # Get disk devices from /dev directory
    if ( opendir( my $dh, "/dev" ) ) {
        my @all_devs = readdir($dh);
        closedir($dh);
        foreach my $dev (@all_devs) {
            if ( $dev =~ /^(ada|ad|da|amrd|nvd|vtbd|nda)(\d+)$/ ) {
                push( @disk_devices, $dev );
            }
        }
    }

    # Fallback: sysctl
    if ( !@disk_devices ) {
        my $sysctl_out = backquote_command("sysctl -n kern.disks 2>/dev/null");
        if ($sysctl_out) {
            chomp($sysctl_out);
            @disk_devices = split( /\s+/, $sysctl_out );
        }
    }

    # Fallback: dmesg
    if ( !@disk_devices ) {
        my $dmesg_out = backquote_command(
            "dmesg | grep -E '(ada|ad|da|amrd|nvd|vtbd|nda)[0-9]+:' 2>/dev/null"
        );
        while ( $dmesg_out =~ /\b(ada|ad|da|amrd|nvd|vtbd|nda)(\d+):/g ) {
            my $disk = "$1$2";
            push( @disk_devices, $disk ) if ( -e "/dev/$disk" );
        }
    }

    # Fallback: geom
    if ( !@disk_devices ) {
        my $geom_out = backquote_command("geom disk list 2>/dev/null");
        while ( $geom_out =~ /Name:\s+(\S+)/g ) {
            my $disk = $1;
            push( @disk_devices, $disk ) if ( -e "/dev/$disk" );
        }
    }

    # Get mount information once for all devices
    my ( $mount_info, $mount_list ) = get_all_mount_points_cached();

    foreach my $disk (@disk_devices) {
        my $disk_device = "/dev/$disk";
        my $diskinfo    = { 'device' => $disk_device, 'name' => $disk };

        # Determine sector size once per disk (4K-aware)
        my $sectorsz = get_disk_sectorsize($disk_device) || 512;
        $diskinfo->{'sectorsize'} = $sectorsz;

        # Cache stat information for the disk device
        unless ( exists $dev_stat_cache{$disk_device} ) {
            my ( $size, $blocks ) = get_dev_stat($disk_device);
            $dev_stat_cache{$disk_device} =
              ( defined $size ) ? [ $size, $blocks || 0 ] : [ 0, 0 ];
        }
        my ( $size, $blocks_cached ) = @{ $dev_stat_cache{$disk_device} };
        if ( $size > 0 ) {
            $diskinfo->{'size'}   = $size;
            $diskinfo->{'blocks'} = int( $size / $sectorsz );
        }
        else {
            my $diskinfo_out = backquote_command(
                "diskinfo " . quote_path($disk) . " 2>/dev/null" );
            if ( $diskinfo_out =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.*)/ ) {
                $diskinfo->{'size'}      = $1;
                $diskinfo->{'blocks'}    = int( $1 / $sectorsz );
                $diskinfo->{'cylinders'} = $2;
                $diskinfo->{'heads'}     = $3;
                $diskinfo->{'sectors'}   = $4;
                if ( defined $5 && $5 ne '' && $5 !~ /^\d+$/ ) {
                    $diskinfo->{'model'} = $5;
                }
            }
            if ( !$diskinfo->{'size'} ) {
                my $diskinfo_v_out = backquote_command(
                    "diskinfo -v " . quote_path($disk) . " 2>/dev/null" );
                if ( $diskinfo_v_out =~ /sectorsize:\s*(\d+)/i ) {
                    $sectorsz = $1;
                    $diskinfo->{'sectorsize'} = $sectorsz;
                }
                if ( $diskinfo_v_out =~ /mediasize in bytes:\s+(\d+)/i ) {
                    $diskinfo->{'size'}   = $1;
                    $diskinfo->{'blocks'} = int( $1 / $sectorsz );
                }
                if ( $diskinfo_v_out =~ /descr:\s+(.*)/ ) {
                    $diskinfo->{'model'} = $1;
                }
            }
            if ( !$diskinfo->{'model'} ) {
                my $cam_id_out =
                  backquote_command( "camcontrol identify "
                      . quote_path($disk)
                      . " 2>/dev/null" );
                if ( $cam_id_out =~ /model\s+(.*)/i ) {
                    my $m = $1;
                    $m =~ s/^\s+|\s+$//g;
                    $diskinfo->{'model'} = $m;
                }
            }
            if ( !$diskinfo->{'model'} ) {
                my $inq_out =
                  backquote_command( "camcontrol inquiry "
                      . quote_path($disk)
                      . " 2>/dev/null" );
                if ( $inq_out =~ /<([^>]+)>/ ) {
                    $diskinfo->{'model'} = $1;
                }
                else {
                    my ($vendor) =
                      ( $inq_out =~ /Vendor:\s*(\S.*?)(?:\s{2,}|$)/i );
                    my ($product) =
                      ( $inq_out =~ /Product:\s*(\S.*?)(?:\s{2,}|$)/i );
                    if ( $vendor || $product ) {
                        $diskinfo->{'model'} = join( ' ',
                            grep { defined && length } ( $vendor, $product ) );
                    }
                }
            }
            if ( !$diskinfo->{'model'} ) {
                my $geom = get_detailed_disk_info($disk_device);
                if ( $geom && $geom->{'descr'} ) {
                    $diskinfo->{'model'} = $geom->{'descr'};
                }
                elsif ( $geom && $geom->{'ident'} ) {
                    $diskinfo->{'model'} = $geom->{'ident'};
                }
            }

            # If size still not known, try GEOM mediasize
            if ( !$diskinfo->{'size'} ) {
                my $geom2 = get_detailed_disk_info($disk_device);
                if ( $geom2 && $geom2->{'mediasize_bytes'} ) {
                    $diskinfo->{'size'}   = $geom2->{'mediasize_bytes'};
                    $diskinfo->{'blocks'} = int( $diskinfo->{'size'} /
                          ( $diskinfo->{'sectorsize'} || 512 ) );
                }
            }
        }

        # Determine disk type
        if ( $disk =~ /^ada/ or $disk =~ /^ad/ ) {
            $diskinfo->{'type'} = 'ide';
        }
        elsif ( $disk =~ /^da/ ) {
            $diskinfo->{'type'} = 'scsi';
        }
        elsif ( $disk =~ /^amrd/ ) {
            $diskinfo->{'type'} = 'memdisk';
        }
          elsif ( $disk =~ /^(?:nvd|nda)/ ) {
              $diskinfo->{'type'} = 'nvme';
          }
        elsif ( $disk =~ /^vtbd/ ) {
            $diskinfo->{'type'} = 'virtio';
        }

        # Process slices and partitions
        $diskinfo->{'slices'} = [];
        if ( has_command("gpart") ) {
            my $gpart_out = backquote_command(
                "gpart show " . quote_path($disk) . " 2>/dev/null" );
            my @lines       = split( /\n/, $gpart_out );
            my $in_disk     = 0;
            my $disk_scheme = undef;                       # GPT, MBR, etc.
            foreach my $line (@lines) {
                if ( $line =~ /=>/ ) {
                    $in_disk = 1;

                    # Try to extract scheme from header line
                    if ( $line =~ /=>.*?\b$disk\b\s+(\S+)/ ) {
                        $disk_scheme = $1;
                    }
                    next;
                }
                if ( $in_disk and $line =~ /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ )
                {
                    my ( $start, $num_blocks, $name_or_idx, $raw_type ) =
                      ( $1, $2, $3, $4 );
                    next if ( $name_or_idx eq '-' or $raw_type eq 'free' );
                    my $slice_type =
                      ( $raw_type eq '-' ) ? "freebsd" : $raw_type;

                    # Determine slice index and device name
                    my ( $slice_index, $slice_devname );
                    if ( $name_or_idx =~ /^$disk(?:p|s)(\d+)$/ ) {
                        $slice_index   = $1;
                        $slice_devname = $name_or_idx;
                    }
                    elsif ( $name_or_idx =~ /^\d+$/ ) {
                        $slice_index = $name_or_idx;
                        my $sep =
                          ( defined $disk_scheme && $disk_scheme =~ /GPT/i )
                          ? 'p'
                          : 's';
                        $slice_devname = $disk . $sep . $slice_index;
                    }
                    else {
                        # Fallback: use as provided
                        $slice_devname = $name_or_idx;

                        # Try to extract index from suffix if possible
                        ($slice_index) = ( $slice_devname =~ /(?:p|s)(\d+)$/ );
                        $slice_index ||= $name_or_idx;
                    }
                    my $slice_device = "/dev/$slice_devname";

                    my $slice = {
                        'number'     => $slice_index,
                        'startblock' => $start,
                        'blocks'     => $num_blocks,
                        'size'       => $num_blocks * $sectorsz,
                        'type'       => $slice_type,
                        'device'     => $slice_device,
                        'parts'      => []
                    };
                    $slice->{'used'} = $mount_info{$slice_device};

           # Get partitions for this slice once, using the correct provider name
                    my $gpart_slice_out =
                      backquote_command( "gpart show "
                          . quote_path($slice_devname)
                          . " 2>/dev/null" );
                    my @slice_lines = split( /\n/, $gpart_slice_out );
                    my $in_slice    = 0;
                    my $slice_scheme;
                    foreach my $slice_line (@slice_lines) {
                        if ( $slice_line =~ /=>.*?\s+$slice_devname\s+(\S+)/ ) {
                            $in_slice     = 1;
                            $slice_scheme = $1;    # e.g., BSD, GPT
                            next;
                        }
                        if (    $in_slice
                            and $slice_line =~
                            /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ )
                        {
                            my (
                                $p_start,          $p_blocks,
                                $part_idx_or_name, $raw_ptype
                            ) = ( $1, $2, $3, $4 );
                            next
                              if ( $part_idx_or_name eq '-'
                                or $raw_ptype eq 'free' );
                            my $part_type =
                              ( $raw_ptype eq '-' and $part_idx_or_name ne '-' )
                              ? "freebsd-ufs"
                              : $raw_ptype;

         # For BSD disklabel, third column is index (1-based), convert to letter
                            my ( $part_letter, $part_device );
                            if (   $slice_scheme
                                && $slice_scheme     =~ /BSD/i
                                && $part_idx_or_name =~ /^\d+$/ )
                            {
                                my $idx = int($part_idx_or_name);
                                $part_letter = chr( ord('a') + $idx - 1 )
                                  ;    # 1 -> 'a', 2 -> 'b', etc.
                                $part_device = $slice_device . $part_letter;
                            }
                            else {
                                # For other schemes or if already a name
                                $part_device = "/dev/$part_idx_or_name";
                                $part_letter = substr( $part_idx_or_name, -1 );
                            }
                            my $part = {
                                'letter'     => $part_letter,
                                'startblock' => $p_start,
                                'blocks'     => $p_blocks,
                                'size'       => $p_blocks * $sectorsz,
                                'type'       => $part_type,
                                'device'     => $part_device,
                            };
                            $part->{'used'} = $mount_info{$part_device};
                            push( @{ $slice->{'parts'} }, $part );
                        }
                    }
                    push( @{ $diskinfo->{'slices'} }, $slice );
                }
            }
        }
        else {
# If no slices found with gpart, use fdisk if available
# (similar caching ideas apply)
            if ( has_command("fdisk") ) {
                my $fdisk_out = backquote_command(
                    "fdisk " . quote_path("/dev/$disk") . " 2>/dev/null" );
                foreach my $line ( split( /\n/, $fdisk_out ) ) {
                    if ( $line =~ /^\s*(\d+):\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/ )
                    {
                        my $slice_device = "/dev/${disk}s$1";
                        my $slice        = {
                            'number'     => $1,
                            'startblock' => $2,
                            'blocks'     => ( $4 - $2 + 1 ),
                            'size'       => ( $4 - $2 + 1 ) * $sectorsz,
                            'type'       => $5,
                            'device'     => $slice_device,
                            'parts'      => []
                        };
                        $slice->{'used'} = $mount_info{$slice_device};
                        my $dlcmd = _disklabel_cmd();
                        my $disklabel_out =
                          $dlcmd
                          ? backquote_command( "$dlcmd "
                              . quote_path($slice_device)
                              . " 2>/dev/null" )
                          : "";
                        foreach my $label_line ( split( /\n/, $disklabel_out ) )
                        {
                            if ( $label_line =~
                                /^(\s*)([a-h]):\s+(\d+)\s+(\d+)\s+(\S+)/ )
                            {
                                my $part_device = "${slice_device}$2";
                                my $part        = {
                                    'letter'     => $2,
                                    'startblock' => $3,
                                    'blocks'     => $4,
                                    'size'       => $4 * $sectorsz,
                                    'type'       => $5,
                                    'device'     => $part_device
                                };
                                $part->{'used'} = $mount_info{$part_device};
                                push( @{ $slice->{'parts'} }, $part );
                            }
                        }
                        push( @{ $diskinfo->{'slices'} }, $slice );
                    }
                }
            }
        }

        # If size was not determined, estimate from slices if available
        if ( !$diskinfo->{'size'} and @{ $diskinfo->{'slices'} } ) {
            my $total_size = 0;
            $total_size += $_->{'size'} for @{ $diskinfo->{'slices'} };
            if ( $total_size > 0 ) {
                $diskinfo->{'size'} = $total_size;
                $diskinfo->{'blocks'} =
                  int( $total_size / ( $diskinfo->{'sectorsize'} || 512 ) );
            }
        }

        # Finally, add this disk (dummy size set if necessary)
        if ( $diskinfo->{'size'} or -e $disk_device ) {
            $diskinfo->{'size'}   = $diskinfo->{'size'}   || 0;
            $diskinfo->{'blocks'} = $diskinfo->{'blocks'} || 0;
            push( @results, $diskinfo );
        }
    }
    return @results;
}

# check_fdisk()  unchanged
sub check_fdisk {
    if ( !has_command("fdisk") and !has_command("gpart") ) {
        return text( 'index_efdisk', "<tt>fdisk</tt>", "<tt>gpart</tt>" );
    }
    return undef;
}

# is_using_gpart()
sub is_using_gpart {
    return has_command("gpart") ? 1 : 0;
}

# disk_name(device) extracts name from /dev/device
sub disk_name {
    my ($device) = @_;
    $device =~ s/^\/dev\///;
    return $device;
}

# slice_name(slice)
sub slice_name {
    my ($slice) = @_;
    if ( $slice->{'device'} =~ /\/dev\/(\S+)/ ) {
        return $1;
    }
    return $slice->{'number'};
}

# slice_number(slice)
sub slice_number {
    my ($slice) = @_;
    if ( $slice->{'device'} =~ /\/dev\/\S+s(\d+)/ ) {
        return $1;
    }
    if ( $slice->{'device'} =~ /\/dev\/\S+p(\d+)/ ) {
        return $1;
    }
    return $slice->{'number'};
}

sub _safe_uint {
    my ($v) = @_;
    return undef unless defined $v;
    return undef unless $v =~ /^\d+$/;
    return int($v);
}

sub _safe_letter {
    my ($v) = @_;
    return undef unless defined $v;
    return undef unless $v =~ /^[a-z]$/i;
    return lc($v);
}

sub _fdisk_type_code {
    my ($t) = @_;
    return undef unless defined $t;
    $t =~ s/^\s+|\s+$//g;
    return undef if $t eq '';

    # Some callers pass decimal type codes, others pass 1-2 digit hex tags
    return int($t) if ( $t =~ /^\d+$/ );
    my $hex = lc($t);
    $hex =~ s/^0x//;
    return undef unless $hex =~ /^[0-9a-f]+$/;
    return hex($hex);
}

sub _fdisk_apply_config {
    my ( $disk_device, $config_text ) = @_;
    return "Missing disk device" unless defined $disk_device;
    return "Missing fdisk config" unless defined $config_text;

    my $tmp = &tempname();
    if ( !open( my $fh, ">", $tmp ) ) {
        return "Failed to write fdisk config file: $!";
    }
    print $fh $config_text;
    close($fh);

    my $cmd =
      "fdisk -f " . quote_path($tmp) . " -v " . quote_path($disk_device);
    my $out = backquote_command("$cmd 2>&1");
    unlink($tmp);
    return ($?) ? $out : undef;
}

sub _disklabel_cmd {
    return "bsdlabel"  if has_command("bsdlabel");
    return "disklabel" if has_command("disklabel");
    return undef;
}

sub _disklabel_read {
    my ($device) = @_;
    return ( undef, "Missing device" ) unless defined $device;
    my $cmd = _disklabel_cmd();
    return ( undef, "Missing bsdlabel/disklabel command" ) unless $cmd;
    my $out = backquote_command( "$cmd " . quote_path($device) . " 2>&1" );
    return ( $? ? ( undef, $out ) : ( $out, undef ) );
}

sub _disklabel_write_default {
    my ($device) = @_;
    return "Missing device" unless defined $device;
    my $cmd = _disklabel_cmd();
    return "Missing bsdlabel/disklabel command" unless $cmd;
    my $out = backquote_command( "$cmd -w " . quote_path($device) . " 2>&1" );
    return ($?) ? $out : undef;
}

sub _disklabel_restore {
    my ( $device, $label_text ) = @_;
    return "Missing device" unless defined $device;
    return "Missing label text" unless defined $label_text;
    my $cmd = _disklabel_cmd();
    return "Missing bsdlabel/disklabel command" unless $cmd;

    my $tmp = &tempname();
    if ( !open( my $fh, ">", $tmp ) ) {
        return "Failed to write disklabel file: $!";
    }
    print $fh $label_text;
    close($fh);

    my $out = backquote_command(
        "$cmd -R " . quote_path($device) . " " . quote_path($tmp) . " 2>&1"
    );
    unlink($tmp);
    return ($?) ? $out : undef;
}

sub _disklabel_update_partition_line {
    my (%args) = @_;
    my $label_text = $args{'label_text'};
    my $letter     = _safe_letter( $args{'letter'} );
    my $size       = $args{'size'};
    my $offset     = $args{'offset'};
    my $fstype     = $args{'fstype'};

    return undef unless defined $label_text;
    return undef unless defined $letter;
    return undef unless defined $size;
    return undef unless defined $offset;
    return undef unless defined $fstype;

    my @lines = split( /\n/, $label_text, -1 );
    my $found = 0;
    my $insert_at;
    for ( my $i = 0 ; $i < @lines ; $i++ ) {
        if ( !defined $insert_at
            && $lines[$i] =~ /^\s*#\s*size\s+offset\s+fstype\b/i )
        {
            $insert_at = $i + 1;
        }
        if ( $lines[$i] =~ /^(\s*)([a-h]):\s+(\S+)\s+(\S+)\s+(\S+)/i ) {
            my $indent = $1 // '';
            my $l      = lc($2);
            if ( $l eq $letter ) {
                $lines[$i] = $indent . $letter . ": $size $offset $fstype";
                $found = 1;
                last;
            }
        }
    }
    if ( !$found ) {
        my $newline = "  $letter: $size $offset $fstype";
        if ( defined $insert_at ) {
            splice( @lines, $insert_at, 0, $newline );
        }
        else {
            push( @lines, $newline );
        }
    }
    return join( "\n", @lines );
}

#---------------------------------------------------------------------
# Filesystem command generation and slice/partition modification functions
sub create_slice {
    my ( $disk, $slice ) = @_;
    my $cmd;
    if ( is_using_gpart() ) {

        # Ensure a partitioning scheme exists (default to MBR for non-GPT,
        # GPT if new) before adding
        my $base = disk_name( $disk->{'device'} );
        my $ds   = get_disk_structure($base);
        my $scheme =
          'MBR';    # default for existing disks or when type suggests MBR
        if ( !$ds || !$ds->{'scheme'} ) {

            # No scheme exists - decide based on partition type
            if ( $slice->{'type'} =~ /^(freebsd|fat32|ntfs|linux)$/i ) {
                $scheme = 'MBR';
            }
            else {
                $scheme = 'GPT';
            }
            my $init     = "gpart create -s $scheme " . quote_path($base);
            my $init_out = backquote_command("$init 2>&1");
            if ( $? != 0 && $init_out !~ /File exists|already exists/i ) {
                return $init_out;
            }

            # Refresh disk structure after creation
            $ds = get_disk_structure($base);
        }
        else {
            $scheme = $ds->{'scheme'};
        }

        # Defense-in-depth: validate type against allowed list for scheme
        my %allowed =
          map { $_->[0] => 1 } list_partition_types($scheme);
        return $text{'nslice_etype'}
          unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );

        $cmd = "gpart add -t " . $slice->{'type'};
        if ( defined $slice->{'startblock'}
            && $slice->{'startblock'} =~ /^\d+$/ )
        {
            $cmd .= " -b " . int( $slice->{'startblock'} );
        }
        if ( defined $slice->{'blocks'} && $slice->{'blocks'} =~ /^\d+$/ ) {
            $cmd .= " -s " . int( $slice->{'blocks'} );
        }
        $cmd .= " " . quote_path($base);
        my $out = backquote_command("$cmd 2>&1");
        if ($?) {
            return $out;
        }

        # After successful creation, populate the device field for the slice
        # Determine the separator based on scheme
        my $sep       = ( $scheme =~ /GPT/i ) ? 'p' : 's';
        my $slice_num = $slice->{'number'};
        $slice->{'device'} = "/dev/${base}${sep}${slice_num}";
        return undef;
    }
    else {
        my %allowed = map { $_ => 1 } fdisk::list_tags();
        return $text{'nslice_etype'}
          unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );

        my $sn = _safe_uint( $slice->{'number'} );
        return $text{'nslice_enumber'} unless defined $sn;
        return $text{'nslice_enumber'} if ( $sn < 1 || $sn > 4 );

        my $sb = _safe_uint( $slice->{'startblock'} );
        my $bl = _safe_uint( $slice->{'blocks'} );
        return $text{'nslice_estart'} unless defined $sb;
        return $text{'nslice_eend'}   unless ( defined $bl && $bl > 0 );

        my $tc = _fdisk_type_code( $slice->{'type'} );
        return $text{'nslice_etype'} unless defined $tc;

        my $cfg = "# Created by bsdfdisk (Webmin)\n";
        $cfg .= "p $sn $tc $sb $bl\n";
        my $err = _fdisk_apply_config( $disk->{'device'}, $cfg );
        return $err if $err;

        my $base = disk_name( $disk->{'device'} );
        $slice->{'device'} = "/dev/${base}s" . $slice->{'number'};
        return undef;
    }
}

sub delete_slice {
    my ( $disk, $slice ) = @_;
    if ( is_boot_partition($slice) ) { return $text{'slice_eboot'}; }
    foreach my $p ( @{ $slice->{'parts'} } ) {
        if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; }
    }
    my $cmd;
    if ( is_using_gpart() ) {
        my $idx = _safe_uint( slice_number($slice) );
        return $text{'slice_egone'} unless defined $idx;
        $cmd =
            "gpart delete -i "
          . $idx . " "
          . quote_path( disk_name( $disk->{'device'} ) );
        my $out = backquote_command("$cmd 2>&1");
        return ($?) ? $out : undef;
    }
    else {
        my $sn = _safe_uint( $slice->{'number'} );
        return $text{'slice_egone'} unless defined $sn;
        return $text{'slice_egone'} if ( $sn < 1 || $sn > 4 );

        my $cfg = "# Created by bsdfdisk (Webmin)\n";
        $cfg .= "p $sn 0 0 0\n";
        my $err = _fdisk_apply_config( $disk->{'device'}, $cfg );
        return $err if $err;
        return undef;
    }
}

sub delete_partition {
    my ( $disk, $slice, $part ) = @_;
    if ( is_boot_partition($part) ) { return $text{'part_eboot'}; }
    my $cmd;
    if ( is_using_gpart() ) {

        # BSD disklabel uses 1-based indexing: 'a' = 1, 'b' = 2, etc.
        my $pl = _safe_letter( $part->{'letter'} );
        return $text{'part_egone'} unless defined $pl;
        my $idx = ( ord($pl) - ord('a') ) + 1;
        $cmd = "gpart delete -i $idx " . quote_path( slice_name($slice) );
        my $out = backquote_command("$cmd 2>&1");
        return ($?) ? $out : undef;
    }
    else {
        my $pl = _safe_letter( $part->{'letter'} );
        return $text{'part_egone'} unless defined $pl;
        my ( $label, $lerr ) = _disklabel_read( $slice->{'device'} );
        return $lerr if $lerr;
        my $new_label = _disklabel_update_partition_line(
            label_text => $label,
            letter     => $pl,
            size       => 0,
            offset     => 0,
            fstype     => 'unused',
        );
        return "Failed to update disklabel" unless defined $new_label;
        my $err = _disklabel_restore( $slice->{'device'}, $new_label );
        return $err if $err;
        return undef;
    }
}

sub modify_slice {
    my ( $disk, $oldslice, $slice, $part ) = @_;
    if ( is_boot_partition($slice) ) { return $text{'slice_eboot'}; }
    foreach my $p ( @{ $slice->{'parts'} } ) {
        if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; }
    }
    my $cmd;
    if ( is_using_gpart() ) {
        my $base   = disk_name( $disk->{'device'} );
        my $ds     = get_disk_structure($base);
        my $scheme = ( $ds && $ds->{'scheme'} ) ? $ds->{'scheme'} : 'GPT';
        my %allowed =
          map { $_->[0] => 1 } list_partition_types($scheme);
        return $text{'nslice_etype'}
          unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );

        my $idx = _safe_uint( slice_number($slice) );
        return $text{'slice_egone'} unless defined $idx;
        $cmd =
            "gpart modify -i "
          . $idx . " -t "
          . $slice->{'type'} . " "
          . quote_path($base);
        my $out = backquote_command("$cmd 2>&1");
        return ($?) ? $out : undef;
    }
    else {
        my %allowed = map { $_ => 1 } fdisk::list_tags();
        return $text{'nslice_etype'}
          unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );

        my $sn = _safe_uint( $oldslice->{'number'} );
        return $text{'slice_egone'} unless defined $sn;
        return $text{'slice_egone'} if ( $sn < 1 || $sn > 4 );

        my $sb = _safe_uint( $oldslice->{'startblock'} );
        my $bl = _safe_uint( $oldslice->{'blocks'} );
        return $text{'slice_egone'} unless defined $sb && defined $bl;

        my $tc = _fdisk_type_code( $slice->{'type'} );
        return $text{'nslice_etype'} unless defined $tc;

        my $cfg = "# Created by bsdfdisk (Webmin)\n";
        $cfg .= "p $sn $tc $sb $bl\n";
        if ( defined $slice->{'active'} && $slice->{'active'} ) {
            $cfg .= "a $sn\n";
        }
        my $err = _fdisk_apply_config( $disk->{'device'}, $cfg );
        return $err if $err;
        return undef;
    }
}

sub save_partition {
    my ( $disk, $slice, $part ) = @_;
    my $cmd;
    if ( is_using_gpart() ) {
        my $provider = slice_name($slice);

        # Detect if this provider is a BSD label (sub-partitions) or GPT/MBR
        my $show =
          backquote_command( "gpart show " . quote_path($provider) . " 2>&1" );
        if ( $show =~ /\bBSD\b/ ) {

        # Inner BSD label: index is 1-based a->1, b->2, etc. Only
        # FreeBSD partition types are valid here.
            my $pl = _safe_letter( $part->{'letter'} );
            return $text{'part_egone'} unless defined $pl;
            my $idx = ( ord($pl) - ord('a') ) + 1;
            my %allowed =
              map { $_->[0] => 1 } list_partition_types('BSD');
            return $text{'part_etype'}
              unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );
            $cmd =
                "gpart modify -i $idx -t "
              . $part->{'type'} . " "
              . quote_path($provider);
        }
        else {
        # Not a BSD label; modifying a top-level partition by letter is
        # invalid. Return an error with guidance.
            return
          "Invalid operation: attempting to modify non-BSD sub-partition "
          . "by letter. Use slice editing for top-level partitions.";
        }
        my $out = backquote_command("$cmd 2>&1");
        return ($?) ? $out : undef;
    }
    else {
        my $pl = _safe_letter( $part->{'letter'} );
        return $text{'part_egone'} unless defined $pl;
        my %allowed = map { $_->[0] => 1 } list_partition_types('BSD');
        return $text{'part_etype'}
          unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );
        my $sb = _safe_uint( $part->{'startblock'} );
        my $bl = _safe_uint( $part->{'blocks'} );
        return $text{'part_egone'} unless defined $sb && defined $bl;

        my ( $label, $lerr ) = _disklabel_read( $slice->{'device'} );
        return $lerr if $lerr;
        my $new_label = _disklabel_update_partition_line(
            label_text => $label,
            letter     => $pl,
            size       => $bl,
            offset     => $sb,
            fstype     => $part->{'type'},
        );
        return "Failed to update disklabel" unless defined $new_label;
        my $err = _disklabel_restore( $slice->{'device'}, $new_label );
        return $err if $err;
        return undef;
    }
}

# Create a new BSD partition inside an MBR slice (gpart BSD label)
sub create_partition {
    my ( $disk, $slice, $part ) = @_;

    # gpart BSD label path (preferred)
    if ( is_using_gpart() ) {
        my $prov = slice_name($slice);

        # Ensure BSD label exists on the slice
        my $show =
          backquote_command( "gpart show " . quote_path($prov) . " 2>&1" );
        if ( $show !~ /\bBSD\b/ ) {
            my $init_err = initialize_slice( $disk, $slice );
            return $init_err if ($init_err);

            # Refresh the show output after initialization
            $show =
              backquote_command( "gpart show " . quote_path($prov) . " 2>&1" );
        }

        # Compute 1-based index
        my $pl = _safe_letter( $part->{'letter'} );
        return $text{'part_egone'} unless defined $pl;
        my $idx = ( ord($pl) - ord('a') ) + 1;

        my %allowed = map { $_->[0] => 1 } list_partition_types('BSD');
        return $text{'part_etype'}
          unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );

        # For BSD disklabel, start blocks are ALWAYS slice-relative
        # BSD partitions use 0-based addressing within the slice
        my $start_rel = _safe_uint( $part->{'startblock'} );
        my $blocks    = _safe_uint( $part->{'blocks'} );
        my $cmd       = "gpart add -i $idx -t $part->{'type'}";
        $cmd .= " -b $start_rel" if ( defined $start_rel && $start_rel > 0 );
        $cmd .= " -s $blocks"    if ( defined $blocks    && $blocks > 0 );
        $cmd .= " " . quote_path($prov);
        my $out = backquote_command("$cmd 2>&1");

        if ($?) {
            return $out;
        }

        # Populate the device field for the partition
        $part->{'device'} = $slice->{'device'} . $part->{'letter'};
        return undef;
    }

    # Legacy disklabel path (no gpart)
    my $pl = _safe_letter( $part->{'letter'} );
    return $text{'part_egone'} unless defined $pl;

    my %allowed = map { $_->[0] => 1 } list_partition_types('BSD');
    return $text{'part_etype'}
      unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );

    my $start_rel = _safe_uint( $part->{'startblock'} );
    my $blocks    = _safe_uint( $part->{'blocks'} );
    return $text{'nslice_estart'} unless defined $start_rel;
    return $text{'nslice_eend'} unless ( defined $blocks && $blocks > 0 );

    my ( $label, $lerr ) = _disklabel_read( $slice->{'device'} );
    if ($lerr) {
        my $init_err = initialize_slice( $disk, $slice );
        return $init_err if $init_err;
        ( $label, $lerr ) = _disklabel_read( $slice->{'device'} );
        return $lerr if $lerr;
    }

    # If start is 0, let disklabel compute a safe offset (similar to gpart
    # default placement) using the '*' offset syntax.
    my $offset = ( $start_rel > 0 ) ? $start_rel : '*';
    my $new_label = _disklabel_update_partition_line(
        label_text => $label,
        letter     => $pl,
        size       => $blocks,
        offset     => $offset,
        fstype     => $part->{'type'},
    );
    return "Failed to update disklabel" unless defined $new_label;

    my $err = _disklabel_restore( $slice->{'device'}, $new_label );
    return $err if $err;

    # Populate the device field for the partition
    $part->{'device'} = $slice->{'device'} . $pl;
    return undef;
}

sub get_create_filesystem_command {
    my ( $disk, $slice, $part, $options ) = @_;
    my $device = $part ? $part->{'device'} : $slice->{'device'};
    my @cmd    = ("newfs");
    if ( defined $options->{'free'} && $options->{'free'} =~ /^\d+$/ ) {
        push( @cmd, "-m", $options->{'free'} );
    }
    if ( defined $options->{'label'} && length $options->{'label'} ) {
        push( @cmd, "-L", quote_path( $options->{'label'} ) );
    }
    push( @cmd, "-t" ) if ( $options->{'trim'} );
    push( @cmd, quote_path($device) );
    return join( " ", @cmd );
}

# Helper to set a GPT or BSD partition label after filesystem creation
sub set_partition_label {
    my (%args) = @_;
    my $disk   = $args{'disk'};
    my $slice  = $args{'slice'};
    my $part   = $args{'part'};    # optional
    my $label  = $args{'label'};
    return if ( !defined $label || $label eq '' );
    my $base = $disk->{'device'};
    $base =~ s{^/dev/}{};
    my $ds = get_disk_structure($base);

    # GPT: label at disk level via gpart modify
    if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
        my $idx =
          $part ? undef : _safe_uint( $slice->{'number'} ); # GPT partition
        if ( defined $idx ) {
            my $cmd =
                "gpart modify -i $idx -l "
              . quote_path($label) . " "
              . quote_path($base);
            my $out = backquote_command("$cmd 2>&1");
            return ( $? ? $out : undef );
        }
        return undef;
    }

    # MBR: use glabel for slice-level or partition-level labels
    my $device = $part ? $part->{'device'} : $slice->{'device'};
    if ( $device && has_command('glabel') ) {

        # Remove existing glabel if present, then add new one
        my $existing = backquote_command(
            "glabel status 2>/dev/null | grep " . quote_path($device) );
        if ( $existing =~ /^(\S+)\s+/ ) {
            my $old_label   = $1;
            my $destroy_out = backquote_command(
                "glabel destroy " . quote_path($old_label) . " 2>&1" );
        }
        my $cmd =
          "glabel label " . quote_path($label) . " " . quote_path($device);
        my $out = backquote_command("$cmd 2>&1");
        return ( $? ? $out : undef );
    }
    return undef;
}

sub remove_partition_label {
    my (%args) = @_;
    my $disk   = $args{'disk'};
    my $slice  = $args{'slice'};
    my $part   = $args{'part'};
    my $base   = $disk->{'device'};
    $base =~ s{^/dev/}{};
    my $ds = get_disk_structure($base);

    # GPT: remove label via gpart modify -l ""
    if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
        my $idx = $part ? undef : _safe_uint( $slice->{'number'} );
        if ( defined $idx ) {
            my $cmd = "gpart modify -i $idx -l \"\" " . quote_path($base);
            my $out = backquote_command("$cmd 2>&1");
            return ( $? ? $out : undef );
        }
        return undef;
    }

    # MBR: remove glabel
    my $device = $part ? $part->{'device'} : $slice->{'device'};
    if ( $device && has_command('glabel') ) {
        my $existing = backquote_command(
            "glabel status 2>/dev/null | grep " . quote_path($device) );
        if ( $existing =~ /^(\S+)\s+/ ) {
            my $label = $1;
            my $cmd   = "glabel destroy " . quote_path($label);
            my $out   = backquote_command("$cmd 2>&1");
            return ( $? ? $out : undef );
        }
    }
    return undef;
}

# Get the current label for a slice/partition device (GPT label or glabel)
sub get_device_label_name {
    my (%args) = @_;
    my $disk   = $args{'disk'};
    my $slice  = $args{'slice'};
    my $device = $args{'device'} || ( $slice ? $slice->{'device'} : undef );
    return undef unless $device;

    my $label;

    # Prefer GPT labels when applicable
    my $base = $disk ? $disk->{'device'} : base_disk_device($device);
    if ($base) {
        $base =~ s{^/dev/}{};
        my $ds = $args{'disk_structure'} || get_disk_structure($base);
        if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
            my $idx = $slice ? slice_number($slice) : undef;
            if ( $idx && $ds->{'partitions'} && $ds->{'partitions'}->{$idx} ) {
                my $pl = $ds->{'partitions'}->{$idx}->{'label'};
                $label = $pl if ( $pl && $pl ne '(null)' );
            }
            if ( !$label && $idx && $ds->{'entries'} ) {
                foreach my $e ( @{ $ds->{'entries'} } ) {
                    next unless ( $e->{'type'} && $e->{'type'} eq 'partition' );
                    next
                      unless ( defined $e->{'index'} && $e->{'index'} == $idx );
                    if ( $e->{'label'} && $e->{'label'} ne '(null)' ) {
                        $label = $e->{'label'};
                        last;
                    }
                }
            }
        }
    }

    # Fallback: GEOM label (glabel)
    if ( !$label && has_command('glabel') ) {
        my $glabel_out = backquote_command("glabel status 2>/dev/null");
        foreach my $line ( split( /\n/, $glabel_out ) ) {
            if ( $line =~ /^\s*(\S+)\s+\S+\s+(\S+)/ ) {
                my ( $lab, $prov ) = ( $1, $2 );
                $prov = "/dev/$prov" if ( $prov !~ m{^/dev/} );
                if ( $prov eq $device ) {
                    $label = $lab;
                    last;
                }
            }
        }
    }
    return $label;
}

sub preferred_device_path {
    my ($device) = @_;
    return $device unless $device;

    # Check for GPT label first
    if ( -e "/dev/gpt" ) {
        my $gpt_label =
          backquote_command( "gpart list 2>/dev/null | grep -A 10 "
              . quote_path($device)
              . " | grep 'label:' | head -1" );
        if ( $gpt_label =~ /label:\s*(\S+)/ && $1 ne '(null)' ) {
            my $label_path = "/dev/gpt/$1";
            return $label_path if ( -e $label_path );
        }
    }

    # Check for glabel label
    if ( has_command('glabel') ) {
        my $glabel_out = backquote_command("glabel status 2>/dev/null");
        foreach my $line ( split( /\n/, $glabel_out ) ) {
            if ( $line =~ /^(\S+)\s+\S+\s+(.+)$/ ) {
                my ( $label, $provider ) = ( $1, $2 );
                $provider =~ s/^\s+|\s+$//g;
                if ( $provider eq $device || "/dev/$provider" eq $device ) {
                    my $label_path = "/dev/label/$label";
                    return $label_path if ( -e $label_path );
                }
            }
        }
    }

    # Check for UFS label
    my $ufs_label =
      backquote_command( "tunefs -p "
          . quote_path($device)
          . " 2>/dev/null | grep 'volume label'" );
    if ( $ufs_label =~ /volume label.*\[([^\]]+)\]/ && $1 ne '' ) {
        my $label_path = "/dev/ufs/$1";
        return $label_path if ( -e $label_path );
    }

    # Default: return original device
    return $device;
}

sub detect_filesystem_type {
    my ( $device, $hint ) = @_;
    my $t;
    if ( has_command('fstyp') ) {
        $t =
          backquote_command( "fstyp " . quote_path($device) . " 2>/dev/null" );
        $t =~ s/[\r\n]+$//;
    }
    $t ||= $hint || '';
    $t = lc($t);
    $t =~ s/^\s+|\s+$//g;

    # Prefer exact matches first
    return 'ufs'     if ( $t =~ /^(ufs|ffs)$/ );
    return 'msdosfs' if ( $t =~ /^(msdos|msdosfs|fat|fat32)$/ );
    return 'ext2fs'  if ( $t =~ /^(ext2|ext2fs|ext3|ext4)$/ );
    return 'zfs'     if ( $t eq 'zfs' );
    return 'swap'    if ( $t eq 'swap' );

    # Then allow bounded substring matches to avoid false positives
    return 'ufs'     if ( $t =~ /\b(ufs|ffs)\b/ );
    return 'msdosfs' if ( $t =~ /\b(msdos|msdosfs|fat|fat32|vfat|exfat)\b/ );
    return 'ext2fs'  if ( $t =~ /\b(ext2|ext3|ext4|ext2fs)\b/ );
    return 'zfs'     if ( $t =~ /\bzfs\b/ );
    return 'swap'    if ( $t =~ /\bswap\b/ );

    return $t || undef;
}

# Apply recommended ACL inherit flags for NFSv4 datasets (best-effort)
sub acl_inherit_flags_cmd {
    my ($dataset) = @_;
    return undef if ( !$dataset );
    my $ds = quote_path($dataset);
    my $cmd =
        'acltype=$(zfs get -H -o value acltype '
      . $ds
      . ' 2>/dev/null); '
      . 'mp=$(zfs get -H -o value mountpoint '
      . $ds
      . ' 2>/dev/null); '
      . 'if [ "$acltype" = "nfsv4" ] && [ -n "$mp" ] && '
      . '[ "$mp" != "-" ] && [ "$mp" != "none" ]; then ';
    if ( $^O eq 'freebsd' ) {
        $cmd .=
        'if command -v getfacl >/dev/null 2>&1 && '
      . 'command -v setfacl >/dev/null 2>&1; then '
          . 'po=$(getfacl "$mp" 2>/dev/null | awk -F: '
          . '"/^[[:space:]]*owner@/{print \\$2; exit}"); '
          . 'pg=$(getfacl "$mp" 2>/dev/null | awk -F: '
          . '"/^[[:space:]]*group@/{print \\$2; exit}"); '
          . 'pe=$(getfacl "$mp" 2>/dev/null | awk -F: '
          . '"/^[[:space:]]*everyone@/{print \\$2; exit}"); '
          . 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then '
          . 'setfacl -m "owner@:$po:fd-----:allow" '
          . '-m "group@:$pg:fd-----:allow" '
          . '-m "everyone@:$pe:fd-----:allow" "$mp"; '
          . 'fi; '
          . 'fi; ';
    }
    else {
        $cmd .=
        'if command -v nfs4_getfacl >/dev/null 2>&1 && '
      . 'command -v nfs4_setfacl >/dev/null 2>&1; then '
          . 'po=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: '
          . '"/OWNER@/{print \\$4; exit}"); '
          . 'pg=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: '
          . '"/GROUP@/{print \\$4; exit}"); '
          . 'pe=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: '
          . '"/EVERYONE@/{print \\$4; exit}"); '
          . 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then '
          . 'nfs4_setfacl -a "A::OWNER@:$po:fd-----:allow" '
          . '-a "A::GROUP@:$pg:fd-----:allow" '
          . '-a "A::EVERYONE@:$pe:fd-----:allow" "$mp"; '
          . 'fi; '
          . 'fi; ';
    }
    $cmd .= "fi";
    return $cmd;
}

sub get_zfs_device_info {
    my ($object) = @_;
    return undef unless ( $object && $object->{'device'} );
    my ( $pools, $zfs_devices ) = build_zfs_devices_cache();
    my $device = $object->{'device'};
    my @ids    = ($device);
    ( my $short = $device ) =~ s{^/dev/}{};
    push( @ids, $short, lc($device), lc($short) );

    # If this is a top-level partition, include GPT label aliases if present
    if ( $device =~ m{^/dev/([a-z]+[0-9]+)([ps])(\d+)$}i ) {
        my ( $base, $sep, $num ) = ( $1, $2, $3 );
        my $ds = get_disk_structure($base);
        my $part_label;
        if ( $ds && $ds->{'partitions'} && $ds->{'partitions'}->{$num} ) {
            my $pl = $ds->{'partitions'}->{$num}->{'label'};
            $part_label = $pl if ( $pl && $pl ne '(null)' );
        }
        if ( !$part_label && $ds && $ds->{'entries'} ) {
            foreach my $e ( @{ $ds->{'entries'} } ) {
                next unless ( $e->{'type'} && $e->{'type'} eq 'partition' );
                next unless ( defined $e->{'index'} && $e->{'index'} == $num );
                if ( $e->{'label'} && $e->{'label'} ne '(null)' ) {
                    $part_label = $e->{'label'};
                    last;
                }
            }
        }
        my $part_name = $base . $sep . $num;
        my $scheme    = ( $sep eq 'p' ) ? 'GPT' : '';
        push(
            @ids,
            _possible_partition_ids(
                $base, $scheme, $num, $part_name, $part_label
            )
        );
    }
    return _find_in_zfs( $zfs_devices, @ids );
}

sub is_zfs_device {
    my ($object) = @_;
    return 0 unless ( $object && $object->{'device'} );

    # Trust explicit type hints first
    if ( defined $object->{'type'} && $object->{'type'} =~ /zfs/i ) {
        return 1;
    }
    return get_zfs_device_info($object) ? 1 : 0;
}

sub get_check_filesystem_command {
    my ( $disk, $slice, $part ) = @_;
    my $device = $part ? $part->{'device'} : $slice->{'device'};
    my $hint   = $part ? $part->{'type'}   : $slice->{'type'};
    my $fstype = detect_filesystem_type( $device, $hint );
    my $qdev   = quote_path($device);

    # Map to specific fsck tools when available; else use fsck -t
    if ( $fstype && $fstype eq 'ufs' ) {
        return has_command('fsck_ufs')
          ? "fsck_ufs -y $qdev"
          : "fsck -t ufs -y $qdev";
    }
    if ( $fstype && $fstype eq 'msdosfs' ) {
        return has_command('fsck_msdosfs')
          ? "fsck_msdosfs -y $qdev"
          : "fsck -t msdosfs -y $qdev";
    }
    if ( $fstype && $fstype eq 'ext2fs' ) {
        return has_command('fsck_ext2fs')
          ? "fsck_ext2fs -y $qdev"
          : "fsck -t ext2fs -y $qdev";
    }
    if ( $fstype && $fstype eq 'zfs' ) {
        return
          "zpool status 2>&1"
          ;    # caller should avoid fsck for ZFS, but safe fallback
    }
    if ( $fstype && $fstype eq 'swap' ) {
        return "echo 'swap device - fsck not applicable'";
    }

    # Generic fallback
    return "fsck -y $qdev";
}

sub show_filesystem_buttons {
    my ( $hiddens, $st, $object, $return_url ) = @_;
    if ( $return_url && $return_url !~ m{^/} ) {
        $return_url = "/$module_name/$return_url";
    }

    # Use preferred device path (label-based if available)
    my $preferred_dev = preferred_device_path( $object->{'device'} );
    print ui_buttons_row( "newfs_form.cgi", $text{'part_newfs'},
        $text{'part_newfsdesc'}, $hiddens );

    # Offer fsck only when a filesystem is detectable and supported
    my $is_swap = ( @$st && $st->[1] eq 'swap' )
      || ( $object->{'type'} && $object->{'type'} =~ /freebsd-swap|^82$/i );
    my $is_zfs = is_zfs_device($object);
    my $fsck_type;
    if ( has_command('fstyp') ) {
        $fsck_type = detect_filesystem_type( $object->{'device'}, undef );
        if ( !$fsck_type ) {
            $fsck_type =
              detect_filesystem_type( $object->{'device'}, $object->{'type'} );
        }
    }
    else {
        $fsck_type =
          detect_filesystem_type( $object->{'device'}, $object->{'type'} );
    }
    if (   $fsck_type
        && $fsck_type ne 'swap'
        && $fsck_type ne 'zfs'
        && !$is_swap
        && !$is_zfs )
    {
        print ui_buttons_row( "fsck.cgi", $text{'part_fsck'},
            $text{'part_fsckdesc'}, $hiddens );
    }
    if ( !@$st ) {
        if (   $object->{'type'} eq 'swap'
            or $object->{'type'} eq '82'
            or $object->{'type'} eq 'freebsd-swap' )
        {
            my $mount_h = ui_hidden( "newdev", $preferred_dev )
              . ui_hidden( "type", "swap" );
            $mount_h .= ui_hidden( "return", $return_url ) if ($return_url);
            print ui_buttons_row( "../mount/edit_mount.cgi",
                $text{'part_newmount2'}, $text{'part_mountmsg2'}, $mount_h );
        }
        else {
            my $fstype =
              detect_filesystem_type( $object->{'device'}, $object->{'type'} );
            my $can_mount =
              ( $fstype && $fstype ne 'zfs' && $fstype ne 'swap' ) ? 1 : 0;
            if ($can_mount) {
                my $mount_h = ui_hidden( "newdev", $preferred_dev )
                  . ui_hidden( "type", $fstype );
                $mount_h .= ui_hidden( "return", $return_url ) if ($return_url);
                print ui_buttons_row( "../mount/edit_mount.cgi",
                    $text{'part_newmount'}, $text{'part_mountmsg'}, $mount_h );
            }
        }
    }
}

#---------------------------------------------------------------------
# ZFS and GEOM related functions are largely unchanged.
sub get_all_zfs_info {

    # Wrapper built from the structured ZFS devices cache
    my ( $pools, $devices ) = build_zfs_devices_cache();
    my %zfs_info;
    foreach my $id ( keys %$devices ) {
        next unless $id =~ /^\/dev\//;    # focus on canonical /dev/* keys
        my $dev = $devices->{$id};
        my $suffix =
          ( $dev->{'vdev_type'} && $dev->{'vdev_type'} eq 'log' )
          ? '(log)'
          : '(data)';
        $zfs_info{$id} = $dev->{'pool'} . ' ' . $suffix;
    }
    return \%zfs_info;
}

sub get_type_description {
    my ($type) = @_;
    my %type_map = (
        'freebsd'       => 'FreeBSD',
        'freebsd-ufs'   => 'FreeBSD UFS',
        'freebsd-swap'  => 'FreeBSD Swap',
        'freebsd-vinum' => 'FreeBSD Vinum',
        'freebsd-zfs'   => 'FreeBSD ZFS',
        'freebsd-boot'  => 'FreeBSD Boot',
        'efi'           => 'EFI System',
        'bios-boot'     => 'BIOS Boot',
        'ms-basic-data' => 'Microsoft Basic Data (FAT/NTFS/exFAT)',
        'ms-reserved'   => 'Microsoft Reserved',
        'ms-recovery'   => 'Microsoft Recovery',
        'apple-ufs'     => 'Apple UFS',
        'apple-hfs'     => 'Apple HFS',
        'apple-boot'    => 'Apple Boot',
        'apple-raid'    => 'Apple RAID',
        'apple-label'   => 'Apple Label',
        'linux-data'    => 'Linux Data (ext2/3/4, xfs, btrfs, etc.)',
        'linux-swap'    => 'Linux Swap',
        'linux-lvm'     => 'Linux LVM',
        'linux-raid'    => 'Linux RAID',
    );
    return $type_map{$type} || $type;
}

sub get_disk_structure {
    my ($device) = @_;
    my $result   = { 'entries' => [], 'partitions' => {} };
    my $cmd      = "gpart show -l " . quote_path($device) . " 2>&1";
    my $out      = backquote_command($cmd);
    if ( $out =~ /=>\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+\(([^)]+)\)/ ) {
        my $start_block = $1;    # starting block
        my $size_blocks = $2;    # number of blocks
        $result->{'total_blocks'} =
          $start_block + $size_blocks;    # last addressable block + 1
        $result->{'device_name'} = $3;    # device name (e.g., da0)
        $result->{'scheme'}      = $4;    # GPT/MBR
        $result->{'size_human'}  = $5;    # human size from header
    }
    foreach my $line ( split( /\n/, $out ) ) {

        # Free space rows
        if ( $line =~ /^\s+(\d+)\s+(\d+)\s+-\s+free\s+-\s+\(([^)]+)\)/ ) {
            push @{ $result->{'entries'} },
              {
                'start'      => $1,
                'size'       => $2,
                'size_human' => $3,
                'type'       => 'free'
              };
            next;
        }

# Partition rows from `gpart show -l` have: start size index label
# [flags] (size_human)
# Some systems include optional tokens like "[active]" after the label.
# Accept them.
        if ( $line =~
            /^\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)(?:\s+\[[^\]]+\])?\s+\(([^)]+)\)/
          )
        {
            push @{ $result->{'entries'} },
              {
                'start'      => $1,
                'size'       => $2,
                'index'      => $3,
                'label'      => $4,
                'size_human' => $5,
                'type'       => 'partition'
              };
        }
    }

    # Merge additional info from 'gpart list' directly, keyed by Name -> index
    my $list_out =
      backquote_command( "gpart list " . quote_path($device) . " 2>&1" );
    my ( %parts, $current_idx );
    foreach my $line ( split( /\n/, $list_out ) ) {
        if ( $line =~ /^\s*(?:\d+\.\s*)?Name:\s*(\S+)/i ) {
            my $name = $1;    # e.g., da0p2 or da0s2
            if ( $name =~ /[ps](\d+)$/ ) {
                $current_idx = int($1);
                $parts{$current_idx} ||= { name => $name };
            }
            else {
                undef $current_idx;    # not a partition provider line
            }
        }
        elsif ( defined $current_idx && $line =~ /^\s*Index:\s*(\d+)/i ) {

            # Optional cross-check; ignore value and trust Name-derived index
            next;
        }
        elsif ( defined $current_idx && $line =~ /^\s*label:\s*(\S+)/i ) {
            $parts{$current_idx}->{'label'} = $1;
        }
        elsif ( defined $current_idx && $line =~ /^\s*type:\s*(\S+)/i ) {
            $parts{$current_idx}->{'type'} = $1;
        }
        elsif ( defined $current_idx && $line =~ /^\s*rawtype:\s*(\S+)/i ) {
            $parts{$current_idx}->{'rawtype'} = $1;
        }
        elsif ( defined $current_idx && $line =~ /^\s*length:\s*(\d+)/i ) {
            $parts{$current_idx}->{'length'} = $1;
        }
        elsif ( defined $current_idx && $line =~ /^\s*offset:\s*(\d+)/i ) {
            $parts{$current_idx}->{'offset'} = $1;
        }
        elsif ( $line =~ /Sectorsize:\s*(\d+)/i ) {
            $result->{'sectorsize'} = int($1);
        }
        elsif ( $line =~ /Mediasize:\s*(\d+)/i ) {
            $result->{'mediasize'} = int($1);
        }
    }
    $result->{'partitions'} = \%parts;
    foreach my $entry ( @{ $result->{'entries'} } ) {
        next unless ( $entry->{'type'} eq 'partition' && $entry->{'index'} );
        my $idx = $entry->{'index'};
        if ( $parts{$idx} ) {

            # Prefer label from gpart list if present and meaningful
            if (   $parts{$idx}->{'label'}
                && $parts{$idx}->{'label'} ne '(null)' )
            {
                $entry->{'label'} = $parts{$idx}->{'label'};
            }

            # Attach resolved type (rawtype/type) for downstream consumers
            $entry->{'part_type'} =
                 $parts{$idx}->{'type'}
              || $parts{$idx}->{'rawtype'}
              || $entry->{'part_type'};

            # Also store rawtype for downstream consumers
            $entry->{'rawtype'} = $parts{$idx}->{'rawtype'}
              if ( $parts{$idx}->{'rawtype'} );
        }
    }
    return $result;
}

sub get_disk_sectorsize {
    my ($device) = @_;

    # Normalize device for diskinfo (expects provider name like da0)
    my $dev = $device;
    $dev =~ s{^/dev/}{};

    # Prefer verbose output which explicitly lists sectorsize
    my $outv =
      backquote_command( "diskinfo -v " . quote_path($dev) . " 2>/dev/null" );
    if ( $outv =~ /sectorsize:\s*(\d+)/i ) {
        return int($1);
    }

    # Fallback to non-verbose; actual format: name sectorsize mediasize ...
    my $out =
      backquote_command( "diskinfo " . quote_path($dev) . " 2>/dev/null" );
    if ( $out =~ /^\S+\s+(\d+)\s+\d+/ ) {
        return int($1);    # second field is sectorsize
    }

    # Last resort: ask gpart list for sectorsize
    my $base = $dev;
    my $ds   = get_disk_structure($base);
    if ( $ds && $ds->{'sectorsize'} ) { return int( $ds->{'sectorsize'} ); }
    return undef;
}

# Derive the base disk device (e.g., /dev/da0 from /dev/da0p2)
sub base_disk_device {
    my ($device) = @_;
    return undef unless $device;
    my $d = $device;
    $d =~ s{^/dev/}{};
    $d =~ s{(p|s)\d+.*$}{};    # strip partition/slice suffix
    return "/dev/$d";
}

# Compute bytes from a block count for a given device
sub bytes_from_blocks {
    my ( $device, $blocks ) = @_;
    return undef unless defined $blocks;
    my $base = base_disk_device($device)  || $device;
    my $ss   = get_disk_sectorsize($base) || 512;
    return $blocks * $ss;
}

# Safe wrapper for nice_size that ensures bytes input
# Accepts either raw bytes or (device, blocks) pair
sub safe_nice_size {
    my ( $arg1, $arg2 ) = @_;
    my $bytes;
    if ( defined $arg2 ) {

        # Called as (device, blocks)
        $bytes = bytes_from_blocks( $arg1, $arg2 );
    }
    else {
        # Called as (bytes)
        $bytes = $arg1;
    }
    return '-' unless defined $bytes && $bytes >= 0;
    my $s = nice_size($bytes);

    # Normalize IEC suffixes to SI-style labels if present
    $s =~ s/\bKiB\b/KB/g;
    $s =~ s/\bMiB\b/MB/g;
    $s =~ s/\bGiB\b/GB/g;
    $s =~ s/\bTiB\b/TB/g;
    $s =~ s/\bPiB\b/PB/g;
    $s =~ s/\bEiB\b/EB/g;
    return $s;
}

sub build_zfs_devices_cache {
    my %pools;
    my %devices;
    my $cmd = "zpool status 2>&1";
    my $out = backquote_command($cmd);
    my (
        $current_pool,       $in_config,   $current_vdev_type,
        $current_vdev_group, $is_mirrored, $is_raidz,
        $raidz_level,        $is_single,   $is_striped,
        $vdev_count
    );
    $current_vdev_type = 'data';
    foreach my $line ( split( /\n/, $out ) ) {
        if ( $line =~ /^\s*pool:\s+(\S+)/ ) {
            $current_pool         = $1;
            $pools{$current_pool} = 1;
            $in_config            = 0;
            $current_vdev_type    = 'data';
        }
        elsif ( $line =~ /^\s*config:/ ) {
            $in_config          = 1;
            $current_vdev_group = undef;
            $is_mirrored        = 0;
            $is_raidz           = 0;
            $raidz_level        = 0;
            $is_single          = 0;
            $is_striped         = 0;
            $vdev_count         = 0;
        }
        elsif ( $in_config and $line =~ /^\s+logs/ ) {
            $current_vdev_type  = 'log';
            $current_vdev_group = undef;
        }
        elsif ( $in_config and $line =~ /^\s+cache/ ) {
            $current_vdev_type  = 'cache';
            $current_vdev_group = undef;
        }
        elsif ( $in_config and $line =~ /^\s+spares/ ) {
            $current_vdev_type  = 'spare';
            $current_vdev_group = undef;
        }
        elsif ( $in_config and $line =~ /^\s+mirror-(\d+)/ ) {
            $current_vdev_group = "mirror-$1";
            $is_mirrored        = 1;
            $is_raidz           = 0;
            $is_single          = 0;
            $is_striped         = 0;
            $vdev_count         = 0;
        }
        elsif ( $in_config and $line =~ /^\s+raidz(\d+)?-(\d+)/ ) {
            $current_vdev_group = "raidz" . ( $1 || "1" ) . "-$2";
            $is_mirrored        = 0;
            $is_raidz           = 1;
            $raidz_level        = $1 || 1;
            $is_single          = 0;
            $is_striped         = 0;
            $vdev_count         = 0;
        }
        elsif ( $in_config and $line =~ /^\s+(\S+)\s+(\S+)/ ) {
            my $device = $1;
            my $state  = $2;
            next
              if ( $device eq $current_pool
                or $device =~ /^mirror-/
                or $device =~ /^raidz\d*-/ );
            if   ($current_vdev_group) { $vdev_count++; }
            else                       { $is_single = 1; }
            my $device_id = $device;
            $device_id = $1 if ( $device =~ /^gpt\/(.*)/ );
            $devices{$device} = {
                'pool'        => $current_pool,
                'vdev_type'   => $current_vdev_type,
                'is_mirrored' => $is_mirrored,
                'is_raidz'    => $is_raidz,
                'raidz_level' => $raidz_level,
                'is_single'   => $is_single,
                'is_striped'  => $is_striped,
                'vdev_group'  => $current_vdev_group,
                'vdev_count'  => $vdev_count
            };
            $devices{"gpt/$device"} = $devices{$device}
              if ( $device !~ /^gpt\// );
            $devices{"/dev/$device"} = $devices{$device};

            if ( $device !~ /^gpt\// ) {
                $devices{"/dev/gpt/$device"} = $devices{$device};
            }
            $devices{ lc($device) } = $devices{$device};
            if ( $device !~ /^gpt\// ) {
                $devices{ "gpt/" . lc($device) }      = $devices{$device};
                $devices{ "/dev/gpt/" . lc($device) } = $devices{$device};
            }
        }
    }
    return ( \%pools, \%devices );
}

sub get_format_type {
    my ($part) = @_;
    if ( $part->{'type'} =~ /^freebsd-/ ) {
        return get_type_description( $part->{'type'} );
    }
    return get_type_description( $part->{'type'} ) || $part->{'type'};
}

# Build possible ids for a partition given base device, scheme and metadata
sub _label_conflicts_with_device {
    my ( $label, $base_device, $scheme, $part_num, $part_name ) = @_;
    return 0 unless defined $label && $label ne '-' && $label ne '(null)';
    my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's';
    my $expected =
      ( defined $base_device && defined $part_num && length($base_device) )
      ? "$base_device$sep$part_num"
      : undef;
    if ( defined $expected && lc($label) eq lc($expected) ) {
        return 0;
    }
    if (   defined $part_name
        && $part_name ne '-'
        && lc($label) eq lc($part_name) )
    {
        return 0;
    }

# If a label looks like a real disk partition but doesn't match this partition,
# Do not use it for ZFS membership detection (avoids cross-disk
# misidentification).
    if ( $label =~ m{^(ada|ad|da|amrd|nvd|vtbd|nda)\d+(?:p|s)\d+$}i ) {
        return 1;
    }
    return 0;
}

sub _possible_partition_ids {
    my ( $base_device, $scheme, $part_num, $part_name, $part_label ) = @_;
    my @ids;
    if ( defined $base_device && defined $part_num && length($base_device) ) {
        my $sep         = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's';
        my $device_path = "/dev/$base_device" . $sep . $part_num;
        push( @ids, $device_path );
        ( my $short = $device_path ) =~ s/^\/dev\///;
        push( @ids, $short );
    }
    if ( $part_name && $part_name ne '-' ) {
        push( @ids, $part_name, "/dev/$part_name" );
    }
    if ( defined $part_label && $part_label ne '-' && $part_label ne '(null)' )
    {
        my $label_conflict =
          _label_conflicts_with_device( $part_label, $base_device, $scheme,
            $part_num, $part_name );
        if ($label_conflict) {

     # Allow only GPT label aliases to avoid false matches to device-like labels
            push( @ids,
                "gpt/$part_label",        "/dev/gpt/$part_label",
                "gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) );
        }
        else {
            push( @ids,
                $part_label,              "gpt/$part_label",
                "/dev/gpt/$part_label",   lc($part_label),
                "gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) );
        }
        if ( $part_label =~ /^(sLOG\w+)$/ ) {
            push( @ids, $1, "gpt/$1", "/dev/gpt/$1" );
        }
    }
    return @ids;
}

# Given ids, find if present in ZFS devices cache
sub _find_in_zfs {
    my ( $zfs_devices, @ids ) = @_;
    foreach my $id (@ids) {
        my $nid = lc($id);
        if ( $zfs_devices->{$nid} ) {
            return $zfs_devices->{$nid};
        }
    }
    return undef;
}

# Classify a partition row: returns (format, usage, role)
sub classify_partition_row {
    my (%args) = @_;
    my $ids = [
        _possible_partition_ids(
            @args{qw/base_device scheme part_num part_name part_label/}
        )
    ];
    my $zdev = _find_in_zfs( $args{'zfs_devices'}, @$ids );

    # Derive type description, avoid label-as-type
    my $type_desc = $args{'entry_part_type'};
    if ( !defined $type_desc || $type_desc eq '-' || $type_desc eq 'unknown' ) {

        # leave undef
    }

# Avoid clearing real types (like 'efi' or 'freebsd-boot') when label
# text matches by case.
# Only drop if the "type" clearly looks like a provider/label path that
# mirrors the label.
    if ( defined $type_desc && defined $args{'part_label'} ) {
        my $pl = $args{'part_label'};
        if ( $type_desc =~ m{^(?:/dev/)?gpt(?:id)?/\Q$pl\E$}i ) {
            undef $type_desc;
        }
    }

    my ( $format, $usage, $role ) = ( '-', $text{'part_nouse'}, '-' );

# Explicit boot detection based on GPT GUIDs and MBR hex codes or
# human-readable type
    my $raw       = lc( $args{'entry_rawtype'} || '' );
    my $t         = lc( $type_desc             || '' );
    my %boot_guid = map { $_ => 1 } qw(
      c12a7328-f81f-11d2-ba4b-00a0c93ec93b   # EFI System
      21686148-6449-6e6f-744e-656564454649   # BIOS Boot (GRUB BIOS)
      83bd6b9d-7f41-11dc-be0b-001560b84f0f   # FreeBSD Boot
      49f48d5a-b10e-11dc-b99b-0019d1879648   # NetBSD boot
      824cc7a0-36a8-11e3-890a-952519ad3f61   # OpenBSD boot
      426f6f74-0000-11aa-aa11-00306543ecac   # Apple Boot
    );
    my %boot_mbr = map { $_ => 1 } qw( 0xef 0xa0 0xa5 0xa6 0xa9 0xab );
    my $is_boot_type =
      ( $t =~
/\b(efi|bios-?boot|freebsd-boot|netbsd-boot|openbsd-boot|apple-boot)\b/
      );
    my $is_boot_raw = ( $raw && ( $boot_guid{$raw} || $boot_mbr{$raw} ) );

    if ( $is_boot_type || $is_boot_raw ) {
        my $fmt =
          (      $t =~ /efi/
              || $raw eq 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b'
              || lc($raw) eq '0xef' )
          ? get_type_description('efi')
          : get_type_description('freebsd-boot');

# Access text properly - %text is in main namespace when this is called from CGI
        my $boot_txt = $text{'disk_boot'};
        my $role_txt = $text{'disk_boot_role'};
        return ( $fmt, $boot_txt, $role_txt );
    }

    # Heuristic fallback only if no explicit identifiers are present
    if (
        (
              !$args{'entry_part_type'}
            || $args{'entry_part_type'} eq '-'
            || $args{'entry_part_type'} eq 'unknown'
        )
        && ( $args{'part_num'} || '' ) eq '1'
      )
    {
        my $sb = $args{'size_blocks'} || 0;
        if ( $sb > 0 ) {
            my $base =
              base_disk_device( '/dev/' . ( $args{'base_device'} || '' ) );
            my $ss    = get_disk_sectorsize($base) || 512;
            my $bytes = $sb * $ss;
            if ( $bytes <= 2 * 1024 * 1024 ) {    # <= 2MiB
                return ( get_type_description('freebsd-boot'),
                    $text{'disk_boot'}, $text{'disk_boot_role'} );
            }
        }
        elsif ($args{'size_human'}
            && $args{'size_human'} =~ /^(?:512k|1m|1\.0m)$/i )
        {
            return ( get_type_description('freebsd-boot'),
                $text{'disk_boot'}, $text{'disk_boot_role'} );
        }
    }
    if ($zdev) {
        $format = 'FreeBSD ZFS';
        my $inzfs_txt = $text{'disk_inzfs'};
        my $z_mirror  = $text{'disk_zfs_mirror'};
        my $z_stripe  = $text{'disk_zfs_stripe'};
        my $z_single  = $text{'disk_zfs_single'};
        my $z_data    = $text{'disk_zfs_data'};
        my $z_log     = $text{'disk_zfs_log'};
        my $z_cache   = $text{'disk_zfs_cache'};
        my $z_spare   = $text{'disk_zfs_spare'};
        $usage = $inzfs_txt . ' ' . $zdev->{'pool'};
        my $vt  = $zdev->{'vdev_type'};
        my $cnt = $zdev->{'vdev_count'} || 0;
        if    ( $vt eq 'log' )   { $role = $z_log; }
        elsif ( $vt eq 'cache' ) { $role = $z_cache; }
        elsif ( $vt eq 'spare' ) { $role = $z_spare; }
        elsif ( $zdev->{'is_mirrored'} ) {
            $role = $z_mirror;
            $role .= " ($cnt in group)" if $cnt;
        }
        elsif ( $zdev->{'is_raidz'} ) {
            my $lvl = $zdev->{'raidz_level'} || 1;
            $role = 'RAID-Z' . $lvl;
            $role .= " ($cnt in group)" if $cnt;
        }
        elsif ( $zdev->{'is_striped'} ) {
            $role = $z_stripe;
            $role .= " ($cnt in group)" if $cnt;
        }
        elsif ( $zdev->{'is_single'} ) { $role = $z_single; }
        else                           { $role = $z_data; }
        return ( $format, $usage, $role );
    }

# Not in ZFS: infer by type_desc, rawtype and size heuristic (this
# shouldn't be reached for boot, but keep as fallback)
    if ( defined $type_desc && $type_desc =~ /(?:freebsd|linux)-swap/i ) {
        $format = 'Swap';
        $usage  = $text{'disk_swap'};
        $role   = $text{'disk_swap_role'};
    }
    elsif ( defined $type_desc && $type_desc =~ /linux-lvm/i ) {
        $format = 'Linux LVM';
    }
    elsif ( defined $type_desc && $type_desc =~ /linux-raid/i ) {
        $format = 'Linux RAID';
    }

    # Recognize common FAT/NTFS identifiers on both GPT and MBR
    elsif ( defined $type_desc && $type_desc =~ /ntfs/i ) { $format = 'NTFS'; }
    elsif ( defined $type_desc && $type_desc =~ /fat32/i ) {
        $format = 'FAT32';
    }
    elsif ( defined $type_desc && $type_desc =~ /fat|msdos/i ) {
        $format = 'FAT';
    }
    elsif ( defined $type_desc && $type_desc =~ /ms-basic/i ) {
        $format = 'FAT/NTFS';
    }
    elsif ( $raw ne '' && $raw =~ /^\d+$/ ) {

        # MBR raw type codes: 7=NTFS, 6/11/12/14 = FAT variants
        my $code = int($raw);
        if    ( $code == 7 )                 { $format = 'NTFS'; }
        elsif ( $code == 11 || $code == 12 ) { $format = 'FAT32'; }
        elsif ( $code == 6 || $code == 14 )  { $format = 'FAT'; }
    }
    elsif ( defined $type_desc && $type_desc =~ /linux/i ) {
        $format = 'Linux';
    }
    elsif ( defined $type_desc && $type_desc =~ /apple-ufs/i ) {
        $format = 'Apple UFS';
    }
    elsif ( defined $type_desc && $type_desc =~ /apple-hfs/i ) {
        $format = 'HFS+';
    }
    elsif ( defined $type_desc && $type_desc =~ /apple-raid/i ) {
        $format = 'Apple RAID';
    }
    elsif ( defined $type_desc && $type_desc =~ /freebsd-ufs/i ) {
        $format = 'FreeBSD UFS';
    }
    elsif ( defined $type_desc && $type_desc =~ /freebsd-zfs/i ) {
        $format = 'FreeBSD ZFS';
    }
    else {
        if ( defined $args{'part_label'}
            && $args{'part_label'} =~ /^swap\d*$/i )
        {
            $format = 'Swap';
            $usage  = $text{'disk_swap'};
            $role   = $text{'disk_swap_role'};
        }
    }
    return ( $format, $usage, $role );
}

# list_partition_types()
# Returns a list suitable for ui_select: [ [ value, label ], ... ]
# Adapts to whether the system uses gpart (GPT) or legacy disklabel.
sub list_partition_types {
    my ($scheme) = @_;
    if ( is_using_gpart() ) {

# BSD-on-MBR inner label (used when creating sub-partitions inside an MBR slice)
        if ( defined $scheme && $scheme =~ /BSD/i ) {
            return (
                [ 'freebsd-ufs',   get_type_description('freebsd-ufs') ],
                [ 'freebsd-zfs',   get_type_description('freebsd-zfs') ],
                [ 'freebsd-swap',  get_type_description('freebsd-swap') ],
                [ 'freebsd-vinum', get_type_description('freebsd-vinum') ],
            );
        }

# If outer scheme is not GPT (e.g. MBR), present MBR partition types for
# top-level slices
        if ( defined $scheme && $scheme !~ /GPT/i ) {
            my @mbr_types = (
                [ 'freebsd',    get_type_description('freebsd') ],
                [ 'fat32lba',   'FAT32 (LBA)' ],
                [ 'fat32',      'FAT32' ],
                [ 'fat16',      'FAT16' ],
                [ 'ntfs',       'NTFS' ],
                [ 'linux',      'Linux' ],
                [ 'linux-swap', get_type_description('linux-swap') ],
                [ 'efi',        get_type_description('efi') ],
            );
            return @mbr_types;
        }

        # Default GPT types (common, practical choices)
        my @gpt_types = (
            [ 'efi',           get_type_description('efi') ],
            [ 'bios-boot',     get_type_description('bios-boot') ],
            [ 'freebsd-boot',  get_type_description('freebsd-boot') ],
            [ 'freebsd-zfs',   get_type_description('freebsd-zfs') ],
            [ 'freebsd-ufs',   get_type_description('freebsd-ufs') ],
            [ 'freebsd-swap',  get_type_description('freebsd-swap') ],
            [ 'freebsd-vinum', get_type_description('freebsd-vinum') ],
            [ 'ms-basic-data', get_type_description('ms-basic-data') ],
            [ 'ms-reserved',   get_type_description('ms-reserved') ],
            [ 'ms-recovery',   get_type_description('ms-recovery') ],
            [ 'linux-data',    get_type_description('linux-data') ],
            [ 'linux-swap',    get_type_description('linux-swap') ],
            [ 'linux-lvm',     get_type_description('linux-lvm') ],
            [ 'linux-raid',    get_type_description('linux-raid') ],
            [ 'apple-boot',    get_type_description('apple-boot') ],
            [ 'apple-hfs',     get_type_description('apple-hfs') ],
            [ 'apple-ufs',     get_type_description('apple-ufs') ],
            [ 'apple-raid',    get_type_description('apple-raid') ],
            [ 'apple-label',   get_type_description('apple-label') ],
        );
        return @gpt_types;
    }
    else {
        # Legacy BSD disklabel types
        my @label_types = (
            [ '4.2BSD', 'FreeBSD UFS' ],
            [ 'swap',   'Swap' ],
            [ 'unused', 'Unused' ],
            [ 'vinum',  'FreeBSD Vinum' ],
        );
        return @label_types;
    }
}

sub get_partition_role {
    my ($part) = @_;
    if ( is_boot_partition($part) ) { return $text{'part_boot'}; }
    my $zfs_info = get_all_zfs_info();
    if (    $zfs_info->{ $part->{'device'} }
        and $zfs_info->{ $part->{'device'} } =~ /\(log\)$/ )
    {
        return $text{'part_zfslog'};
    }
    if ( $zfs_info->{ $part->{'device'} } ) {
        return $text{'part_zfsdata'};
    }
    my @mounts = mount::list_mounted();
    foreach my $m (@mounts) {
        if ( $m->[0] eq $part->{'device'} ) {
            return text( 'part_mounted', $m->[1] );
        }
    }
    return $text{'part_unused'};
}

sub get_detailed_disk_info {
    my ($device) = @_;
    my $info = {};
    ( my $dev_name = $device ) =~ s/^\/dev\///;
    my $out = backquote_command(
        "geom disk list " . quote_path($dev_name) . " 2>/dev/null" );
    return undef if ($?);
    foreach my $line ( split( /\n/, $out ) ) {
        if ( $line =~ /^\s+Mediasize:\s+(\d+)\s+\(([^)]+)\)/ ) {
            $info->{'mediasize_bytes'} = $1;
            $info->{'mediasize'}       = $2;
        }
        elsif ( $line =~ /^\s+Sectorsize:\s+(\d+)/ ) {
            $info->{'sectorsize'} = $1;
        }
        elsif ( $line =~ /^\s+Stripesize:\s+(\d+)/ ) {
            $info->{'stripesize'} = $1;
        }
        elsif ( $line =~ /^\s+Stripeoffset:\s+(\d+)/ ) {
            $info->{'stripeoffset'} = $1;
        }
        elsif ( $line =~ /^\s+Mode:\s+(.*)/ ) {
            $info->{'mode'} = $1;
        }
        elsif ( $line =~ /^\s+rotationrate:\s+(\d+)/ ) {
            $info->{'rotationrate'} = $1;
        }
        elsif ( $line =~ /^\s+ident:\s+(.*)/ ) {
            $info->{'ident'} = $1;
        }
        elsif ( $line =~ /^\s+lunid:\s+(.*)/ ) {
            $info->{'lunid'} = $1;
        }
        elsif ( $line =~ /^\s+descr:\s+(.*)/ ) {
            $info->{'descr'} = $1;
        }
    }
    return $info;
}

sub initialize_slice {
    my ( $disk, $slice ) = @_;

    # If the outer disk is GPT, we are not creating inner BSD labels
    if ( is_using_gpart() ) {
        my $base = $disk->{'device'};
        $base =~ s{^/dev/}{};
        my $ds = get_disk_structure($base);
        if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
            return undef;
        }
    }

    # For MBR: initialize BSD disklabel on the slice only if not already present
    if ( is_using_gpart() ) {
        my $prov =
          slice_name($slice);    # provider name (e.g. ada0s1 / ada0p2)
        my $show =
          backquote_command( "gpart show " . quote_path($prov) . " 2>&1" );
        return undef if ( $show =~ /\bBSD\b/ );
        my $cmd = "gpart create -s BSD " . quote_path($prov) . " 2>&1";
        my $out = backquote_command($cmd);
        if ( $? != 0 ) {
            return "Failed to initialize slice: $out";
        }
        return undef;
    }

    # Legacy disklabel (no gpart): create a default BSD label on the slice
    # device (e.g. /dev/da0s1) if missing.
    my $dev = $slice->{'device'};
    return "Missing slice device" unless $dev;

    # Safety: only initialize on FreeBSD MBR slices (0xA5 / 'freebsd')
    my $is_freebsd = 0;
    if ( defined $slice->{'type'} ) {
        $is_freebsd = 1 if ( $slice->{'type'} =~ /freebsd/i );
        my $tc = _fdisk_type_code( $slice->{'type'} );
        $is_freebsd = 1 if ( defined $tc && $tc == 0xA5 );
    }
    return "Refusing to initialize disklabel on non-FreeBSD slice"
      unless $is_freebsd;

    my ( $label, $lerr ) = _disklabel_read($dev);
    return undef if !$lerr;    # already has a label

    my $werr = _disklabel_write_default($dev);
    return "Failed to initialize slice: $werr" if $werr;
    return undef;
}

# ---------------------------------------------------------------------
# partition_select(name, value)
# Provide a selector for partitions/slices for external modules (e.g., mount)
# Returns an HTML <select> element populated with available devices.
sub partition_select {
    my ( $name, $value ) = @_;
    my @opts;
    my @disks = list_disks_partitions();
    foreach my $d (@disks) {
        foreach my $s ( @{ $d->{'slices'} || [] } ) {
            my $stype   = get_type_description( $s->{'type'} ) || $s->{'type'};
            my $ssize_b = bytes_from_blocks( $s->{'device'}, $s->{'blocks'} );
            my $ssize   = defined $ssize_b ? nice_size($ssize_b) : undef;
            my $slabel  = $s->{'device'}
              . ( defined $ssize ? " ($stype, $ssize)" : " ($stype)" );
            push @opts, [ $s->{'device'}, $slabel ];
            foreach my $p ( @{ $s->{'parts'} || [] } ) {
                my $ptype =
                  get_type_description( $p->{'type'} ) || $p->{'type'};
                my $psz_b = bytes_from_blocks( $p->{'device'}, $p->{'blocks'} );
                my $psz   = defined $psz_b ? nice_size($psz_b) : undef;
                my $plabel = $p->{'device'}
                  . ( defined $psz ? " ($ptype, $psz)" : " ($ptype)" );
                push @opts, [ $p->{'device'}, $plabel ];
            }
        }
    }

    # Sort options by device name for consistency
    @opts = sort { $a->[0] cmp $b->[0] } @opts;
    $value ||= ( $opts[0] ? $opts[0]->[0] : undef );
    return ui_select( $name, $value, \@opts, 1, 0, 0, 0 );
}

# Return a short human-readable description for a given device
# Example: "FreeBSD ZFS, 39G"
sub partition_description {
    my ($device) = @_;
    return undef unless $device;
    my ( $ptype, $blocks );
    my @disks = list_disks_partitions();
    foreach my $d (@disks) {
        foreach my $s ( @{ $d->{'slices'} || [] } ) {
            if ( $s->{'device'} && $s->{'device'} eq $device ) {
                $ptype  = get_type_description( $s->{'type'} ) || $s->{'type'};
                $blocks = $s->{'blocks'};
                last;
            }
            foreach my $p ( @{ $s->{'parts'} || [] } ) {
                if ( $p->{'device'} && $p->{'device'} eq $device ) {
                    $ptype =
                      get_type_description( $p->{'type'} ) || $p->{'type'};
                    $blocks = $p->{'blocks'};
                    last;
                }
            }
        }
    }
    return undef unless defined $ptype;
    my $bytes = bytes_from_blocks( $device, $blocks );
    my $sz    = defined $bytes ? nice_size($bytes) : '-';
    return "$ptype, $sz";
}

1;
