use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

use powerio_dist::{DistNetwork, DistTargetFormat};
use powerio_matrix::{
    Network, TargetFormat, parse_matpower_file, parse_str as parse_transmission_str,
    write_as as write_transmission_as,
};

const REPORT_ENV: &str = "POWERIO_CONVERSION_MATRIX_REPORT";
const DETAILS_ENV: &str = "POWERIO_CONVERSION_MATRIX_DETAILS";
const REPORT_MARKER: &str = "<!-- powerio-conversion-matrix-report -->";
const DETAILS_MARKER: &str = "<!-- powerio-conversion-matrix-details -->";
const MAX_WARNING_DETAILS_PER_PAIR: usize = 6;
const SOURCE_PARSE: &str = "source parse";
const TARGET_WRITE: &str = "target write";
const TARGET_READBACK: &str = "target readback";
static DSS_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);

#[test]
fn conversion_matrix_report_matches_baseline() {
    let report = build_report();
    assert_no_report_path_leaks(&report.markdown);
    assert_no_report_path_leaks(&report.details_markdown);
    write_env_report(REPORT_ENV, &report.markdown);
    write_env_report(DETAILS_ENV, &report.details_markdown);
    assert!(
        report.failures.is_empty(),
        "{}\n\n{}\n\n{}",
        report.failures.join("\n"),
        report.markdown,
        report.details_markdown
    );
}

fn write_env_report(env: &str, markdown: &str) {
    let Ok(path) = std::env::var(env) else {
        return;
    };
    let path = PathBuf::from(path);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).unwrap();
    }
    std::fs::write(&path, markdown).unwrap();
}

fn assert_no_report_path_leaks(markdown: &str) {
    let temp_dir = std::env::temp_dir().to_string_lossy().into_owned();
    assert!(
        !markdown.contains(&temp_dir),
        "report leaked temp dir: {temp_dir}"
    );
    assert!(
        !markdown.contains(env!("CARGO_MANIFEST_DIR")),
        "report leaked cargo manifest dir"
    );
    assert!(
        !markdown.contains("powerio-conversion-matrix/"),
        "report leaked generated temp file path"
    );
}

struct Report {
    markdown: String,
    details_markdown: String,
    failures: Vec<String>,
}

fn build_report() -> Report {
    let transmission = run_transmission_matrix();
    let distribution = run_distribution_matrix();
    let mut failures = Vec::new();
    failures.extend(transmission.failures.clone());
    failures.extend(distribution.failures.clone());

    let mut markdown = String::new();
    writeln!(markdown, "{REPORT_MARKER}").unwrap();
    writeln!(markdown).unwrap();
    writeln!(markdown, "## Conversion Matrix").unwrap();
    writeln!(markdown).unwrap();
    writeln!(markdown, "### Legend").unwrap();
    writeln!(markdown).unwrap();
    writeln!(
        markdown,
        "Cells show `X/Y`: observed warnings / expected warnings. Counts include source parse, target write, and target readback."
    )
    .unwrap();
    writeln!(markdown).unwrap();
    writeln!(
        markdown,
        "- 🟢 `0/0`: no warnings and checked invariants held."
    )
    .unwrap();
    writeln!(
        markdown,
        "- 🟡 `X=Y`: observed warnings match the reviewed expected count, and that count is nonzero."
    )
    .unwrap();
    writeln!(
        markdown,
        "- 🔴 `X!=Y` or invariant failure: behavior changed. If warnings decreased because fidelity improved, update the expected counts in the same PR."
    )
    .unwrap();
    writeln!(
        markdown,
        "- Expected counts are the `*_WARNING_BASELINE` arrays in `powerio-cli/tests/conversion_matrix_report.rs`; accept an intentional change by editing the matching source/target entry in the same PR."
    )
    .unwrap();
    writeln!(markdown).unwrap();
    write_matrix_section(&mut markdown, "Transmission", &transmission);
    writeln!(markdown).unwrap();
    write_matrix_section(&mut markdown, "Distribution", &distribution);

    let mut details_markdown = String::new();
    writeln!(details_markdown, "{DETAILS_MARKER}").unwrap();
    writeln!(details_markdown).unwrap();
    writeln!(details_markdown, "## Conversion Matrix Warning Details").unwrap();
    writeln!(details_markdown).unwrap();
    writeln!(
        details_markdown,
        "This file is generated by the conversion matrix workflow. It records warning text by source to target pair for the same run that posted the PR comment."
    )
    .unwrap();
    writeln!(
        details_markdown,
        "Warning lines are tagged by phase: source parse, target write, or target readback."
    )
    .unwrap();
    writeln!(
        details_markdown,
        "Expected counts live in `powerio-cli/tests/conversion_matrix_report.rs`; update the matching baseline value in the same PR when warning count changes are intentional."
    )
    .unwrap();
    writeln!(details_markdown).unwrap();
    write_warning_summary(&mut details_markdown, "Transmission", &transmission);
    writeln!(details_markdown).unwrap();
    write_warning_summary(&mut details_markdown, "Distribution", &distribution);

    Report {
        markdown,
        details_markdown,
        failures,
    }
}

