use std::{collections::HashMap, io::Read, path::PathBuf};

use anyhow::bail;
use chrono::NaiveDateTime;

#[derive(Hash, PartialEq, PartialOrd)]
pub struct Filter {
    pub filter_type: FilterType,
    pub file: PathBuf,
    pub field: String,
    pub value: String,
}

#[derive(Hash, PartialEq, PartialOrd, Eq, Ord, Clone)]
pub enum SourceType {
    CodingDiagnosis,
    CodingProcedure,
    Encounter,
    // TODO: Incident isn't used right now
    // Incident,
    Patient,
    Revenue,
    Service,
    Transfer,
}

impl TryFrom<&String> for SourceType {
    type Error = anyhow::Error;

    fn try_from(value: &String) -> Result<Self, Self::Error> {
        match value.as_str() {
            "CD" => Ok(SourceType::CodingDiagnosis),
            "CP" => Ok(SourceType::CodingProcedure),
            "E" => Ok(SourceType::Encounter),
            "P" => Ok(SourceType::Patient),
            "R" => Ok(SourceType::Revenue),
            "S" => Ok(SourceType::Service),
            "T" => Ok(SourceType::Transfer),
            _ => bail!("Source Type is not valid"),
        }
    }
}

impl SourceType {
    fn from_component_source_type(value: &str) -> anyhow::Result<Self> {
        match value {
            "CD" => Ok(SourceType::CodingDiagnosis),
            "CP" => Ok(SourceType::CodingProcedure),
            "E" => Ok(SourceType::Encounter),
            "P" => Ok(SourceType::Patient),
            "R" => Ok(SourceType::Revenue),
            "S" => Ok(SourceType::Service),
            "T" => Ok(SourceType::Transfer),
            "EC" => Ok(SourceType::Encounter),
            "CDX" => Ok(SourceType::CodingDiagnosis),
            "CPX" => Ok(SourceType::CodingProcedure),
            "EX" => Ok(SourceType::Encounter),
            "PX" => Ok(SourceType::Patient),
            "RX" => Ok(SourceType::Revenue),
            "SX" => Ok(SourceType::Service),
            "TX" => Ok(SourceType::Transfer),
            _ => bail!("Invalid ComponentSourceType found: {}", value),
        }
    }

    fn to_file_path(&self) -> String {
        match self {
            SourceType::CodingDiagnosis => "coding_diagnoses.csv".to_owned(),
            SourceType::CodingProcedure => "coding_procedures.csv".to_owned(),
            SourceType::Encounter => "encounters.csv".to_owned(),
            SourceType::Patient => "patients.csv".to_owned(),
            SourceType::Revenue => "revenues.csv".to_owned(),
            SourceType::Service => "services.csv".to_owned(),
            SourceType::Transfer => "transfers.csv".to_owned(),
        }
    }
}

#[derive(Hash, PartialEq, PartialOrd)]
pub enum FilterType {
    Equal,
    GreaterThan,
    GreaterThanOrEqualTo,
    LessThan,
    LessThanOrEqualTo,
    NotEqualTo,
}

impl TryFrom<&String> for FilterType {
    type Error = anyhow::Error;

    fn try_from(value: &String) -> Result<Self, Self::Error> {
        match value.as_str() {
            "=" => Ok(FilterType::Equal),
            ">" => Ok(FilterType::GreaterThan),
            ">=" => Ok(FilterType::GreaterThanOrEqualTo),
            "<" => Ok(FilterType::LessThan),
            "<=" => Ok(FilterType::LessThanOrEqualTo),
            "!=" => Ok(FilterType::NotEqualTo),
            _ => bail!("Invalid FilterType found: {}", value),
        }
    }
}

#[derive(PartialEq)]
pub enum ExtraType {
    CodingDiagnosis,
    CodingProcedure,
    Encounter,
    Patient,
    Revenue,
    Service,
    Transfer,
}

pub enum Component {
    Constant(String),
    // File, column_name
    Field(PathBuf, String),
}

