"""
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

from pathlib import Path

import numpy as np

from .config import ELEMENTS, MOLECULE_TYPES, Molecule
from .names import to_python_name
from .residue import (
    ResidueDefinition,
    compute_dihedral_patterns,
    compute_atom_dihedral_ownership,
    compute_canonical_zmatrix_refs,
)


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 ..utils import IndexEnum',
        '',
        '',
        'class Molecule(IndexEnum):',
        '    """',
        '    Types of molecules that can appear in a structure.',
        '',
        '    Used to classify chains by their molecular type, enabling',
        '    filtering and type-specific operations.',
        '',
        '    Example:',
        '        >>> Molecule.dict()  # Returns name -> value mapping',
        '        {"PROTEIN": 0, "RNA": 1, ...}',
        '        >>> Molecule.index()  # Returns numpy array of indices',
        '        array([0, 1, 2, ...])',
        '    """',
        '',
    ]

    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',
        '',
    ])

    (biochem_dir / "_generated_molecule.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_molecule.py")


def generate_python_elements(biochem_dir: Path) -> None:
    """Generate Python Element enum from ELEMENTS dict."""

    lines = [
        '"""',
        'Chemical element definitions.',
        '',
        'AUTO-GENERATED by ciffy/src/codegen/generate.py - DO NOT EDIT MANUALLY.',
        '"""',
        '',
        'from ..utils import IndexEnum',
        '',
        '',
        'class Element(IndexEnum):',
        '    """',
        '    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}',
        '',
    ])

    (biochem_dir / "_generated_elements.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_elements.py")


