// Filesystem-aware helpers for the resize flow.
//
// `crate::disks` only resizes the partition-table entry. Touching
// the filesystem inside requires the per-fs tools shipped by Debian
// (`resize2fs`, `ntfsresize`, `fatresize`, …); this module probes
// each one for the minimum safe size and runs the actual shrink.
// Growing is not handled here — for ext / ntfs / fat the user can
// re-grow the FS with the same tool after the table entry is
// enlarged; we only enforce the floor when shrinking.

use std::path::Path;
use std::process::Command;

/// Filesystem family the tooling understands. Anything else falls
/// to `Unknown`, which the screen treats as "shrink-only down to a
/// hard 1 MiB floor".
#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
pub enum FsKind
{
	Ext,    // ext2 / ext3 / ext4
	Btrfs,
	Xfs,
	Ntfs,
	Fat,    // fat12 / fat16 / fat32 / vfat
	Exfat,
	Swap,
	Unknown,
}

/// Map a filesystem name string (as returned by `blkid -s TYPE`) into
/// [`FsKind`].
pub fn parse( name: Option<&str> ) -> FsKind
{
	let s = match name { Some( s ) => s.to_ascii_lowercase(), None => return FsKind::Unknown };
	match s.as_str()
	{
		"ext2" | "ext3" | "ext4"  => FsKind::Ext,
		"btrfs"                   => FsKind::Btrfs,
		"xfs"                     => FsKind::Xfs,
		"ntfs"                    => FsKind::Ntfs,
		"fat12" | "fat16" | "fat32" | "vfat" => FsKind::Fat,
		"exfat"                   => FsKind::Exfat,
		"linux-swap" | "linux-swap(v1)" | "swap" => FsKind::Swap,
		_                         => FsKind::Unknown,
	}
}

/// Whether the toolchain supports shrinking this kind of FS at all.
/// `xfs` cannot shrink by design, `exfat` ships no resize tool, and
/// online btrfs needs the FS to be mounted (we refuse offline).
pub fn can_shrink( kind: FsKind ) -> bool
{
	matches!( kind, FsKind::Ext | FsKind::Ntfs | FsKind::Fat )
}

/// `true` when every external tool needed to resize this FS is on
/// `$PATH`. Only checks executable presence — not whether running
/// it will actually work — but it is enough to keep the resize
/// button hidden when the host lacks `resize2fs` / `ntfsresize` /
/// `fatresize`.
pub fn tools_available( kind: FsKind ) -> bool
{
	match kind
	{
		FsKind::Ext  => exe_in_path( "resize2fs" ) && exe_in_path( "tune2fs" ),
		FsKind::Ntfs => exe_in_path( "ntfsresize" ),
		FsKind::Fat  => exe_in_path( "fatresize" ),
		_            => false,
	}
}

/// Whether the toolchain supports growing this kind of FS. Same
/// candidates as `can_shrink` — ext / ntfs / fat all support grow
/// when the partition is enlarged first.
pub fn can_grow( kind: FsKind ) -> bool
{
	matches!( kind, FsKind::Ext | FsKind::Ntfs | FsKind::Fat )
}

/// Grow the filesystem at `path` so it occupies `new_bytes` on
/// disk. Must be called *after* the partition table is enlarged —
/// the FS reads from the new partition end. Caller should check
/// `can_grow` and `tools_available` first.
pub fn grow( path: &Path, kind: FsKind, new_bytes: u64 ) -> Result<(), String>
{
	match kind
	{
		FsKind::Ext  => ext_grow( path ),
		FsKind::Ntfs => ntfs_grow( path, new_bytes ),
		FsKind::Fat  => fat_grow( path, new_bytes ),
		other        => Err( format!( "grow unsupported for {:?}", other ) ),
	}
}

fn ext_grow( path: &Path ) -> Result<(), String>
{
	let _ = run( Command::new( "e2fsck" ).arg( "-f" ).arg( "-y" ).arg( path ) );
	// `resize2fs PATH` with no size grows to fill the partition.
	run( Command::new( "resize2fs" ).arg( path ) ).map( |_| () )
}

fn ntfs_grow( path: &Path, new_bytes: u64 ) -> Result<(), String>
{
	run( Command::new( "ntfsresize" )
		.arg( "--size" ).arg( new_bytes.to_string() )
		.arg( "--force" )
		.arg( "--no-progress-bar" )
		.arg( path ) ).map( |_| () )
}

fn fat_grow( path: &Path, new_bytes: u64 ) -> Result<(), String>
{
	run( Command::new( "fatresize" )
		.arg( "--size" ).arg( new_bytes.to_string() )
		.arg( path ) ).map( |_| () )
}

fn exe_in_path( name: &str ) -> bool
{
	use std::env;
	let Ok( path ) = env::var( "PATH" ) else { return false };
	env::split_paths( &path ).any( |dir| dir.join( name ).is_file() )
}

/// Probe the FS for the smallest safe on-disk size. `None` when no
/// reliable answer is available (unknown FS, missing tool, parse
/// failure); the caller should fall back to a conservative floor.
pub fn min_size_bytes( path: &Path, kind: FsKind ) -> Option<u64>
{
	match kind
	{
		FsKind::Ext   => ext_min_size( path ),
		FsKind::Ntfs  => ntfs_min_size( path ),
		FsKind::Fat   => fat_min_size( path ),
		_             => None,
	}
}

