use std::collections::BTreeSet;
use std::fs;
use std::path::Path;

use rrt_model::finance::{FinanceOutcome, FinanceSnapshot};
use serde_json::Value;

use crate::app::reports::state::{FinanceDiffEntry, FinanceDiffReport};

pub(crate) fn run_finance_eval(snapshot_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let outcome = load_finance_outcome(snapshot_path)?;
    println!("{}", serde_json::to_string_pretty(&outcome)?);
    Ok(())
}

pub(crate) fn run_finance_diff(
    left_path: &Path,
    right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
    let left = load_finance_outcome(left_path)?;
    let right = load_finance_outcome(right_path)?;
    let report = diff_finance_outcomes(&left, &right)?;
    println!("{}", serde_json::to_string_pretty(&report)?);
    Ok(())
}

pub(crate) fn load_finance_outcome(
    path: &Path,
) -> Result<FinanceOutcome, Box<dyn std::error::Error>> {
    let text = fs::read_to_string(path)?;
    if let Ok(snapshot) = serde_json::from_str::<FinanceSnapshot>(&text) {
        return Ok(snapshot.evaluate());
    }
    if let Ok(outcome) = serde_json::from_str::<FinanceOutcome>(&text) {
        return Ok(outcome);
    }

    Err(format!(
        "unable to parse {} as FinanceSnapshot or FinanceOutcome",
        path.display()
    )
    .into())
}

pub(crate) fn diff_finance_outcomes(
    left: &FinanceOutcome,
    right: &FinanceOutcome,
) -> Result<FinanceDiffReport, Box<dyn std::error::Error>> {
    let left_value = serde_json::to_value(left)?;
    let right_value = serde_json::to_value(right)?;
    let mut differences = Vec::new();
    collect_json_differences("$", &left_value, &right_value, &mut differences);

    Ok(FinanceDiffReport {
        matches: differences.is_empty(),
        difference_count: differences.len(),
        differences,
    })
}

fn collect_json_differences(
    path: &str,
    left: &Value,
    right: &Value,
    differences: &mut Vec<FinanceDiffEntry>,
) {
    match (left, right) {
        (Value::Object(left_map), Value::Object(right_map)) => {
            let mut keys = BTreeSet::new();
            keys.extend(left_map.keys().cloned());
            keys.extend(right_map.keys().cloned());

            for key in keys {
                let next_path = format!("{path}.{key}");
                match (left_map.get(&key), right_map.get(&key)) {
                    (Some(left_value), Some(right_value)) => {
                        collect_json_differences(&next_path, left_value, right_value, differences);
                    }
                    (left_value, right_value) => differences.push(FinanceDiffEntry {
                        path: next_path,
                        left: left_value.cloned().unwrap_or(Value::Null),
                        right: right_value.cloned().unwrap_or(Value::Null),
                    }),
                }
            }
        }
        (Value::Array(left_items), Value::Array(right_items)) => {
            let max_len = left_items.len().max(right_items.len());
            for index in 0..max_len {
                let next_path = format!("{path}[{index}]");
                match (left_items.get(index), right_items.get(index)) {
                    (Some(left_value), Some(right_value)) => {
                        collect_json_differences(&next_path, left_value, right_value, differences);
                    }
                    (left_value, right_value) => differences.push(FinanceDiffEntry {
                        path: next_path,
                        left: left_value.cloned().unwrap_or(Value::Null),
                        right: right_value.cloned().unwrap_or(Value::Null),
                    }),
                }
            }
        }
        _ if left != right => differences.push(FinanceDiffEntry {
            path: path.to_string(),
            left: left.clone(),
            right: right.clone(),
        }),
        _ => {}
    }
}
