use std::path::{Path, PathBuf};

use crate::HexFile;

use super::error::CliError;
use super::ini::load_ini;
use super::parse_util::parse_number;
use super::types::Args;
use super::types::OutputFormat;

pub(super) trait ReadProvider {
    fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, std::io::Error>;

    fn read_string(&self, path: &Path) -> Result<String, std::io::Error> {
        let bytes = self.read_bytes(path)?;
        String::from_utf8(bytes)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
    }
}

pub(super) struct FsProvider;

impl ReadProvider for FsProvider {
    fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, std::io::Error> {
        std::fs::read(path)
    }
}

pub(super) fn load_input(provider: &impl ReadProvider, path: &Path) -> Result<HexFile, CliError> {
    let content = provider.read_bytes(path)?;

    let mut ascii_only = true;
    let mut first_nonempty_line: Option<Vec<u8>> = None;
    let mut ascii_lines_checked = 0usize;
    let mut current_line: Vec<u8> = Vec::new();

    for &b in &content {
        if b == b'\n' || b == b'\r' {
            if !current_line.is_empty() {
                if ascii_lines_checked < 25 {
                    if !current_line.is_ascii() {
                        ascii_only = false;
                    }
                    ascii_lines_checked += 1;
                }
                if first_nonempty_line.is_none() {
                    first_nonempty_line = Some(current_line.clone());
                }
                if ascii_lines_checked >= 25 {
                    break;
                }
            }
            current_line.clear();
            continue;
        }
        current_line.push(b);
    }
    if !current_line.is_empty() && first_nonempty_line.is_none() {
        first_nonempty_line = Some(current_line.clone());
    }

    if ascii_lines_checked == 0 && !content.is_empty() {
        ascii_only = content.is_ascii();
    }

    if !ascii_only {
        return Ok(crate::parse_binary(&content, 0)?);
    }

    let first_line = first_nonempty_line.unwrap_or_default();
    if first_line.first() == Some(&b':') {
        let hexfile = crate::parse_intel_hex(&content)?;
        Ok(hexfile)
    } else if matches!(first_line.first(), Some(b'S') | Some(b's')) {
        let hexfile = crate::parse_srec(&content)?;
        Ok(hexfile)
    } else {
        let hexfile = crate::parse_binary(&content, 0)?;
        Ok(hexfile)
    }
}

pub(super) fn load_binary_input(
    provider: &impl ReadProvider,
    path: &Path,
    offset: u32,
) -> Result<HexFile, CliError> {
    let content = provider.read_bytes(path)?;
    let hexfile = crate::parse_binary(&content, offset)?;
    Ok(hexfile)
}

pub(super) fn load_hex_ascii_input(
    provider: &impl ReadProvider,
    path: &Path,
    offset: u32,
) -> Result<HexFile, CliError> {
    let content = provider.read_bytes(path)?;
    let hexfile = crate::parse_hex_ascii(&content, offset)?;
    Ok(hexfile)
}

pub(super) fn hexfiles_overlap(a: &HexFile, b: &HexFile) -> bool {
    let mut a_segments = a.segments().to_vec();
    let mut b_segments = b.segments().to_vec();
    a_segments.sort_by_key(|s| s.start_address);
    b_segments.sort_by_key(|s| s.start_address);

    let mut i = 0usize;
    let mut j = 0usize;
    while i < a_segments.len() && j < b_segments.len() {
        let a_seg = &a_segments[i];
        let b_seg = &b_segments[j];
        if a_seg.end_address() < b_seg.start_address {
            i += 1;
            continue;
        }
        if b_seg.end_address() < a_seg.start_address {
            j += 1;
            continue;
        }
        return true;
    }
    false
}

pub(super) fn load_intel_hex_16bit_input(
    provider: &impl ReadProvider,
    path: &Path,
) -> Result<HexFile, CliError> {
    let content = provider.read_bytes(path)?;
    let hexfile = crate::parse_intel_hex_16bit(&content)?;
    Ok(hexfile)
}