#[derive(Clone)]
struct MatrixReport {
    formats: Vec<&'static str>,
    case_count: usize,
    cells: Vec<Vec<Cell>>,
    failures: Vec<String>,
}

#[derive(Clone)]
struct Cell {
    observed_warnings: usize,
    baseline_warnings: usize,
    failures: Vec<String>,
    warning_counts: BTreeMap<(String, String), usize>,
}

impl Cell {
    fn new(baseline_warnings: usize) -> Self {
        Self {
            observed_warnings: 0,
            baseline_warnings,
            failures: Vec::new(),
            warning_counts: BTreeMap::new(),
        }
    }

    fn ok(&self) -> bool {
        self.failures.is_empty() && self.observed_warnings == self.baseline_warnings
    }

    fn parity(&self) -> bool {
        self.ok() && self.observed_warnings == 0
    }

    fn record_warnings(&mut self, phase: &str, warnings: &[String]) {
        self.observed_warnings += warnings.len();
        for warning in warnings {
            *self
                .warning_counts
                .entry((phase.to_string(), sanitize_report_text(warning)))
                .or_default() += 1;
        }
    }
}

fn write_matrix_section(markdown: &mut String, title: &str, report: &MatrixReport) {
    writeln!(markdown, "### {title}").unwrap();
    writeln!(markdown).unwrap();
    writeln!(markdown, "{} cases.", report.case_count).unwrap();
    writeln!(markdown).unwrap();
    write!(markdown, "| Source ↓ / target → |").unwrap();
    for format in &report.formats {
        write!(markdown, " {format} |").unwrap();
    }
    writeln!(markdown).unwrap();
    write!(markdown, "| --- |").unwrap();
    for _ in &report.formats {
        write!(markdown, " --- |").unwrap();
    }
    writeln!(markdown).unwrap();
    for (source, row) in report.formats.iter().zip(&report.cells) {
        write!(markdown, "| {source} |").unwrap();
        for cell in row {
            write!(markdown, " {} |", cell_summary(cell)).unwrap();
        }
        writeln!(markdown).unwrap();
    }
}

fn cell_summary(cell: &Cell) -> String {
    let icon = if cell.parity() {
        "🟢"
    } else if cell.ok() {
        "🟡"
    } else {
        "🔴"
    };
    format!(
        "{icon} {}/{}",
        cell.observed_warnings, cell.baseline_warnings
    )
}

fn write_warning_summary(markdown: &mut String, title: &str, report: &MatrixReport) {
    writeln!(markdown, "#### {title} Warning Details").unwrap();
    writeln!(markdown).unwrap();
    writeln!(
        markdown,
        "Rows list only source to target pairs that produced warnings or failed an invariant."
    )
    .unwrap();
    writeln!(markdown).unwrap();
    let mut wrote_row = false;
    for (source, row) in report.formats.iter().zip(&report.cells) {
        for (target, cell) in report.formats.iter().zip(row) {
            if cell.observed_warnings == 0 && cell.failures.is_empty() {
                continue;
            }
            wrote_row = true;
            writeln!(
                markdown,
                "- **{source} → {target}** (`{}/{}`)",
                cell.observed_warnings, cell.baseline_warnings
            )
            .unwrap();
            write_cell_details(markdown, cell);
        }
    }

    if !wrote_row {
        writeln!(markdown, "No warnings observed.").unwrap();
    }
}

