import os
import threading
import configparser
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import traceback

try:
    from ttkthemes import ThemedTk
except Exception:
    ThemedTk = None

#from translate5 import (
from translate5_com import (
    initialize,
    update_variables,
    execute,
)

PROGRAM_NAME = "translate5 GUI Runner"

# translate5.py に合わせた API 一覧
API_CHOICES = ["openai5", "openai", "google", "gemini", "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-2.5-flash", "gemini-2.5-pro", "gemini-2.5-light", "gemini-3-pro-preview", "gemini-3.0-flash", "gemini-3.0-pro"]
DEEPL_MODELS = ["(auto)"]

REASONING_EFFORTS = ["low", "medium", "high"]
PROCESS_UNITS = ["paragraph", "run", "md"]  # md は GUI 専用で use_md フラグにマップ
MODES = [("日本語→英語 (je)", "je"), ("英語→日本語 (ej)", "ej")]

CONFIG_INI = os.path.splitext(os.path.abspath(__file__))[0] + ".ini"


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()

        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] Initialize translate5 config ---")
        self.cfg, self.parser = initialize()
        self.cfg = update_variables(self.cfg, self.parser)
        print("endpoint (from env/args) =", getattr(self.cfg, "endpoint", None))

        self._busy = False

        self._build_ui()
        self._load_config()
        self._apply_api_to_models()

        master.protocol("WM_DELETE_WINDOW", self.on_close)

    # ---------------- UI 構築 ----------------
    def _build_ui(self):
        # Input / Template
        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/.txt):").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 / Options
        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
        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")

        # Endpointは DeepL/custom 用のみ
        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(env) =", 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)

        # Generation Parameters
        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/Gemini):").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")

        # Translation Rules
        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")

        # Prompts（translate_prompt / reformat_prompt のみ）
        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 Prompt (full instructions):").grid(
            row=0, column=0, padx=6, pady=6, sticky="nw"
        )
        self.tr_prompt = tk.Text(rp_frame, height=8, wrap="word")
        self.tr_prompt.grid(row=0, column=1, padx=6, pady=6, sticky="nsew")
        self.tr_prompt.insert(
            "1.0",
            getattr(self.cfg, "translate_prompt", ""),
        )

        ttk.Label(rp_frame, text="Reformat Prompt (PDF→MD etc.):").grid(
            row=1, column=0, padx=6, pady=6, sticky="nw"
        )
        self.rf_prompt = tk.Text(rp_frame, height=6, wrap="word")
        self.rf_prompt.grid(row=1, column=1, padx=6, pady=6, sticky="nsew")
        self.rf_prompt.insert(
            "1.0",
            getattr(self.cfg, "reformat_prompt", ""),
        )

        rp_frame.grid_columnconfigure(1, weight=1)
        rp_frame.grid_rowconfigure(0, weight=1)
        rp_frame.grid_rowconfigure(1, weight=1)

        # Buttons
        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
        )

        # Status bar
        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)

    # APIごとにモデル候補を切り替え（translate5仕様に合わせ）
    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 in ("google", "gemini"):
            self.model_cb["values"] = GOOGLE_MODELS
            new_default = GOOGLE_MODELS[0] if GOOGLE_MODELS else ""
            default = getattr(self.cfg, "google_model", new_default)
            self.model_var.set(
                default if default in GOOGLE_MODELS else new_default
            )
        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:
            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):
        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()

        # process_unit / use_md (md は GUI 独自指定として use_md=True にマップ)
        proc_unit = self.proc_unit_var.get()
        if proc_unit == "md":
            self.cfg.use_md = True
            # 実際の処理で使わないが一応 paragraph にしておく
            self.cfg.process_unit = "paragraph"
        else:
            self.cfg.use_md = False
            self.cfg.process_unit = proc_unit or "paragraph"

        # API / model
        api = self.api_var.get()
        self.cfg.api = api
        model = self.model_var.get()

        if api == "openai":
            self.cfg.openai_model = model
            self.cfg.openai_model5 = None
            self.cfg.google_model = None
        elif api == "openai5":
            self.cfg.openai_model5 = model
            self.cfg.openai_model = None
            self.cfg.google_model = None
        elif api in ("google", "gemini"):
            self.cfg.google_model = model
            self.cfg.openai_model = None
            self.cfg.openai_model5 = None
        elif api == "deepl":
            self.cfg.openai_model = None
            self.cfg.openai_model5 = None
            self.cfg.google_model = None
        else:
            self.cfg.openai_model = None
            self.cfg.openai_model5 = None
            self.cfg.google_model = None

        # Endpoint は DeepL / custom のみ使用
        if api == "deepl":
            self.cfg.endpoint = (
                self.endpoint_var.get().strip()
                or getattr(self.cfg, "endpoint", None)
            )
        else:
            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())

        # translate5 新仕様: prompt は一体型のみ
        self.cfg.translate_prompt = (
            self.tr_prompt.get("1.0", "end").strip()
            or getattr(self.cfg, "translate_prompt", "")
        )
        self.cfg.reformat_prompt = (
            self.rf_prompt.get("1.0", "end").strip()
            or getattr(self.cfg, "reformat_prompt", "")
        )

        # APIキーなど（環境変数から）
        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")

        # その他固定値（translate5 と同等の既定値）
        self.cfg.output_html_path = None
        self.cfg.force_server_charcode = "utf-8"
        self.cfg.tsleep_rpm = getattr(self.cfg, "tsleep_rpm", 0.0)
        self.cfg.top_p = getattr(self.cfg, "top_p", 0.8)
        self.cfg.top_k = getattr(self.cfg, "top_k", 40)

        # mode から言語方向（translate5 と同じロジックに合わせて再計算）
        m = self.cfg.mode
        if m[0] == "j":
            self.cfg.source_lang = "JA"
            # GUIのチェック優先だが、je系ならデフォルトTrueでもよい
            if not hasattr(self.cfg, "limit_to_multibyte_str"):
                self.cfg.limit_to_multibyte_str = True
        else:
            self.cfg.source_lang = "EN"

        if m[1] == "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(),
                    )
                )

                # promptのみ
                tr_p = s.get("translate_prompt", "").strip()
                if tr_p:
                    self.tr_prompt.delete("1.0", "end")
                    self.tr_prompt.insert("1.0", tr_p)

                rf_p = s.get("reformat_prompt", "").strip()
                if rf_p:
                    self.rf_prompt.delete("1.0", "end")
                    self.rf_prompt.insert("1.0", rf_p)

            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_prompt"] = self.tr_prompt.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:
            style = ttk.Style()
            style.theme_use("clam")
        except Exception:
            pass
    App(root)
    root.mainloop()


if __name__ == "__main__":
    main()
