use std::{
    collections::{HashMap, HashSet},
    io::Read,
};

use csv::Reader;
use itertools::Itertools;
use nalgebra::{DMatrix, Dyn, LU};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};

use crate::{io::RecordSerializer, CsvAccount, CsvCost};

#[derive(Debug, PartialEq, Eq)]
pub enum DepartmentType {
    Operating,
    Overhead,
}

impl DepartmentType {
    pub fn from(s: &str) -> DepartmentType {
        if s == "P" {
            DepartmentType::Operating
        } else {
            DepartmentType::Overhead
        }
    }
}

#[derive(Deserialize)]
pub struct CsvAllocationStatistic {
    #[serde(rename = "Name")]
    name: String,
    #[serde(rename = "AccountType")]
    account_type: String,
    #[serde(rename = "AccountRanges")]
    account_ranges: String,
}

pub struct AllocationStatisticAccountRange {
    start: usize,
    end: usize,
}

// Note: remember these are overhead departments only when calculating the lu decomposition or pseudoinverse, and for each department,
// you either need -1 or rest negative for a row to subtract the initial amounts so we end up effectively 0 (simultaneous equations end
// up with negative there so yes this is expected)
pub struct OverheadAllocationRule {
    from_overhead_department: String,
    to_department: String,
    percent: f64,
    to_department_type: DepartmentType,
}

#[derive(Debug, PartialEq)]
pub struct TotalDepartmentCost {
    department: String,
    value: f64,
}

#[derive(Debug, PartialEq)]
pub struct AccountCost {
    account: String,
    summed_department_costs: Vec<TotalDepartmentCost>,
}

#[derive(Debug, Serialize, Deserialize)]
struct MovedAmount {
    account: String,
    cost_centre: String,
    value: f64,
    from_cost_centre: String,
}

pub trait ReciprocalAllocationSolver {
    fn solve(&self, costs: &DMatrix<f64>) -> DMatrix<f64>;
}

impl ReciprocalAllocationSolver for LU<f64, Dyn, Dyn> {
    fn solve(&self, costs: &DMatrix<f64>) -> DMatrix<f64> {
        self.solve(costs).unwrap()
    }
}

impl ReciprocalAllocationSolver for DMatrix<f64> {
    fn solve(&self, costs: &DMatrix<f64>) -> DMatrix<f64> {
        self * costs
    }
}

pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCentre>(
    lines: &mut csv::Reader<Lines>,
    accounts: &mut csv::Reader<Account>,
    allocation_statistics: &mut csv::Reader<AllocationStatistic>,
    areas: &mut csv::Reader<Area>,
    cost_centres: &mut csv::Reader<CostCentre>,
    output: &mut impl RecordSerializer,
    use_numeric_accounts: bool,
    exclude_negative_allocation_statistics: bool,
    any_limit_criteria: bool,
    account_type: String,
    show_from: bool,
    zero_threshold: f64,
) -> anyhow::Result<()>
where
    Lines: Read,
    Account: Read,
    AllocationStatistic: Read,
    Area: Read,
    CostCentre: Read,
{
    let lines = lines
        .deserialize()
        .collect::<Result<Vec<CsvCost>, csv::Error>>()?;

    let all_accounts_sorted: Vec<String> =
        get_accounts_sorted(use_numeric_accounts, &account_type, accounts);

    let allocation_statistics = allocation_statistics
        .deserialize::<CsvAllocationStatistic>()
        .filter(|allocation_statistic| {
            allocation_statistic.as_ref().unwrap().account_type == account_type
        })
        .collect::<Result<Vec<CsvAllocationStatistic>, csv::Error>>()?;

    let split_allocation_ranges: Vec<(String, Vec<AllocationStatisticAccountRange>)> =
        allocation_statistics
            .iter()
            .map(|allocation_statistic| {
                (
                    allocation_statistic.name.clone(),
                    split_allocation_statistic_range(allocation_statistic, &all_accounts_sorted),
                )
            })
            .collect();

    // For each allocation statistic, sum the cost centres across accounts in the allocaiton statistic range
    // value is (cc, allocation_statistic, total)
    let mut totals: HashMap<(String, String), f64> = HashMap::new();
    for line in lines.iter() {
        let line_index = all_accounts_sorted
            .iter()
            .position(|account| account == &line.account);
        // Skip account as it doesn't exist (likely due to wrong account type)
        if line_index.is_none() {
            continue;
        }
        let line_index = line_index.unwrap();
        // Find the allocation statistics this line is in
        for (allocation_statistic, range) in split_allocation_ranges.iter() {
            if range
                .iter()
                .find(|range| line_index >= range.start && line_index <= range.end)
                .is_some()
            {
                *totals
                    .entry((line.department.clone(), allocation_statistic.clone()))
                    .or_insert(0.) += line.value;
            }
        }
    }

    // If ignore negative is used, then set values < 0 to 0
    if exclude_negative_allocation_statistics {
        for ((_, _), total) in totals.iter_mut() {
            if *total < 0. {
                *total = 0.;
            }
        }
    }

    // Group ccs by area
    let mut area_ccs: HashMap<String, Vec<String>> = HashMap::new();
    let headers = cost_centres.headers()?;
    // Group ccs by rollup, and group rollups into their slot
    let mut rollups: HashMap<String, HashMap<String, Vec<String>>> = headers
        .iter()
        .filter(|name| name.to_lowercase().starts_with("rollupslot:"))
        .map(|rollupslot| (rollupslot.to_owned(), HashMap::new()))
        .collect();
    for cost_centre in cost_centres.deserialize() {
        let cost_centre: HashMap<String, String> = cost_centre?;
        let name = cost_centre.get("Code").unwrap();
        let area = cost_centre.get("Area").unwrap();
        if area.is_empty() {
            continue;
        }
        area_ccs
            .entry(area.clone())
            .or_insert(Vec::new())
            .push(name.clone());

        for rollupslot in rollups.iter_mut() {
            let rollup_name = cost_centre.get(rollupslot.0).unwrap();
            rollupslot
                .1
                .entry(rollup_name.clone())
                .or_insert(Vec::new())
                .push(name.clone());
        }
    }

    let headers = areas.headers()?;
    let limit_tos: Vec<String> = headers
        .iter()
        .filter(|header| header.to_lowercase().starts_with("limitto:"))
        .map(|header| header["limitto:".len()..].to_owned())
        .collect();
    let mut overhead_other_total: Vec<(String, String, f64)> = Vec::new();

    // Save overhead ccs, so we later know whether a to cc is overhead or operating
    let mut overhead_ccs: HashSet<String> = HashSet::new();
    // For each overhead area, get the cost centres in the area (overhead cost centres), and get all cost centres
    // that fit the limit to criteria for the area (skip any cases of overhead cc = other cc).
    // Then get the totals for the other ccs, by looking in the flat_department_costs, where the
    // allocation statistic matches the allocation statistic for this area
    for area in areas.deserialize() {
        let area: HashMap<String, String> = area?;
        // Check for limitTos, should probably somehow build out the list of allocation rules from this point.
        let area_name = area.get("Name").unwrap();
        let allocation_statistic = area.get("AllocationStatistic").unwrap();
        let department_type: DepartmentType = DepartmentType::from(area.get("Type").unwrap());

        // TODO: I don't think this check is enough. I think we actually need to check the allocation statistic too,
        // and whether it holds the account types is cares about. If it's the wrong account type, then we should discard
        // all ccs in this area.
        // This is because I found some lines where the account is the right type (e.g. expense), however there was
        // an overhead cc in that account, which had an area that had an allocation statistic that only applied
        // to unit accounts, so that cost centre probably shouldn't have been included at all. I don't think it
        // messed up the calculation in this instance, but there may be other cases that do get tripped up by this
        if department_type == DepartmentType::Overhead {
            let current_area_ccs = area_ccs.get(area_name);

            if current_area_ccs.is_none() {
                continue;
            }
            let current_area_ccs = current_area_ccs.unwrap().clone();

            for cc in current_area_ccs {
                overhead_ccs.insert(cc);
            }
            let overhead_ccs = area_ccs.get(area_name).unwrap();
            // TODO: This depends on the area limit criteria. For now just doing any limit criteria
            let mut limited_ccs: Vec<String> = Vec::new();
            for limit_to in limit_tos.iter() {
                // TODO: It is technically possible to have more than one limit to (I think?) for a slot, so consider eventually splitting this and doing a foreach
                //      Also there's an exclude criteria that needs to be considered, which can exclude a rollup that would normally get included
                let limit_value = area.get(&(format!("LimitTo:{}", limit_to))).unwrap();
                if limit_value.is_empty() {
                    continue;
                }
                if limit_to.eq_ignore_ascii_case("costcentre") {
                    limited_ccs.push(limit_value.clone());
                } else {
                    let mut found_ccs = rollups
                        .get(&(format!("RollupSlot:{}", limit_to)))
                        .map(|rollups| rollups.get(limit_value))
                        .flatten()
                        .unwrap()
                        .clone();
                    limited_ccs.append(&mut found_ccs);
                }
            }
            if limited_ccs.is_empty() {
                let mut other_ccs: Vec<String> = area_ccs
                    .values()
                    .flat_map(|ccs| ccs.iter().map(|cc| cc.clone()))
                    .collect();
                // No limit criteria, use all ccs
                limited_ccs.append(&mut other_ccs);
            }
            let mut totals: Vec<(String, String, f64)> = overhead_ccs
                .par_iter()
                .flat_map(|overhead_cc| {
                    limited_ccs
                        .iter()
                        .map(|other_cc| (other_cc.clone(), allocation_statistic.clone()))
                        .filter_map(|(other_cc, allocation_statistic)| {
                            let combined_stat = (other_cc, allocation_statistic);
                            if !totals.contains_key(&combined_stat) {
                                None
                            } else {
                                Some((
                                    overhead_cc.clone(),
                                    combined_stat.0.clone(),
                                    totals.get(&combined_stat).map(|f| *f).unwrap(),
                                ))
                            }
                        })
                        .filter(|(_, _, value)| *value != 0.)
                        .filter(|(from_cc, to_cc, _)| from_cc != to_cc)
                        .collect_vec()
                })
                .collect();
            overhead_other_total.append(&mut totals);
        }
    }
    // TODO: This seems to do nothing even in a complex setting where I'd expect it to do something, can probably be removed
    let error_amounts: Vec<usize> = overhead_other_total
        .iter()
        .filter(|(_, _, value)| *value < zero_threshold && *value > -1. * zero_threshold)
        .enumerate()
        .map(|(index, _)| index)
        .collect();
    for index in error_amounts.iter() {
        let (overhead_cc, _, value) = overhead_other_total.remove(*index);
        let non_error_match = overhead_other_total
            .iter_mut()
            .filter(|next| {
                next.0 == *overhead_cc && (next.2 > zero_threshold || next.2 < -1. * zero_threshold)
            })
            .next();
        if let Some((_, _, match_value)) = non_error_match {
            *match_value = *match_value + value;
        }
    }

    // overhead department -> total (summed limit to costs)
    let mut overhead_cc_totals: HashMap<String, f64> = HashMap::new();
    // Using rayon and doing another pass later proves to be
    for (overhead_cc, _, value) in overhead_other_total.iter() {
        *overhead_cc_totals.entry(overhead_cc.clone()).or_insert(0.) += value;
    }

    // Export initial totals for operating departments
    for line in lines.iter() {
        // TODO: Should we still output accounts that aren't in the accounts file anyway?
        if all_accounts_sorted
            .iter()
            .find(|account| **account == line.account)
            .is_some()
            && !overhead_ccs.contains(&line.department)
            && (show_from
            // When we write out the final amounts rather than changes,
            // ensure we still output departments that won't be receiving
            // any costs.
            || !overhead_other_total
            .iter()
            .any(|(_, to_department, _)| *to_department == line.department))
        {
            if show_from {
                output.serialize(MovedAmount {
                    account: line.account.clone(),
                    cost_centre: line.department.clone(),
                    value: line.value,
                    from_cost_centre: line.department.clone(),
                })?;
            } else {
                output.serialize(CsvCost {
                    account: line.account.clone(),
                    department: line.department.clone(),
                    value: line.value,
                })?;
            }
        }
    }

    // Finally, for each cc match total produced previously, sum the overhead cc where overhead cc appears in other cc, then
    // divide the other cc by this summed amount (thus getting the relative cost)
    // At this point we convert to our format that's actually used in overhead allocation
    let allocation_rules: Vec<OverheadAllocationRule> = overhead_other_total
        .iter()
        .map(
            |(from_overhead_department, to_department, percent)| OverheadAllocationRule {
                from_overhead_department: from_overhead_department.clone(),
                to_department: to_department.clone(),
                percent: percent / overhead_cc_totals.get(from_overhead_department).unwrap(),
                to_department_type: if overhead_ccs.contains(to_department) {
                    DepartmentType::Overhead
                } else {
                    DepartmentType::Operating
                },
            },
        )
        .collect();

    // TODO: THIS CAN BE WRONG WHEN USING A FILE WITH ALL PASSES, for now ensure the input movement
    // file only contains the final pass/outputs.
    let mut initial_account_costs: HashMap<String, Vec<TotalDepartmentCost>> = HashMap::new();
    for line in lines {
        // Only include accounts we've already filtered on (i.e. by account type)
        if all_accounts_sorted
            .iter()
            .find(|account| **account == line.account)
            .is_some()
        {
            initial_account_costs
                .entry(line.account)
                .or_insert(Vec::new())
                .push(TotalDepartmentCost {
                    department: line.department,
                    value: line.value,
                });
        }
    }

    // TODO: (Consider) We could actually cheat here and not use this matrix implementation at all (and thus be more
    // memory efficient, but maybe slower)
    // Since we know each operating department in an account will get the proportion of the total overhead amount relative
    // according to its operating amount from the total amount of the overhead departments, we can just directly calculate
    // these totals and do some simple multiplications (it does get trickier with multiple accounts, as the cost drivers
    // are consistent across all accounts, but depend on the allocation statistic to determine which lines to pick from).
    let results = reciprocal_allocation_impl(
        allocation_rules,
        initial_account_costs
            .into_iter()
            .map(|(account, total_cost)| AccountCost {
                account,
                summed_department_costs: total_cost,
            })
            .collect(),
        output,
        show_from,
        zero_threshold,
    )?;

    if !show_from {
        for cost in results {
            for department in cost.summed_department_costs {
                // Any consumers should assume missing cc/account value was 0 (we already ignore overhead, as they all 0 out)
                if department.value != 0_f64 {
                    output.serialize(CsvCost {
                        account: cost.account.clone(),
                        department: department.department,
                        value: department.value,
                    })?;
                }
            }
        }
    }
    Ok(())
}

