use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::vec::IntoIter;

use crate::error::{TuphiumError, TuphiumResult};

/// Represents the possible data types a CSV field can contain
#[derive(Debug)]
pub enum CsvValue {
    Integer(i64),
    Float(f64),
    Boolean(bool),
    Text(String),
}

impl CsvValue {
    /// Returns the column type tag matching this value's variant.
    fn column_type(&self) -> CsvColumnType {
        match self {
            CsvValue::Integer(_) => CsvColumnType::Integer,
            CsvValue::Float(_) => CsvColumnType::Float,
            CsvValue::Boolean(_) => CsvColumnType::Boolean,
            CsvValue::Text(_) => CsvColumnType::Text,
        }
    }
}

/// Represents the inferred type of a CSV column
#[derive(Debug, PartialEq)]
pub enum CsvColumnType {
    Integer,
    Float,
    Boolean,
    Text,
}

impl CsvColumnType {
    /// Widening rank used when a column mixes types: the widest variant wins
    /// (Text > Float > Integer > Boolean).
    fn widening_rank(&self) -> u8 {
        match self {
            CsvColumnType::Boolean => 0,
            CsvColumnType::Integer => 1,
            CsvColumnType::Float => 2,
            CsvColumnType::Text => 3,
        }
    }

    /// Returns whichever of `self` and `other` is the wider type.
    fn wider(self, other: Self) -> Self {
        if other.widening_rank() > self.widening_rank() {
            other
        } else {
            self
        }
    }
}

/// Represents a CSV file with its header and rows
#[derive(Debug)]
pub struct CsvData {
    pub header: Vec<String>,
    pub rows: Vec<Vec<CsvValue>>,
}

impl CsvData {
    /// Returns the inferred type for each column by scanning all rows.
    /// When a column has mixed types, the widest type wins: Text > Float > Integer > Boolean.
    pub fn column_types(&self) -> Vec<CsvColumnType> {
        (0..self.header.len())
            .map(|col| self.infer_column_type(col))
            .collect()
    }

    /// Infers a single column's type by widening across every row's value.
    fn infer_column_type(&self, col: usize) -> CsvColumnType {
        self.rows
            .iter()
            .filter_map(|row| row.get(col))
            .fold(CsvColumnType::Boolean, |widest, value| {
                widest.wider(value.column_type())
            })
    }
}

// ─── Private functions ────────────────────────────────────────────────────────

fn validate_csv_extension(path_file: &str) -> TuphiumResult<()> {
    let path: &Path = Path::new(path_file);
    match path.extension() {
        Some(ext) if ext == "csv" => Ok(()),
        _ => Err(TuphiumError::InvalidFileExtension {
            operation: "read_csv",
            path: path_file.to_string(),
            expected_extension: ".csv".to_string(),
        }),
    }
}

fn open_csv(path_file: &str) -> TuphiumResult<BufReader<File>> {
    File::open(path_file)
        .map(BufReader::new)
        .map_err(|error| TuphiumError::IoError {
            operation: "read_csv",
            path: Some(path_file.to_string()),
            message: error.to_string(),
        })
}

fn parse_field(field: &str) -> CsvValue {
    let trimmed: &str = field.trim();

    if let Ok(integer) = trimmed.parse::<i64>() {
        return CsvValue::Integer(integer);
    }
    if let Ok(float) = trimmed.parse::<f64>() {
        return CsvValue::Float(float);
    }
    if let Some(boolean) = parse_boolean(trimmed) {
        return CsvValue::Boolean(boolean);
    }

    CsvValue::Text(trimmed.to_string())
}

fn parse_boolean(field: &str) -> Option<bool> {
    match field.to_lowercase().as_str() {
        "true" | "yes" => Some(true),
        "false" | "no" => Some(false),
        _ => None,
    }
}

fn parse_lines(reader: BufReader<File>) -> TuphiumResult<Vec<Vec<String>>> {
    reader
        .lines()
        .map(|line| {
            line.map_err(|error| TuphiumError::IoError {
                operation: "read_csv",
                path: None,
                message: error.to_string(),
            })
            .map(|line| {
                line.split(',')
                    .map(|field| field.trim().to_string())
                    .collect()
            })
        })
        .collect()
}

// ─── Public functions ─────────────────────────────────────────────────────────

/// Reads a CSV file treating the first row as a header.
///
/// Returns a [`CsvData`] with the header and the typed rows separated. Each
/// field is parsed into the narrowest matching [`CsvValue`] variant.
///
/// # Errors
///
/// Returns an error if `path_file` does not end in `.csv`, cannot be opened, or
/// is empty (no header row).
pub fn read_csv(path_file: &str) -> TuphiumResult<CsvData> {
    validate_csv_extension(path_file)?;
    let reader: BufReader<File> = open_csv(path_file)?;

    let mut lines: IntoIter<Vec<String>> = parse_lines(reader)?.into_iter();

    let header: Vec<String> = lines.next().ok_or_else(|| TuphiumError::EmptyData {
        operation: "read_csv",
        target: "CSV file".to_string(),
    })?;

    let rows: Vec<Vec<CsvValue>> = lines
        .map(|row| row.iter().map(|field| parse_field(field)).collect())
        .collect();

    Ok(CsvData { header, rows })
}

/// Reads a CSV file without treating the first row as a header.
///
/// Every row, including the first, is parsed as data and returned as typed cells.
///
/// # Errors
///
/// Returns an error if `path_file` does not end in `.csv` or cannot be opened.
pub fn read_csv_without_header(path_file: &str) -> TuphiumResult<Vec<Vec<CsvValue>>> {
    validate_csv_extension(path_file)?;
    let reader: BufReader<File> = open_csv(path_file)?;

    let data: Vec<Vec<CsvValue>> = parse_lines(reader)?
        .into_iter()
        .map(|row| row.iter().map(|field| parse_field(field)).collect())
        .collect();

    Ok(data)
}
