"""
Python code generation for enums and type definitions.

Generates:
- types/molecule.py - Molecule enum
- biochemistry/_generated_elements.py - Element enum
- biochemistry/_generated_atoms.py - Atom classes
- biochemistry/_generated_residues.py - Residue class
"""

from __future__ import annotations

import ast
from pathlib import Path

from .config import MOLECULE_TYPES, Molecule, TERMINAL_ATOMS
from .names import to_python_name
from .residue import ResidueDefinition


class CodegenError(Exception):
    """Error during code generation."""
    pass


def _write_validated_python(filepath: Path, lines: list[str], description: str) -> None:
    """
    Validate and write generated Python code.

    Args:
        filepath: Where to write the file.
        lines: Lines of Python code to join and write.
        description: Human-readable description for error messages.

    Raises:
        CodegenError: If the generated code has syntax errors.
    """
    code = '\n'.join(lines)

    # Validate syntax before writing
    try:
        ast.parse(code)
    except SyntaxError as e:
        # Find the problematic line for context
        code_lines = code.split('\n')
        context_start = max(0, (e.lineno or 1) - 3)
        context_end = min(len(code_lines), (e.lineno or 1) + 2)
        context = '\n'.join(
            f"  {i+1:4d} | {line}"
            for i, line in enumerate(code_lines[context_start:context_end], start=context_start)
        )

        raise CodegenError(
            f"Generated code has syntax error in {description}:\n"
            f"  Line {e.lineno}: {e.msg}\n"
            f"  Context:\n{context}\n"
            f"  This is a bug in the code generator."
        ) from None

    filepath.write_text(code)
    print(f"Generated: {filepath.relative_to(filepath.parent.parent)}")


def generate_python_molecule(biochem_dir: Path) -> None:
    """Generate Python Molecule enum from MOLECULE_TYPES."""

    lines = [
        '"""',
        'Molecule type enumeration.',
        '',
        'Based on PDB/mmCIF entity types from wwPDB:',
        '- _entity.type: polymer, non-polymer, water, branched',
        '- _entity_poly.type: polypeptide(L/D), polyribonucleotide, etc.',
        '',
        'See: https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity_poly.type.html',
        '',
        'AUTO-GENERATED by codegen/generate.py - DO NOT EDIT MANUALLY.',
        '"""',
        '',
        'from enum import IntEnum',
        '',
        '',
        'class Molecule(IntEnum):',
        '    """',
        '    Types of molecules that can appear in a structure.',
        '',
        '    Used to classify chains by their molecular type, enabling',
        '    filtering and type-specific operations.',
        '    """',
        '',
    ]

    for idx, mt in enumerate(MOLECULE_TYPES):
        lines.append(f"    {mt.name} = {idx}  # {mt.description}")

    lines.extend([
        '',
        '',
        'def molecule_type(value: int) -> Molecule:',
        '    """',
        '    Convert an integer value to the corresponding Molecule type.',
        '',
        '    Args:',
        '        value: Integer representing molecule type.',
        '',
        '    Returns:',
        '        The corresponding Molecule enum value.',
        '',
        '    Raises:',
        '        ValueError: If value doesn\'t correspond to a known molecule type.',
        '    """',
        '    try:',
        '        return Molecule(value)',
        '    except ValueError as e:',
        '        raise ValueError(f"Unknown molecule type value: {value}") from e',
        '',
        '',
        '# Terminal atoms per molecule type: (start_atoms, end_atoms)',
        '# Used for filtering during chain building with AtomGroup.terminal()',
        'TERMINAL_ATOMS: dict[int, tuple[frozenset[str], frozenset[str]]] = {',
    ])

    # Generate TERMINAL_ATOMS dict entries
    for mol_type, (start_atoms, end_atoms) in TERMINAL_ATOMS.items():
        # Use double quotes for atom names containing apostrophes
        start_str = ', '.join(f'"{a}"' for a in sorted(start_atoms))
        end_str = ', '.join(f'"{a}"' for a in sorted(end_atoms))
        lines.append(f"    Molecule.{MOLECULE_TYPES[mol_type].name}: (frozenset({{{start_str}}}), frozenset({{{end_str}}})),")

    lines.extend([
        '}',
        '',
    ])

    _write_validated_python(
        biochem_dir / "_generated_molecule.py",
        lines,
        "Molecule enum"
    )