fn get_accounts_sorted(
    use_numeric_accounts: bool,
    account_type: &String,
    accounts: &mut Reader<impl Read>,
) -> Vec<String> {
    if use_numeric_accounts {
        accounts
            .deserialize::<CsvAccount>()
            .filter(|account| {
                account.is_ok() && account.as_ref().unwrap().account_type == *account_type
            })
            .map(|line| line.unwrap().code.clone().parse::<i32>().unwrap())
            .unique()
            .sorted()
            .map(|account| account.to_string())
            .collect()
    } else {
        accounts
            .deserialize::<CsvAccount>()
            .filter(|account| {
                account.is_ok() && account.as_ref().unwrap().account_type == *account_type
            })
            .map(|line| line.unwrap().code.clone())
            .unique()
            .sorted()
            .collect()
    }
}

fn split_allocation_statistic_range(
    allocation_statistic: &CsvAllocationStatistic,
    accounts_sorted: &Vec<String>,
) -> Vec<AllocationStatisticAccountRange> {
    // TODO: This split needs to be more comprehensive so that we don't split between quotes, so use a regex
    let split = allocation_statistic.account_ranges.split(";");
    split
        .map(|split| {
            let range_split = split.split('-').collect::<Vec<_>>();
            let start = remove_quote_and_padding(range_split[0]);
            let start_index = accounts_sorted
                .iter()
                .position(|account| *account == start)
                .unwrap();
            if range_split.len() == 1 {
                AllocationStatisticAccountRange {
                    start: start_index,
                    end: start_index,
                }
            } else {
                let end = remove_quote_and_padding(range_split[1]);
                let end_index = accounts_sorted
                    .iter()
                    .position(|account| *account == end)
                    .unwrap();
                AllocationStatisticAccountRange {
                    start: start_index,
                    end: end_index,
                }
            }
        })
        .collect()
}

