# translate5_GUI.py (全体をこちらに差し替え)

import os
import threading
import configparser
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from types import SimpleNamespace
import traceback # tracebackモジュールをインポート

try:
    from ttkthemes import ThemedTk
except Exception:
    ThemedTk = None

from tkai_lib import read_ai_config
from translate5 import (
    initialize,
    update_variables,
    execute,
    role_content as DEFAULT_TRANSLATE_ROLE,
    prompt_template as DEFAULT_TRANSLATE_PROMPT,
    reformat_role as DEFAULT_REFORMAT_ROLE,
    reformat_prompt as DEFAULT_REFORMAT_PROMPT,
)

PROGRAM_NAME = "translate5 GUI Runner"
API_CHOICES = ["openai", "openai5", "google", "deepl"]
OPENAI_MODELS_V4 = ["gpt-4o", "gpt-4.5", "gpt-3.5-turbo"]
OPENAI_MODELS_V5 = ["gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5-chat-latest"]
GOOGLE_MODELS = ["gemini-1.5-flash", "gemini-1.5-pro"]
DEEPL_MODELS = ["(auto)"]
REASONING_EFFORTS = ["low", "medium", "high"]
PROCESS_UNITS = ["paragraph", "run", "md"]
MODES = [("日本語→英語 (je)", "je"), ("英語→日本語 (ej)", "ej")]
CONFIG_INI = os.path.splitext(os.path.abspath(__file__))[0] + ".ini"


# 修正点2: コピー可能なエラーダイアログを定義
class CopyableErrorDialog(tk.Toplevel):
    def __init__(self, parent, title, message):
        super().__init__(parent)
        self.title(title)
        self.geometry("700x400")

        # ウィンドウをモーダルにする (他のウィンドウを操作できなくする)
        self.transient(parent)
        self.grab_set()

        # UI要素
        text_widget = tk.Text(self, wrap="word", font=("Courier", 10))
        text_widget.pack(padx=10, pady=10, fill="both", expand=True)
        text_widget.insert("1.0", message)
        text_widget.config(state="disabled") # 読み取り専用にする (コピーは可能)

        button = ttk.Button(self, text="Close", command=self.destroy)
        button.pack(pady=10)

        # 親ウィンドウの中央に表示
        self.update_idletasks()
        x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
        y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
        self.geometry(f"+{x}+{y}")
        
        button.focus_set()
        self.wait_window(self)