def generate_python_elements(biochem_dir: Path, elements: dict[str, int]) -> None:
    """
    Generate Python Element enum from elements dict.

    Args:
        biochem_dir: Output directory for generated file.
        elements: Dictionary mapping element symbol (uppercase) to atomic number.
                  Loaded from PubChem via elements.load_elements().
    """
    lines = [
        '"""',
        'Chemical element definitions.',
        '',
        'AUTO-GENERATED by ciffy/src/codegen/generate.py - DO NOT EDIT MANUALLY.',
        'Source: PubChem Periodic Table (https://pubchem.ncbi.nlm.nih.gov)',
        '"""',
        '',
        'from enum import IntEnum',
        '',
        '',
        'class Element(IntEnum):',
        '    """',
        '    Chemical elements with their atomic numbers.',
        '',
        '    Values correspond to atomic numbers for common biological elements.',
        '    """',
        '',
    ]

    # Group elements by category for readability
    organic = ["H", "C", "N", "O", "P", "S"]
    halogens = ["F", "CL", "BR", "I"]
    alkali = ["LI", "NA", "K", "RB", "CS"]
    alkaline = ["MG", "CA", "SR", "BA"]
    transition = ["MN", "FE", "CO", "NI", "CU", "ZN", "MO", "AG", "CD", "W", "PT", "AU", "HG"]
    other = ["AL", "SE", "PB"]

    def write_group(name: str, symbols: list[str]) -> None:
        lines.append(f"    # {name}")
        for sym in symbols:
            if sym in elements:
                lines.append(f"    {sym} = {elements[sym]}")
        lines.append("")

    write_group("Common organic elements", organic)
    write_group("Halogens", halogens)
    write_group("Alkali metals", alkali)
    write_group("Alkaline earth metals", alkaline)
    write_group("Transition metals", transition)
    write_group("Other elements", other)

    lines.extend([
        '',
        '# Pre-computed reverse lookup: atomic number -> element name',
        'ELEMENT_NAMES: dict[int, str] = {e.value: e.name for e in Element}',
        '',
    ])

    _write_validated_python(
        biochem_dir / "_generated_elements.py",
        lines,
        "Element enum"
    )


# =============================================================================
# NEW ATOM SYSTEM (AtomGroup-based)
# =============================================================================