pub(super) fn write_output(
    hexfile: &HexFile,
    path: &PathBuf,
    format: &Option<OutputFormat>,
    bytes_per_line: Option<u8>,
) -> Result<(), CliError> {
    let format = format
        .as_ref()
        .unwrap_or(&OutputFormat::IntelHex { record_type: None });

    match format {
        OutputFormat::IntelHex { record_type } => {
            let mode = match record_type {
                Some(1) => crate::IntelHexMode::ExtendedLinear,
                Some(2) => crate::IntelHexMode::ExtendedSegment,
                _ => crate::IntelHexMode::Auto,
            };
            let options = crate::IntelHexWriteOptions {
                bytes_per_line: bytes_per_line.unwrap_or(32),
                mode,
            };
            let output = crate::write_intel_hex(hexfile, &options);
            std::fs::write(path, output)?;
        }
        OutputFormat::SRecord { record_type } => {
            let record_type = match record_type {
                None => None,
                Some(0) => Some(crate::SRecordType::S1),
                Some(1) => Some(crate::SRecordType::S2),
                Some(2) => Some(crate::SRecordType::S3),
                Some(other) => {
                    return Err(CliError::Other(format!(
                        "unsupported S-Record type {other}"
                    )));
                }
            };
            let options = crate::SRecordWriteOptions {
                bytes_per_line: bytes_per_line.unwrap_or(16),
                record_type,
            };
            let output = crate::write_srec(hexfile, &options)?;
            std::fs::write(path, output)?;
        }
        OutputFormat::Binary => {
            let options = crate::BinaryWriteOptions::default();
            let output = crate::write_binary(hexfile, &options);
            std::fs::write(path, output)?;
        }
        OutputFormat::HexAscii {
            line_length,
            separator,
        } => {
            let options = crate::HexAsciiWriteOptions {
                line_length: line_length.unwrap_or(16) as usize,
                separator: separator.clone(),
            };
            let output = crate::write_hex_ascii(hexfile, &options);
            std::fs::write(path, output)?;
        }
        OutputFormat::SeparateBinary => write_separate_binary(hexfile, path)?,
        OutputFormat::CCode => {
            return Err(CliError::Other(
                "C-code output must be handled by caller".into(),
            ));
        }
        OutputFormat::Porsche => {
            return Err(CliError::Other(
                "Porsche output must be handled by caller".into(),
            ));
        }
        _ => {
            return Err(CliError::Other(format!(
                "Output format {:?} not yet implemented",
                format
            )));
        }
    }

    Ok(())
}

pub(super) fn write_output_for_args(
    args: &Args,
    hexfile: &HexFile,
    provider: &impl ReadProvider,
) -> Result<(), CliError> {
    match args.output_format {
        Some(OutputFormat::CCode) => {
            let path = resolve_c_code_output_path(args)?;
            write_c_code_output(args, hexfile, &path, provider)?;
            Ok(())
        }
        Some(OutputFormat::FordIntelHex) => {
            let path = resolve_ford_output_path(args)?;
            write_ford_ihex_output(args, hexfile, &path, provider)?;
            Ok(())
        }
        Some(OutputFormat::Porsche) => {
            let path = resolve_porsche_output_path(args)?;
            write_porsche_output(args, hexfile, &path)?;
            Ok(())
        }
        _ => {
            if let Some(ref path) = args.output_file {
                write_output(hexfile, path, &args.output_format, args.bytes_per_line)?;
            }
            Ok(())
        }
    }
}

pub(super) fn write_c_code_output(
    args: &Args,
    hexfile: &HexFile,
    output_path: &Path,
    provider: &impl ReadProvider,
) -> Result<(), CliError> {
    let ini_path = resolve_ini_path(args)?;
    let ini = load_ini(&ini_path, provider)?;

    let prefix = ini
        .get("prefix")
        .cloned()
        .unwrap_or_else(|| "flashDrv".to_string());
    let word_size = ini
        .get("wordsize")
        .map(|v| parse_number(v))
        .transpose()?
        .unwrap_or(0);
    let word_type = ini
        .get("wordtype")
        .map(|v| parse_number(v))
        .transpose()?
        .unwrap_or(0);
    let decrypt = ini
        .get("decryption")
        .map(|v| parse_number(v).map(|n| n != 0))
        .transpose()?
        .unwrap_or(false);
    let decrypt_value = ini
        .get("decryptvalue")
        .map(|v| parse_number(v))
        .transpose()?
        .unwrap_or(0);

    let word_type = match word_type {
        0 => crate::CCodeWordType::Intel,
        1 => crate::CCodeWordType::Motorola,
        other => {
            return Err(CliError::Other(format!("unsupported WordType {other}")));
        }
    };

    let header_name = output_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or(&prefix)
        .to_string();

    let options = crate::CCodeWriteOptions {
        prefix: prefix.clone(),
        header_name,
        word_size: word_size as u8,
        word_type,
        decrypt,
        decrypt_value,
    };
    let output = crate::write_c_code(hexfile, &options)?;

    let (c_path, h_path) = derive_c_code_paths(output_path, &prefix);
    std::fs::write(c_path, output.c)?;
    std::fs::write(h_path, output.h)?;
    Ok(())
}