/// Resize the filesystem at `path` so it occupies `new_bytes` on
/// disk. Must be called *before* the partition table is shrunk —
/// otherwise the FS reads past the new partition end. Caller checks
/// `can_shrink` first.
pub fn shrink( path: &Path, kind: FsKind, new_bytes: u64 ) -> Result<(), String>
{
	match kind
	{
		FsKind::Ext  => ext_shrink( path, new_bytes ),
		FsKind::Ntfs => ntfs_shrink( path, new_bytes ),
		FsKind::Fat  => fat_shrink( path, new_bytes ),
		other        => Err( format!( "shrink unsupported for {:?}", other ) ),
	}
}

// ── ext2/3/4 ─────────────────────────────────────────────────────────────────

fn ext_min_size( path: &Path ) -> Option<u64>
{
	let out = run( Command::new( "resize2fs" ).arg( "-P" ).arg( path ) ).ok()?;
	// "Estimated minimum size of the filesystem: NN (4k) blocks"
	let blocks: u64 = out
		.lines()
		.find_map( |l| l.split( ':' ).nth( 1 ) )
		.and_then( |tail| tail.split_whitespace().next() )
		.and_then( |n| n.parse().ok() )?;
	let block_size = ext_block_size( path ).unwrap_or( 4096 );
	Some( blocks.saturating_mul( block_size ) )
}

fn ext_block_size( path: &Path ) -> Option<u64>
{
	let out = run( Command::new( "tune2fs" ).arg( "-l" ).arg( path ) ).ok()?;
	for line in out.lines()
	{
		if let Some( tail ) = line.strip_prefix( "Block size:" )
		{
			return tail.trim().parse().ok();
		}
	}
	None
}

fn ext_shrink( path: &Path, new_bytes: u64 ) -> Result<(), String>
{
	// Force fsck first so resize2fs does not refuse on a dirty fs.
	let _ = run( Command::new( "e2fsck" ).arg( "-f" ).arg( "-y" ).arg( path ) );
	// `resize2fs PATH SIZE` accepts K / M / G suffixes — request
	// rounded-down MiB so resize2fs's own block alignment fits.
	let mib = new_bytes / ( 1024 * 1024 );
	let size_arg = format!( "{}M", mib.max( 1 ) );
	run( Command::new( "resize2fs" ).arg( path ).arg( size_arg ) ).map( |_| () )
}

// ── ntfs ─────────────────────────────────────────────────────────────────────

fn ntfs_min_size( path: &Path ) -> Option<u64>
{
	let out = run( Command::new( "ntfsresize" ).args( [ "--info", "--force" ] ).arg( path ) ).ok()?;
	// "You might resize at NNNNN bytes or NN MB"
	for line in out.lines()
	{
		if let Some( idx ) = line.find( "resize at " )
		{
			let rest  = &line[ idx + "resize at ".len() .. ];
			if let Some( num_str ) = rest.split_whitespace().next()
			{
				if let Ok( n ) = num_str.parse::<u64>() { return Some( n ); }
			}
		}
	}
	None
}

fn ntfs_shrink( path: &Path, new_bytes: u64 ) -> Result<(), String>
{
	run( Command::new( "ntfsresize" )
		.arg( "--size" ).arg( new_bytes.to_string() )
		.arg( "--force" )
		.arg( "--no-progress-bar" )
		.arg( path ) ).map( |_| () )
}

// ── FAT (fat12/16/32, vfat) ──────────────────────────────────────────────────

fn fat_min_size( path: &Path ) -> Option<u64>
{
	let out = run( Command::new( "fatresize" ).arg( "--info" ).arg( path ) ).ok()?;
	// "Min size: NNNN bytes"
	for line in out.lines()
	{
		if let Some( tail ) = line.trim().strip_prefix( "Min size:" )
		{
			let n: u64 = tail.split_whitespace().next()?.parse().ok()?;
			return Some( n );
		}
	}
	None
}

fn fat_shrink( path: &Path, new_bytes: u64 ) -> Result<(), String>
{
	run( Command::new( "fatresize" )
		.arg( "--size" ).arg( new_bytes.to_string() )
		.arg( path ) ).map( |_| () )
}

// ── Helpers ─────────────────────────────────────────────────────────────────

fn run( cmd: &mut Command ) -> Result<String, String>
{
	let prog = cmd.get_program().to_string_lossy().into_owned();
	let out  = match cmd.output()
	{
		Ok( o )  => o,
		Err( e ) =>
		{
			let msg = format!( "spawn {prog}: {e}" );
			eprintln!( "fs::run: {msg}" );
			return Err( msg );
		}
	};
	let stdout = String::from_utf8_lossy( &out.stdout ).into_owned();
	if out.status.success()
	{
		Ok( stdout )
	}
	else
	{
		let stderr = String::from_utf8_lossy( &out.stderr );
		let msg = format!( "{prog} failed: {}", stderr.trim() );
		eprintln!( "fs::run: {msg}" );
		Err( msg )
	}
}
