//! Molecule I/O format bindings: SDF/MOL, CML, CDXML, MOL2, XYZ, PDB, PDBQT, InChI.

use crate::{
    MolHandle, WASM_MAX_ATOMS, WASM_MAX_BATCH_ITEMS, WASM_MAX_INPUT_BYTES,
    WASM_MAX_JSON_STRING_BYTES, enforce_wasm_molecule_size, escape_json_string,
    parse_smiles_json_array, parse_wasm_string_json_array,
};
use wasm_bindgen::prelude::*;

/// Generate 3D coordinates for the molecule and return a PDB string.
///
/// Coordinates are generated using distance-geometry placement with ring templates.
/// Returns heavy-atom PDB (HETATM records, no explicit H).
#[wasm_bindgen]
pub fn generate_3d_pdb(mol: &MolHandle) -> String {
    let coords = chematic_3d::generate_coords(&mol.inner);
    chematic_3d::write_pdb(&mol.inner, &coords)
}

/// Generate energy-minimized 3D coordinates and return a PDB string.
///
/// Runs distance-geometry placement followed by gradient-descent force-field
/// minimization.  Geometry quality is better than `generate_3d_pdb` for
/// flexible molecules; the force field is approximate (not MMFF94/UFF).
#[wasm_bindgen]
pub fn generate_3d_minimized_pdb(mol: &MolHandle) -> String {
    let coords = chematic_3d::generate_coords(&mol.inner);
    let minimized = chematic_3d::minimize(&mol.inner, coords);
    chematic_3d::write_pdb(&mol.inner, &minimized)
}

/// Generate 3D coordinates using ETKDG (torsion angle preferences) and return PDB block.
/// ETKDG produces higher-quality conformations than rule-based DG by applying
/// experimental torsion angle preferences to common structural patterns.
#[wasm_bindgen]
pub fn generate_3d_etkdg_pdb(mol: &MolHandle) -> String {
    let coords = chematic_3d::generate_coords_etkdg(&mol.inner);
    chematic_3d::write_pdb(&mol.inner, &coords)
}

/// Generate 3D coordinates using ETKDG and minimize with DREIDING force field.
#[wasm_bindgen]
pub fn generate_3d_etkdg_minimized_pdb(mol: &MolHandle) -> String {
    let coords = chematic_3d::generate_coords_etkdg(&mol.inner);
    let minimized = chematic_3d::minimize(&mol.inner, coords);
    chematic_3d::write_pdb(&mol.inner, &minimized)
}

/// Parse a MOL V3000 block and return a `MolHandle`.
///
/// Returns a JS error string on parse failure.
#[wasm_bindgen]
pub fn mol_from_v3000_block(block: &str) -> Result<MolHandle, JsValue> {
    if block.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("V3000 block too large"));
    }
    let (mol, _meta) =
        chematic_mol::parse_mol_v3000(block).map_err(|e| JsValue::from_str(&e.to_string()))?;
    if mol.atom_count() > WASM_MAX_ATOMS {
        return Err(JsValue::from_str(&format!(
            "molecule too large (max {} atoms)",
            WASM_MAX_ATOMS
        )));
    }
    Ok(MolHandle {
        inner: std::rc::Rc::new(mol),
    })
}

/// Parse a MOL V2000 block and return a `MolHandle`.
///
/// Returns a JS error string on parse failure.
#[wasm_bindgen]
pub fn mol_from_sdf_block(block: &str) -> Result<MolHandle, JsValue> {
    if block.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("MOL block too large"));
    }
    let (mol, _meta) =
        chematic_mol::parse_mol(block).map_err(|e| JsValue::from_str(&e.to_string()))?;
    if mol.atom_count() > WASM_MAX_ATOMS {
        return Err(JsValue::from_str(&format!(
            "molecule too large (max {} atoms)",
            WASM_MAX_ATOMS
        )));
    }
    Ok(MolHandle {
        inner: std::rc::Rc::new(mol),
    })
}