def generate_python_atoms(
    biochem_dir: Path,
    atom_index: dict[tuple[str, str], int],
    all_residues: list[ResidueDefinition],
    backbone_values: dict[str, int],
) -> None:
    """Generate Python atom data for new AtomGroup system.

    Generates loader functions for lazy loading of atom data.
    Each loader returns (atoms_dict, ideal_coords, bonds) when called.
    Also generates UNIFIED_BACKBONE_VALUES for runtime use.
    """

    # Build per-residue atom dicts
    # Maps residue_cif_name -> {py_atom_name: (global_idx, cif_atom_name)}
    residue_atoms: dict[str, dict[str, tuple[int, str]]] = {}
    for (cif_name, atom), idx in atom_index.items():
        residue_atoms.setdefault(cif_name, {})[to_python_name(atom)] = (idx, atom)

    # Group residues by type
    by_type: dict[int, list[ResidueDefinition]] = {}
    for res in all_residues:
        by_type.setdefault(res.molecule_type, []).append(res)

    lines = [
        '"""',
        'Auto-generated atom loader functions for AtomGroup system.',
        'DO NOT EDIT - Generated by ciffy/codegen/generate.py from CCD.',
        '',
        'This file contains lazy loader functions for atom data.',
        'Data is loaded only when first accessed to minimize import time.',
        '"""',
        '',
        'import numpy as np',
        '',
        'from .atom import Atom',
        '',
        '',
        '# ' + '=' * 77,
        '# UNIFIED BACKBONE VALUES',
        '# ' + '=' * 77,
        '# Backbone atoms have unified values shared across all residue types.',
        '# Values are auto-assigned in CCD order (first residue containing each atom).',
        '',
        'UNIFIED_BACKBONE_VALUES: dict[str, int] = {',
    ]

    # Generate backbone values dict (sorted by value for readability)
    for atom_name, value in sorted(backbone_values.items(), key=lambda x: x[1]):
        lines.append(f'    "{atom_name}": {value},')

    lines.extend([
        '}',
        '',
        f'NUM_UNIFIED_BACKBONE: int = {len(backbone_values)}',
        '',
        '',
    ])

    # Generate per-residue loader functions
    sections = [
        ("RNA", Molecule.RNA),
        ("DNA", Molecule.DNA),
        ("PROTEIN", Molecule.PROTEIN),
        ("WATER", Molecule.WATER),
        ("ION", Molecule.ION),
        ("LIGAND", Molecule.LIGAND),
    ]

    for section_name, mol_type in sections:
        residues = by_type.get(mol_type, [])
        if not residues:
            continue

        lines.append(f"# {'=' * 77}")
        lines.append(f"# {section_name}")
        lines.append(f"# {'=' * 77}")
        lines.append('')

        for res in residues:
            cif = res.cif_names[0]
            atoms = residue_atoms.get(cif, {})
            if not atoms:
                continue

            # Determine if we have geometry
            has_ideal = res.ideal_coords and res.atoms
            has_bonds = bool(res.bonds)

            # Generate loader function
            lines.append(f'def _load_{res.class_name}():')
            lines.append(f'    """Load {res.class_name} residue data."""')

            # Atoms dict - key is Python-safe name, Atom.name is PDB/CIF standard
            lines.append('    atoms = {')
            for local_idx, (py_name, (global_idx, cif_name)) in enumerate(atoms.items()):
                lines.append(f'        "{py_name}": Atom("{cif_name}", {global_idx}, {local_idx}),')
            lines.append('    }')

            # Ideal coordinates
            if has_ideal:
                coords = []
                for atom in res.atoms:
                    if atom in res.ideal_coords:
                        x, y, z = res.ideal_coords[atom]
                        coords.append(f"[{x:.3f}, {y:.3f}, {z:.3f}]")
                    else:
                        coords.append("[0.0, 0.0, 0.0]")
                if coords:
                    lines.append("    ideal = np.array([")
                    for i, coord in enumerate(coords):
                        comma = "," if i < len(coords) - 1 else ""
                        lines.append(f"        {coord}{comma}")
                    lines.append("    ], dtype=np.float32)")
                else:
                    lines.append("    ideal = None")
            else:
                lines.append("    ideal = None")

            # Bonds
            if has_bonds:
                lines.append("    bonds = np.array([")
                for atom1, atom2 in res.bonds:
                    py_atom1 = to_python_name(atom1)
                    py_atom2 = to_python_name(atom2)
                    local_idx1 = list(atoms.keys()).index(py_atom1)
                    local_idx2 = list(atoms.keys()).index(py_atom2)
                    lines.append(f"        [{local_idx1}, {local_idx2}],")
                lines.append("    ], dtype=np.int64)")
            else:
                lines.append("    bonds = None")

            lines.append("    return atoms, ideal, bonds")
            lines.append('')
            lines.append('')

    # Reverse lookup
    # Note: With unified backbone atoms, multiple (res, atom) pairs map to the same idx.
    # We deduplicate by building a dict first - all backbone atoms with the same idx
    # have the same name anyway.
    lines.append(f"# {'=' * 77}")
    lines.append("# REVERSE LOOKUP")
    lines.append(f"# {'=' * 77}")
    lines.append('')
    lines.append("# Maps atom index -> atom name (backbone atoms are shared across residues)")
    atom_names_dedup: dict[int, str] = {}
    for (res, atom), idx in atom_index.items():
        if idx not in atom_names_dedup:
            atom_names_dedup[idx] = atom
    lines.append("ATOM_NAMES: dict[int, str] = {")
    for idx in sorted(atom_names_dedup.keys()):
        lines.append(f'    {idx}: "{atom_names_dedup[idx]}",')
    lines.append("}")
    lines.append('')

    _write_validated_python(
        biochem_dir / "_generated_atoms.py",
        lines,
        "Atom definitions"
    )