// Removes quotes and padding from accounts int he allocation statistic account range.
// e.g. "'100' " becomes "100"
fn remove_quote_and_padding(s: &str) -> String {
    if s.contains('\'') {
        s.trim()[1..s.trim().len() - 1].to_owned()
    } else {
        s.trim().to_owned()
    }
}

// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
// to functional departments. Basically just a matrix solve, uses regression (moore-penrose pseudoinverse) when
// matrix is singular
fn reciprocal_allocation_impl(
    allocations: Vec<OverheadAllocationRule>,
    account_costs: Vec<AccountCost>,
    movement_writer: &mut impl RecordSerializer,
    show_from: bool,
    zero_threshold: f64,
) -> anyhow::Result<Vec<AccountCost>> {
    let overhead_department_mappings = get_rules_indexes(&allocations, DepartmentType::Overhead);

    let mut slice_allocations =
        vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()];

    for allocation in allocations
        .iter()
        .filter(|allocation| allocation.to_department_type == DepartmentType::Overhead)
    {
        let from_index = overhead_department_mappings
            .get(&allocation.from_overhead_department)
            .unwrap();
        let to_index = overhead_department_mappings
            .get(&allocation.to_department)
            .unwrap();
        slice_allocations[from_index * overhead_department_mappings.len() + to_index] =
            allocation.percent * -1.;
    }

    let mut mat: DMatrix<f64> = DMatrix::from_vec(
        overhead_department_mappings.len(),
        overhead_department_mappings.len(),
        slice_allocations,
    );
    mat.fill_diagonal(1.);

    if mat.determinant() == 0. {
        let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001);
        do_solve_reciprocal(
            pseudo_inverse.unwrap(),
            account_costs,
            overhead_department_mappings,
            allocations,
            movement_writer,
            show_from,
            zero_threshold,
        )
    } else {
        do_solve_reciprocal(
            mat.lu(),
            account_costs,
            overhead_department_mappings,
            allocations,
            movement_writer,
            show_from,
            zero_threshold,
        )
    }
}

