use std::{
    collections::{HashMap, HashSet},
    path::PathBuf,
};

use anyhow::anyhow;
use chrono::NaiveDateTime;
use itertools::Itertools;
// inluding dsl works better for completion with rust analyzer
use polars::lazy::dsl::*;
use polars::prelude::*;
use serde::Serialize;

use super::csv::{read_definitions, Component, Definition};

// TODO: Polars suggests this, but docs suggest it doesn't have very good platform support
//use jemallocator::Jemalloc;
// #[global_allocator]
// static GLOBAL: Jemalloc = Jemalloc;

#[derive(Debug, Serialize, Default)]
struct Product {
    // Parse datetime from string: https://rust-lang-nursery.github.io/rust-cookbook/datetime/parse.html#parse-string-into-datetime-struct
    // TODO: Serialisers.
    start_date_time: NaiveDateTime,
    end_date_time: NaiveDateTime,
    encounter_start_date_time: Option<NaiveDateTime>,
    encounter: Option<String>,
    service: Option<String>,
    transfer: Option<String>,
    quantity: Option<f64>,
    duration: Option<f64>,
    actual_charge: Option<f64>,
    standard_cost: Option<f64>,
    // TODO: Enum this?
    day_of_stay: Option<u8>,
    source_allocated_amount: Option<f64>,
}

pub struct InputFile {
    pub file_path: PathBuf,
    pub joins: HashMap<PathBuf, String>,
    // if not specified, then don't allow change in type builds, as there's no way to detect changes over time
    pub date_order_column: Option<String>,
}

pub fn create_products_polars(
    definitions_path: PathBuf,
    inputs: Vec<InputFile>,
    output_path: PathBuf,
) -> anyhow::Result<()> {
    let definitions = read_definitions(&mut csv::Reader::from_path(definitions_path)?)?;
    let definitions = definitions.values().collect_vec();
    for definition in definitions {
        build_polars(definition, &inputs, &output_path)?;
    }
    Ok(())
}

pub fn build_polars(
    definition: &Definition,
    inputs: &Vec<InputFile>,
    output_path: &PathBuf,
) -> anyhow::Result<()> {
    // 1. Apply filters to limit encounters
    let filter = definition
        .filters
        .iter()
        .map(|filter| {
            let col = col(&filter.field);
            match filter.filter_type {
                super::csv::FilterType::Equal => col.eq(lit(filter.value.clone())),
                super::csv::FilterType::GreaterThan => col.gt(lit(filter.value.clone())),
                super::csv::FilterType::GreaterThanOrEqualTo => {
                    col.gt_eq(lit(filter.value.clone()))
                }
                super::csv::FilterType::LessThan => col.lt(lit(filter.value.clone())),
                super::csv::FilterType::LessThanOrEqualTo => col.lt_eq(lit(filter.value.clone())),
                super::csv::FilterType::NotEqualTo => col.neq(lit(filter.value.clone())),
            }
        })
        .reduce(|prev, next| prev.and(next));

    let input_file = inputs
        .iter()
        .find(|input| input.file_path == definition.source)
        .ok_or(anyhow!("Failed to find valid file"))?;
    let mut reader = LazyCsvReader::new(&input_file.file_path)
        .with_has_header(true)
        .finish()?;
    let mut required_files = HashSet::new();
    for component in &definition.components {
        if let Component::Field(file, _) = component {
            required_files.insert(file);
        }
    }
    for filter in &definition.filters {
        required_files.insert(&filter.file);
    }
    for source_type in required_files {
        // TODO: Better error messages
        if source_type != &definition.source {
            let source_file = inputs
                .iter()
                .find(|input| input.file_path == definition.source)
                .ok_or(anyhow!("Input file was not specified for source type"))?;
            // TODO: Alias the joined columns so they don't potentially clash with the current column
            let join_reader = LazyCsvReader::new(source_file.file_path.clone()).finish()?;
            let left_column = input_file
                .joins
                .get(source_type)
                .ok_or(anyhow!("Failed to get left join column"))?;
            let right_column = source_file
                .joins
                .get(&definition.source)
                .ok_or(anyhow!("Failed to get right join column"))?;
            reader = reader.inner_join(join_reader, col(left_column), col(right_column));
        }
    }
    // TODO: Also work out how to expand rows, so that transfers can have stuff like daily or change in x expanded into multiple rows
    // Since it's related to time it is probably related to this: https://docs.pola.rs/user-guide/transformations/time-series/parsing/
    // I'm guessing upsampling is what I'm looking for: https://docs.pola.rs/user-guide/transformations/time-series/resampling/#upsampling-to-a-higher-frequency
    // Can use different strategies to break the time period down, range can be calculated by using start/end datetime
    // Wonder if this can be done more generally (e.g. splitting up based on a number?)
    // Note: This must occur before creating the components, since we'll need to create one for every upsampled row
    let mut built_expression = lit("");
    // Create component columns
    for component in &definition.components {
        match component {
            Component::Constant(constant) => {
                built_expression = built_expression + lit(constant.clone())
            }
            // TODO: Do we need to worry about the source type? Might be clashing column names we need to think about earlier then address here?
            // TODO: What I really want to do is not use source type, instead I want to be referring to a file, which we translate from the sourcetype
            //       to an actual filename. I don't want to be limited by a concept of 'sourcetype' at all, instead the definition should treat everything
            //       the same, and just translate the imported csv format to the necessary files and columns in files that are expected to be input.
            Component::Field(_, column) => built_expression = built_expression + col(column),
        }
    }

    // TODO: Build out the rest of the product definition, depending on the input definition
    let select_columns = [built_expression];

    // Filter and select the required data in one step, so optimiser can speed things up if necessary
    let mut filtered = match filter {
        Some(filter) => reader.filter(filter),
        None => reader,
    }
    .select(select_columns)
    .with_streaming(true)
    .collect()?;

    let mut file = std::fs::File::create(output_path).unwrap();
    CsvWriter::new(&mut file).finish(&mut filtered)?;
    Ok(())
}