def generate_python_residues(
    biochem_dir: Path,
    all_residues: list[ResidueDefinition],
) -> None:
    """Generate Python Residue class using new AtomGroup system.

    Creates AtomGroup instances with lazy loaders for deferred data loading.
    Data is only loaded when the residue is first accessed, minimizing import time.
    """

    # Determine which residues have atoms (and thus need loaders)
    residues_with_atoms = [res for res in all_residues if res.atoms]
    atom_class_names = [res.class_name for res in residues_with_atoms]

    # Build set of residue names with atoms
    has_atoms = {res.class_name for res in residues_with_atoms}

    lines = [
        '"""',
        'Auto-generated residue definitions.',
        'DO NOT EDIT - Generated by ciffy/codegen/generate.py from CCD.',
        '',
        'Uses lazy loading: atom data is loaded only when first accessed.',
        '"""',
        '',
        'from .atom import AtomGroup',
        'from ._residue_base import ResidueBase',
        'from ._generated_molecule import Molecule',
    ]

    # Import loader functions
    if atom_class_names:
        lines.append('from ._generated_atoms import (')
        for name in atom_class_names:
            lines.append(f'    _load_{name},')
        lines.append(')')

    lines.append('')
    lines.append('')
    lines.append('class Residue(ResidueBase):')
    lines.append('    """See ResidueBase for documentation and methods."""')
    lines.append('')

    # Generate residue members with lazy loaders
    # Note: indices start at 1 (0 is reserved for unknown/sentinel)
    for idx, res in enumerate(all_residues):
        mol_name = MOLECULE_TYPES[res.molecule_type].name
        value = idx + 1  # Start at 1, not 0
        if res.class_name in has_atoms:
            # Use lazy loader - members=None, loader=function
            lines.append(
                f'    {res.name} = AtomGroup('
                f'"{res.name}", None, '
                f'value={value}, molecule_type=Molecule.{mol_name}.value, '
                f'abbrev="{res.abbreviation}", '
                f'loader=_load_{res.class_name})'
            )
        else:
            # No atoms - use empty dict directly
            lines.append(
                f'    {res.name} = AtomGroup('
                f'"{res.name}", {{}}, '
                f'value={value}, molecule_type=Molecule.{mol_name}.value, '
                f'abbrev="{res.abbreviation}")'
            )

    lines.append('')
    lines.append('')
    lines.append('Residue._init_members()')
    lines.append('')

    _write_validated_python(
        biochem_dir / "_generated_residues.py",
        lines,
        "Residue definitions"
    )