// Frequency per type:
// Linked Dataset: One Per Area, One per Built Service code, One per source item
// Coding Diagnosis, Coding Procedure, Encounter, Revenue, Service: One per source item
// Transfer: Change in bed number, change in clinic, change in consultant, change in consultant specialty, change in consultant specialty rollup, change in unit, change in ward, daily, daily or Change in bed number, daily or change in clinic, daily or change in consultant, daily or change in consultant specialty, cdaily or hange in consultant specialty rollup, daily or change in unit, daily or change in ward, daily except on admission day, daily except on admission day (incl same day), daily except on discharge day, daily except on discharge day (incl same day), Only admission location, only discharge location, one per source item
pub enum Frequency {
    ChangeInBedNumber,
    ChangeInClinic,
    ChangeInConsultant,
    ChangeInConsultantSpecialty,
    ChangeInConsultantSpecialtyRollup,
    ChangeInUnit,
    ChangeInWard,
    Daily,
    DailyOrChangeInBedNumber,
    DailyOrChangeInClinic,
    DailyOrChangeInConsultant,
    DailyOrChangeInConsultantSpecialty,
    DailyOrChangeInConsultantSpecialtyRollup,
    DailyOrChangeInUnit,
    DailyOrChangeInWard,
    DailyExceptOnAdmissionDay,
    DailyExceptOnAdmissionDayInclSameDay,
    DailyExceptOnDischargeDay,
    DailyExceptOnDischargeDayInclSameDay,
    OnlyAdmissionLocation,
    OnlyDischargeLocation,
    OnePerSource,
}

impl TryFrom<&String> for Frequency {
    type Error = anyhow::Error;

    fn try_from(frequency: &String) -> Result<Self, Self::Error> {
        match frequency.as_str() {
            "O" => Ok(Frequency::OnePerSource),
            "DOCW" => Ok(Frequency::DailyOrChangeInWard),
            "D" => Ok(Frequency::Daily),
            "DOCC" => Ok(Frequency::DailyOrChangeInClinic),
            "DEAD" => Ok(Frequency::DailyExceptOnAdmissionDay),
            "OAL" => Ok(Frequency::OnlyAdmissionLocation),
            "CIW" => Ok(Frequency::ChangeInWard),
            "DDSD" => Ok(Frequency::DailyExceptOnDischargeDay),
            _ => bail!("Invalid Frequency found: {}", frequency),
        }
    }
}

pub enum RoundingMode {
    ToClosestWhole,
    UpToClosestWhole,
    DownToClosestWhole,
    None,
}

impl TryFrom<&String> for RoundingMode {
    type Error = anyhow::Error;

    fn try_from(rounding: &String) -> Result<Self, Self::Error> {
        match rounding.as_str() {
            "U" => Ok(RoundingMode::UpToClosestWhole),
            "N" => Ok(RoundingMode::None),
            "D" => Ok(RoundingMode::DownToClosestWhole),
            "T" => Ok(RoundingMode::ToClosestWhole),
            _ => bail!("Invalid rounding mode found: {}", rounding),
        }
    }
}

enum ExtraValue {
    String(String),
    Sumeric(f64),
    Datetime(NaiveDateTime),
}

struct Extra {
    extra_type: String,
    value: ExtraValue,
}

// Quantities per type:
// Built Service: Constant, SourceQuantity
// Coding Diagnosis: Costant, Extra
// Coding Procedure: Costant, Extra
// Encounter: Admission Weight, Age, Constant, Days, Expected Length of Stay, Extra, Hours, ICU Hours, Length of Stay, Mech Vent Hours, Revenue, Weighted Separation
// Revenue: Constant, Extra, SourceQuantity
// Service: Constant, Extra, SourceQuantity
// Transfer: Constant, Days, Extra, Hours
pub enum Quantity {
    Constant(f64),
    // Name of the extra
    Extra(String),
    SourceQuantity,
    Hours,
    Days,
    AdmissionWeight,
    Age,
    ExpectedLengthOfStay,
    ICUHours,
    LengthOfStay,
    MechVentHours,
    Revenue,
    WeightedSeparation,
}

// TODO: Pretty sure rounding mode can be used with all quantities, but check this
pub struct BuiltQuantity {
    pub quantity: Quantity,
    pub rounding_mode: RoundingMode,
}

pub enum DurationFallback {
    BuiltService,
    Encounter,
    Service,
}

pub struct FileJoin {
    join_column: String,
    file: String,
    file_join_column: String,
}

pub struct Definition {
    pub name: String,
    pub components: Vec<Component>,
    pub filters: Vec<Filter>,
    pub source: PathBuf,
    pub frequency: Frequency,
    pub quantity: BuiltQuantity,
    pub duration_fallback: DurationFallback,
    // TODO: Need a way to define joins between different files. Or put that at some higher level might be better
    // At the very least we still need a source/file type, and there should be one file supplied for each type
}