def generate_python_atoms(
    biochem_dir: Path,
    atom_index: dict[tuple[str, str], int],
    all_residues: list[ResidueDefinition],
) -> None:
    """Generate Python atom enum file."""

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

    # 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 enum definitions.',
        'DO NOT EDIT - Generated by ciffy/src/codegen/generate.py from CCD.',
        '"""',
        '',
        'import numpy as np',
        '',
        'from ..utils import IndexEnum, PairEnum',
        '',
        '',
    ]

    # Generate per-residue classes
    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

            lines.append(f"class {res.class_name}(IndexEnum):")
            lines.append(f'    """{res.class_name} ({cif}) atom indices."""')
            for py_name, idx in atoms.items():
                lines.append(f"    {py_name} = {idx}")
            lines.append('')

            # Add ideal coordinates as class attribute
            if res.ideal_coords and res.atoms:
                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(f"{res.class_name}.ideal = np.array([")
                    for i, coord in enumerate(coords):
                        comma = "," if i < len(coords) - 1 else ""
                        lines.append(f"    {coord}{comma}  # {res.atoms[i]}")
                    lines.append("], dtype=np.float32)")
                    lines.append('')

            # Add bonds as PairEnum
            if res.bonds:
                lines.append(f"{res.class_name}.bonds = PairEnum([")
                for atom1, atom2 in res.bonds:
                    py_atom1 = to_python_name(atom1)
                    py_atom2 = to_python_name(atom2)
                    lines.append(f"    ({res.class_name}.{py_atom1}, {res.class_name}.{py_atom2}),")
                lines.append("])")
                lines.append('')

                # Add bond_indices as numpy array
                lines.append(f"{res.class_name}.bond_indices = np.array([")
                for atom1, atom2 in res.bonds:
                    py_atom1 = to_python_name(atom1)
                    py_atom2 = to_python_name(atom2)
                    idx1 = atoms[py_atom1]
                    idx2 = atoms[py_atom2]
                    lines.append(f"    [{idx1}, {idx2}],")
                lines.append("], dtype=np.int32)")
                lines.append('')

            # Add dihedral_patterns as dict of numpy arrays
            # Each pattern is a (4, 2) array: [[offset1, idx1], [offset2, idx2], ...]
            dihedral_patterns = compute_dihedral_patterns(res)
            if dihedral_patterns:
                lines.append(f"{res.class_name}.dihedral_patterns = {{")
                for type_idx, pattern in sorted(dihedral_patterns.items()):
                    # pattern is list of (offset, idx) tuples
                    array_repr = [[offset, idx] for offset, idx in pattern]
                    lines.append(f"    {type_idx}: np.array({array_repr}, dtype=np.int32),")
                lines.append("}")
                lines.append('')

            lines.append('')

    # Combined enums
    lines.append(f"# {'=' * 77}")
    lines.append("# COMBINED ENUMS")
    lines.append(f"# {'=' * 77}")
    lines.append('')

    rna = by_type.get(Molecule.RNA, [])
    dna = by_type.get(Molecule.DNA, [])
    protein = by_type.get(Molecule.PROTEIN, [])

    rna_bases = [r for r in rna if r.cif_names[0] in ("A", "C", "G", "U")]
    if rna_bases:
        lines.append("RibonucleicAcid = IndexEnum(")
        lines.append("    'RibonucleicAcid',")
        parts = [f'{r.class_name}.dict("{r.cif_names[0]}_")' for r in rna_bases]
        lines.append("    " + " |\n    ".join(parts))
        lines.append(")")
        lines.append('')
        lines.append("RibonucleicAcidNoPrefix = IndexEnum(")
        lines.append("    'RibonucleicAcid',")
        parts = [f'{r.class_name}.dict()' for r in rna_bases]
        lines.append("    " + " |\n    ".join(parts))
        lines.append(")")
        lines.append('')

    dna_bases = [r for r in dna if r.cif_names[0] in ("DA", "DC", "DG", "DT")]
    if dna_bases:
        lines.append("DeoxyribonucleicAcid = IndexEnum(")
        lines.append("    'DeoxyribonucleicAcid',")
        parts = [f'{r.class_name}.dict("{r.cif_names[0]}_")' for r in dna_bases]
        lines.append("    " + " |\n    ".join(parts))
        lines.append(")")
        lines.append('')

    modified = [r for r in all_residues
                if r.molecule_type in (Molecule.RNA, Molecule.DNA)
                and r.cif_names[0] not in ("A", "C", "G", "U", "DA", "DC", "DG", "DT")]
    if modified:
        lines.append("ModifiedNucleotides = IndexEnum(")
        lines.append("    'ModifiedNucleotides',")
        parts = [f'{r.class_name}.dict("{r.cif_names[0]}_")' for r in modified]
        lines.append("    " + " |\n    ".join(parts))
        lines.append(")")
        lines.append('')

    if protein:
        lines.append("AminoAcids = IndexEnum(")
        lines.append("    'AminoAcids',")
        parts = [f'{r.class_name}.dict("{r.cif_names[0]}_")' for r in protein]
        lines.append("    " + " |\n    ".join(parts))
        lines.append(")")
        lines.append('')

    # Reverse lookup
    lines.append(f"# {'=' * 77}")
    lines.append("# REVERSE LOOKUP")
    lines.append(f"# {'=' * 77}")
    lines.append('')
    lines.append("ATOM_NAMES: dict[int, str] = {")
    for (res, atom), idx in sorted(atom_index.items(), key=lambda x: x[1]):
        lines.append(f'    {idx}: "{atom}",')
    lines.append("}")
    lines.append('')

    (biochem_dir / "_generated_atoms.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_atoms.py")


