#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BSSE ORCA GUI
=============

Interface grafica (Tkinter, biblioteca padrao do Python - sem dependencias
externas) que reproduz o fluxo de trabalho descrito no script de compound
"BSSECorrection.cmp" do ORCA, mas gerando arquivos .inp comuns (nao usa a
linguagem de "Compound Script" do ORCA).

CONTEXTO
--------
O usuario possui:
  - Uma pasta com N monomeros do mesmo cluster supramolecular
    (ex.: M1.xyz, M2.xyz, ..., Mn.xyz), cada um com a posicao espacial
    correta dentro do cluster.
  - Uma pasta com os dimeros correspondentes (ex.: M1_M2.xyz, M1_M3.xyz,
    ...), onde cada arquivo e a simples concatenacao das coordenadas do
    monomero A (atomos reais) seguidas das coordenadas do monomero B
    (podendo ja conter ou nao a marcacao ':' de atomo fantasma - o programa
    NAO depende dessa marcacao, ele reconstroi tudo a partir da contagem de
    atomos de cada monomero).

Para cada par (A, B) o programa gera, em arquivos .inp separados (padrao
ORCA, com coordenadas XYZ embutidas em angstrom), os 5 calculos do metodo de
Counterpoise (Boys-Bernardi) descritos no script original:

  1. FragA   -> geometria do dimero, A real + B fantasma (basis ghost)
  2. FragB   -> geometria do dimero, B real + A fantasma (basis ghost)
  3. Total   -> geometria do dimero, A e B reais (dimero completo)
  4. MonomerA-> monomero A isolado, sem atomos fantasma (opcionalmente com
                otimizacao de geometria)
  5. MonomerB-> monomero B isolado, sem atomos fantasma (idem)

Com isso e possivel calcular, apos rodar o ORCA:

  DeltaE            = E_Total - E_FragA - E_FragB                 (com ghost)
  DeltaE_sem_ghost  = E_Total - E_MonomerA - E_MonomerB            (sem ghost)
  DeltaE_correcao   = DeltaE - DeltaE_sem_ghost                    (correcao BSSE)

exatamente como o bloco final de "print" do script BSSECorrection.cmp.

Como os monomeros A aparecem repetidos em varios pares (ex.: M1 e comum a
todos os pares em uma topologia tipo "estrela"), o arquivo .inp do monomero
isolado e gerado apenas uma vez por nome de monomero.

O programa tambem pode (opcionalmente):
  - Gerar um script "run_all.sh" para rodar todos os calculos em sequencia.
  - Rodar o ORCA localmente (se o executavel estiver disponivel na maquina).
  - Analisar os arquivos .out gerados e montar a tabela final de resultados
    (mesmos numeros que o script original imprime), com exportacao em CSV.

Autor: gerado com auxilio do Claude (Anthropic) a partir do script
       BSSECorrection.cmp de Dimitrios G. Liakos.
