use crate::io::{BioReader, error::Error, util};
use crate::model::system::System;
use bio_forge as bf;
use std::io::BufRead;

pub fn read<R: BufRead>(builder: BioReader<R>) -> Result<System, Error> {
    let bio_context = bf::io::IoContext::new_default();
    let mut bio_struct = bf::io::read_mmcif_structure(builder.reader, &bio_context)?;

    if let Some(clean_config) = builder.clean_config {
        let bf_clean_config = util::to_bf_clean_config(clean_config);
        bf::ops::clean_structure(&mut bio_struct, &bf_clean_config)?;
    }

    bf::ops::repair_structure(&mut bio_struct)?;

    let bf_hydro_config = util::to_bf_hydro_config(builder.protonation_config);
    bf::ops::add_hydrogens(&mut bio_struct, &bf_hydro_config)?;

    if let Some(solvate_config) = builder.solvate_config {
        let bf_solvate_config = util::to_bf_solvate_config(solvate_config);
        bf::ops::solvate_structure(&mut bio_struct, &bf_solvate_config)?;
    }

    let bf_topo = util::build_topology(bio_struct, &builder.topology_config)?;
    let system = util::from_bio_topology(bf_topo)?;

    Ok(system)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::io::{Format, Template, TopologyConfig};
    use crate::model::{
        atom::Atom,
        metadata::{AtomResidueInfo, BioMetadata, ResidueCategory, ResiduePosition},
        system::{Bond, System},
        types::{BondOrder, Element},
    };
    use std::collections::HashSet;
    use std::io::Cursor;

    fn sample_system() -> (System, Template) {
        let atoms = vec![
            Atom::new(Element::N, [0.0, 0.0, 0.0]),
            Atom::new(Element::C, [1.0, 0.0, 0.0]),
            Atom::new(Element::O, [2.0, 0.0, 0.0]),
        ];
        let metadata = BioMetadata {
            atom_info: vec![
                AtomResidueInfo::builder("N", "LIG", 1, 'A')
                    .category(ResidueCategory::Hetero)
                    .position(ResiduePosition::None)
                    .build(),
                AtomResidueInfo::builder("CA", "LIG", 1, 'A')
                    .category(ResidueCategory::Hetero)
                    .position(ResiduePosition::None)
                    .build(),
                AtomResidueInfo::builder("O", "LIG", 1, 'A')
                    .category(ResidueCategory::Hetero)
                    .position(ResiduePosition::None)
                    .build(),
            ],
        };
        let bonds = vec![
            Bond::new(0, 1, BondOrder::Single),
            Bond::new(1, 2, BondOrder::Double),
        ];
        let system = System {
            atoms,
            bonds,
            box_vectors: Some([[15.0, 0.0, 0.0], [0.0, 15.0, 0.0], [0.0, 0.0, 15.0]]),
            bio_metadata: Some(metadata),
        };

        let template = Template::new(
            "LIG",
            vec!["N".into(), "CA".into(), "O".into()],
            vec![
                ("N".into(), "CA".into(), bf::BondOrder::Single),
                ("CA".into(), "O".into(), bf::BondOrder::Double),
            ],
        );

        (system, template)
    }

    fn assert_systems_equivalent(expected: &System, actual: &System) {
        match (expected.box_vectors, actual.box_vectors) {
            (Some(a), Some(b)) => {
                for i in 0..3 {
                    for j in 0..3 {
                        assert!(
                            (a[i][j] - b[i][j]).abs() < 1e-9,
                            "box vectors differ at [{i}][{j}]: {} vs {}",
                            a[i][j],
                            b[i][j]
                        );
                    }
                }
            }
            (None, None) => {}
            _ => panic!("box vector presence mismatch"),
        }

        let meta_a = expected.bio_metadata.as_ref().expect("expected metadata");
        let meta_b = actual.bio_metadata.as_ref().expect("actual metadata");
        assert_eq!(
            meta_a.atom_info.len(),
            meta_b.atom_info.len(),
            "atom_info len"
        );
        assert_eq!(expected.atom_count(), actual.atom_count(), "atom count");
        assert_eq!(expected.bond_count(), actual.bond_count(), "bond count");

        let key = |info: &AtomResidueInfo| {
            (
                info.chain_id,
                info.residue_id,
                info.insertion_code,
                info.atom_name.clone(),
            )
        };
        let mut map = std::collections::HashMap::new();
        for (i, info) in meta_a.atom_info.iter().enumerate() {
            map.insert(key(info), i);
        }

        for (j, info_b) in meta_b.atom_info.iter().enumerate() {
            let i = *map.get(&key(info_b)).expect("atom key missing in expected");
            assert_eq!(
                expected.atoms[i].element, actual.atoms[j].element,
                "element mismatch"
            );
            for k in 0..3 {
                assert!((expected.atoms[i].position[k] - actual.atoms[j].position[k]).abs() < 1e-6);
            }
            assert_eq!(meta_a.atom_info[i], *info_b, "metadata mismatch");
        }

        let bonds_a: HashSet<_> = expected.bonds.iter().cloned().collect();
        let bonds_b: HashSet<_> = actual
            .bonds
            .iter()
            .map(|b| {
                let info_i = &meta_b.atom_info[b.i];
                let info_j = &meta_b.atom_info[b.j];
                let i = *map
                    .get(&(
                        info_i.chain_id,
                        info_i.residue_id,
                        info_i.insertion_code,
                        info_i.atom_name.clone(),
                    ))
                    .unwrap();
                let j = *map
                    .get(&(
                        info_j.chain_id,
                        info_j.residue_id,
                        info_j.insertion_code,
                        info_j.atom_name.clone(),
                    ))
                    .unwrap();
                Bond::new(i, j, b.order)
            })
            .collect();
        assert_eq!(bonds_a, bonds_b, "bond sets differ");
    }

    #[test]
    fn reads_mmcif_topology_roundtrip() {
        let (system, template) = sample_system();
        let bf_topo = util::to_bio_topology(&system).expect("to bio topo");

        let mut buf = Vec::new();
        bf::io::write_mmcif_topology(&mut buf, &bf_topo).expect("write bf mmcif");

        let roundtrip = BioReader::new(Cursor::new(buf), Format::Mmcif)
            .topology(TopologyConfig {
                hetero_templates: vec![template],
                disulfide_bond_cutoff: 2.2,
            })
            .read()
            .expect("read mmcif");

        assert_systems_equivalent(&system, &roundtrip);
    }

    #[test]
    fn handles_invalid_input_gracefully() {
        let bogus = b"data_".as_slice();
        let system = BioReader::new(Cursor::new(bogus), Format::Mmcif)
            .read()
            .expect("parsing should not panic");
        assert_eq!(
            system.atom_count(),
            0,
            "expected empty system for invalid input"
        );
    }
}