fn write_cell_details(markdown: &mut String, cell: &Cell) {
    let mut details = Vec::new();
    for failure in &cell.failures {
        details.push(format!("failure: {}", sanitize_report_text(failure)));
    }

    let mut warnings: Vec<_> = cell.warning_counts.iter().collect();
    warnings.sort_by(
        |((phase_a, warning_a), count_a), ((phase_b, warning_b), count_b)| {
            count_b
                .cmp(count_a)
                .then_with(|| phase_order(phase_a).cmp(&phase_order(phase_b)))
                .then_with(|| warning_a.cmp(warning_b))
        },
    );
    for ((phase, warning), count) in warnings.iter().take(MAX_WARNING_DETAILS_PER_PAIR) {
        details.push(format!("{phase}: {count}x {warning}"));
    }
    let omitted = warnings.len().saturating_sub(MAX_WARNING_DETAILS_PER_PAIR);
    if omitted > 0 {
        details.push(format!("{omitted} more warning texts"));
    }

    if details.is_empty() {
        writeln!(markdown, "  - No warning text recorded.").unwrap();
        return;
    }
    for detail in details {
        writeln!(markdown, "  - {}", markdown_list_text(&detail)).unwrap();
    }
}

fn phase_order(phase: &str) -> usize {
    match phase {
        SOURCE_PARSE => 0,
        TARGET_WRITE => 1,
        TARGET_READBACK => 2,
        _ => 3,
    }
}

fn markdown_list_text(text: &str) -> String {
    text.replace('\n', " ")
}

fn sanitize_report_text(text: &str) -> String {
    const GENERATED_DSS_DIR: &str = "powerio-conversion-matrix/";
    if let Some(dir_idx) = text.find(GENERATED_DSS_DIR) {
        let prefix = &text[..dir_idx];
        let path_start = prefix
            .char_indices()
            .rev()
            .find(|(_, c)| c.is_whitespace())
            .map_or(0, |(idx, c)| idx + c.len_utf8());
        let prelude = prefix[..path_start].trim_end();
        let suffix = &text[dir_idx + GENERATED_DSS_DIR.len()..];
        if prelude.is_empty() {
            return format!("generated DSS {suffix}");
        }
        return format!("{prelude} generated DSS {suffix}");
    }

    let temp_dir = std::env::temp_dir().to_string_lossy().into_owned();
    let text = text
        .replace(&temp_dir, "<tmp>")
        .replace(env!("CARGO_MANIFEST_DIR"), "<crate>");
    code_object_name(&text)
}

#[test]
fn sanitize_report_text_handles_multibyte_whitespace_before_generated_dss_dir() {
    // A non-breaking space (U+00A0, 2 UTF-8 bytes) immediately before the
    // generated DSS dir marker must not panic on a non char boundary slice.
    let text = "wrote\u{a0}/tmp/xyz/powerio-conversion-matrix/case.dss";
    let sanitized = sanitize_report_text(text);
    assert_eq!(sanitized, "wrote generated DSS case.dss");
}

fn code_object_name(text: &str) -> String {
    const OBJECT_PREFIXES: &[&str] = &[
        "voltage source",
        "vsource",
        "load",
        "capacitor",
        "reactor",
        "generator",
        "shunt",
        "transformer",
        "switch",
        "linecode",
        "line",
        "bus",
    ];

    for prefix in OBJECT_PREFIXES {
        let Some(rest) = text.strip_prefix(prefix).and_then(|s| s.strip_prefix(' ')) else {
            continue;
        };
        let Some((name, suffix)) = rest.split_once(':') else {
            continue;
        };
        if name.contains(char::is_whitespace) || name.starts_with('`') {
            continue;
        }
        return format!("{prefix} `{name}`:{suffix}");
    }

    text.to_string()
}

#[derive(Clone, Copy)]
struct TransmissionFormat {
    name: &'static str,
    token: &'static str,
    target: TargetFormat,
}