/// Serialize a molecule to a MOL V2000 block with 2D coordinates.
///
/// Atom positions are computed via the same layout engine used for SVG depiction
/// and converted to Ångström units (`1.5 Å` per bond).
#[wasm_bindgen]
pub fn to_mol_block(mol: &MolHandle) -> String {
    let meta = chematic_mol::MolMetadata::default();
    let layout = chematic_depict::compute_layout(&mol.inner);
    // SVG bond length is 40 px; standard MOL bond length is 1.5 Å.
    // Negate Y because SVG y-axis points down, MOL y-axis points up.
    const SCALE: f64 = 1.5 / 40.0;
    let coords: Vec<(f64, f64)> = (0..mol.inner.atom_count())
        .map(|i| {
            let p = layout.get(chematic_core::AtomIdx(i as u32));
            (p.x * SCALE, -p.y * SCALE)
        })
        .collect();
    chematic_mol::write_mol_with_coords(&mol.inner, &meta, &coords)
}

/// Parse an SDF string and return a JSON array of canonical SMILES strings.
///
/// Invalid records are represented as `null` in the array.
#[wasm_bindgen]
pub fn sdf_to_smiles_json(sdf: &str) -> String {
    if sdf.len() > WASM_MAX_INPUT_BYTES {
        return format!(
            r#"[{{"error":"SDF input too large ({} bytes)"}}]"#,
            sdf.len()
        );
    }
    let entries: Vec<String> = chematic_mol::SdfReader::new(sdf)
        .take(WASM_MAX_BATCH_ITEMS)
        .map(|r| match r {
            Ok((mol, _)) => {
                let smi = chematic_smiles::canonical_smiles(&mol);
                format!("\"{}\"", smi.replace('"', "\\\""))
            }
            Err(_) => "null".to_string(),
        })
        .collect();
    format!("[{}]", entries.join(","))
}

// ---------------------------------------------------------------------------
// EState free functions (Sprint P)
// ---------------------------------------------------------------------------

/// Serialize a SMILES string directly to a MOL V2000 block with 2D coordinates.
///
/// Returns a JS error on SMILES parse failure.
#[wasm_bindgen]
pub fn mol_block_from_smiles(smiles: &str) -> Result<String, JsValue> {
    let mol = chematic_smiles::parse(smiles)
        .map(|m| MolHandle {
            inner: std::rc::Rc::new(m),
        })
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    Ok(to_mol_block(&mol))
}

/// Parse an SDF string and return a JSON array of record objects.
///
/// Each record has the shape:
/// ```json
/// {"smiles":"CC(=O)O","name":"aspirin","properties":{"MW":"180.2","Activity":"high"}}
/// ```
///
/// Invalid records are represented as `null`.  SD data fields are included in
/// `properties`; multi-line values are joined with `\n`.
#[wasm_bindgen]
pub fn sdf_to_records_json(sdf: &str) -> String {
    if sdf.len() > WASM_MAX_INPUT_BYTES {
        return format!(
            r#"[{{"error":"SDF input too large ({} bytes)"}}]"#,
            sdf.len()
        );
    }
    let entries: Vec<String> = chematic_mol::SdfRecordReader::new(sdf)
        .take(WASM_MAX_BATCH_ITEMS)
        .map(|r| match r {
            Ok(rec) => {
                let smi = chematic_smiles::canonical_smiles(&rec.mol);
                let name = escape_json_string(&rec.meta.name);
                let props: Vec<String> = rec
                    .properties
                    .iter()
                    .map(|(k, v)| {
                        format!(
                            "\"{}\":\"{}\"",
                            escape_json_string(k),
                            escape_json_string(v)
                        )
                    })
                    .collect();
                format!(
                    "{{\"smiles\":\"{}\",\"name\":\"{}\",\"properties\":{{{}}}}}",
                    escape_json_string(&smi),
                    name,
                    props.join(",")
                )
            }
            Err(_) => "null".to_string(),
        })
        .collect();
    format!("[{}]", entries.join(","))
}