class App:
    def __init__(self, master):
        self.master = master
        master.title(PROGRAM_NAME)
        master.geometry("960x800")
        self.config = configparser.ConfigParser(comment_prefixes=('~',))
        print("--- [INFO] Loading .env files...")
        self.cfg, self.parser = initialize()
        self.cfg = update_variables(self.cfg, self.parser)
        print("endpoint=", self.cfg.endpoint)
        self._busy = False
        self._build_ui()
        self._load_config()
        self._apply_api_to_models()
        master.protocol("WM_DELETE_WINDOW", self.on_close)

    # ( _build_ui などの変更のない部分は省略 )
    # ...
    # ---------------- UI 構築 ----------------
    def _build_ui(self):
        # (UI構築コードは変更なし)
        io_frame = ttk.LabelFrame(self.master, text="Input / Template")
        io_frame.pack(fill="x", padx=10, pady=8)
        ttk.Label(io_frame, text="Input file (.docx/.pptx/.pdf/.html/.md):").grid(row=0, column=0, sticky="w", padx=6, pady=6)
        self.infile_var = tk.StringVar(value=self.cfg.infile if hasattr(self.cfg, "infile") else "")
        e_in = ttk.Entry(io_frame, textvariable=self.infile_var)
        e_in.grid(row=0, column=1, sticky="ew", padx=6, pady=6)
        ttk.Button(io_frame, text="Browse", command=self._pick_infile).grid(row=0, column=2, padx=6, pady=6)
        ttk.Label(io_frame, text="HTML template (template_translate.html):").grid(row=1, column=0, sticky="w", padx=6, pady=6)
        self.tpl_var = tk.StringVar(value=getattr(self.cfg, "html_template_path", "template_translate.html"))
        e_tpl = ttk.Entry(io_frame, textvariable=self.tpl_var)
        e_tpl.grid(row=1, column=1, sticky="ew", padx=6, pady=6)
        ttk.Button(io_frame, text="Browse", command=self._pick_template).grid(row=1, column=2, padx=6, pady=6)
        io_frame.grid_columnconfigure(1, weight=1)
        mode_frame = ttk.LabelFrame(self.master, text="Mode / Options")
        mode_frame.pack(fill="x", padx=10, pady=8)
        self.mode_var = tk.StringVar(value=getattr(self.cfg, "mode", "je"))
        col = 0
        for label, val in MODES:
            ttk.Radiobutton(mode_frame, text=label, variable=self.mode_var, value=val).grid(row=0, column=col, padx=6, pady=6, sticky="w")
            col += 1
        ttk.Label(mode_frame, text="Process unit:").grid(row=0, column=col, padx=12, pady=6, sticky="e")
        self.proc_unit_var = tk.StringVar(value=getattr(self.cfg, "process_unit", "paragraph"))
        cb_proc = ttk.Combobox(mode_frame, state="readonly", values=PROCESS_UNITS, textvariable=self.proc_unit_var, width=10)
        cb_proc.grid(row=0, column=col + 1, padx=6, pady=6, sticky="w")
        api_frame = ttk.LabelFrame(self.master, text="API")
        api_frame.pack(fill="x", padx=10, pady=8)
        ttk.Label(api_frame, text="API:").grid(row=0, column=0, padx=6, pady=6, sticky="w")
        self.api_var = tk.StringVar(value=getattr(self.cfg, "api", "openai5"))
        self.api_cb = ttk.Combobox(api_frame, state="readonly", values=API_CHOICES, textvariable=self.api_var, width=10)
        self.api_cb.grid(row=0, column=1, padx=6, pady=6, sticky="w")
        self.api_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_api_to_models())
        ttk.Label(api_frame, text="Model:").grid(row=0, column=2, padx=12, pady=6, sticky="e")
        self.model_var = tk.StringVar(value="")
        self.model_cb = ttk.Combobox(api_frame, state="readonly", values=[], textvariable=self.model_var, width=24)
        self.model_cb.grid(row=0, column=3, padx=6, pady=6, sticky="w")
        ttk.Label(api_frame, text="Endpoint (DeepL / custom):").grid(row=0, column=4, padx=12, pady=6, sticky="e")
        endpoint_default = os.environ.get("endpoint", "")
        print("endpoint_default=", endpoint_default)
        self.endpoint_var = tk.StringVar(value=endpoint_default if endpoint_default else "")
        ttk.Entry(api_frame, textvariable=self.endpoint_var, width=50).grid(row=0, column=5, padx=6, pady=6, sticky="w")
        api_frame.grid_columnconfigure(6, weight=1)
        gen_frame = ttk.LabelFrame(self.master, text="Generation Parameters")
        gen_frame.pack(fill="x", padx=10, pady=8)
        ttk.Label(gen_frame, text="temperature (OpenAI/Google):").grid(row=0, column=0, padx=6, pady=6, sticky="e")
        self.temp_var = tk.StringVar(value=str(getattr(self.cfg, "temperature", 0.3)))
        ttk.Entry(gen_frame, textvariable=self.temp_var, width=8).grid(row=0, column=1, padx=6, pady=6, sticky="w")
        ttk.Label(gen_frame, text="max_tokens (or max_output_tokens):").grid(row=0, column=2, padx=12, pady=6, sticky="e")
        self.max_tok_var = tk.StringVar(value=str(getattr(self.cfg, "max_tokens", 2000)))
        ttk.Entry(gen_frame, textvariable=self.max_tok_var, width=10).grid(row=0, column=3, padx=6, pady=6, sticky="w")
        ttk.Label(gen_frame, text="reasoning_effort (OpenAI5):").grid(row=0, column=4, padx=12, pady=6, sticky="e")
        self.effort_var = tk.StringVar(value=getattr(self.cfg, "reasoning_effort", "low"))
        ttk.Combobox(gen_frame, state="readonly", values=REASONING_EFFORTS, textvariable=self.effort_var, width=8).grid(row=0, column=5, padx=6, pady=6, sticky="w")
        rule_frame = ttk.LabelFrame(self.master, text="Translation Rules")
        rule_frame.pack(fill="x", padx=10, pady=8)
        ttk.Label(rule_frame, text="min_translate_length:").grid(row=0, column=0, padx=6, pady=6, sticky="e")
        self.min_len_var = tk.StringVar(value=str(getattr(self.cfg, "min_translate_length", 5)))
        ttk.Entry(rule_frame, textvariable=self.min_len_var, width=6).grid(row=0, column=1, padx=6, pady=6, sticky="w")
        ttk.Label(rule_frame, text="allowed_translation_length_ratio:").grid(row=0, column=2, padx=12, pady=6, sticky="e")
        self.len_ratio_var = tk.StringVar(value=str(getattr(self.cfg, "allowed_translation_length_ratio", 5.0)))
        ttk.Entry(rule_frame, textvariable=self.len_ratio_var, width=6).grid(row=0, column=3, padx=6, pady=6, sticky="w")
        self.limit_mb_var = tk.BooleanVar(value=bool(getattr(self.cfg, "limit_to_multibyte_str", self._mode_implies_mb(getattr(self.cfg, "mode", "je")))))
        ttk.Checkbutton(rule_frame, text="Limit to multibyte strings (JA source)", variable=self.limit_mb_var).grid(row=0, column=4, padx=12, pady=6, sticky="w")
        rp_frame = ttk.LabelFrame(self.master, text="Prompts")
        rp_frame.pack(fill="both", expand=True, padx=10, pady=8)
        ttk.Label(rp_frame, text="Translate Role:").grid(row=0, column=0, padx=6, pady=6, sticky="nw")
        self.tr_role = tk.Text(rp_frame, height=4)
        self.tr_role.grid(row=0, column=1, padx=6, pady=6, sticky="nsew")
        self.tr_role.insert("1.0", DEFAULT_TRANSLATE_ROLE)
        ttk.Label(rp_frame, text="Translate Prompt:").grid(row=1, column=0, padx=6, pady=6, sticky="nw")
        self.tr_prompt = tk.Text(rp_frame, height=8, wrap="word")
        self.tr_prompt.grid(row=1, column=1, padx=6, pady=6, sticky="nsew")
        self.tr_prompt.insert("1.0", DEFAULT_TRANSLATE_PROMPT)
        ttk.Label(rp_frame, text="Reformat Role (PDF→MD 整形):").grid(row=2, column=0, padx=6, pady=6, sticky="nw")
        self.rf_role = tk.Text(rp_frame, height=3)
        self.rf_role.grid(row=2, column=1, padx=6, pady=6, sticky="nsew")
        self.rf_role.insert("1.0", DEFAULT_REFORMAT_ROLE)
        ttk.Label(rp_frame, text="Reformat Prompt:").grid(row=3, column=0, padx=6, pady=6, sticky="nw")
        self.rf_prompt = tk.Text(rp_frame, height=6, wrap="word")
        self.rf_prompt.grid(row=3, column=1, padx=6, pady=6, sticky="nsew")
        self.rf_prompt.insert("1.0", DEFAULT_REFORMAT_PROMPT)
        rp_frame.grid_columnconfigure(1, weight=1)
        rp_frame.grid_rowconfigure(1, weight=1)
        rp_frame.grid_rowconfigure(3, weight=1)
        btn_frame = ttk.Frame(self.master)
        btn_frame.pack(pady=10)
        self.run_btn = ttk.Button(btn_frame, text="Run execute()", command=self._run_execute_thread)
        self.run_btn.grid(row=0, column=0, padx=6)
        ttk.Button(btn_frame, text="Close", command=self.on_close).grid(row=0, column=1, padx=6)
        self.status = ttk.Label(self.master, text="Ready", relief="sunken", anchor="w")
        self.status.pack(fill="x", padx=0, pady=(4, 0), ipady=2)
    def _mode_implies_mb(self, mode):
        return True if mode and mode.startswith("j") else False
    def _pick_infile(self):
        fp = filedialog.askopenfilename(title="Select input file", filetypes=[("Supported", "*.docx;*.pptx;*.pdf;*.html;*.htm;*.md;*.txt"),("All files", "*.*")])
        if fp: self.infile_var.set(fp)
    def _pick_template(self):
        fp = filedialog.askopenfilename(title="Select HTML template", filetypes=[("HTML", "*.html;*.htm"), ("All files", "*.*")])
        if fp: self.tpl_var.set(fp)
    def _apply_api_to_models(self):
        api = self.api_var.get()
        if api == "openai":
            self.model_cb["values"] = OPENAI_MODELS_V4
            default = getattr(self.cfg, "openai_model", "gpt-4o")
            self.model_var.set(default if default in OPENAI_MODELS_V4 else "gpt-4o")
        elif api == "openai5":
            self.model_cb["values"] = OPENAI_MODELS_V5
            default = getattr(self.cfg, "openai_model5", "gpt-5-nano")
            self.model_var.set(default if default in OPENAI_MODELS_V5 else "gpt-5-nano")
        elif api == "google":
            self.model_cb["values"] = GOOGLE_MODELS
            default = getattr(self.cfg, "google_model", "gemini-1.5-flash")
            self.model_var.set(default if default in GOOGLE_MODELS else "gemini-1.5-flash")
        elif api == "deepl":
            self.model_cb["values"] = DEEPL_MODELS
            self.model_var.set("(auto)")
        else:
            self.model_cb["values"] = []
            self.model_var.set("")

    def _worker_run(self):
        try:
            execute(self.cfg)
            self._set_status("Done.")
        except SystemExit:
            self._set_status("Finished (SystemExit).")
        except Exception as e:
            # 修正点2: 詳細なエラー情報を取得してカスタムダイアログに渡す
            error_details = traceback.format_exc()
            self._set_status(f"Error: {e}")
            self._show_error_async("Execution Error", error_details)
        finally:
            self.master.after(0, lambda: self.run_btn.config(state="normal"))
            self._busy = False

    def _show_error_async(self, title, msg):
        # 修正点2: messageboxの代わりにカスタムダイアログを呼び出す
        self.master.after(0, lambda: CopyableErrorDialog(self.master, title, msg))
    
    # ... (変更のない他のメソッドは省略) ...
    def _run_execute_thread(self):
        if self._busy: return
        try:
            self._update_cfg_from_ui()
        except Exception as e:
            messagebox.showerror("設定エラー", f"設定の読み取りに失敗しました:\n{e}")
            return
        if not os.path.isfile(self.cfg.infile):
            messagebox.showerror("入力エラー", f"入力ファイルが見つかりません:\n{self.cfg.infile}")
            return
        self._busy = True
        self.status.config(text="Running execute() ...")
        self.run_btn.config(state="disabled")
        t = threading.Thread(target=self._worker_run)
        t.daemon = True
        t.start()
    def _set_status(self, text):
        self.master.after(0, lambda: self.status.config(text=text))
    def _update_cfg_from_ui(self):
        self.cfg.infile = self.infile_var.get().strip()
        self.cfg.html_template_path = self.tpl_var.get().strip() or "template_translate.html"
        self.cfg.mode = self.mode_var.get()
        self.cfg.process_unit = self.proc_unit_var.get()
        self.cfg.use_md = True if self.cfg.process_unit == 'md' else False
        api = self.api_var.get()
        self.cfg.api = api
        model = self.model_var.get()
        if api == "openai": self.cfg.openai_model = model
        else: self.cfg.openai_model = None
        if api == "openai5": self.cfg.openai_model5 = model
        else: self.cfg.openai_model5 = None
        if api == "google": self.cfg.google_model = model
        else: self.cfg.google_model = None
        self.cfg.endpoint = self.endpoint_var.get().strip() or getattr(self.cfg, "endpoint", None)
        self.cfg.temperature = float(self.temp_var.get())
        self.cfg.max_tokens = int(float(self.max_tok_var.get()))
        self.cfg.reasoning_effort = self.effort_var.get()
        self.cfg.min_translate_length = int(float(self.min_len_var.get()))
        self.cfg.allowed_translation_length_ratio = float(self.len_ratio_var.get())
        self.cfg.limit_to_multibyte_str = bool(self.limit_mb_var.get())
        self.cfg.translate_role = self.tr_role.get("1.0", "end").strip() or DEFAULT_TRANSLATE_ROLE
        self.cfg.translate_prompt = self.tr_prompt.get("1.0", "end").strip() or DEFAULT_TRANSLATE_PROMPT
        self.cfg.reformat_role = self.rf_role.get("1.0", "end").strip() or DEFAULT_REFORMAT_ROLE
        self.cfg.reformat_prompt = self.rf_prompt.get("1.0", "end").strip() or DEFAULT_REFORMAT_PROMPT
        self.cfg.openai_api_key = os.getenv("OPENAI_API_KEY")
        self.cfg.google_api_key = os.getenv("GOOGLE_API_KEY")
        self.cfg.deepl_api_key = os.getenv("DEEPL_API_KEY")
        self.cfg.output_html_path = None
        self.cfg.force_server_charcode = 'utf-8'
        self.cfg.tsleep_rpm = 0.0
        self.cfg.top_p = 0.8
        self.cfg.top_k = 40
        if self.cfg.mode.startswith("j"): self.cfg.source_lang = "JA"
        else: self.cfg.source_lang = "EN"
        if self.cfg.mode.endswith("j"): self.cfg.target_lang = "JA"
        else: self.cfg.target_lang = "EN"
    def _load_config(self):
        if os.path.exists(CONFIG_INI):
            try:
                self.config.read(CONFIG_INI, encoding="utf-8")
                s = self.config["Settings"]
                if "geometry" in s:
                    self.master.geometry(s.get("geometry"))
                self.infile_var.set(s.get("infile", self.infile_var.get()))
                self.tpl_var.set(s.get("html_template_path", self.tpl_var.get()))
                self.mode_var.set(s.get("mode", self.mode_var.get()))
                self.proc_unit_var.set(s.get("process_unit", self.proc_unit_var.get()))
                self.api_var.set(s.get("api", self.api_var.get()))
                self.model_var.set(s.get("model", self.model_var.get()))
                self.endpoint_var.set(s.get("endpoint", self.endpoint_var.get()))
                self.temp_var.set(s.get("temperature", self.temp_var.get()))
                self.max_tok_var.set(s.get("max_tokens", self.max_tok_var.get()))
                self.effort_var.set(s.get("reasoning_effort", self.effort_var.get()))
                self.min_len_var.set(s.get("min_translate_length", self.min_len_var.get()))
                self.len_ratio_var.set(s.get("allowed_translation_length_ratio", self.len_ratio_var.get()))
                self.limit_mb_var.set(s.getboolean("limit_to_multibyte_str", self.limit_mb_var.get()))
                self.tr_role.delete("1.0", "end"); self.tr_role.insert("1.0", s.get("translate_role", DEFAULT_TRANSLATE_ROLE))
                self.tr_prompt.delete("1.0", "end"); self.tr_prompt.insert("1.0", s.get("translate_prompt", DEFAULT_TRANSLATE_PROMPT))
                self.rf_role.delete("1.0", "end"); self.rf_role.insert("1.0", s.get("reformat_role", DEFAULT_REFORMAT_ROLE))
                self.rf_prompt.delete("1.0", "end"); self.rf_prompt.insert("1.0", s.get("reformat_prompt", DEFAULT_REFORMAT_PROMPT))
            except Exception:
                pass
    def _save_config(self):
        if "Settings" not in self.config:
            self.config["Settings"] = {}
        s = self.config["Settings"]
        s["geometry"] = self.master.winfo_geometry()
        s["infile"] = self.infile_var.get()
        s["html_template_path"] = self.tpl_var.get()
        s["mode"] = self.mode_var.get()
        s["process_unit"] = self.proc_unit_var.get()
        s["api"] = self.api_var.get()
        s["model"] = self.model_var.get()
        s["endpoint"] = self.endpoint_var.get()
        s["temperature"] = self.temp_var.get()
        s["max_tokens"] = self.max_tok_var.get()
        s["reasoning_effort"] = self.effort_var.get()
        s["min_translate_length"] = self.min_len_var.get()
        s["allowed_translation_length_ratio"] = self.len_ratio_var.get()
        s["limit_to_multibyte_str"] = str(bool(self.limit_mb_var.get()))
        s["translate_role"] = self.tr_role.get("1.0", "end").strip()
        s["translate_prompt"] = self.tr_prompt.get("1.0", "end").strip()
        s["reformat_role"] = self.rf_role.get("1.0", "end").strip()
        s["reformat_prompt"] = self.rf_prompt.get("1.0", "end").strip()
        try:
            with open(CONFIG_INI, "w", encoding="utf-8") as f:
                self.config.write(f)
        except Exception as e:
            messagebox.showwarning("保存警告", f"設定保存に失敗しました: {e}")
    def on_close(self):
        try:
            self._save_config()
        finally:
            self.master.destroy()

def main():
    root = ThemedTk(theme="plastik") if ThemedTk else tk.Tk()
    if not ThemedTk:
        try:
            root.style = ttk.Style()
            root.style.theme_use("clam")
        except Exception:
            pass
    App(root)
    root.mainloop()

if __name__ == "__main__":
    main()