def generate_python_dihedraltypes(biochem_dir: Path) -> None:
    """Generate Python DihedralType IntEnum from DIHEDRAL_TYPES."""
    from .config import DIHEDRAL_TYPES, Molecule

    lines = [
        '"""',
        'Dihedral angle type enumeration.',
        '',
        'AUTO-GENERATED by codegen/generate.py - DO NOT EDIT MANUALLY.',
        '"""',
        '',
        'from enum import IntEnum',
        '',
        '',
        'class DihedralType(IntEnum):',
        '    """',
        '    Named dihedral angle types for proteins and nucleic acids.',
        '',
        '    Dihedral angles describe rotations around bonds and are fundamental',
        '    to describing the conformation of biomolecules. Values are integers',
        '    that can be used directly as array indices.',
        '',
        '    Protein backbone dihedrals:',
        '        PHI: C(i-1) - N(i) - CA(i) - C(i)',
        '        PSI: N(i) - CA(i) - C(i) - N(i+1)',
        '        OMEGA: CA(i) - C(i) - N(i+1) - CA(i+1)',
        '',
        '    Nucleic acid backbone dihedrals:',
        '        ALPHA: O3\'(i-1) - P(i) - O5\'(i) - C5\'(i)',
        '        BETA: P(i) - O5\'(i) - C5\'(i) - C4\'(i)',
        '        GAMMA: O5\'(i) - C5\'(i) - C4\'(i) - C3\'(i)',
        '        DELTA: C5\'(i) - C4\'(i) - C3\'(i) - O3\'(i)',
        '        EPSILON: C4\'(i) - C3\'(i) - O3\'(i) - P(i+1)',
        '        ZETA: C3\'(i) - O3\'(i) - P(i+1) - O5\'(i+1)',
        '',
        '    Glycosidic dihedrals:',
        '        CHI_PURINE: O4\' - C1\' - N9 - C4 (adenine, guanine)',
        '        CHI_PYRIMIDINE: O4\' - C1\' - N1 - C2 (cytosine, uracil, thymine)',
        '',
        '    Protein sidechain dihedrals:',
        '        CHI1: N - CA - CB - XG (varies by residue)',
        '        CHI2: CA - CB - CG - XD (varies by residue)',
        '        CHI3: CB - CG - CD - XE (varies by residue)',
        '        CHI4: CG - CD - XE - XZ (LYS, ARG only)',
        '    """',
        '',
    ]

    # Generate enum members
    for idx, d in enumerate(DIHEDRAL_TYPES):
        lines.append(f'    {d.name} = {idx}')

    lines.append('')
    lines.append('')

    # Generate convenience tuples for dihedral groupings
    protein_backbone = [d.name for d in DIHEDRAL_TYPES
                        if d.is_backbone and Molecule.PROTEIN in d.molecule_types]
    rna_backbone = [d.name for d in DIHEDRAL_TYPES
                    if d.is_backbone and Molecule.RNA in d.molecule_types]
    rna_glycosidic = [d.name for d in DIHEDRAL_TYPES
                      if d.is_glycosidic and Molecule.RNA in d.molecule_types]
    protein_sidechain = [d.name for d in DIHEDRAL_TYPES
                         if d.is_sidechain and Molecule.PROTEIN in d.molecule_types]

    # PROTEIN_BACKBONE
    lines.append('# Convenience tuples for common dihedral groups')
    members = ', '.join(f'DihedralType.{name}' for name in protein_backbone)
    lines.append(f'PROTEIN_BACKBONE: tuple[DihedralType, ...] = ({members},)')
    lines.append('')

    # RNA_BACKBONE
    members = ', '.join(f'DihedralType.{name}' for name in rna_backbone)
    lines.append(f'RNA_BACKBONE: tuple[DihedralType, ...] = ({members},)')
    lines.append('')

    # RNA_GLYCOSIDIC
    members = ', '.join(f'DihedralType.{name}' for name in rna_glycosidic)
    lines.append(f'RNA_GLYCOSIDIC: tuple[DihedralType, ...] = ({members},)')
    lines.append('')

    # RNA_BACKBONE_EXTENDED (includes CHI_PYRIMIDINE for common use case)
    lines.append('# Extended backbone includes glycosidic for full conformational description')
    lines.append('RNA_BACKBONE_EXTENDED: tuple[DihedralType, ...] = RNA_BACKBONE + (DihedralType.CHI_PYRIMIDINE,)')
    lines.append('')

    # PROTEIN_SIDECHAIN
    members = ', '.join(f'DihedralType.{name}' for name in protein_sidechain)
    lines.append(f'PROTEIN_SIDECHAIN: tuple[DihedralType, ...] = ({members},)')
    lines.append('')

    # String -> enum lookup
    lines.append('')
    lines.append('# String name -> DihedralType lookup')
    lines.append('DIHEDRAL_NAME_TO_TYPE: dict[str, DihedralType] = {')
    for d in DIHEDRAL_TYPES:
        lines.append(f'    "{d.name.lower()}": DihedralType.{d.name},')
    lines.append('}')
    lines.append('')

    # Atom definitions per dihedral type
    lines.append('')
    lines.append('# Atom names defining each dihedral type (Python naming convention)')
    lines.append('DIHEDRAL_ATOMS: dict[DihedralType, tuple[str, str, str, str]] = {')
    for d in DIHEDRAL_TYPES:
        atoms_str = ', '.join(f'"{a}"' for a in d.atoms)
        lines.append(f'    DihedralType.{d.name}: ({atoms_str}),')
    lines.append('}')
    lines.append('')

    # Legacy constants for backwards compatibility
    lines.append('')
    lines.append('# Legacy constants (for backwards compatibility)')
    lines.append('INDEX_TO_DIHEDRAL_TYPE: dict[int, DihedralType] = {dt.value: dt for dt in DihedralType}')
    lines.append('NUM_PROTEIN_BACKBONE_DIHEDRALS: int = len(PROTEIN_BACKBONE)')
    lines.append('NUM_RNA_BACKBONE_DIHEDRALS: int = len(RNA_BACKBONE_EXTENDED)')
    lines.append('MAX_DIHEDRALS_PER_RESIDUE: int = max(NUM_PROTEIN_BACKBONE_DIHEDRALS, NUM_RNA_BACKBONE_DIHEDRALS)')
    lines.append('')

    _write_validated_python(
        biochem_dir / "_generated_dihedraltypes.py",
        lines,
        "DihedralType enum"
    )