pub fn read_definitions<R>(
    definitions: &mut csv::Reader<R>,
) -> anyhow::Result<HashMap<String, Definition>>
where
    R: Read,
{
    let mut all_definitions: HashMap<String, Definition> = HashMap::new();
    for record in definitions.deserialize::<HashMap<String, String>>() {
        let record = record?;
        // Get the type, then switch based on that, as that's how we determine whether we've got a definition/filter/component/constraint (definition should always come first)
        let record_type = record.get("Type").unwrap();
        match record_type.as_str() {
            "Definition" => {
                let quantity_type = record.get("BuiltQuantity").unwrap();
                let rounding_mode =
                    RoundingMode::try_from(record.get("BuiltQuantityRounding").unwrap())?;
                let quantity: anyhow::Result<Quantity> = match quantity_type.as_str() {
                    "S" => Ok(Quantity::SourceQuantity),
                    "C" => {
                        let constant_value =
                            record.get("BuiltQuantityConstant").unwrap().parse()?;
                        Ok(Quantity::Constant(constant_value))
                    }
                    "H" => Ok(Quantity::Hours),
                    "D" => Ok(Quantity::Days),
                    // Above 3 are all that's needed for now
                    invalid_quantity => {
                        anyhow::bail!("Invalid quantity found: {}", invalid_quantity)
                    }
                };
                let quantity = quantity?;
                let built_quantity = BuiltQuantity {
                    quantity,
                    rounding_mode,
                };
                let build_from = SourceType::try_from(record.get("BuildFrom").unwrap())?;
                let frequency = Frequency::try_from(record.get("Frequency").unwrap())?;
                all_definitions.insert(
                    record.get("Name").unwrap().to_owned(),
                    Definition {
                        name: record.get("Name").unwrap().to_owned(),
                        components: vec![],
                        filters: vec![],
                        source: build_from.to_file_path().into(),
                        frequency,
                        quantity: built_quantity,
                        // TODO: Figure this out
                        // Not even in use, can ignore, or will BuiltService always be the default?
                        duration_fallback: DurationFallback::BuiltService,
                    },
                );
            }
            "Filter" => {
                let new_filter = {
                    let source_type =
                        SourceType::try_from(record.get("FilterSourceType").unwrap())?;
                    Filter {
                        // TODO: This looks wrong
                        filter_type: if record.get("FilterNotIn").unwrap() != "" {
                            FilterType::Equal
                        } else {
                            FilterType::NotEqualTo
                        },
                        // TODO: extra/classification types need to append Extra:/Classification: to the start of the field
                        field: record.get("FilterField").unwrap().clone(),
                        value: record.get("FilterValue").unwrap().clone(),
                        file: source_type.to_file_path().into(),
                    }
                };
                let all_filters = &mut all_definitions
                    .get_mut(record.get("Name").unwrap())
                    .unwrap()
                    .filters;
                all_filters.push(new_filter);
            }
            "Component" => {
                let component = match record.get("ComponentSource").unwrap().as_str() {
                    "C" => {
                        Component::Constant(record.get("ComponentValueOrField").unwrap().to_owned())
                    }
                    "MC" => {
                        Component::Constant(record.get("ComponentValueOrField").unwrap().to_owned())
                    }
                    source => {
                        let component_source_type = SourceType::from_component_source_type(source)?;
                        Component::Field(
                            component_source_type.to_file_path().into(),
                            record.get("ComponentValueOrField").unwrap().to_owned(),
                        )
                    }
                };
                let all_components = &mut all_definitions
                    .get_mut(record.get("Name").unwrap())
                    .unwrap()
                    .components;
                all_components.push(component);
            }
            "Constraint" => {
                let constraint = {
                    let filter_type = FilterType::try_from(record.get("FilterType").unwrap())?;
                    let source_type =
                        SourceType::try_from(record.get("ConstraintSourceType").unwrap())?;
                    Filter {
                        field: record.get("ConstraintColumn").unwrap().to_owned(),
                        filter_type,
                        value: record.get("ConstraintValue").unwrap().to_owned(),
                        file: source_type.to_file_path().into(),
                    }
                };
                let all_filters = &mut all_definitions
                    .get_mut(record.get("Name").unwrap())
                    .unwrap()
                    .filters;
                all_filters.push(constraint);
            }
            unknown => println!("Invalid type found: {}", unknown),
        }
    }
    Ok(all_definitions)
}

#[cfg(test)]
mod tests {
    use super::read_definitions;

    #[test]
    fn test_read_definitions() {
        let definitions = read_definitions(
            &mut csv::Reader::from_path(
                "testing/input/create_products/service_builder_definitions.csv",
            )
            .unwrap(),
        );
        if let Err(error) = &definitions {
            println!("{}", error)
        }
        assert!(definitions.is_ok())
    }
}