/// Serialise a JSON array of SMILES to an SDF string.
///
/// Generates 2D coordinates for each molecule.  Property data can be
/// included by using `sdf_from_records_json` instead.
#[wasm_bindgen]
#[allow(clippy::type_complexity)]
pub fn smiles_array_to_sdf(smiles_json: &str) -> Result<String, JsValue> {
    let smiles_list = parse_smiles_json_array(smiles_json)?;
    let mut records: Vec<(
        chematic_core::Molecule,
        chematic_mol::MolMetadata,
        Vec<(f64, f64)>,
    )> = Vec::new();

    for smi in &smiles_list {
        let mol = chematic_smiles::parse(smi).map_err(|e| JsValue::from_str(&e.to_string()))?;
        enforce_wasm_molecule_size(&mol)?;
        let layout = chematic_depict::compute_layout(&mol);
        const SCALE: f64 = 1.5 / 40.0;
        let coords: Vec<(f64, f64)> = (0..mol.atom_count())
            .map(|i| {
                let p = layout.get(chematic_core::AtomIdx(i as u32));
                (p.x * SCALE, -p.y * SCALE)
            })
            .collect();
        let meta = chematic_mol::MolMetadata::default();
        records.push((mol, meta, coords));
    }

    let refs: Vec<(
        &chematic_core::Molecule,
        &chematic_mol::MolMetadata,
        &[(f64, f64)],
    )> = records
        .iter()
        .map(|(m, meta, c)| (m, meta, c.as_slice()))
        .collect();

    Ok(chematic_mol::write_sdf(&refs))
}

/// Serialise a `MolHandle` to MOL V3000 format with 2D coordinates.
#[wasm_bindgen]
pub fn to_mol_v3000_block(mol: &MolHandle) -> String {
    let layout = chematic_depict::compute_layout(&mol.inner);
    const SCALE: f64 = 1.5 / 40.0;
    let coords: Vec<(f64, f64)> = (0..mol.inner.atom_count())
        .map(|i| {
            let p = layout.get(chematic_core::AtomIdx(i as u32));
            (p.x * SCALE, -p.y * SCALE)
        })
        .collect();
    let meta = chematic_mol::MolMetadata::default();
    chematic_mol::write_mol_v3000(&mol.inner, &meta, &coords)
}

// ---------------------------------------------------------------------------
// DepictData
// ---------------------------------------------------------------------------

/// Parse a CML string into a `MolHandle`.
///
/// Returns a JS error if the CML is invalid (unknown element, bad bond, etc.).
#[wasm_bindgen]
pub fn mol_from_cml(cml: &str) -> Result<MolHandle, JsValue> {
    if cml.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("CML input too large"));
    }
    let (mol, _coords) =
        chematic_mol::parse_cml(cml).map_err(|e| JsValue::from_str(&e.to_string()))?;
    if mol.atom_count() > WASM_MAX_ATOMS {
        return Err(JsValue::from_str(&format!(
            "molecule too large (max {} atoms)",
            WASM_MAX_ATOMS
        )));
    }
    Ok(MolHandle {
        inner: std::rc::Rc::new(mol),
    })
}

/// Serialise a `MolHandle` to a CML string with 2D coordinates.
///
/// Coordinates are generated using the same 2D layout engine as `to_mol_block`.
#[wasm_bindgen]
pub fn to_cml(mol: &MolHandle) -> String {
    let layout = chematic_depict::compute_layout(&mol.inner);
    const SCALE: f64 = 1.5 / 40.0;
    let coords: Vec<(f64, f64)> = (0..mol.inner.atom_count())
        .map(|i| {
            let p = layout.get(chematic_core::AtomIdx(i as u32));
            (p.x * SCALE, -p.y * SCALE)
        })
        .collect();
    chematic_mol::write_cml(&mol.inner, Some(&coords))
}