def generate_linking_constants(biochem_dir: Path, monlib_path: Path) -> None:
    """
    Generate inter-residue linkage geometry constants from MonomerLibrary.

    Parses _chem_link_bond and _chem_link_angle to extract geometry for:
    - Phosphodiester linkage (RNA/DNA): O3'-P bond
    - Peptide linkage (protein): C-N bond

    Args:
        biochem_dir: Output directory for generated file.
        monlib_path: Path to links_and_mods.cif.
    """
    from .monlib import parse_link_bonds, parse_link_angles

    # Parse links
    p_bonds = []
    p_angles = []
    trans_bonds = []
    trans_angles = []

    for bond in parse_link_bonds(monlib_path):
        if bond.link_id == "p":
            p_bonds.append(bond)
        elif bond.link_id == "TRANS":
            trans_bonds.append(bond)

    for angle in parse_link_angles(monlib_path):
        if angle.link_id == "p":
            p_angles.append(angle)
        elif angle.link_id == "TRANS":
            trans_angles.append(angle)

    # Validate we found what we expected
    if not p_bonds:
        raise CodegenError("No phosphodiester bond (link_id='p') found in MonomerLibrary")
    if not trans_bonds:
        raise CodegenError("No peptide bond (link_id='TRANS') found in MonomerLibrary")

    # Get the primary linking bonds (should be exactly one each)
    p_bond = p_bonds[0]  # O3'-P
    trans_bond = trans_bonds[0]  # C-N

    lines = [
        '"""',
        'Inter-residue linkage geometry constants.',
        '',
        'AUTO-GENERATED from MonomerLibrary (links_and_mods.cif) - DO NOT EDIT MANUALLY.',
        '',
        'Source: https://github.com/MonomerLibrary/monomers',
        'License: LGPL-3.0',
        '',
        'Contains geometry data for inter-residue bonds and angles:',
        "- Phosphodiester linkage (RNA/DNA): O3'(i) - P(i+1)",
        '- Peptide linkage (protein): C(i) - N(i+1)',
        '"""',
        '',
        'from dataclasses import dataclass',
        '',
        '',
        '@dataclass(frozen=True)',
        'class LinkAngleData:',
        '    """Inter-residue bond angle with uncertainty."""',
        '    atoms: tuple[str, str, str]  # (atom1, vertex, atom3)',
        '    comps: tuple[int, int, int]  # Component ids (1=prev, 2=next residue)',
        '    value: float  # Angle in degrees',
        '    esd: float  # Standard deviation',
        '',
        '',
        '@dataclass(frozen=True)',
        'class LinkGeometry:',
        '    """Complete inter-residue linkage geometry."""',
        '    prev_atom: str  # Atom from previous residue',
        '    next_atom: str  # Atom from next residue',
        '    bond_length: float  # Bond length in Angstroms',
        '    bond_length_esd: float  # Standard deviation',
        '    angles: tuple[LinkAngleData, ...]  # Angles involving the linkage',
        '',
        '',
    ]

    # Generate phosphodiester geometry
    lines.append('# Phosphodiester linkage: O3\'(i) - P(i+1)')
    lines.append('NUCLEIC_ACID_LINK_GEOMETRY = LinkGeometry(')
    lines.append(f'    prev_atom="{p_bond.atom_1}",')
    lines.append(f'    next_atom="{p_bond.atom_2}",')
    lines.append(f'    bond_length={p_bond.value_dist:.4f},')
    lines.append(f'    bond_length_esd={p_bond.value_dist_esd:.5f},')
    lines.append('    angles=(')
    for angle in p_angles:
        lines.append(f'        LinkAngleData(')
        lines.append(f'            atoms=("{angle.atom_1}", "{angle.atom_2}", "{angle.atom_3}"),')
        lines.append(f'            comps=({angle.comp_1}, {angle.comp_2}, {angle.comp_3}),')
        lines.append(f'            value={angle.value_angle:.3f},')
        lines.append(f'            esd={angle.value_angle_esd:.2f},')
        lines.append(f'        ),')
    lines.append('    ),')
    lines.append(')')
    lines.append('')
    lines.append('')

    # Generate peptide geometry
    lines.append('# Peptide linkage: C(i) - N(i+1)')
    lines.append('PEPTIDE_LINK_GEOMETRY = LinkGeometry(')
    lines.append(f'    prev_atom="{trans_bond.atom_1}",')
    lines.append(f'    next_atom="{trans_bond.atom_2}",')
    lines.append(f'    bond_length={trans_bond.value_dist:.4f},')
    lines.append(f'    bond_length_esd={trans_bond.value_dist_esd:.5f},')
    lines.append('    angles=(')
    for angle in trans_angles:
        lines.append(f'        LinkAngleData(')
        lines.append(f'            atoms=("{angle.atom_1}", "{angle.atom_2}", "{angle.atom_3}"),')
        lines.append(f'            comps=({angle.comp_1}, {angle.comp_2}, {angle.comp_3}),')
        lines.append(f'            value={angle.value_angle:.3f},')
        lines.append(f'            esd={angle.value_angle_esd:.2f},')
        lines.append(f'        ),')
    lines.append('    ),')
    lines.append(')')
    lines.append('')

    _write_validated_python(
        biochem_dir / "_generated_linking.py",
        lines,
        "Linking geometry constants"
    )