pub(super) fn resolve_c_code_output_path(args: &Args) -> Result<PathBuf, CliError> {
    if let Some(path) = args.output_file.clone() {
        return Ok(path);
    }

    if let Some(input) = primary_input_path(args) {
        return Ok(input.with_extension("c"));
    }

    Err(CliError::Other(
        "output file required for /XC (use -o <file>)".into(),
    ))
}

pub(super) fn write_ford_ihex_output(
    args: &Args,
    hexfile: &HexFile,
    output_path: &Path,
    provider: &impl ReadProvider,
) -> Result<(), CliError> {
    let ini_path = resolve_ini_path(args)?;
    let ini = load_ini(&ini_path, provider)?;

    let header = build_ford_header(args, hexfile, output_path, &ini)?;
    let options = crate::IntelHexWriteOptions {
        bytes_per_line: args.bytes_per_line.unwrap_or(32),
        mode: crate::IntelHexMode::Auto,
    };
    let data = crate::write_intel_hex(hexfile, &options);
    let data = String::from_utf8(data)
        .map_err(|e| CliError::Other(format!("invalid Intel HEX output: {e}")))?;

    let mut output = Vec::new();
    output.extend_from_slice(normalize_crlf(&header).as_bytes());
    output.extend_from_slice(normalize_crlf(&data).as_bytes());
    std::fs::write(output_path, output)?;
    Ok(())
}

pub(super) fn resolve_ford_output_path(args: &Args) -> Result<PathBuf, CliError> {
    if let Some(path) = args.output_file.clone() {
        return Ok(path);
    }

    if let Some(input) = primary_input_path(args) {
        return Ok(input.with_extension("hex"));
    }

    Err(CliError::Other(
        "output file required for /XF (use -o <file>)".into(),
    ))
}

pub(super) fn write_porsche_output(
    args: &Args,
    hexfile: &HexFile,
    output_path: &Path,
) -> Result<(), CliError> {
    let mut normalized = hexfile.normalized();
    if normalized.segments().is_empty() {
        std::fs::write(output_path, [])?;
        return Ok(());
    }

    let fill = args.align_fill;
    normalized.fill_gaps(fill);
    let data = normalized.segments()[0].data.clone();
    let checksum = byte_sum_u16(&data);
    let mut output = data;
    output.extend_from_slice(&checksum.to_be_bytes());
    std::fs::write(output_path, output)?;
    Ok(())
}

pub(super) fn resolve_porsche_output_path(args: &Args) -> Result<PathBuf, CliError> {
    if let Some(path) = args.output_file.clone() {
        return Ok(path);
    }

    if let Some(input) = primary_input_path(args) {
        return Ok(input.with_extension("bin"));
    }

    Err(CliError::Other(
        "output file required for /XP (use -o <file>)".into(),
    ))
}

fn resolve_ini_path(args: &Args) -> Result<PathBuf, CliError> {
    if let Some(path) = args.ini_file.clone() {
        return Ok(path);
    }

    if let Some(input) = primary_input_path(args) {
        return Ok(input.with_extension("ini"));
    }

    Err(CliError::Other(
        "INI file required for /XC (use /P:<file>)".into(),
    ))
}

fn primary_input_path(args: &Args) -> Option<&Path> {
    args.input_file
        .as_deref()
        .or(args
            .import_binary
            .as_ref()
            .map(|import| import.file.as_path()))
        .or(args
            .import_hex_ascii
            .as_ref()
            .map(|import| import.file.as_path()))
        .or(args.import_i16.as_deref())
}

fn derive_c_code_paths(output_path: &Path, prefix: &str) -> (PathBuf, PathBuf) {
    let dir = output_path.parent().unwrap_or_else(|| Path::new("."));
    let stem = output_path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or(prefix);
    let c_path = dir.join(format!("{stem}.c"));
    let h_path = dir.join(format!("{stem}.h"));
    (c_path, h_path)
}