def generate_python_residues(
    biochem_dir: Path,
    all_residues: list[ResidueDefinition],
) -> None:
    """Generate Python Residue class with ResidueType instances."""

    # Determine which residues have atom classes (non-empty atoms list)
    residues_with_atoms = [res for res in all_residues if res.atoms]
    atom_class_names = [res.class_name for res in residues_with_atoms]

    lines = [
        '"""',
        'Auto-generated residue definitions.',
        'DO NOT EDIT - Generated by ciffy/src/codegen/generate.py from CCD.',
        '"""',
        '',
        'from ..utils import ResidueType, ResidueMeta',
        'from ._generated_molecule import Molecule',
    ]

    # Import atom classes (only those that exist)
    if atom_class_names:
        lines.append('from ._generated_atoms import (')
        for name in atom_class_names:
            lines.append(f'    {name},')
        lines.append(')')

    lines.append('')
    lines.append('')
    lines.append('class Residue(metaclass=ResidueMeta):')
    lines.append('    """')
    lines.append('    Consolidated residue definitions with nested atom access.')
    lines.append('')
    lines.append('    Usage:')
    lines.append('        Residue.A.value         # -> residue index')
    lines.append('        Residue.A.C3p.value     # -> atom index')
    lines.append('        Residue.A.molecule_type # -> Molecule.RNA')
    lines.append('        Residue.A.bonds         # -> bond list')
    lines.append('')
    lines.append('        list(Residue)           # -> iterate all residues')
    lines.append('        Residue(0)              # -> lookup by index')
    lines.append('        Residue["ALA"]          # -> lookup by name')
    lines.append('    """')
    lines.append('')

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

    for idx, res in enumerate(all_residues):
        mol_name = MOLECULE_TYPES[res.molecule_type].name
        atom_class = res.class_name if res.class_name in has_atoms else "None"
        lines.append(
            f'    {res.name} = ResidueType('
            f'"{res.name}", {idx}, {atom_class}, '
            f'Molecule.{mol_name}, "{res.abbreviation}")'
        )

    lines.append('')
    lines.append('    _members: dict[str, ResidueType] = {}')
    lines.append('    _by_index: dict[int, ResidueType] = {}')
    lines.append('')
    lines.append('')

    # Initialization function
    lines.append('def _init_residue_class() -> None:')
    lines.append('    """Populate lookup tables from class attributes."""')
    lines.append('    for name in dir(Residue):')
    lines.append('        if name.startswith("_"):')
    lines.append('            continue')
    lines.append('        attr = getattr(Residue, name)')
    lines.append('        if isinstance(attr, ResidueType):')
    lines.append('            Residue._members[name] = attr')
    lines.append('            Residue._by_index[attr.value] = attr')
    lines.append('')
    lines.append('')
    lines.append('_init_residue_class()')
    lines.append('')
    lines.append('')

    # Keep CIF name mappings (needed for C extension compatibility)
    lines.append("# CIF name -> Residue index (for C extension)")
    lines.append("CIF_RESIDUE_NAMES: dict[str, int] = {")
    for idx, res in enumerate(all_residues):
        for cif in res.cif_names:
            lines.append(f'    "{cif}": {idx},')
    lines.append("}")
    lines.append('')

    lines.append("# Residue index -> CIF name (for C extension)")
    lines.append("RESIDUE_CIF_NAMES: dict[int, str] = {")
    for idx, res in enumerate(all_residues):
        lines.append(f'    {idx}: "{res.cif_names[0]}",')
    lines.append("}")
    lines.append('')

    (biochem_dir / "_generated_residues.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_residues.py")


def generate_dihedral_arrays(
    biochem_dir: Path,
    all_residues: list[ResidueDefinition],
    atom_index: dict[tuple[str, str], int],
) -> None:
    """Generate Python dihedral ownership arrays."""

    # Compute global arrays
    atom_dihedral_type, atom_dihedral_refs = compute_atom_dihedral_ownership(
        all_residues, atom_index
    )

    # Count how many atoms own dihedrals (for stats)
    n_owners = np.sum(atom_dihedral_type >= 0)
    print(f"  {n_owners} atoms own named dihedrals")

    lines = [
        '"""',
        'Auto-generated dihedral ownership arrays.',
        'DO NOT EDIT - Generated by ciffy/src/codegen/generate.py from CCD.',
        '',
        'These arrays map atom enum values to dihedral type ownership:',
        '- ATOM_DIHEDRAL_TYPE[atom_idx] -> dihedral type index (-1 if not owner)',
        '- ATOM_DIHEDRAL_REFS[atom_idx] -> reference atoms for Z-matrix construction',
        '"""',
        '',
        'import numpy as np',
        '',
        '',
        '# Atom enum value -> dihedral type index (-1 = not a dihedral owner)',
        '# Type indices match DihedralType IntEnum values in ciffy.types.dihedral:',
        '#   PHI=0, PSI=1, OMEGA=2, ALPHA=3, BETA=4, GAMMA=5,',
        '#   DELTA=6, EPSILON=7, ZETA=8, CHI_PURINE=9, CHI_PYRIMIDINE=10',
        f'ATOM_DIHEDRAL_TYPE = np.array([',
    ]

    # Write ATOM_DIHEDRAL_TYPE array
    for i, dtype in enumerate(atom_dihedral_type):
        lines.append(f'    {dtype},  # {i}')

    lines.append('], dtype=np.int8)')
    lines.append('')

    # Write ATOM_DIHEDRAL_REFS array
    lines.append('')
    lines.append('# Atom enum value -> reference atoms for Z-matrix construction')
    lines.append('# Shape: (num_atoms, 3, 2) where inner dim is [residue_offset, local_atom_idx]')
    lines.append('# refs[0] = dihedral reference (A in [A,B,C,D])')
    lines.append('# refs[1] = angle reference (B in [A,B,C,D])')
    lines.append('# refs[2] = distance reference (C in [A,B,C,D])')
    lines.append('# Only meaningful where ATOM_DIHEDRAL_TYPE >= 0')
    lines.append('ATOM_DIHEDRAL_REFS = np.array([')

    for i, refs in enumerate(atom_dihedral_refs):
        ref_str = str(refs.tolist())
        lines.append(f'    {ref_str},  # {i}')

    lines.append('], dtype=np.int8)')
    lines.append('')

    (biochem_dir / "_generated_dihedrals.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_dihedrals.py")


