#!/usr/bin/env python3
# coding: utf-8
# pip install tkinter tkinterdnd2 windnd

import tkinter as tk
from tkinter import filedialog, messagebox
import subprocess
import os
import sys
import threading
import tempfile
import stat
import platform

# ---------------- detect dnd libs ----------------
HAS_TKDND = False
HAS_WINDND = False
try:
    from tkinterdnd2 import TkinterDnD, DND_FILES
    HAS_TKDND = True
except Exception:
    HAS_TKDND = False

# windnd is windows-only drop helper (optional)
try:
    import windnd
    HAS_WINDND = True
except Exception:
    HAS_WINDND = False


# ---------------- helpers ----------------
def check_openssl():
    try:
        subprocess.run(["openssl", "version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        return True
    except Exception:
        return False


def _sanitize_dropped_path(s: str) -> str:
    """Limpa a string recebida pelo drop para retornar o primeiro caminho válido."""
    if not s:
        return s
    s = s.strip()

    # windows windnd may give bytes via callback; but here we expect string
    # Remove surrounding braces that some drag handlers add: {C:\path\with space\file.txt}
    if s.startswith("{") and s.endswith("}"):
        s = s[1:-1]

    # Se houver múltiplos caminhos separados por espaços, tente encontrar um válido
    parts = s.split()
    for p in parts:
        # remover chaves possíveis em cada parte
        if p.startswith("{") and p.endswith("}"):
            p = p[1:-1]
        if os.path.exists(p):
            return p

    # se não encontrou por espaços, talvez o próprio s seja caminho relativo com espaços; testar diretamente
    if os.path.exists(s):
        return s

    # nenhuma correspondência: retornar original (o caller validará)
    return s


def set_widgets_state(state: str):
    """Desabilita/habilita widgets principais."""
    entry_file.config(state=state)
    entry_password.config(state=state)
    entry_password_confirm.config(state=state)
    btn_file.config(state=state)
    btn_encrypt.config(state=state)
    btn_decrypt.config(state=state)


# ---------------- run openssl com arquivo de senha ----------------
def _run_openssl_with_passfile(cmd_list, password):
    """Cria arquivo temporário com a senha, substitui placeholder {PASSFILE} por file:<path>.
       Executa openssl e remove o temporário ao final.
       Retorna (returncode, stdout, stderr)."""
    tf = None
    try:
        tf = tempfile.NamedTemporaryFile(delete=False)
        tf_name = tf.name
        tf.write(password.encode("utf-8"))
        tf.flush()
        tf.close()
        try:
            # tentar restringir permissões onde for possível
            os.chmod(tf_name, stat.S_IRUSR | stat.S_IWUSR)
        except Exception:
            pass

        cmd = []
        for part in cmd_list:
            if part == "{PASSFILE}":
                cmd.append("file:" + tf_name)
            else:
                cmd.append(part)

        proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out = proc.stdout.decode(errors="ignore")
        err = proc.stderr.decode(errors="ignore")
        return proc.returncode, out, err
    finally:
        if tf is not None:
            try:
                os.remove(tf_name)
            except Exception:
                pass


# ---------------- criptografia/descriptografia ----------------
def encrypt_task(infile, password, password_confirm, outfile):
    try:
        if not password:
            messagebox.showerror("Erro", "Digite uma senha.")
            return
        if password != password_confirm:
            messagebox.showerror("Erro", "As senhas não coincidem.")
            return
        if not infile or not os.path.isfile(infile):
            messagebox.showerror("Erro", "Arquivo de entrada inválido.")
            return
        if not outfile:
            messagebox.showerror("Erro", "Selecione onde salvar o arquivo encriptado.")
            return

        set_widgets_state("disabled")

        cmd = [
            "openssl", "aes-256-cbc",
            "-e", "-salt", "-pbkdf2", "-iter", "500000",
            "-in", infile,
            "-out", outfile,
            "-pass", "{PASSFILE}"
        ]

        code, out, err = _run_openssl_with_passfile(cmd, password)
        if code != 0:
            messagebox.showerror("Erro", f"Falha ao encriptar. Saída do openssl:\n{err.strip()}")
        else:
            messagebox.showinfo("Sucesso", f"Arquivo encriptado salvo em:\n{outfile}")

    except Exception as e:
        messagebox.showerror("Erro inesperado", str(e))
    finally:
        set_widgets_state("normal")


def decrypt_task(infile, password, outfile):
    try:
        if not password:
            messagebox.showerror("Erro", "Digite a senha.")
            return
        if not infile or not os.path.isfile(infile):
            messagebox.showerror("Erro", "Arquivo de entrada inválido.")
            return
        if not outfile:
            messagebox.showerror("Erro", "Selecione onde salvar o arquivo desencriptado.")
            return

        set_widgets_state("disabled")

        cmd = [
            "openssl", "aes-256-cbc",
            "-d", "-salt", "-pbkdf2", "-iter", "500000",
            "-in", infile,
            "-out", outfile,
            "-pass", "{PASSFILE}"
        ]

        code, out, err = _run_openssl_with_passfile(cmd, password)
        if code != 0:
            messagebox.showerror("Erro", f"Falha ao desencriptar. Saída do openssl:\n{err.strip()}")
        else:
            messagebox.showinfo("Sucesso", f"Arquivo desencriptado salvo em:\n{outfile}")

    except Exception as e:
        messagebox.showerror("Erro inesperado", str(e))
    finally:
        set_widgets_state("normal")


def run_encrypt():
    infile = entry_file_var.get()
    password = entry_password.get()
    password_confirm = entry_password_confirm.get()

    if not infile or not os.path.isfile(infile):
        messagebox.showerror("Erro", "Selecione um arquivo válido para encriptar.")
        return

    # sugerir arquivo.ext.aepo
    base = os.path.basename(infile)
    suggested = base + ".aepo"
    out = filedialog.asksaveasfilename(title="Salvar arquivo encriptado como", initialfile=suggested, defaultextension="")
    if not out:
        return

    t = threading.Thread(target=encrypt_task, args=(infile, password, password_confirm, out), daemon=True)
    t.start()


def run_decrypt():
    infile = entry_file_var.get()
    password = entry_password.get()

    if not infile or not os.path.isfile(infile):
        messagebox.showerror("Erro", "Selecione um arquivo válido para desencriptar.")
        return

    base = os.path.basename(infile)
    if base.lower().endswith(".aepo"):
        suggested = base[:-5]
    else:
        suggested = base

    out = filedialog.asksaveasfilename(title="Salvar arquivo desencriptado como", initialfile=suggested, defaultextension="")
    if not out:
        return

    t = threading.Thread(target=decrypt_task, args=(infile, password, out), daemon=True)
    t.start()


# ---------------- drag & drop setup ----------------
def _bind_dnd_for_entry(entry_widget):
    """Configura drag & drop para o entry. Usa tkinterdnd2 se disponível, senão windnd no Windows."""
    # usando tkinterdnd2 (recomendado; funciona em Linux/Windows se instalado)
    if HAS_TKDND:
        try:
            # DND_FILES vem do tkinterdnd2 importado no topo
            def _handle_drop(event):
                path = _sanitize_dropped_path(event.data)
                if path:
                    entry_file_var.set(path)
            entry_widget.drop_target_register(DND_FILES)
            entry_widget.dnd_bind('<<Drop>>', _handle_drop)
            return True
        except Exception:
            pass

    # fallback windnd (Windows)
    if HAS_WINDND and platform.system().lower().startswith("win"):
        try:
            def _windnd_cb(paths):
                # windnd fornece lista de bytes
                if not paths:
                    return
                first = paths[0]
                if isinstance(first, bytes):
                    try:
                        p = first.decode("utf-8")
                    except Exception:
                        p = first.decode("mbcs", errors="ignore")
                else:
                    p = str(first)
                p = _sanitize_dropped_path(p)
                if p:
                    # atualizar via after para garantir thread-safe
                    entry_widget.after(0, lambda: entry_file_var.set(p))
            # hook to the root window (windnd expects window handle)
            windnd.hook_dropfiles(root.winfo_id(), _windnd_cb)
            return True
        except Exception:
            pass

    # nenhum DnD disponível
    return False


# ---------------- GUI principal ----------------
if not check_openssl():
    messagebox.showerror("Erro", "OpenSSL não encontrado no sistema. Instale-o e coloque no PATH.")
    sys.exit(1)

# criar root: se tkinterdnd2 disponível, usamos TkinterDnD.Tk() para compatibilidade
if HAS_TKDND:
    root = TkinterDnD.Tk()
else:
    root = tk.Tk()

root.title("AES-256-CBC (OpenSSL) — Encrypt/Decrypt")
root.geometry("640x380")
root.resizable(False, False)
padx = 12

frame_top = tk.Frame(root)
frame_top.pack(pady=14, fill="x", padx=padx)

tk.Label(frame_top, text="Arquivo:").grid(row=0, column=0, sticky="w")
entry_file_var = tk.StringVar()
entry_file = tk.Entry(frame_top, textvariable=entry_file_var, width=68)
entry_file.grid(row=1, column=0, columnspan=2, sticky="w")

btn_file = tk.Button(frame_top, text="Selecionar...", command=lambda: entry_file_var.set(filedialog.askopenfilename()), width=14)
btn_file.grid(row=1, column=2, padx=(10, 0))

# tentar habilitar drag & drop
dnd_ok = _bind_dnd_for_entry(entry_file)
if dnd_ok:
    lbl_dnd = tk.Label(frame_top, text="(Arraste e solte um arquivo sobre o campo acima)", fg="gray")
else:
    lbl_dnd = tk.Label(frame_top, text="(Drag & Drop não habilitado. Instale 'tkinterdnd2' ou 'windnd')", fg="gray")
lbl_dnd.grid(row=2, column=0, columnspan=3, sticky="w", pady=(6, 0))

# senhas
frame_pw = tk.Frame(root)
frame_pw.pack(pady=8, fill="x", padx=padx)

tk.Label(frame_pw, text="Senha:").grid(row=0, column=0, sticky="w")
entry_password = tk.Entry(frame_pw, show="*", width=44)
entry_password.grid(row=1, column=0, sticky="w")

tk.Label(frame_pw, text="Confirmar senha (somente ao encriptar):").grid(row=0, column=1, sticky="w", padx=(12, 0))
entry_password_confirm = tk.Entry(frame_pw, show="*", width=36)
entry_password_confirm.grid(row=1, column=1, sticky="w", padx=(12, 0))

# botões
frame_buttons = tk.Frame(root)
frame_buttons.pack(pady=18)

btn_encrypt = tk.Button(frame_buttons, text="Encriptar", width=20, command=run_encrypt)
btn_encrypt.grid(row=0, column=0, padx=14)

btn_decrypt = tk.Button(frame_buttons, text="Desencriptar", width=20, command=run_decrypt)
btn_decrypt.grid(row=0, column=1, padx=14)

footer = tk.Label(root, text="Compatível com: openssl aes-256-cbc -e/-d -salt -pbkdf2 -iter 500000", fg="black")
footer.pack(side="bottom", pady=8)

root.mainloop()