fn get_rules_indexes(
    allocations: &Vec<OverheadAllocationRule>,
    department_type: DepartmentType,
) -> HashMap<String, usize> {
    allocations
        .iter()
        .filter(|allocation| allocation.to_department_type == department_type)
        .flat_map(|department| {
            if department.to_department_type == DepartmentType::Operating {
                vec![department.to_department.clone()]
            } else {
                vec![
                    department.from_overhead_department.clone(),
                    department.to_department.clone(),
                ]
            }
        })
        .unique()
        .enumerate()
        .map(|(index, department)| (department, index))
        .collect()
}

fn do_solve_reciprocal<T: ReciprocalAllocationSolver + Sync + Send>(
    solver: T,
    account_costs: Vec<AccountCost>,
    overhead_department_mappings: HashMap<String, usize>,
    allocations: Vec<OverheadAllocationRule>,
    temp_writer: &mut impl RecordSerializer,
    show_from: bool,
    zero_threshold: f64,
) -> anyhow::Result<Vec<AccountCost>> {
    let operating_department_mappings = get_rules_indexes(&allocations, DepartmentType::Operating);
    let mut operating_overhead_mappings =
        vec![0.; overhead_department_mappings.len() * operating_department_mappings.len()];
    for rule in allocations {
        if rule.to_department_type == DepartmentType::Operating {
            let from_index = *overhead_department_mappings
                .get(&rule.from_overhead_department)
                .unwrap();
            let to_index = *operating_department_mappings
                .get(&rule.to_department)
                .unwrap();
            operating_overhead_mappings
                [from_index * operating_department_mappings.len() + to_index] = rule.percent;
        }
    }
    let operating_overhead_mappings_mat: DMatrix<f64> = DMatrix::from_vec(
        operating_department_mappings.len(),
        overhead_department_mappings.len(),
        operating_overhead_mappings,
    );
    if show_from {
        solve_reciprocal_with_from(
            solver,
            account_costs,
            overhead_department_mappings,
            operating_department_mappings,
            operating_overhead_mappings_mat,
            temp_writer,
            zero_threshold,
        )?;
        Ok(vec![])
    } else {
        Ok(solve_reciprocal_no_from(
            solver,
            account_costs,
            overhead_department_mappings,
            operating_department_mappings,
            operating_overhead_mappings_mat,
        ))
    }
}