const TRANSMISSION_FORMATS: [TransmissionFormat; 9] = [
    TransmissionFormat {
        name: "MATPOWER .m",
        token: "matpower",
        target: TargetFormat::Matpower,
    },
    TransmissionFormat {
        name: "PowerIO JSON",
        token: "powerio-json",
        target: TargetFormat::PowerioJson,
    },
    TransmissionFormat {
        name: "PowerModels JSON",
        token: "powermodels-json",
        target: TargetFormat::PowerModelsJson,
    },
    TransmissionFormat {
        name: "PSS/E .raw",
        token: "psse",
        target: TargetFormat::Psse { rev: 33 },
    },
    TransmissionFormat {
        name: "PowerWorld .aux",
        token: "powerworld",
        target: TargetFormat::PowerWorld,
    },
    TransmissionFormat {
        name: "egret JSON",
        token: "egret-json",
        target: TargetFormat::EgretJson,
    },
    TransmissionFormat {
        name: "pandapower JSON",
        token: "pandapower-json",
        target: TargetFormat::PandapowerJson,
    },
    TransmissionFormat {
        name: "Surge JSON",
        token: "surge-json",
        target: TargetFormat::SurgeJson,
    },
    TransmissionFormat {
        name: "PSLF .epc",
        token: "pslf",
        target: TargetFormat::Pslf,
    },
];

const TRANSMISSION_WARNING_BASELINE: [[usize; 9]; 9] = [
    [0, 0, 0, 8, 8, 0, 5, 0, 11],
    [6, 0, 1, 14, 14, 1, 11, 24, 16],
    [6, 0, 0, 14, 14, 1, 11, 24, 16],
    [13, 0, 1, 0, 1, 1, 3, 1, 10],
    [12, 0, 0, 0, 0, 0, 2, 0, 5],
    [0, 0, 0, 8, 8, 0, 5, 0, 11],
    [6, 0, 0, 11, 11, 5, 0, 0, 11],
    [2, 1, 2, 10, 10, 2, 7, 2, 17],
    [17, 4, 5, 5, 5, 5, 7, 5, 8],
];

const TRANSMISSION_CASES: [(&str, &str); 6] = [
    ("case9", "case9.m"),
    ("case14", "case14.m"),
    ("case30", "case30.m"),
    ("dcline", "t_case9_dcline.m"),
    ("out of service", "t_case9_oos.m"),
    ("PGLib 5", "pglib/pglib_opf_case5_pjm.m"),
];

struct TransmissionPayload {
    label: &'static str,
    network: Network,
    parse_warnings: Vec<String>,
    core: TransmissionCore,
}

#[derive(Debug, PartialEq, Eq)]
struct TransmissionCore {
    buses: usize,
    branches: usize,
    generators: usize,
    loads: usize,
    shunts: usize,
    load_p: i64,
    load_q: i64,
    gen_p: i64,
    base_mva: i64,
}

fn run_transmission_matrix() -> MatrixReport {
    let formats = TRANSMISSION_FORMATS.iter().map(|fmt| fmt.name).collect();
    let mut cells = Vec::new();
    let mut failures = Vec::new();

    for (source_idx, source) in TRANSMISSION_FORMATS.iter().enumerate() {
        let payloads = transmission_payloads(*source);
        let mut row = Vec::new();
        for (target_idx, target) in TRANSMISSION_FORMATS.iter().enumerate() {
            let mut cell = Cell::new(TRANSMISSION_WARNING_BASELINE[source_idx][target_idx]);
            match &payloads {
                Ok(payloads) => {
                    for payload in payloads {
                        cell.record_warnings(SOURCE_PARSE, &payload.parse_warnings);
                        validate_transmission_pair(payload, *target, &mut cell);
                    }
                }
                Err(err) => cell.failures.push(err.clone()),
            }
            if !cell.ok() {
                failures.push(format!(
                    "transmission {} -> {}: observed {} warnings, baseline {}; {}",
                    source.name,
                    target.name,
                    cell.observed_warnings,
                    cell.baseline_warnings,
                    cell.failures.join("; ")
                ));
            }
            row.push(cell);
        }
        cells.push(row);
    }

    MatrixReport {
        formats,
        case_count: TRANSMISSION_CASES.len(),
        cells,
        failures,
    }
}

fn transmission_payloads(format: TransmissionFormat) -> Result<Vec<TransmissionPayload>, String> {
    TRANSMISSION_CASES
        .iter()
        .map(|(label, rel)| {
            let mut base =
                parse_matpower_file(data(rel)).map_err(|err| format!("parse {rel}: {err}"))?;
            base.source = None;
            let rendered = write_transmission_as(&base, format.target)
                .map_err(|err| format!("write {rel} as {}: {err}", format.name))?;
            let parsed = parse_transmission_str(&rendered.text, format.token)
                .map_err(|err| format!("read generated {rel} as {}: {err}", format.name))?;
            let core = transmission_core(&parsed.network);
            Ok(TransmissionPayload {
                label,
                network: parsed.network,
                parse_warnings: parsed.warnings,
                core,
            })
        })
        .collect()
}