fn build_ford_header(
    args: &Args,
    hexfile: &HexFile,
    output_path: &Path,
    ini: &std::collections::HashMap<String, String>,
) -> Result<String, CliError> {
    let mut lines = Vec::new();

    let required = [
        "application",
        "mask number",
        "module type",
        "production module part number",
        "wers notice",
        "comments",
        "released by",
        "module name",
        "module id",
    ];
    for key in required {
        let value = ini
            .get(key)
            .ok_or_else(|| CliError::Other(format!("missing [FORDHEADER] {key}")))?;
        lines.push(format!("{}>{}", key.to_ascii_uppercase(), value));
    }

    let file_name = ini.get("file name").cloned().unwrap_or_else(|| {
        output_path
            .file_name()
            .and_then(|s| s.to_str())
            .unwrap_or("output.hex")
            .to_string()
    });
    lines.insert(2, format!("FILE NAME>{file_name}"));

    let release_date = ini
        .get("release date")
        .cloned()
        .unwrap_or_else(|| current_date_mmddyyyy().unwrap_or_else(|| "01/01/1970".to_string()));
    lines.insert(3, format!("RELEASE DATE>{release_date}"));

    let download_format = ini
        .get("download format")
        .cloned()
        .unwrap_or_else(|| "0x00".to_string());
    lines.push(format!("DOWNLOAD FORMAT>{download_format}"));

    let checksum = compute_ford_checksum(hexfile);
    lines.push(format!("FILE CHECKSUM>0x{checksum:04X}"));

    let flash_indicator = ini
        .get("flash indicator")
        .cloned()
        .unwrap_or_else(|| "0".to_string());
    lines.push(format!("FLASH INDICATOR>{flash_indicator}"));

    let erase = ini
        .get("flash erase sectors")
        .cloned()
        .unwrap_or_else(|| format_erase_sectors(hexfile, args.align_erase));
    lines.push(format!("FLASH ERASE SECTORS>{erase}"));

    lines.push("$".to_string());
    Ok(lines.join("\n") + "\n")
}

fn normalize_crlf(text: &str) -> String {
    text.replace("\r\n", "\n")
        .replace('\r', "\n")
        .replace('\n', "\r\n")
}

fn compute_ford_checksum(hexfile: &HexFile) -> u16 {
    let mut sum: u16 = 0;
    let mut segments = hexfile.normalized().into_segments();
    segments.sort_by_key(|s| s.start_address);
    for segment in segments {
        for byte in segment.data {
            sum = sum.wrapping_add(byte as u16);
        }
    }
    sum
}

fn format_erase_sectors(hexfile: &HexFile, alignment: Option<u32>) -> String {
    let mut segments = hexfile.normalized().into_segments();
    segments.sort_by_key(|s| s.start_address);
    let mut parts = Vec::new();

    for segment in segments {
        let start = segment.start_address;
        let len = segment.len() as u32;
        let (aligned_start, aligned_len) = if let Some(align) = alignment.filter(|a| *a > 0) {
            let start64 = start as u64;
            let len64 = len as u64;
            let align64 = align as u64;
            let aligned_start = (start64 / align64) * align64;
            let end = start64 + len64 - 1;
            let aligned_end = (end + 1).div_ceil(align64) * align64 - 1;
            let aligned_len = aligned_end - aligned_start + 1;
            (aligned_start as u32, aligned_len as u32)
        } else {
            (start, len)
        };
        parts.push(format!("0x{aligned_start:X},0x{aligned_len:X}"));
    }

    parts
        .into_iter()
        .map(|p| format!(":{p}"))
        .collect::<String>()
}

fn byte_sum_u16(data: &[u8]) -> u16 {
    data.iter().fold(0u16, |acc, &b| acc.wrapping_add(b as u16))
}

fn current_date_mmddyyyy() -> Option<String> {
    let output = std::process::Command::new("date")
        .arg("+%m/%d/%Y")
        .output()
        .ok()?;
    let date = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if date.is_empty() { None } else { Some(date) }
}