fn solve_reciprocal_no_from(
    solver: impl ReciprocalAllocationSolver + Sync + Send,
    account_costs: Vec<AccountCost>,
    overhead_department_mappings: HashMap<String, usize>,
    operating_department_mappings: HashMap<String, usize>,
    operating_overhead_mappings_mat: DMatrix<f64>,
) -> Vec<AccountCost> {
    account_costs
        .par_iter()
        // .filter(|cost| cost.account == "A480200")
        .map(|total_costs| {
            // To get the from/to ccs like ppm does, we ignore the initial totals. Then for each overhead cc,
            // we zero out all the calculated overheads except for this cc and do
            // operating_overhead_mappings * calculated_overheads (basically the first part of the normal calculation)

            // TODO: There has to be a cleaner way to do this, perhaps by presorting things?
            let mut overhead_slice_costs = vec![0.; overhead_department_mappings.len()];
            for cost in total_costs.summed_department_costs.iter() {
                if overhead_department_mappings.contains_key(&cost.department) {
                    overhead_slice_costs
                        [*overhead_department_mappings.get(&cost.department).unwrap()] = cost.value
                }
            }
            let overhead_costs_vec: DMatrix<f64> = DMatrix::from_row_slice(
                overhead_department_mappings.len(),
                1,
                &overhead_slice_costs,
            );
            let calculated_overheads = solver.solve(&overhead_costs_vec);

            let mut operating_slice_costs = vec![0.; operating_department_mappings.len()];
            for cost in &total_costs.summed_department_costs {
                if operating_department_mappings.contains_key(&cost.department) {
                    let elem = &mut operating_slice_costs
                        [*operating_department_mappings.get(&cost.department).unwrap()];
                    *elem = cost.value;
                }
            }
            let operating_costs_vec: DMatrix<f64> = DMatrix::from_row_slice(
                operating_department_mappings.len(),
                1,
                &operating_slice_costs,
            );

            // Borrow so we don't move between loops
            let operating_overhead_mappings = &operating_overhead_mappings_mat;
            let calculated_overheads = &calculated_overheads;

            // Calculation: operating_overhead_usage . calculated_overheads + initial_totals
            // Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the
            // solved overheads usages after taking into account usage between departments, and initial_totals is the initial values
            // for the operating departments.
            let calculated =
                operating_overhead_mappings * calculated_overheads + operating_costs_vec;

            let converted_result: Vec<TotalDepartmentCost> = operating_department_mappings
                .iter()
                .map(|(department, index)| TotalDepartmentCost {
                    department: department.clone(),
                    value: *calculated.get(*index).unwrap(),
                })
                .collect();
            // Redistribute floating point errors (only for ccs we actually allocated from/to)
            // Considered removing this since redistribution should be done in cost driver calculations, however since that usually
            // does nothing, we may as well keep this just in case.
            // TODO: Not sure we actually need this, would probably be better to have a better storage format than
            // csv/string conversions
            // let initial_cost: f64 = total_costs
            //     .summed_department_costs
            //     .iter()
            //     .filter(|cost| {
            //         operating_department_mappings.contains_key(&cost.department)
            //             || overhead_department_mappings.contains_key(&cost.department)
            //     })
            //     .map(|cost| cost.value)
            //     .sum();
            // let new_cost: f64 = converted_result.iter().map(|cost| cost.value).sum();
            // let diff = initial_cost - new_cost;
            AccountCost {
                account: total_costs.account.clone(),
                summed_department_costs: converted_result
                    .into_iter()
                    .map(|cost| TotalDepartmentCost {
                        department: cost.department,
                        value: cost.value, // + if new_cost == 0_f64 || diff == 0_f64 {
                                           //     0_f64
                                           // } else {
                                           //     cost.value / new_cost * diff
                                           // },
                    })
                    .filter(|cost| cost.value != 0_f64)
                    .collect(),
            }
        })
        .collect()
}

