Documentation
use anyhow::{anyhow, Result};
use gpt::{disk::LogicalBlockSize, GptConfig};
use std::{fs::File, path::Path};

use super::types::{PartitionInfo, PartitionSpec, PartitionTarget};
use super::utils::{align_up, parse_u64_any};

const LB_SIZE_BYTES: u64 = 512;

pub fn open_gpt(disk: &Path, writable: bool) -> Result<gpt::GptDisk<File>> {
    GptConfig::new()
        .writable(writable)
        .logical_block_size(LogicalBlockSize::Lb512)
        .open(disk)
        .map_err(|e| anyhow!("failed to open GPT: {e}"))
}

pub fn map_partitions(gdisk: &gpt::GptDisk<File>) -> Result<Vec<PartitionInfo>> {
    let mut out = Vec::new();
    for (idx, part) in gdisk.partitions().iter() {
        if !part.is_used() {
            continue;
        }
        let start = part
            .bytes_start(LogicalBlockSize::Lb512)
            .map_err(|e| anyhow!("invalid partition start: {e}"))?;
        let size = part
            .bytes_len(LogicalBlockSize::Lb512)
            .map_err(|e| anyhow!("invalid partition size: {e}"))?;
        out.push(PartitionInfo {
            index: *idx,
            name: part.name.clone(),
            first_lba: part.first_lba,
            last_lba: part.last_lba,
            start_bytes: start,
            size_bytes: size,
        });
    }
    out.sort_by_key(|p| p.index);
    Ok(out)
}

pub fn parse_parameter_file(path: &Path) -> Result<Vec<PartitionSpec>> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| anyhow!("failed to read parameter file {}: {e}", path.display()))?;

    let cmdline = content
        .lines()
        .find_map(|line| line.strip_prefix("CMDLINE:"))
        .map(|s| s.trim())
        .ok_or_else(|| anyhow!("CMDLINE not found in parameter file"))?;

    let mtd = cmdline
        .split_whitespace()
        .find_map(|part| part.strip_prefix("mtdparts="))
        .ok_or_else(|| anyhow!("mtdparts not found in CMDLINE"))?;

    let (_, parts_str) = mtd
        .split_once(':')
        .ok_or_else(|| anyhow!("invalid mtdparts format"))?;

    let mut specs = Vec::new();
    for raw in parts_str.split(',') {
        let raw = raw.trim();
        if raw.is_empty() {
            continue;
        }
        let (size_off, name_part) = raw
            .split_once('(')
            .ok_or_else(|| anyhow!("invalid partition entry: {raw}"))?;
        let name_part = name_part.trim_end_matches(')');
        let (name, flags) = match name_part.split_once(':') {
            Some((n, f)) => (n.trim(), Some(f.trim())),
            None => (name_part.trim(), None),
        };

        let (size_str, off_str) = size_off
            .split_once('@')
            .ok_or_else(|| anyhow!("invalid partition entry: {raw}"))?;

        let grow = flags.is_some_and(|f| f.contains("grow"));
        let size_bytes = if size_str.trim() == "-" {
            None
        } else {
            Some(parse_u64_any(size_str.trim())?)
        };
        let offset_bytes = parse_u64_any(off_str.trim())?;

        specs.push(PartitionSpec {
            name: name.to_string(),
            offset_bytes,
            size_bytes,
            grow,
        });
    }

    Ok(specs)
}

pub fn resolve_partition_target(disk: &Path, part: Option<&str>) -> Result<PartitionTarget> {
    let disk_size = std::fs::metadata(disk)
        .map_err(|e| anyhow!("failed to stat disk {}: {e}", disk.display()))?
        .len();

    let Some(part) = part else {
        return Ok(PartitionTarget {
            offset_bytes: 0,
            size_bytes: disk_size,
        });
    };

    let gdisk = open_gpt(disk, false).map_err(|_| anyhow!("no GPT found on disk"))?;
    let parts = gdisk.partitions();

    let mut resolved: Option<(u32, gpt::partition::Partition)> = None;
    if let Ok(idx) = part.parse::<u32>() {
        if let Some(p) = parts.get(&idx) {
            resolved = Some((idx, p.clone()));
        }
    } else {
        for (idx, p) in parts.iter() {
            if p.is_used() && p.name == part {
                resolved = Some((*idx, p.clone()));
                break;
            }
        }
    }

    let (_index, part) = resolved.ok_or_else(|| {
        let list = parts
            .iter()
            .filter(|(_, p)| p.is_used())
            .map(|(idx, p)| format!("{}:{}", idx, p.name))
            .collect::<Vec<_>>()
            .join(", ");
        anyhow!("partition not found. available: {list}")
    })?;

    let start = part
        .bytes_start(LogicalBlockSize::Lb512)
        .map_err(|e| anyhow!("invalid partition start: {e}"))?;
    let size = part
        .bytes_len(LogicalBlockSize::Lb512)
        .map_err(|e| anyhow!("invalid partition size: {e}"))?;

    Ok(PartitionTarget {
        offset_bytes: start,
        size_bytes: size,
    })
}

pub fn align_partition_start(offset_bytes: u64, align_bytes: u64) -> u64 {
    let mut start = align_up(offset_bytes, align_bytes.max(LB_SIZE_BYTES));
    if !start.is_multiple_of(LB_SIZE_BYTES) {
        start = align_up(start, LB_SIZE_BYTES);
    }
    start
}

pub fn clamp_size_to_lba(size_bytes: u64) -> u64 {
    size_bytes - (size_bytes % LB_SIZE_BYTES)
}

pub fn lb_size_bytes() -> u64 {
    LB_SIZE_BYTES
}