fn write_separate_binary(hexfile: &HexFile, path: &Path) -> Result<(), CliError> {
    let normalized = hexfile.normalized();
    let mut segments = normalized.into_segments();
    segments.sort_by_key(|s| s.start_address);

    if segments.is_empty() {
        return Ok(());
    }

    let dir = path.parent().unwrap_or_else(|| Path::new("."));
    let stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("output");
    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("bin");

    for segment in segments {
        let filename = format!("{stem}_{:x}.{ext}", segment.start_address);
        let out_path = dir.join(filename);
        std::fs::write(out_path, segment.data)?;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Segment;
    use std::fs;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::time::{SystemTime, UNIX_EPOCH};

    fn unique_temp_dir() -> PathBuf {
        static COUNTER: AtomicUsize = AtomicUsize::new(0);
        let stamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis();
        let count = COUNTER.fetch_add(1, Ordering::Relaxed);
        let dir = std::env::temp_dir().join(format!(
            "h3xy_test_{stamp}_{}_{}",
            std::process::id(),
            count
        ));
        let _ = fs::create_dir_all(&dir);
        dir
    }

    #[test]
    fn test_write_separate_binary_outputs_segments() {
        let dir = unique_temp_dir();
        let output = dir.join("out.bin");
        let hexfile = HexFile::with_segments(vec![
            Segment::new(0x1000, vec![0xAA, 0xBB]),
            Segment::new(0x2000, vec![0xCC]),
        ]);

        write_output(&hexfile, &output, &Some(OutputFormat::SeparateBinary), None).unwrap();

        let file1 = dir.join("out_1000.bin");
        let file2 = dir.join("out_2000.bin");
        assert_eq!(fs::read(file1).unwrap(), vec![0xAA, 0xBB]);
        assert_eq!(fs::read(file2).unwrap(), vec![0xCC]);

        let _ = fs::remove_dir_all(dir);
    }

    #[test]
    fn test_write_ford_ihex_happy_path() {
        let dir = unique_temp_dir();
        let ini_path = dir.join("ford.ini");
        let output = dir.join("ford.hex");
        let ini = "[FORDHEADER]\nAPPLICATION=APP\nMASK NUMBER=7\nMODULE TYPE=TYPE\nPRODUCTION MODULE PART NUMBER=PN\nWERS NOTICE=WERS\nCOMMENTS=Note\nRELEASED BY=Dev\nMODULE NAME=MOD\nMODULE ID=0x1234\nRELEASE DATE=01/02/2003\nDOWNLOAD FORMAT=0x01\nFLASH INDICATOR=1\n";
        fs::write(&ini_path, ini).unwrap();

        let args = Args {
            ini_file: Some(ini_path),
            bytes_per_line: Some(16),
            ..Args::default()
        };
        let hexfile = HexFile::with_segments(vec![Segment::new(0x1000, vec![0x01, 0x02])]);
        let provider = FsProvider;

        write_ford_ihex_output(&args, &hexfile, &output, &provider).unwrap();
        let content = fs::read_to_string(&output).unwrap();
        assert!(content.contains("APPLICATION>APP"));
        assert!(content.contains("FILE CHECKSUM>"));
        assert!(content.contains("$"));
        assert!(content.contains(":"));
        assert!(content.contains("FLASH ERASE SECTORS>:0x1000,0x2"));
        let bytes = fs::read(&output).unwrap();
        assert!(bytes.windows(2).any(|w| w == b"\r\n"));

        let _ = fs::remove_dir_all(dir);
    }

    #[test]
    fn test_write_ford_ihex_missing_required() {
        let dir = unique_temp_dir();
        let ini_path = dir.join("ford.ini");
        let output = dir.join("ford.hex");
        fs::write(&ini_path, "[FORDHEADER]\nAPPLICATION=APP\n").unwrap();

        let args = Args {
            ini_file: Some(ini_path),
            ..Args::default()
        };
        let hexfile = HexFile::with_segments(vec![Segment::new(0x1000, vec![0x01])]);
        let provider = FsProvider;
        let result = write_ford_ihex_output(&args, &hexfile, &output, &provider);
        assert!(result.is_err());

        let _ = fs::remove_dir_all(dir);
    }

    #[test]
    fn test_write_porsche_output_appends_checksum() {
        let dir = unique_temp_dir();
        let output = dir.join("porsche.bin");
        let args = Args {
            align_fill: 0xFF,
            ..Args::default()
        };
        let hexfile = HexFile::with_segments(vec![
            Segment::new(0x1000, vec![0x01, 0x02]),
            Segment::new(0x1004, vec![0x03]),
        ]);

        write_porsche_output(&args, &hexfile, &output).unwrap();
        let data = fs::read(&output).unwrap();
        // data: 0x01,0x02,0xFF,0xFF,0x03 then checksum
        assert_eq!(&data[..5], &[0x01, 0x02, 0xFF, 0xFF, 0x03]);
        let checksum = u16::from_be_bytes([data[5], data[6]]);
        assert_eq!(checksum, 0x01 + 0x02 + 0xFF + 0xFF + 0x03);

        let _ = fs::remove_dir_all(dir);
    }

    #[test]
    fn test_resolve_default_paths_from_i16_input() {
        let input = PathBuf::from("input.hex");
        let args = Args {
            import_i16: Some(input.clone()),
            ..Args::default()
        };

        assert_eq!(
            resolve_c_code_output_path(&args).unwrap(),
            input.with_extension("c")
        );
        assert_eq!(
            resolve_ford_output_path(&args).unwrap(),
            input.with_extension("hex")
        );
        assert_eq!(
            resolve_porsche_output_path(&args).unwrap(),
            input.with_extension("bin")
        );
        assert_eq!(
            resolve_ini_path(&args).unwrap(),
            input.with_extension("ini")
        );
    }
}