"""

import csv
import os
import re
import subprocess
import threading
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext

# ============================================================================
# Constantes
# ============================================================================

HARTREE_TO_KCAL = 627.5096080305927

METHOD_PRESETS = [
    "BP86", "r2SCAN-3c", "PBEh-3c", "B97-3c", "wB97X-3c", "LDA", "PBE", "B97M-V", "B97M-D4", "r2SCAN", "PBE0", "r2SCAN0", "wB97X-D4", "wB97X-V", "wB97M-D4", "wB97M-V", "Pr2SCAN69", "B2GP-PLYP", "HF", "RI-MP2", "DLPNO-MP2", "SCS-DLPNO-MP2", "RI-CCSD(T)", "DLPNO-CCSD(T1)"
]
BASIS_PRESETS = [
    "def2-SVP", "def2-TZVP", "def2-QZVP", "def2-TZVPP", "def2-QZVPP", "def2-SVPD", "def2-TZVPPD", "def2-QZVPPD", "def2-QZVPPD", "cc-pVDZ", "cc-pVTZ", "cc-pVQZ", "aug-cc-pVDZ", "aug-cc-pVTZ", "aug-cc-pVQZ", "pc-1", "pc-2", "pc-3", "aug-pc-1", "aug-pc-2", "aug-pc-2", "aug-pc-3",
]

FINAL_ENERGY_RE = re.compile(r"FINAL SINGLE POINT ENERGY\s+(-?\d+\.\d+)")

APP_TITLE = "BSSE ORCA GUI - Correcao de Counterpoise para clusters supramoleculares"


# ============================================================================
# Utilitarios de leitura/escrita de arquivos .xyz
# ============================================================================

Atom = Tuple[str, float, float, float]  # (elemento SEM ':' , x, y, z)


def strip_ghost(elem: str) -> str:
    """Remove o sufixo ':' (marcacao de atomo fantasma), se existir."""
    return elem[:-1] if elem.endswith(":") else elem


def is_ghost(elem: str) -> bool:
    return elem.endswith(":")


def read_xyz(path: Path) -> Tuple[int, str, List[Atom]]:
    """Le um arquivo .xyz padrao (numero de atomos / comentario / atomos).

    Qualquer marcacao ':' ja existente no elemento e removida - a
    informacao de "fantasma" e decidida por este programa, e nao herdada
    do arquivo de entrada.
    """
    with open(path, "r", encoding="utf-8", errors="replace") as f:
        lines = [l.rstrip("\n") for l in f if l.strip() != ""]
    if len(lines) < 2:
        raise ValueError(f"Arquivo xyz invalido (poucas linhas): {path}")
    try:
        n = int(lines[0].strip().split()[0])
    except (ValueError, IndexError):
        raise ValueError(f"Primeira linha de {path} deveria conter o numero de atomos.")
    comment = lines[1]
    atoms: List[Atom] = []
    body = lines[2:2 + n]
    if len(body) < n:
        raise ValueError(
            f"{path.name}: cabecalho diz {n} atomos, mas o arquivo tem apenas {len(body)}."
        )
    for l in body:
        parts = l.split()
        if len(parts) < 4:
            raise ValueError(f"{path.name}: linha de atomo invalida: '{l}'")
        elem = strip_ghost(parts[0])
        try:
            x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
        except ValueError:
            raise ValueError(f"{path.name}: coordenadas invalidas na linha: '{l}'")
        atoms.append((elem, x, y, z))
    return n, comment, atoms


def format_atom_line(elem: str, x: float, y: float, z: float, ghost: bool) -> str:
    tag = elem + (":" if ghost else "")
    return f"{tag:<5s}{x:16.8f}{y:16.8f}{z:16.8f}"


def atom_lines_from_block(atoms: List[Atom], ghost: bool) -> List[str]:
    return [format_atom_line(e, x, y, z, ghost) for (e, x, y, z) in atoms]


# ============================================================================
# Estruturas de dados
# ============================================================================

@dataclass
class MonomerInfo:
    name: str
    path: Path
    natoms: int
    comment: str
    atoms: List[Atom]


@dataclass
class PairInfo:
    name_a: str
    name_b: str
    dimer_path: Path
    natoms_total: int
    atoms: List[Atom]         # todos os atomos do arquivo do dimero, na ordem
    natoms_a: int              # quantidade esperada de atomos de A
    natoms_b: int              # quantidade esperada de atomos de B
    ok: bool                   # True se a validacao geometrica bateu
    warnings: List[str] = field(default_factory=list)

    @property
    def pair_key(self) -> str:
        return f"{self.name_a}_{self.name_b}"

    def block_a(self) -> List[Atom]:
        return self.atoms[: self.natoms_a]

    def block_b(self) -> List[Atom]:
        return self.atoms[self.natoms_a: self.natoms_a + self.natoms_b]


@dataclass
class CalcParams:
    method: str = "BP86"
    basis: str = "def2-SVP"
    extra_keywords: str = ""
    extra_blocks: str = ""
    nprocs: int = 4
    maxcore: int = 3000
    do_correction: bool = True
    do_optimization: bool = True
    charge_default: int = 0
    mult_default: int = 1
    total_mult_default: int = 1
    charge_overrides: Dict[str, int] = field(default_factory=dict)
    mult_overrides: Dict[str, int] = field(default_factory=dict)

    def charge_mult(self, monomer_name: str) -> Tuple[int, int]:
        c = self.charge_overrides.get(monomer_name, self.charge_default)
        m = self.mult_overrides.get(monomer_name, self.mult_default)
        return c, m

    def keywords_line(self) -> str:
        parts = [self.method.strip(), self.basis.strip(), self.extra_keywords.strip()]
        return " ".join(p for p in parts if p)


# ============================================================================
# Escaneamento de pastas
# ============================================================================

def scan_monomers(folder: Path) -> Tuple[Dict[str, MonomerInfo], List[str]]:
    """Le todos os .xyz da pasta de monomeros.

    Retorna (dict nome->MonomerInfo, lista de mensagens de erro).
    """
    monomers: Dict[str, MonomerInfo] = {}
    errors: List[str] = []
    if not folder or not folder.is_dir():
        return monomers, ["Pasta de monomeros invalida."]
    for p in sorted(folder.glob("*.xyz")):
        name = p.stem
        try:
            n, comment, atoms = read_xyz(p)
            monomers[name] = MonomerInfo(name=name, path=p, natoms=n, comment=comment, atoms=atoms)
        except Exception as e:
            errors.append(f"{p.name}: {e}")
    return monomers, errors


def _find_split(stem: str, known_names: List[str]) -> Optional[Tuple[str, str]]:
    """Tenta separar o nome de um arquivo de dimero (sem extensao) em dois
    nomes de monomeros conhecidos, ex.: 'M1_M2' -> ('M1', 'M2').
    """
    underscore_positions = [i for i, ch in enumerate(stem) if ch == "_"]
    candidates = []
    for i in underscore_positions:
        left, right = stem[:i], stem[i + 1:]
        if left in known_names and right in known_names:
            candidates.append((left, right))
    if candidates:
        # Prefere a divisao que usa os nomes mais longos (mais especifica)
        candidates.sort(key=lambda ab: -(len(ab[0]) + len(ab[1])))
        return candidates[0]
    if underscore_positions:
        i = underscore_positions[0]
        return stem[:i], stem[i + 1:]
    return None


def scan_pairs(dimer_folder: Path, monomers: Dict[str, MonomerInfo]) -> Tuple[List[PairInfo], List[str]]:
    """Le todos os .xyz da pasta de dimeros e tenta casar cada um com dois
    monomeros conhecidos, validando a geometria.
    """
    pairs: List[PairInfo] = []
    errors: List[str] = []
    if not dimer_folder or not dimer_folder.is_dir():
        return pairs, ["Pasta de dimeros invalida."]
    known_names = list(monomers.keys())
    for p in sorted(dimer_folder.glob("*.xyz")):
        stem = p.stem
        split = _find_split(stem, known_names)
        if split is None:
            errors.append(f"{p.name}: nao foi possivel identificar os dois monomeros no nome do arquivo.")
            continue
        name_a, name_b = split
        try:
            n_total, comment, atoms = read_xyz(p)
        except Exception as e:
            errors.append(f"{p.name}: {e}")
            continue

        warnings: List[str] = []
        ok = True
        mono_a = monomers.get(name_a)
        mono_b = monomers.get(name_b)
        if mono_a is None:
            warnings.append(f"monomero '{name_a}' nao encontrado na pasta de monomeros.")
            ok = False
        if mono_b is None:
            warnings.append(f"monomero '{name_b}' nao encontrado na pasta de monomeros.")
            ok = False

        if mono_a is not None and mono_b is not None:
            natoms_a, natoms_b = mono_a.natoms, mono_b.natoms
            if natoms_a + natoms_b != n_total:
                warnings.append(
                    f"soma de atomos dos monomeros ({natoms_a}+{natoms_b}={natoms_a + natoms_b}) "
                    f"difere do total no dimero ({n_total})."
                )
                ok = False
            else:
                # valida coordenadas (tolerancia de 1e-3 A) contra os monomeros,
                # na ordem A depois B; se nao bater, tenta a ordem invertida.
                block1, block2 = atoms[:natoms_a], atoms[natoms_a:]

                def matches(block, mono, tol=1e-3):
                    if len(block) != mono.natoms:
                        return False
                    for (e1, x1, y1, z1), (e2, x2, y2, z2) in zip(block, mono.atoms):
                        if e1 != e2 or abs(x1 - x2) > tol or abs(y1 - y2) > tol or abs(z1 - z2) > tol:
                            return False
                    return True

                if matches(block1, mono_a) and matches(block2, mono_b):
                    pass  # ordem correta, tudo certo
                elif natoms_a == natoms_b and matches(block1, mono_b) and matches(block2, mono_a):
                    # arquivo esta na ordem B depois A: apenas um aviso,
                    # o programa usa a ordem do NOME do arquivo (A_B) de qualquer forma,
                    # entao troca os blocos internamente mais adiante nao e necessario
                    # pois natoms_a==natoms_b aqui - mantemos como esta e avisamos.
                    warnings.append(
                        "as coordenadas parecem estar na ordem B,A dentro do arquivo "
                        "(mas os monomeros tem o mesmo numero de atomos, entao isso nao afeta o resultado)."
                    )
                else:
                    warnings.append(
                        "as coordenadas do dimero nao batem exatamente com os arquivos de monomero "
                        "correspondentes (tolerancia 0.001 A). Os arquivos serao gerados mesmo assim, "
                        "usando a contagem de atomos de cada monomero, mas confira o resultado."
                    )
        else:
            natoms_a = mono_a.natoms if mono_a else n_total // 2
            natoms_b = mono_b.natoms if mono_b else n_total - natoms_a

        pairs.append(PairInfo(
            name_a=name_a, name_b=name_b, dimer_path=p,
            natoms_total=n_total, atoms=atoms,
            natoms_a=natoms_a, natoms_b=natoms_b,
            ok=ok, warnings=warnings,
        ))
    return pairs, errors


# ============================================================================
# Geracao dos arquivos .inp do ORCA
# ============================================================================

def build_input_text(keywords: str, charge: int, mult: int, atom_lines: List[str],
                      nprocs: int = 1, maxcore: Optional[int] = None,
                      extra_blocks: str = "", opt: bool = False) -> str:
    kw = keywords.strip()
    if opt:
        kw = (kw + " Opt").strip()
    lines = [f"! {kw}"]
    if nprocs and nprocs > 1:
        lines.append(f"%pal nprocs {nprocs} end")
    if maxcore:
        lines.append(f"%maxcore {maxcore}")
    if extra_blocks and extra_blocks.strip():
        lines.append(extra_blocks.strip())
    lines.append(f"* xyz {charge} {mult}")
    lines.extend(atom_lines)
    lines.append("*")
    lines.append("")
    return "\n".join(lines)


def generate_monomer_input(mono: MonomerInfo, params: CalcParams, out_dir: Path) -> Path:
    charge, mult = params.charge_mult(mono.name)
    atom_lines = atom_lines_from_block(mono.atoms, ghost=False)
    text = build_input_text(
        params.keywords_line(), charge, mult, atom_lines,
        nprocs=params.nprocs, maxcore=params.maxcore,
        extra_blocks=params.extra_blocks, opt=params.do_optimization,
    )
    out_path = out_dir / f"{mono.name}.inp"
    out_path.write_text(text, encoding="utf-8")
    return out_path


def generate_pair_inputs(pair: PairInfo, monomers: Dict[str, MonomerInfo], params: CalcParams,
                          out_root: Path, generated_monomers: set) -> List[Path]:
    generated: List[Path] = []
    dimer_dir = out_root / "dimeros" / pair.pair_key
    dimer_dir.mkdir(parents=True, exist_ok=True)
    monomeros_dir = out_root / "monomeros"
    monomeros_dir.mkdir(parents=True, exist_ok=True)

    block_a = pair.block_a()
    block_b = pair.block_b()
    charge_a, mult_a = params.charge_mult(pair.name_a)
    charge_b, mult_b = params.charge_mult(pair.name_b)
    charge_total = charge_a + charge_b
    mult_total = params.total_mult_default
    kw = params.keywords_line()

    # 1. FragA: A real + B fantasma
    lines_fraga = atom_lines_from_block(block_a, ghost=False) + atom_lines_from_block(block_b, ghost=True)
    text_fraga = build_input_text(kw, charge_a, mult_a, lines_fraga,
                                   nprocs=params.nprocs, maxcore=params.maxcore,
                                   extra_blocks=params.extra_blocks, opt=False)
    p1 = dimer_dir / f"{pair.pair_key}_FragA.inp"
    p1.write_text(text_fraga, encoding="utf-8")
    generated.append(p1)

    # 2. FragB: B real + A fantasma
    lines_fragb = atom_lines_from_block(block_a, ghost=True) + atom_lines_from_block(block_b, ghost=False)
    text_fragb = build_input_text(kw, charge_b, mult_b, lines_fragb,
                                   nprocs=params.nprocs, maxcore=params.maxcore,
                                   extra_blocks=params.extra_blocks, opt=False)
    p2 = dimer_dir / f"{pair.pair_key}_FragB.inp"
    p2.write_text(text_fragb, encoding="utf-8")
    generated.append(p2)

    # 3. Total: A e B reais (dimero completo)
    lines_total = atom_lines_from_block(block_a, ghost=False) + atom_lines_from_block(block_b, ghost=False)
    text_total = build_input_text(kw, charge_total, mult_total, lines_total,
                                   nprocs=params.nprocs, maxcore=params.maxcore,
                                   extra_blocks=params.extra_blocks, opt=False)
    p3 = dimer_dir / f"{pair.pair_key}_Total.inp"
    p3.write_text(text_total, encoding="utf-8")
    generated.append(p3)

    # 4 e 5. Monomeros isolados (sem ghost) - gerados uma unica vez por nome
    if params.do_correction:
        for name in (pair.name_a, pair.name_b):
            if name in generated_monomers:
                continue
            mono = monomers.get(name)
            if mono is None:
                continue
            p_mono = generate_monomer_input(mono, params, monomeros_dir)
            generated.append(p_mono)
            generated_monomers.add(name)

    return generated


def generate_all(pairs: List[PairInfo], monomers: Dict[str, MonomerInfo], params: CalcParams,
                  out_root: Path, log=lambda msg: None) -> Tuple[List[Path], Path]:
    out_root.mkdir(parents=True, exist_ok=True)
    generated_monomers: set = set()
    all_generated: List[Path] = []
    for pair in pairs:
        files = generate_pair_inputs(pair, monomers, params, out_root, generated_monomers)
        all_generated.extend(files)
        log(f"Par {pair.pair_key}: {len(files)} arquivo(s) gerado(s).")
    run_all_path = write_run_all_script(all_generated, out_root)
    return all_generated, run_all_path


def write_run_all_script(files: List[Path], out_root: Path) -> Path:
    lines = [
        "#!/bin/bash",
        "# Script gerado automaticamente pelo BSSE ORCA GUI",
        "# Ajuste o caminho do executavel do ORCA (variavel ORCA_EXE) conforme necessario.",
        'ORCA_EXE="${ORCA_EXE:-orca}"',
        "",
        'cd "$(dirname "$0")"',
        "",
    ]
    for f in files:
        rel = f.relative_to(out_root)
        out_rel = rel.with_suffix(".out")
        lines.append(f'echo "Rodando {rel} ..."')
        lines.append(f'"$ORCA_EXE" "{rel}" > "{out_rel}" 2>&1')
        lines.append("")
    run_path = out_root / "run_all.sh"
    run_path.write_text("\n".join(lines), encoding="utf-8")
    try:
        os.chmod(run_path, 0o755)
    except OSError:
        pass
    return run_path


# ============================================================================
# Leitura dos resultados (.out) e analise final
# ============================================================================

def parse_final_energy(out_path: Path) -> Optional[float]:
    if not out_path.exists():
        return None
    try:
        text = out_path.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return None
    matches = FINAL_ENERGY_RE.findall(text)
    if not matches:
        return None
    return float(matches[-1])


@dataclass
class PairResult:
    pair_key: str
    e_fraga: Optional[float]
    e_fragb: Optional[float]
    e_total: Optional[float]
    e_mona: Optional[float]
    e_monb: Optional[float]
    missing: List[str]

    @property
    def de_hartree(self) -> Optional[float]:
        if None in (self.e_total, self.e_fraga, self.e_fragb):
            return None
        return self.e_total - self.e_fraga - self.e_fragb

    @property
    def de_noghost_hartree(self) -> Optional[float]:
        if None in (self.e_total, self.e_mona, self.e_monb):
            return None
        return self.e_total - self.e_mona - self.e_monb

    @property
    def de_correction_hartree(self) -> Optional[float]:
        a, b = self.de_hartree, self.de_noghost_hartree
        if a is None or b is None:
            return None
        return a - b


def collect_results(pairs: List[PairInfo], out_root: Path, do_correction: bool) -> List[PairResult]:
    results = []
    for pair in pairs:
        dimer_dir = out_root / "dimeros" / pair.pair_key
        monomeros_dir = out_root / "monomeros"
        missing = []

        def get(path, label):
            e = parse_final_energy(path)
            if e is None:
                missing.append(label)
            return e

        e_fraga = get(dimer_dir / f"{pair.pair_key}_FragA.out", "FragA")
        e_fragb = get(dimer_dir / f"{pair.pair_key}_FragB.out", "FragB")
        e_total = get(dimer_dir / f"{pair.pair_key}_Total.out", "Total")
        e_mona = e_monb = None
        if do_correction:
            e_mona = get(monomeros_dir / f"{pair.name_a}.out", f"Monomero {pair.name_a}")
            e_monb = get(monomeros_dir / f"{pair.name_b}.out", f"Monomero {pair.name_b}")
        results.append(PairResult(
            pair_key=pair.pair_key, e_fraga=e_fraga, e_fragb=e_fragb, e_total=e_total,
            e_mona=e_mona, e_monb=e_monb, missing=missing,
        ))
    return results


def export_results_csv(results: List[PairResult], csv_path: Path) -> None:
    with open(csv_path, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow([
            "Par", "E_FragA (Ha)", "E_FragB (Ha)", "E_Total (Ha)",
            "E_MonomeroA (Ha)", "E_MonomeroB (Ha)",
            "DeltaE (Ha)", "DeltaE (kcal/mol)",
            "DeltaE_sem_ghost (Ha)", "DeltaE_sem_ghost (kcal/mol)",
            "DeltaE_correcao_BSSE (Ha)", "DeltaE_correcao_BSSE (kcal/mol)",
            "Pendencias",
        ])
        for r in results:
            de = r.de_hartree
            deng = r.de_noghost_hartree
            dec = r.de_correction_hartree
            w.writerow([
                r.pair_key,
                r.e_fraga, r.e_fragb, r.e_total, r.e_mona, r.e_monb,
                de, de * HARTREE_TO_KCAL if de is not None else "",
                deng, deng * HARTREE_TO_KCAL if deng is not None else "",
                dec, dec * HARTREE_TO_KCAL if dec is not None else "",
                "; ".join(r.missing),
            ])


# ============================================================================
# Interface grafica (Tkinter)
# ============================================================================

class ScrollableFrame(ttk.Frame):
    """Um Frame com barra de rolagem vertical, usado para a tabela de
    cargas/multiplicidades por monomero."""

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        canvas = tk.Canvas(self, highlightthickness=0)
        scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
        self.inner = ttk.Frame(canvas)

        self.inner.bind(
            "<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
        )
        canvas.create_window((0, 0), window=self.inner, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)

        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")


class AdvancedChargesDialog(tk.Toplevel):
    """Janela para editar carga/multiplicidade por monomero individualmente."""

    def __init__(self, parent, monomer_names: List[str], params: CalcParams):
        super().__init__(parent)
        self.title("Avancado: cargas e multiplicidades por monomero")
        self.geometry("420x480")
        self.params = params
        self.entries: Dict[str, Tuple[tk.StringVar, tk.StringVar]] = {}

        info = ttk.Label(
            self, wraplength=390, justify="left",
            text=("Por padrao todos os monomeros usam a carga/multiplicidade global "
                  "definida na aba de parametros. Aqui voce pode sobrescrever valores "
                  "individuais (por exemplo, se algum fragmento do cluster for ionico "
                  "ou tiver estado de spin diferente)."),
        )
        info.pack(fill="x", padx=10, pady=(10, 5))

        sf = ScrollableFrame(self)
        sf.pack(fill="both", expand=True, padx=10, pady=5)

        header = ttk.Frame(sf.inner)
        header.pack(fill="x")
        ttk.Label(header, text="Monomero", width=14).grid(row=0, column=0)
        ttk.Label(header, text="Carga", width=8).grid(row=0, column=1)
        ttk.Label(header, text="Mult.", width=8).grid(row=0, column=2)

        for name in monomer_names:
            row = ttk.Frame(sf.inner)
            row.pack(fill="x", pady=1)
            ttk.Label(row, text=name, width=14).grid(row=0, column=0)
            c_var = tk.StringVar(value=str(params.charge_overrides.get(name, params.charge_default)))
            m_var = tk.StringVar(value=str(params.mult_overrides.get(name, params.mult_default)))
            ttk.Entry(row, textvariable=c_var, width=8).grid(row=0, column=1, padx=2)
            ttk.Entry(row, textvariable=m_var, width=8).grid(row=0, column=2, padx=2)
            self.entries[name] = (c_var, m_var)

        btns = ttk.Frame(self)
        btns.pack(fill="x", padx=10, pady=10)
        ttk.Button(btns, text="Salvar", command=self._save).pack(side="right", padx=5)
        ttk.Button(btns, text="Cancelar", command=self.destroy).pack(side="right")

    def _save(self):
        try:
            new_charge = {}
            new_mult = {}
            for name, (c_var, m_var) in self.entries.items():
                c = int(c_var.get())
                m = int(m_var.get())
                if c != self.params.charge_default:
                    new_charge[name] = c
                if m != self.params.mult_default:
                    new_mult[name] = m
            self.params.charge_overrides = new_charge
            self.params.mult_overrides = new_mult
        except ValueError:
            messagebox.showerror("Erro", "Carga e multiplicidade devem ser numeros inteiros.")
            return
        self.destroy()


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.geometry("1080x720")

        self.monomer_folder: Optional[Path] = None
        self.dimer_folder: Optional[Path] = None
        self.output_folder: Optional[Path] = None
        self.monomers: Dict[str, MonomerInfo] = {}
        self.pairs: List[PairInfo] = []
        self.params = CalcParams()

        self._build_menu()

        notebook = ttk.Notebook(self)
        notebook.pack(fill="both", expand=True)

        self.tab_folders = ttk.Frame(notebook)
        self.tab_params = ttk.Frame(notebook)
        self.tab_generate = ttk.Frame(notebook)
        self.tab_run = ttk.Frame(notebook)

        notebook.add(self.tab_folders, text="1. Pastas e pares")
        notebook.add(self.tab_params, text="2. Parametros de calculo")
        notebook.add(self.tab_generate, text="3. Gerar arquivos .inp")
        notebook.add(self.tab_run, text="4. Executar / Analisar (opcional)")

        self._build_tab_folders()
        self._build_tab_params()
        self._build_tab_generate()
        self._build_tab_run()

        self.status_var = tk.StringVar(value="Pronto.")
        status_bar = ttk.Label(self, textvariable=self.status_var, relief="sunken", anchor="w")
        status_bar.pack(fill="x", side="bottom")

    # ---------------------------------------------------------------- menu
    def _build_menu(self):
        menubar = tk.Menu(self)
        helpmenu = tk.Menu(menubar, tearoff=0)
        helpmenu.add_command(label="Sobre / Como funciona", command=self._show_about)
        menubar.add_cascade(label="Ajuda", menu=helpmenu)
        self.config(menu=menubar)

    def _show_about(self):
        messagebox.showinfo(
            "Sobre",
            "Este programa reproduz o fluxo de correcao de BSSE (Counterpoise, "
            "Boys-Bernardi) do script BSSECorrection.cmp do ORCA.\n\n"
            "Para cada par monomero A / monomero B ele gera 5 arquivos .inp:\n"
            "  FragA (A real + B fantasma), FragB (B real + A fantasma),\n"
            "  Total (dimero completo), MonomeroA e MonomeroB isolados.\n\n"
            "As coordenadas sao usadas em Angstrom (unidade padrao do ORCA), "
            "diretamente a partir dos seus arquivos .xyz.\n\n"
            "DeltaE = E_Total - E_FragA - E_FragB\n"
            "DeltaE_sem_ghost = E_Total - E_MonomeroA - E_MonomeroB\n"
            "DeltaE_correcao_BSSE = DeltaE - DeltaE_sem_ghost",
        )

    # --------------------------------------------------------- aba pastas
    def _build_tab_folders(self):
        f = self.tab_folders
        pad = dict(padx=8, pady=6)

        grp = ttk.LabelFrame(f, text="Pastas de entrada e saida")
        grp.pack(fill="x", **pad)

        self.var_monomer_folder = tk.StringVar()
        self.var_dimer_folder = tk.StringVar()
        self.var_output_folder = tk.StringVar()

        self._folder_row(grp, "Pasta de monomeros (ex.: M1.xyz ... Mn.xyz):",
                          self.var_monomer_folder, self._pick_monomer_folder, row=0)
        self._folder_row(grp, "Pasta de dimeros (ex.: M1_M2.xyz, M1_M3.xyz ...):",
                          self.var_dimer_folder, self._pick_dimer_folder, row=1)
        self._folder_row(grp, "Pasta de saida (onde os .inp serao gerados):",
                          self.var_output_folder, self._pick_output_folder, row=2)

        btn_row = ttk.Frame(f)
        btn_row.pack(fill="x", **pad)
        ttk.Button(btn_row, text="Reescanear pastas", command=self._rescan).pack(side="left")
        self.lbl_scan_summary = ttk.Label(btn_row, text="")
        self.lbl_scan_summary.pack(side="left", padx=15)

        panes = ttk.PanedWindow(f, orient="vertical")
        panes.pack(fill="both", expand=True, **pad)

        # --- lista de monomeros
        mono_frame = ttk.LabelFrame(panes, text="Monomeros encontrados")
        panes.add(mono_frame, weight=1)
        cols = ("nome", "atomos", "arquivo")
        self.tree_monomers = ttk.Treeview(mono_frame, columns=cols, show="headings", height=6)
        for c, w in zip(cols, (100, 70, 500)):
            self.tree_monomers.heading(c, text=c.capitalize())
            self.tree_monomers.column(c, width=w, anchor="w")
        self.tree_monomers.pack(fill="both", expand=True, side="left")
        sb1 = ttk.Scrollbar(mono_frame, orient="vertical", command=self.tree_monomers.yview)
        self.tree_monomers.configure(yscrollcommand=sb1.set)
        sb1.pack(side="right", fill="y")

        # --- lista de pares
        pair_frame = ttk.LabelFrame(
            panes, text="Pares (dimeros) detectados - selecione os que deseja gerar (Ctrl/Shift+clique)"
        )
        panes.add(pair_frame, weight=2)
        cols2 = ("par", "monoA", "atomosA", "monoB", "atomosB", "status", "arquivo")
        self.tree_pairs = ttk.Treeview(pair_frame, columns=cols2, show="headings", height=10,
                                        selectmode="extended")
        widths = (110, 70, 70, 70, 70, 260, 260)
        for c, w in zip(cols2, widths):
            self.tree_pairs.heading(c, text=c.capitalize())
            self.tree_pairs.column(c, width=w, anchor="w")
        self.tree_pairs.pack(fill="both", expand=True, side="left")
        sb2 = ttk.Scrollbar(pair_frame, orient="vertical", command=self.tree_pairs.yview)
        self.tree_pairs.configure(yscrollcommand=sb2.set)
        sb2.pack(side="right", fill="y")

        sel_row = ttk.Frame(f)
        sel_row.pack(fill="x", **pad)
        ttk.Button(sel_row, text="Selecionar todos", command=self._select_all_pairs).pack(side="left")
        ttk.Button(sel_row, text="Limpar selecao", command=self._clear_pair_selection).pack(side="left", padx=5)

    def _folder_row(self, parent, label, var, command, row):
        ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=5, pady=4)
        ttk.Entry(parent, textvariable=var, width=60).grid(row=row, column=1, sticky="we", padx=5)
        ttk.Button(parent, text="Escolher...", command=command).grid(row=row, column=2, padx=5)
        parent.columnconfigure(1, weight=1)

    def _pick_monomer_folder(self):
        d = filedialog.askdirectory(title="Selecione a pasta de monomeros")
        if d:
            self.var_monomer_folder.set(d)
            self.monomer_folder = Path(d)
            self._rescan()

    def _pick_dimer_folder(self):
        d = filedialog.askdirectory(title="Selecione a pasta de dimeros")
        if d:
            self.var_dimer_folder.set(d)
            self.dimer_folder = Path(d)
            self._rescan()

    def _pick_output_folder(self):
        d = filedialog.askdirectory(title="Selecione a pasta de saida")
        if d:
            self.var_output_folder.set(d)
            self.output_folder = Path(d)

    def _rescan(self):
        if self.var_monomer_folder.get():
            self.monomer_folder = Path(self.var_monomer_folder.get())
        if self.var_dimer_folder.get():
            self.dimer_folder = Path(self.var_dimer_folder.get())

        self.tree_monomers.delete(*self.tree_monomers.get_children())
        self.tree_pairs.delete(*self.tree_pairs.get_children())

        errors = []
        if self.monomer_folder:
            self.monomers, mono_errors = scan_monomers(self.monomer_folder)
            errors.extend(mono_errors)
            for name, m in self.monomers.items():
                self.tree_monomers.insert("", "end", iid=name, values=(name, m.natoms, str(m.path)))

        if self.dimer_folder and self.monomers:
            self.pairs, pair_errors = scan_pairs(self.dimer_folder, self.monomers)
            errors.extend(pair_errors)
            for pair in self.pairs:
                status = "OK" if pair.ok and not pair.warnings else "; ".join(pair.warnings) or "OK"
                iid = pair.pair_key
                self.tree_pairs.insert("", "end", iid=iid, values=(
                    pair.pair_key, pair.name_a, pair.natoms_a, pair.name_b, pair.natoms_b,
                    status, str(pair.dimer_path),
                ))
            self._select_all_pairs()
        else:
            self.pairs = []

        self.lbl_scan_summary.config(
            text=f"{len(self.monomers)} monomero(s), {len(self.pairs)} par(es) de dimero(s) detectado(s)."
        )
        self._refresh_advanced_names()
        if errors:
            self.status_var.set(f"Concluido com {len(errors)} aviso(s)/erro(s) - veja o log da aba 3.")
        else:
            self.status_var.set("Pastas escaneadas com sucesso.")

    def _select_all_pairs(self):
        self.tree_pairs.selection_set(self.tree_pairs.get_children())

    def _clear_pair_selection(self):
        self.tree_pairs.selection_remove(self.tree_pairs.get_children())

    # -------------------------------------------------------- aba parametros
    def _build_tab_params(self):
        f = self.tab_params
        pad = dict(padx=8, pady=6)

        grp1 = ttk.LabelFrame(f, text="Nivel de teoria")
        grp1.pack(fill="x", **pad)

        ttk.Label(grp1, text="Metodo (funcional / HF / etc.):").grid(row=0, column=0, sticky="w", padx=5, pady=4)
        self.var_method = tk.StringVar(value=self.params.method)
        cb_method = ttk.Combobox(grp1, textvariable=self.var_method, values=METHOD_PRESETS, width=30)
        cb_method.grid(row=0, column=1, sticky="w", padx=5)

        ttk.Label(grp1, text="Base:").grid(row=1, column=0, sticky="w", padx=5, pady=4)
        self.var_basis = tk.StringVar(value=self.params.basis)
        cb_basis = ttk.Combobox(grp1, textvariable=self.var_basis, values=BASIS_PRESETS, width=30)
        cb_basis.grid(row=1, column=1, sticky="w", padx=5)

        ttk.Label(grp1, text="Palavras-chave extras (linha '!'):").grid(row=2, column=0, sticky="w", padx=5, pady=4)
        self.var_extra_kw = tk.StringVar(value=self.params.extra_keywords)
        ttk.Entry(grp1, textvariable=self.var_extra_kw, width=45).grid(row=2, column=1, sticky="w", padx=5)
        ttk.Label(grp1, text="(ex.: Extreme sloppy Tight)").grid(row=2, column=2, sticky="w")

        ttk.Label(grp1, text="Blocos extras (opcional, ex. %scf ... end):").grid(row=3, column=0, sticky="nw", padx=5, pady=4)
        self.txt_extra_blocks = tk.Text(grp1, width=55, height=4)
        self.txt_extra_blocks.grid(row=3, column=1, columnspan=2, sticky="w", padx=5, pady=4)

        grp2 = ttk.LabelFrame(f, text="Recursos computacionais")
        grp2.pack(fill="x", **pad)
        ttk.Label(grp2, text="Numero de nucleos (%pal nprocs):").grid(row=0, column=0, sticky="w", padx=5, pady=4)
        self.var_nprocs = tk.IntVar(value=self.params.nprocs)
        ttk.Spinbox(grp2, from_=1, to=256, textvariable=self.var_nprocs, width=8).grid(row=0, column=1, sticky="w")
        ttk.Label(grp2, text="Memoria por nucleo em MB (%maxcore):").grid(row=0, column=2, sticky="w", padx=(20, 5))
        self.var_maxcore = tk.IntVar(value=self.params.maxcore)
        ttk.Spinbox(grp2, from_=0, to=200000, increment=500, textvariable=self.var_maxcore, width=10).grid(row=0, column=3, sticky="w")

        grp3 = ttk.LabelFrame(f, text="Opcoes do fluxo de trabalho (equivalentes ao script original)")
        grp3.pack(fill="x", **pad)
        self.var_do_correction = tk.BooleanVar(value=self.params.do_correction)
        ttk.Checkbutton(grp3, text="Calcular correcao de BSSE (gera tambem MonomeroA e MonomeroB isolados)",
                         variable=self.var_do_correction).grid(row=0, column=0, sticky="w", padx=5, pady=3, columnspan=2)
        self.var_do_optimization = tk.BooleanVar(value=self.params.do_optimization)
        ttk.Checkbutton(grp3, text="Otimizar a geometria dos monomeros isolados (palavra-chave 'Opt')",
                         variable=self.var_do_optimization).grid(row=1, column=0, sticky="w", padx=5, pady=3, columnspan=2)

        grp4 = ttk.LabelFrame(f, text="Carga e multiplicidade")
        grp4.pack(fill="x", **pad)
        ttk.Label(grp4, text="Carga padrao (por monomero):").grid(row=0, column=0, sticky="w", padx=5, pady=4)
        self.var_charge = tk.IntVar(value=self.params.charge_default)
        ttk.Spinbox(grp4, from_=-10, to=10, textvariable=self.var_charge, width=6).grid(row=0, column=1, sticky="w")
        ttk.Label(grp4, text="Multiplicidade padrao (por monomero):").grid(row=0, column=2, sticky="w", padx=(20, 5))
        self.var_mult = tk.IntVar(value=self.params.mult_default)
        ttk.Spinbox(grp4, from_=1, to=10, textvariable=self.var_mult, width=6).grid(row=0, column=3, sticky="w")
        ttk.Label(grp4, text="Multiplicidade do dimero (Total):").grid(row=1, column=0, sticky="w", padx=5, pady=4)
        self.var_mult_total = tk.IntVar(value=self.params.total_mult_default)
        ttk.Spinbox(grp4, from_=1, to=10, textvariable=self.var_mult_total, width=6).grid(row=1, column=1, sticky="w")
        ttk.Label(
            grp4, wraplength=520, justify="left",
            text=("A carga do dimero (Total e FragA/FragB) e sempre a soma das cargas dos dois "
                  "monomeros envolvidos. Use o botao abaixo para definir carga/multiplicidade "
                  "diferentes para monomeros especificos."),
        ).grid(row=2, column=0, columnspan=4, sticky="w", padx=5, pady=(6, 2))
        ttk.Button(grp4, text="Avancado: cargas/multiplicidades por monomero...",
                   command=self._open_advanced_charges).grid(row=3, column=0, columnspan=2, sticky="w", padx=5, pady=6)

        note = ttk.Label(
            f, wraplength=1000, justify="left", foreground="#555555",
            text=("Nota: as coordenadas dos seus arquivos .xyz sao tratadas como Angstrom "
                  "(unidade padrao do ORCA) e copiadas diretamente para os arquivos .inp - "
                  "nao ha conversao para Bohr, diferente do que o comando 'CreateBSSE' do "
                  "ORCA faz internamente. Os atomos fantasma sao marcados com ':' apos o "
                  "simbolo do elemento, como no script original."),
        )
        note.pack(fill="x", **pad)

    def _open_advanced_charges(self):
        self._sync_params_from_ui()
        names = sorted(self.monomers.keys()) if self.monomers else []
        if not names:
            messagebox.showinfo("Aviso", "Escaneie a pasta de monomeros primeiro (aba 1).")
            return
        AdvancedChargesDialog(self, names, self.params)

    def _refresh_advanced_names(self):
        # apenas garante que overrides de monomeros que nao existem mais sejam mantidos
        # (nao remove nada automaticamente, para nao perder configuracao do usuario)
        pass

    def _sync_params_from_ui(self):
        self.params.method = self.var_method.get()
        self.params.basis = self.var_basis.get()
        self.params.extra_keywords = self.var_extra_kw.get()
        self.params.extra_blocks = self.txt_extra_blocks.get("1.0", "end").strip()
        self.params.nprocs = int(self.var_nprocs.get())
        self.params.maxcore = int(self.var_maxcore.get())
        self.params.do_correction = bool(self.var_do_correction.get())
        self.params.do_optimization = bool(self.var_do_optimization.get())
        self.params.charge_default = int(self.var_charge.get())
        self.params.mult_default = int(self.var_mult.get())
        self.params.total_mult_default = int(self.var_mult_total.get())

    # -------------------------------------------------------- aba gerar
    def _build_tab_generate(self):
        f = self.tab_generate
        pad = dict(padx=8, pady=6)

        info = ttk.Label(
            f, wraplength=1020, justify="left",
            text=("Isso ira gerar, para cada par selecionado na aba 1, os arquivos "
                  "FragA.inp, FragB.inp e Total.inp (em uma subpasta 'dimeros/<par>/'), alem "
                  "dos arquivos de monomero isolado em 'monomeros/' (um por monomero, "
                  "reaproveitado entre os pares). Um script 'run_all.sh' tambem e criado."),
        )
        info.pack(fill="x", **pad)

        btn_row = ttk.Frame(f)
        btn_row.pack(fill="x", **pad)
        ttk.Button(btn_row, text="Gerar arquivos .inp para os pares selecionados",
                   command=self._on_generate).pack(side="left")
        self.lbl_generate_summary = ttk.Label(btn_row, text="")
        self.lbl_generate_summary.pack(side="left", padx=15)

        self.txt_log = scrolledtext.ScrolledText(f, height=28)
        self.txt_log.pack(fill="both", expand=True, **pad)

    def _log(self, msg: str):
        self.txt_log.insert("end", msg + "\n")
        self.txt_log.see("end")
        self.update_idletasks()

    def _selected_pairs(self) -> List[PairInfo]:
        selected_keys = set(self.tree_pairs.selection())
        return [p for p in self.pairs if p.pair_key in selected_keys]

    def _on_generate(self):
        self._sync_params_from_ui()
        if not self.params.method.strip() or not self.params.basis.strip():
            messagebox.showerror("Erro", "Defina metodo e base na aba 2 antes de gerar os arquivos.")
            return
        if not self.output_folder:
            if self.var_output_folder.get():
                self.output_folder = Path(self.var_output_folder.get())
            else:
                messagebox.showerror("Erro", "Escolha a pasta de saida na aba 1.")
                return
        pairs = self._selected_pairs()
        if not pairs:
            messagebox.showwarning("Aviso", "Nenhum par selecionado na aba 1.")
            return

        self.txt_log.delete("1.0", "end")
        self._log(f"Gerando arquivos para {len(pairs)} par(es) em: {self.output_folder}")
        self._log(f"Metodo: {self.params.keywords_line()}")
        self._log(f"Correcao BSSE: {self.params.do_correction}   Otimizar monomeros: {self.params.do_optimization}")
        self._log("-" * 70)
        try:
            all_files, run_all_path = generate_all(pairs, self.monomers, self.params, self.output_folder, self._log)
        except Exception as e:
            messagebox.showerror("Erro ao gerar arquivos", str(e))
            self._log(f"ERRO: {e}")
            return
        self._log("-" * 70)
        self._log(f"Total de {len(all_files)} arquivo(s) .inp gerado(s).")
        self._log(f"Script de execucao criado em: {run_all_path}")
        self.lbl_generate_summary.config(text=f"{len(all_files)} arquivo(s) gerado(s) com sucesso.")
        self.status_var.set("Geracao concluida.")
        # Atualiza a lista de arquivos .out esperados na aba de execucao/analise
        self._refresh_expected_outputs()

    # -------------------------------------------------------- aba executar
    def _build_tab_run(self):
        f = self.tab_run
        pad = dict(padx=8, pady=6)

        grp1 = ttk.LabelFrame(f, text="Executar localmente (opcional - requer ORCA instalado nesta maquina)")
        grp1.pack(fill="x", **pad)
        ttk.Label(grp1, text="Caminho do executavel do ORCA:").grid(row=0, column=0, sticky="w", padx=5, pady=4)
        self.var_orca_exe = tk.StringVar(value="orca")
        ttk.Entry(grp1, textvariable=self.var_orca_exe, width=50).grid(row=0, column=1, sticky="we", padx=5)
        ttk.Button(grp1, text="Escolher...", command=self._pick_orca_exe).grid(row=0, column=2, padx=5)
        grp1.columnconfigure(1, weight=1)
        self.progress = ttk.Progressbar(grp1, mode="determinate")
        self.progress.grid(row=1, column=0, columnspan=3, sticky="we", padx=5, pady=6)
        ttk.Button(grp1, text="Executar todos os calculos gerados", command=self._on_run_all).grid(
            row=2, column=0, sticky="w", padx=5, pady=4
        )
        ttk.Label(
            grp1, wraplength=1000, justify="left", foreground="#555555",
            text=("Se voce vai rodar os calculos em um cluster/HPC remoto, use o arquivo "
                  "'run_all.sh' gerado na pasta de saida em vez deste botao."),
        ).grid(row=3, column=0, columnspan=3, sticky="w", padx=5)

        grp2 = ttk.LabelFrame(f, text="Analisar resultados (le os arquivos .out ja calculados)")
        grp2.pack(fill="both", expand=True, **pad)
        btn_row = ttk.Frame(grp2)
        btn_row.pack(fill="x", padx=5, pady=5)
        ttk.Button(btn_row, text="Analisar resultados", command=self._on_analyze).pack(side="left")
        ttk.Button(btn_row, text="Exportar CSV...", command=self._on_export_csv).pack(side="left", padx=5)

        cols = ("par", "E_FragA", "E_FragB", "E_Total", "E_MonA", "E_MonB",
                "DeltaE (Ha)", "DeltaE (kcal/mol)", "Correcao BSSE (kcal/mol)", "Pendencias")
        self.tree_results = ttk.Treeview(grp2, columns=cols, show="headings", height=14)
        widths = (90, 100, 100, 100, 100, 100, 100, 110, 140, 200)
        for c, w in zip(cols, widths):
            self.tree_results.heading(c, text=c)
            self.tree_results.column(c, width=w, anchor="w")
        self.tree_results.pack(fill="both", expand=True, side="left", padx=5, pady=5)
        sb = ttk.Scrollbar(grp2, orient="vertical", command=self.tree_results.yview)
        self.tree_results.configure(yscrollcommand=sb.set)
        sb.pack(side="right", fill="y")

        self._last_results: List[PairResult] = []

    def _refresh_expected_outputs(self):
        total_steps = 0
        for _ in self.pairs:
            total_steps += 3  # FragA, FragB, Total
        total_steps += len(self.monomers) if self.params.do_correction else 0
        self.progress.config(maximum=max(total_steps, 1), value=0)

    def _pick_orca_exe(self):
        p = filedialog.askopenfilename(title="Selecione o executavel do ORCA")
        if p:
            self.var_orca_exe.set(p)

    def _on_run_all(self):
        if not self.output_folder or not self.output_folder.exists():
            messagebox.showerror("Erro", "Gere os arquivos .inp primeiro (aba 3).")
            return
        run_all_path = self.output_folder / "run_all.sh"
        if not run_all_path.exists():
            messagebox.showerror("Erro", "run_all.sh nao encontrado. Gere os arquivos novamente.")
            return
        orca_exe = self.var_orca_exe.get().strip() or "orca"
        answer = messagebox.askyesno(
            "Confirmar",
            "Isso ira executar TODOS os calculos ORCA gerados, em sequencia, nesta maquina. "
            "Dependendo do numero de pares e do metodo escolhido, isso pode levar bastante tempo. Continuar?",
        )
        if not answer:
            return
        thread = threading.Thread(target=self._run_all_worker, args=(run_all_path, orca_exe), daemon=True)
        thread.start()

    def _run_all_worker(self, run_all_path: Path, orca_exe: str):
        env = os.environ.copy()
        env["ORCA_EXE"] = orca_exe
        try:
            proc = subprocess.Popen(
                ["bash", str(run_all_path)], cwd=str(self.output_folder),
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env,
            )
            for line in proc.stdout:
                self.after(0, self._log_run, line.rstrip())
            proc.wait()
            self.after(0, self._log_run, f"\nProcesso finalizado (codigo {proc.returncode}).")
        except FileNotFoundError:
            self.after(0, lambda: messagebox.showerror(
                "Erro", "Nao foi possivel executar 'bash'. Rode run_all.sh manualmente no seu terminal."
            ))
        except Exception as e:
            self.after(0, lambda: messagebox.showerror("Erro ao executar", str(e)))

    def _log_run(self, line: str):
        self._log(line)
        if "Rodando" in line:
            self.progress.step(1)

    def _on_analyze(self):
        if not self.output_folder or not self.output_folder.exists():
            messagebox.showerror("Erro", "Defina/gere a pasta de saida primeiro.")
            return
        self._sync_params_from_ui()
        pairs = self.pairs if not self.tree_pairs.selection() else self._selected_pairs()
        results = collect_results(pairs, self.output_folder, self.params.do_correction)
        self._last_results = results
        self.tree_results.delete(*self.tree_results.get_children())
        for r in results:
            de = r.de_hartree
            de_kcal = de * HARTREE_TO_KCAL if de is not None else None
            dec = r.de_correction_hartree
            dec_kcal = dec * HARTREE_TO_KCAL if dec is not None else None

            def fmt(v, nd=6):
                return f"{v:.{nd}f}" if v is not None else "-"

            self.tree_results.insert("", "end", values=(
                r.pair_key, fmt(r.e_fraga, 8), fmt(r.e_fragb, 8), fmt(r.e_total, 8),
                fmt(r.e_mona, 8), fmt(r.e_monb, 8),
                fmt(de, 8), fmt(de_kcal, 3), fmt(dec_kcal, 3),
                "; ".join(r.missing) if r.missing else "",
            ))
        self.status_var.set(f"Analise concluida para {len(results)} par(es).")

    def _on_export_csv(self):
        if not self._last_results:
            messagebox.showinfo("Aviso", "Rode 'Analisar resultados' primeiro.")
            return
        path = filedialog.asksaveasfilename(
            title="Salvar CSV", defaultextension=".csv", filetypes=[("CSV", "*.csv")]
        )
        if not path:
            return
        export_results_csv(self._last_results, Path(path))
        messagebox.showinfo("Sucesso", f"Resultados exportados para:\n{path}")


def main():
    app = App()
    app.mainloop()


if __name__ == "__main__":
    main()