fn validate_transmission_pair(
    payload: &TransmissionPayload,
    target: TransmissionFormat,
    cell: &mut Cell,
) {
    match write_transmission_as(&payload.network, target.target) {
        Ok(conversion) => {
            cell.record_warnings(TARGET_WRITE, &conversion.warnings);
            match parse_transmission_str(&conversion.text, target.token) {
                Ok(parsed) => {
                    cell.record_warnings(TARGET_READBACK, &parsed.warnings);
                    let actual = transmission_core(&parsed.network);
                    if actual != payload.core {
                        cell.failures.push(format!(
                            "{} core changed for {}: before {:?}, after {:?}",
                            payload.label, target.name, payload.core, actual
                        ));
                    }
                }
                Err(err) => cell.failures.push(format!(
                    "{} output did not parse as {}: {err}",
                    payload.label, target.name
                )),
            }
        }
        Err(err) => cell.failures.push(format!(
            "{} did not write as {}: {err}",
            payload.label, target.name
        )),
    }
}

fn transmission_core(net: &Network) -> TransmissionCore {
    let rounded = |x: f64| (x * 1e3).round() as i64;
    TransmissionCore {
        buses: net.buses.len(),
        branches: net.branches.len(),
        generators: net.generators.len(),
        loads: net.loads.len(),
        shunts: net.shunts.len(),
        load_p: rounded(net.loads.iter().map(|load| load.p).sum()),
        load_q: rounded(net.loads.iter().map(|load| load.q).sum()),
        gen_p: rounded(net.generators.iter().map(|generator| generator.pg).sum()),
        base_mva: rounded(net.base_mva),
    }
}

fn data(rel: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../tests/data")
        .join(rel)
}

#[derive(Clone, Copy)]
struct DistributionFormat {
    name: &'static str,
    token: &'static str,
    target: DistTargetFormat,
}

const DISTRIBUTION_FORMATS: [DistributionFormat; 3] = [
    DistributionFormat {
        name: "OpenDSS .dss",
        token: "dss",
        target: DistTargetFormat::Dss,
    },
    DistributionFormat {
        name: "BMOPF JSON",
        token: "bmopf-json",
        target: DistTargetFormat::BmopfJson,
    },
    DistributionFormat {
        name: "PMD JSON",
        token: "pmd-json",
        target: DistTargetFormat::PmdJson,
    },
];

const DISTRIBUTION_WARNING_BASELINE: [[usize; 3]; 3] = [[0, 152, 84], [1, 0, 0], [22, 117, 0]];

const DISTRIBUTION_CASES: [(&str, &str, DistributionFormat); 7] = [
    (
        "single phase transformer",
        "dist/micro/xfmr_single_phase.dss",
        DISTRIBUTION_FORMATS[0],
    ),
    (
        "center tap transformer",
        "dist/micro/xfmr_center_tap.dss",
        DISTRIBUTION_FORMATS[0],
    ),
    (
        "switch states",
        "dist/micro/switch.dss",
        DISTRIBUTION_FORMATS[0],
    ),
    (
        "four wire linecode",
        "dist/micro/fourwire_linecode.dss",
        DISTRIBUTION_FORMATS[0],
    ),
    (
        "ten conductor linecode",
        "dist/micro/linecode_10x10.dss",
        DISTRIBUTION_FORMATS[0],
    ),
    (
        "BMOPF IEEE 13",
        "dist/bmopf/example_ieee13.json",
        DISTRIBUTION_FORMATS[1],
    ),
    (
        "PMD four wire",
        "dist/pmd/fourwire_linecode.json",
        DISTRIBUTION_FORMATS[2],
    ),
];

struct DistributionPayload {
    label: &'static str,
    network: DistNetwork,
    parse_warnings: Vec<String>,
    core: DistributionCore,
}

#[derive(Debug, PartialEq, Eq)]
struct DistributionCore {
    buses: usize,
    loads: usize,
    generators: usize,
    shunts: usize,
    load_p: i64,
    load_q: i64,
}