def generate_zmatrix_arrays(
    biochem_dir: Path,
    all_residues: list[ResidueDefinition],
    atom_index: dict[tuple[str, str], int],
) -> None:
    """Generate Python canonical Z-matrix reference arrays."""

    # Compute global arrays
    atom_canonical_refs, atom_has_canonical_refs = compute_canonical_zmatrix_refs(
        all_residues, atom_index
    )

    # Count how many atoms have canonical refs (for stats)
    n_with_refs = np.sum(atom_has_canonical_refs)
    n_total = len(atom_has_canonical_refs)
    print(f"  {n_with_refs}/{n_total} atoms have canonical Z-matrix refs")

    lines = [
        '"""',
        'Auto-generated canonical Z-matrix reference arrays.',
        'DO NOT EDIT - Generated by ciffy/src/codegen/generate.py from CCD.',
        '',
        'These arrays define canonical Z-matrix references for deterministic construction:',
        '- ATOM_CANONICAL_REFS[atom_idx] -> [dist_type, ang_type, dih_type, dist_off, ang_off, dih_off]',
        '- ATOM_HAS_CANONICAL_REFS[atom_idx] -> True if canonical refs are defined',
        '',
        'Reference types are global atom enum values (-1 if no reference).',
        'Reference offsets are residue offsets: -1 (prev), 0 (current), +1 (next).',
        '"""',
        '',
        'import numpy as np',
        '',
        '',
        '# Canonical Z-matrix references for each atom type.',
        '# Shape: (num_atoms, 6) where columns are:',
        '#   [dist_ref_type, ang_ref_type, dih_ref_type, dist_off, ang_off, dih_off]',
        '# ref_type values are global atom enum values (-1 means no reference)',
        '# ref_off values are residue offsets (-1=prev, 0=current, +1=next)',
        'ATOM_CANONICAL_REFS = np.array([',
    ]

    # Write ATOM_CANONICAL_REFS array
    for i, refs in enumerate(atom_canonical_refs):
        ref_str = str(refs.tolist())
        lines.append(f'    {ref_str},  # {i}')

    lines.append('], dtype=np.int16)')
    lines.append('')

    # Write ATOM_HAS_CANONICAL_REFS array
    lines.append('')
    lines.append('# Boolean array indicating which atoms have canonical refs defined.')
    lines.append('# Atoms without canonical refs will fall back to BFS-based construction.')
    lines.append('ATOM_HAS_CANONICAL_REFS = np.array([')

    for i, has_refs in enumerate(atom_has_canonical_refs):
        lines.append(f'    {str(has_refs)},  # {i}')

    lines.append('], dtype=bool)')
    lines.append('')

    (biochem_dir / "_generated_zmatrix.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_zmatrix.py")


def generate_python_dihedraltypes(biochem_dir: Path) -> None:
    """Generate Python DihedralType IndexEnum 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 ..utils import IndexEnum',
        '',
        '',
        'class DihedralType(IndexEnum):',
        '    """',
        '    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)',
        '',
        '    Example:',
        '        >>> DihedralType.PHI.value  # Returns 0, usable as array index',
        '        0',
        '        >>> DihedralType.dict()  # Returns name -> value mapping',
        '        {\'PHI\': 0, \'PSI\': 1, ...}',
        '    """',
        '',
    ]

    # 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('')

    (biochem_dir / "_generated_dihedraltypes.py").write_text('\n'.join(lines))
    print("Generated: biochemistry/_generated_dihedraltypes.py")