/// Parse a MolJSON string into a `MolHandle`.
///
/// MolJSON is a JSON-based molecular representation designed for LLM
/// (large language model) compatibility.  Returns a JS error on invalid input.
#[wasm_bindgen]
pub fn mol_from_moljson(json: &str) -> Result<MolHandle, JsValue> {
    if json.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("MolJSON input too large"));
    }
    let mol = chematic_mol::parse_moljson(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
    if mol.atom_count() > WASM_MAX_ATOMS {
        return Err(JsValue::from_str(&format!(
            "molecule too large (max {} atoms)",
            WASM_MAX_ATOMS
        )));
    }
    Ok(MolHandle {
        inner: std::rc::Rc::new(mol),
    })
}

/// Serialise a `MolHandle` to a MolJSON string (pretty-printed).
///
/// Atom IDs are assigned as `"a1"`, `"a2"`, … in molecule atom order.
/// The `hydrogens` field reflects computed implicit H count.
#[wasm_bindgen]
pub fn to_moljson(mol: &MolHandle) -> String {
    chematic_mol::write_moljson(&mol.inner)
}

/// Only the first molecular fragment in the document is returned.
/// Returns a JS error if the document cannot be parsed.
#[wasm_bindgen]
pub fn mol_from_cdxml(cdxml: &str) -> Result<MolHandle, JsValue> {
    if cdxml.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("CDXML input too large"));
    }
    let (mol, _coords) =
        chematic_mol::parse_cdxml(cdxml).map_err(|e| JsValue::from_str(&e.to_string()))?;
    if mol.atom_count() > WASM_MAX_ATOMS {
        return Err(JsValue::from_str(&format!(
            "molecule too large (max {} atoms)",
            WASM_MAX_ATOMS
        )));
    }
    Ok(MolHandle {
        inner: std::rc::Rc::new(mol),
    })
}

/// Parse all molecular fragments from a CDXML string.
///
/// Returns a JSON array of SMILES strings, one per fragment:
/// `["CC","c1ccccc1"]`
///
/// Stereochemistry (wedge/dash bonds) is read from the `Display` attribute
/// of bond elements.
#[wasm_bindgen]
pub fn cdxml_to_smiles_json(cdxml: &str) -> Result<String, JsValue> {
    if cdxml.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("CDXML input too large"));
    }
    let fragments =
        chematic_mol::parse_cdxml_all(cdxml).map_err(|e| JsValue::from_str(&e.to_string()))?;
    let parts: Vec<String> = fragments
        .iter()
        .take(WASM_MAX_BATCH_ITEMS)
        .map(|(mol, _)| {
            format!(
                "\"{}\"",
                escape_json_string(&chematic_smiles::canonical_smiles(mol))
            )
        })
        .collect();
    Ok(format!("[{}]", parts.join(",")))
}

/// Parse a MOL V2000 string and return 2D coordinates as a JSON array.
///
/// Returns `[[x0,y0],[x1,y1],...]` in atom-insertion order.
/// Coordinates are in Ångström as stored in the MOL file.
#[wasm_bindgen]
pub fn mol_block_coords_json(mol_block: &str) -> Result<String, JsValue> {
    let (_mol, _meta, coords) = chematic_mol::parse_mol_with_coords(mol_block)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    let parts: Vec<String> = coords
        .iter()
        .map(|(x, y)| {
            let xf = if x.is_finite() {
                format!("{x:.4}")
            } else {
                "0".to_string()
            };
            let yf = if y.is_finite() {
                format!("{y:.4}")
            } else {
                "0".to_string()
            };
            format!("[{xf},{yf}]")
        })
        .collect();
    Ok(format!("[{}]", parts.join(",")))
}