fn run_distribution_matrix() -> MatrixReport {
    let formats = DISTRIBUTION_FORMATS.iter().map(|fmt| fmt.name).collect();
    let mut cells = Vec::new();
    let mut failures = Vec::new();

    for (source_idx, source) in DISTRIBUTION_FORMATS.iter().enumerate() {
        let payloads = distribution_payloads(*source);
        let mut row = Vec::new();
        for (target_idx, target) in DISTRIBUTION_FORMATS.iter().enumerate() {
            let mut cell = Cell::new(DISTRIBUTION_WARNING_BASELINE[source_idx][target_idx]);
            match &payloads {
                Ok(payloads) => {
                    for payload in payloads {
                        cell.record_warnings(SOURCE_PARSE, &payload.parse_warnings);
                        validate_distribution_pair(payload, *target, &mut cell);
                    }
                }
                Err(err) => cell.failures.push(err.clone()),
            }
            if !cell.ok() {
                failures.push(format!(
                    "distribution {} -> {}: observed {} warnings, baseline {}; {}",
                    source.name,
                    target.name,
                    cell.observed_warnings,
                    cell.baseline_warnings,
                    cell.failures.join("; ")
                ));
            }
            row.push(cell);
        }
        cells.push(row);
    }

    MatrixReport {
        formats,
        case_count: DISTRIBUTION_CASES.len(),
        cells,
        failures,
    }
}

fn distribution_payloads(format: DistributionFormat) -> Result<Vec<DistributionPayload>, String> {
    DISTRIBUTION_CASES
        .iter()
        .map(|(label, rel, native_format)| {
            let mut base = powerio_dist::parse_file(data(rel), Some(native_format.token))
                .map_err(|err| format!("parse {rel}: {err}"))?;
            base.source = None;
            base.source_format = None;
            let rendered = base.to_format(format.target);
            let mut parsed = parse_distribution_text(&rendered.text, format)
                .map_err(|err| format!("read generated {rel} as {}: {err}", format.name))?;
            let warnings = std::mem::take(&mut parsed.warnings);
            let core = distribution_core(&parsed);
            Ok(DistributionPayload {
                label,
                network: parsed,
                parse_warnings: warnings,
                core,
            })
        })
        .collect()
}

fn validate_distribution_pair(
    payload: &DistributionPayload,
    target: DistributionFormat,
    cell: &mut Cell,
) {
    let conversion = payload.network.to_format(target.target);
    cell.record_warnings(TARGET_WRITE, &conversion.warnings);
    match parse_distribution_text(&conversion.text, target) {
        Ok(mut parsed) => {
            cell.record_warnings(TARGET_READBACK, &parsed.warnings);
            parsed.warnings.clear();
            let actual = distribution_core(&parsed);
            if actual != payload.core {
                cell.failures.push(format!(
                    "{} core changed for {}: before {:?}, after {:?}",
                    payload.label, target.name, payload.core, actual
                ));
            }
        }
        Err(err) => cell.failures.push(format!(
            "{} output did not parse as {}: {err}",
            payload.label, target.name
        )),
    }
}

fn parse_distribution_text(
    text: &str,
    format: DistributionFormat,
) -> powerio_dist::Result<DistNetwork> {
    if format.target != DistTargetFormat::Dss {
        return powerio_dist::parse_str(text, format.token);
    }

    let dir = std::env::temp_dir().join("powerio-conversion-matrix");
    std::fs::create_dir_all(&dir).unwrap();
    let path = dir.join(format!(
        "case-{}.dss",
        DSS_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
    ));
    std::fs::write(&path, text).unwrap();
    let parsed = powerio_dist::parse_file(&path, Some(format.token));
    let _ = std::fs::remove_file(path);
    parsed
}

fn distribution_core(net: &DistNetwork) -> DistributionCore {
    let rounded = |x: f64| (x * 1e3).round() as i64;
    DistributionCore {
        buses: net.buses.len(),
        loads: net.loads.len(),
        generators: net.generators.len(),
        shunts: net.shunts.len(),
        load_p: rounded(
            net.loads
                .iter()
                .flat_map(|load| load.p_nom.iter())
                .sum::<f64>(),
        ),
        load_q: rounded(
            net.loads
                .iter()
                .flat_map(|load| load.q_nom.iter())
                .sum::<f64>(),
        ),
    }
}
