//! CSV reading and wiring of the modules. Errors that correspond to exit
//! code 2 (invalid arguments, IO failures, parse failures) are returned as
//! `Err`; the pass/fail judgement of the sequence itself is left to
//! `CheckReport`.

use std::fs::File;
use std::io::{self, Read};

use crate::checker::{CheckReport, Checker};
use crate::cli::CheckArgs;
use crate::parser::{self, Radix};

pub fn run(args: &CheckArgs) -> Result<CheckReport, String> {
    validate(args)?;
    let file = args
        .file
        .as_deref()
        .ok_or("no input file given (see --help)")?;
    let reader: Box<dyn Read> = if file == "-" {
        Box::new(io::stdin())
    } else {
        Box::new(File::open(file).map_err(|e| format!("{file}: {e}"))?)
    };
    check_reader(args, reader)
}

fn validate(args: &CheckArgs) -> Result<(), String> {
    if args.step == 0 {
        return Err("--step must not be 0".into());
    }
    if args.col == 0 {
        return Err("--col is 1-based and must be >= 1".into());
    }
    if args.cols == 0 {
        return Err("--cols must be >= 1".into());
    }
    if !args.quote.is_ascii() {
        return Err("--quote must be a single ASCII character".into());
    }
    if args.cols > 1 {
        let col_bytes = args.col_bytes.unwrap_or(1) as usize;
        if args.cols * col_bytes * 8 > 128 {
            return Err(format!(
                "combined width {} bytes exceeds the 16-byte limit",
                args.cols * col_bytes
            ));
        }
    }
    Ok(())
}

fn check_reader(args: &CheckArgs, reader: impl Read) -> Result<CheckReport, String> {
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(args.header)
        .flexible(true)
        .trim(csv::Trim::All)
        .quote(args.quote as u8)
        .from_reader(reader);
    let radix = args.radix();
    let first_col = args.col - 1;
    let mut col_bytes = args.col_bytes;
    let mut checker = Checker::new(args.step);

    // Read as ByteRecord and UTF-8-decode only the cells under check, so that
    // non-UTF-8 bytes (e.g. CP932 text) in unrelated columns do not abort the run.
    for record in rdr.byte_records() {
        let record = record.map_err(|e| e.to_string())?;
        let line = record.position().map_or(0, |p| p.line());
        if record.len() < first_col + args.cols {
            return Err(format!(
                "row {line}: has {} column(s), need column {} to {}",
                record.len(),
                args.col,
                first_col + args.cols
            ));
        }
        let cells: Vec<&str> = (first_col..first_col + args.cols)
            .map(|i| {
                std::str::from_utf8(&record[i])
                    .map_err(|_| format!("row {line}: column {}: invalid UTF-8", i + 1))
            })
            .collect::<Result<_, _>>()?;
        let value = parse_record(&cells, args, radix, &mut col_bytes)
            .map_err(|e| format!("row {line}: {e}"))?;
        checker.push(line, value);
    }
    Ok(checker.finish())
}

fn parse_record(
    cells: &[&str],
    args: &CheckArgs,
    radix: Radix,
    col_bytes: &mut Option<u32>,
) -> Result<i128, String> {
    if args.cols == 1 {
        return parser::parse_single(cells[0], radix);
    }
    let bytes = *col_bytes.get_or_insert_with(|| parser::infer_col_bytes(cells[0], radix));
    if args.cols as u32 * bytes * 8 > 128 {
        return Err(format!(
            "combined width {} bytes (inferred {bytes} byte(s)/column) exceeds the 16-byte limit",
            args.cols as u32 * bytes
        ));
    }
    parser::parse_multi(cells, radix, args.endian.into(), bytes)
}