/// Serialize multiple molecules with properties to an SDF string.
///
/// # Arguments
/// * `smiles_json` — JSON array of SMILES strings, e.g. `["CC(=O)O","c1ccccc1"]`
/// * `names_json`  — JSON array of molecule names (same length as `smiles_json`)
/// * `props_json`  — JSON array where each element encodes one molecule's SD data fields
///   as `"key1\tvalue1\nkey2\tvalue2"` (tab-separated key/value, `\n`-separated pairs;
///   pass `""` for a molecule with no properties)
///
/// Returns the SDF string, or a JS error if any SMILES fails to parse or the
/// arrays have mismatched lengths.
///
/// The `\n` and `\t` sequences in `props_json` are JSON-escaped — they are
/// decoded to the actual characters before SDF formatting.
#[wasm_bindgen]
pub fn sdf_from_records_json(
    smiles_json: &str,
    names_json: &str,
    props_json: &str,
) -> Result<String, JsValue> {
    let smiles_list = parse_smiles_json_array(smiles_json)?;
    let names_list = parse_wasm_string_json_array(names_json, "names_json")?;
    let props_list = parse_wasm_string_json_array(props_json, "props_json")?;

    let n = smiles_list.len();
    if names_list.len() != n || props_list.len() != n {
        return Err(JsValue::from_str(
            "sdf_from_records_json: smiles_json, names_json, and props_json must have the same length",
        ));
    }

    let mut sdf = String::new();
    for i in 0..n {
        let mol = chematic_smiles::parse(&smiles_list[i])
            .map_err(|e| JsValue::from_str(&e.to_string()))?;
        enforce_wasm_molecule_size(&mol)?;

        // Build 2D coordinates using the same pipeline as to_mol_block.
        let layout = chematic_depict::compute_layout(&mol);
        const SCALE: f64 = 1.5 / 40.0;
        let coords: Vec<(f64, f64)> = (0..mol.atom_count())
            .map(|j| {
                let p = layout.get(chematic_core::AtomIdx(j as u32));
                (p.x * SCALE, -p.y * SCALE)
            })
            .collect();

        let meta = chematic_mol::MolMetadata {
            name: names_list[i].clone(),
            comment: String::new(),
        };
        sdf.push_str(&chematic_mol::write_mol_with_coords(&mol, &meta, &coords));

        // SD data fields from "key\tvalue\nkey\tvalue" format.
        for pair in props_list[i].lines() {
            if let Some((key, value)) = pair.split_once('\t') {
                sdf.push_str(&format!("> <{key}>\n{value}\n\n"));
            }
        }

        sdf.push_str("$$$$\n");
    }

    Ok(sdf)
}

// ---------------------------------------------------------------------------
// Sprint Y: XYZ/PDB I/O, per-atom descriptors, SSSR rings,
//           custom ECFP, stereo isomer enumeration
// ---------------------------------------------------------------------------

/// Parse an XYZ file and return a `MolHandle` (topology only; coordinates are discarded).
///
/// Returns a JS error on parse failure.
#[wasm_bindgen]
pub fn mol_from_xyz(xyz: &str) -> Result<MolHandle, JsValue> {
    if xyz.len() > WASM_MAX_INPUT_BYTES {
        return Err(JsValue::from_str("XYZ input too large"));
    }
    let (mol, _coords) =
        chematic_3d::parse_xyz(xyz).map_err(|e| JsValue::from_str(&format!("{e:?}")))?;
    if mol.atom_count() > WASM_MAX_ATOMS {
        return Err(JsValue::from_str(&format!(
            "molecule too large (max {} atoms)",
            WASM_MAX_ATOMS
        )));
    }
    Ok(MolHandle {
        inner: std::rc::Rc::new(mol),
    })
}

// determine_bonds_from_xyz_json removed to reduce WASM bundle size.

/// Serialize a molecule to XYZ format.
///
/// 3D coordinates are generated via distance-geometry placement.
#[wasm_bindgen]
pub fn to_xyz(mol: &MolHandle) -> String {
    let coords = chematic_3d::generate_coords(&mol.inner);
    chematic_3d::write_xyz(&mol.inner, &coords, "")
}