fn solve_reciprocal_with_from<T: ReciprocalAllocationSolver + Sync + Send>(
    solver: T,
    total_costs: Vec<AccountCost>,
    overhead_department_mappings: HashMap<String, usize>,
    operating_department_mappings: HashMap<String, usize>,
    operating_overhead_mappings: DMatrix<f64>,
    temp_writer: &mut impl RecordSerializer,
    zero_threshold: f64,
) -> anyhow::Result<()> {
    for total_costs in total_costs {
        let moved_amounts: Vec<MovedAmount> = total_costs
            .summed_department_costs
            .par_iter()
            .filter(|overhead_department_cost| {
                overhead_department_mappings.contains_key(&overhead_department_cost.department)
                    && overhead_department_cost.value != 0_f64
            })
            .flat_map(|overhead_department_cost| {
                let mut overhead_slice_costs = vec![0.; overhead_department_mappings.len()];
                overhead_slice_costs[*overhead_department_mappings
                    .get(&overhead_department_cost.department)
                    .unwrap()] = overhead_department_cost.value;

                let overhead_costs_vec: DMatrix<f64> = DMatrix::from_row_slice(
                    overhead_department_mappings.len(),
                    1,
                    &overhead_slice_costs,
                );
                let calculated_overheads = solver.solve(&overhead_costs_vec);
                let calculated = &operating_overhead_mappings * calculated_overheads;
                operating_department_mappings
                    .iter()
                    .map(|(department, index)| (department, *calculated.get(*index).unwrap()))
                    .map(|(department, value)| MovedAmount {
                        account: total_costs.account.clone(),
                        cost_centre: department.clone(),
                        value,
                        from_cost_centre: overhead_department_cost.department.clone(),
                    })
                    .filter(|cost| cost.value != 0_f64)
                    .collect::<Vec<MovedAmount>>()
            })
            .collect();

        for moved_amount in moved_amounts {
            temp_writer.serialize(moved_amount)?;
        }
    }
    temp_writer.flush()?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use crate::reciprocal_allocation;
    use crate::AccountCost;
    use crate::DepartmentType;
    use crate::OverheadAllocationRule;
    use crate::TotalDepartmentCost;

    use super::reciprocal_allocation_impl;

    #[test]
    fn test_basic() {
        let allocation_rules = vec![
            OverheadAllocationRule {
                from_overhead_department: "Y".to_owned(),
                to_department: "Z".to_owned(),
                percent: 0.2,
                to_department_type: DepartmentType::Overhead,
            },
            OverheadAllocationRule {
                from_overhead_department: "Z".to_owned(),
                to_department: "Y".to_owned(),
                percent: 0.3,
                to_department_type: DepartmentType::Overhead,
            },
            OverheadAllocationRule {
                from_overhead_department: "Y".to_owned(),
                to_department: "A".to_owned(),
                percent: 0.4,
                to_department_type: DepartmentType::Operating,
            },
            OverheadAllocationRule {
                from_overhead_department: "Y".to_owned(),
                to_department: "B".to_owned(),
                percent: 0.4,
                to_department_type: DepartmentType::Operating,
            },
            OverheadAllocationRule {
                from_overhead_department: "Z".to_owned(),
                to_department: "A".to_owned(),
                percent: 0.2,
                to_department_type: DepartmentType::Operating,
            },
            OverheadAllocationRule {
                from_overhead_department: "Z".to_owned(),
                to_department: "B".to_owned(),
                percent: 0.5,
                to_department_type: DepartmentType::Operating,
            },
        ];
        let initial_totals = vec![AccountCost {
            account: "Default".to_owned(),
            summed_department_costs: vec![
                TotalDepartmentCost {
                    department: "Y".to_owned(),
                    value: 7260.,
                },
                TotalDepartmentCost {
                    department: "Z".to_owned(),
                    value: 4000.,
                },
                TotalDepartmentCost {
                    department: "A".to_owned(),
                    value: 12000.,
                },
                TotalDepartmentCost {
                    department: "B".to_owned(),
                    value: 16000.,
                },
            ],
        }];
        let expected_final_allocations = vec![AccountCost {
            account: "Default".to_owned(),
            summed_department_costs: vec![
                TotalDepartmentCost {
                    department: "A".to_owned(),
                    value: 16760.,
                },
                TotalDepartmentCost {
                    department: "B".to_owned(),
                    value: 22500.,
                },
            ],
        }];

        let mut movement_writer = csv::Writer::from_path("test_output.csv").unwrap();
        let result = reciprocal_allocation_impl(
            allocation_rules,
            initial_totals,
            &mut movement_writer,
            false,
            0.00001,
        )
        .unwrap();
        assert_eq!(expected_final_allocations.len(), result.len());
        let expected_account = &expected_final_allocations[0];
        let final_account = &result[0];
        assert_eq!(expected_account.account, final_account.account);
        let expected_department_a = &expected_account.summed_department_costs[0];
        let expected_department_b = &expected_account.summed_department_costs[1];
        for final_department in &final_account.summed_department_costs {
            if final_department.department == expected_department_a.department {
                assert_eq!(*expected_department_a, *final_department);
            } else if final_department.department == expected_department_b.department {
                assert_eq!(*expected_department_b, *final_department);
            } else {
                panic!("Unknown department found!");
            }
        }
    }

    #[test]
    fn test_real() {
        let result = reciprocal_allocation(
            &mut csv::Reader::from_path("testing/input/moved_lines.csv").unwrap(),
            &mut csv::Reader::from_path("testing/input/account.csv").unwrap(),
            &mut csv::Reader::from_path("testing/input/allocstat.csv").unwrap(),
            &mut csv::Reader::from_path("testing/input/area.csv").unwrap(),
            &mut csv::Reader::from_path("testing/input/costcentre.csv").unwrap(),
            &mut csv::Writer::from_path("testing/output/output_alloc_stat.csv").unwrap(),
            false,
            false,
            true,
            "E".to_owned(),
            true,
            0.001,
        );
        assert!(result.is_ok())
    }
}