/// Parse a PDB file and return a `MolHandle` (topology only; coordinates are discarded).
///
/// Uses CONECT records for connectivity if present; otherwise infers bonds from
/// atom distances (the same heuristic as the internal `pdb_to_molecule` function).
#[wasm_bindgen]
pub fn mol_from_pdb(pdb: &str) -> MolHandle {
    if pdb.len() > WASM_MAX_JSON_STRING_BYTES {
        return MolHandle {
            inner: std::rc::Rc::new(chematic_core::MoleculeBuilder::new().build()),
        };
    }
    let atoms = chematic_3d::parse_pdb_atoms(pdb);
    if atoms.len() > WASM_MAX_ATOMS {
        return MolHandle {
            inner: std::rc::Rc::new(chematic_core::MoleculeBuilder::new().build()),
        };
    }
    let (mol, _coords) = chematic_3d::pdb_to_molecule(&atoms);
    MolHandle {
        inner: std::rc::Rc::new(mol),
    }
}

/// Parse a Tripos MOL2 string and return SMILES.
///
/// Returns `"error:<msg>"` on failure.
#[wasm_bindgen]
pub fn mol2_to_smiles(mol2_str: &str) -> String {
    match chematic_mol::parse_mol2(mol2_str) {
        Ok((mol, _)) => chematic_smiles::write(&mol),
        Err(e) => format!("error:{e}"),
    }
}

/// Convert a SMILES to a minimal Tripos MOL2 string (no 3D coordinates).
///
/// Returns `"error:<msg>"` on parse failure.
#[wasm_bindgen]
pub fn smiles_to_mol2(smiles: &str) -> String {
    match chematic_smiles::parse(smiles) {
        Ok(mol) => chematic_mol::write_mol2(&mol, &[]),
        Err(e) => format!("error:{e}"),
    }
}

/// Write a molecule to AutoDock PDBQT format.
///
/// `coords_json` — JSON array of `[x,y,z]` arrays (Å). Pass `"[]"` for zero coords.
/// `charges_json` — JSON array of partial charges. Pass `"[]"` to write zeros.
/// `name` — ligand name for the REMARK header.
///
/// Returns the PDBQT string, or `"error:<msg>"` on failure.
#[wasm_bindgen]
pub fn smiles_to_pdbqt(smiles: &str, coords_json: &str, charges_json: &str, name: &str) -> String {
    let mol = match chematic_smiles::parse(smiles) {
        Ok(m) => m,
        Err(e) => return format!("error:{e}"),
    };
    let coords: Vec<(f64, f64, f64)> = serde_json::from_str::<Vec<[f64; 3]>>(coords_json)
        .unwrap_or_default()
        .into_iter()
        .map(|c| (c[0], c[1], c[2]))
        .collect();
    let charges: Vec<f64> = serde_json::from_str(charges_json).unwrap_or_default();
    chematic_mol::write_pdbqt(&mol, &coords, &charges, name)
}

/// Generate InChI string from SMILES.
///
/// Returns `"error:<msg>"` on parse failure.
#[wasm_bindgen]
pub fn inchi_from_smiles(smiles: &str) -> String {
    match chematic_smiles::parse(smiles) {
        Ok(mol) => chematic_inchi::inchi(&mol),
        Err(e) => format!("error:{e}"),
    }
}

/// Generate InChIKey from SMILES (27-character identifier).
///
/// Returns `"error:<msg>"` on parse failure.
#[wasm_bindgen]
pub fn inchikey_from_smiles(smiles: &str) -> String {
    match chematic_smiles::parse(smiles) {
        Ok(mol) => {
            let inchi_str = chematic_inchi::inchi(&mol);
            chematic_inchi::inchi_key(&inchi_str)
        }
        Err(e) => format!("error:{e}"),
    }
}
