#editor executableを選択するentry,ボタンを作る
#テキストファイルを選択するentry、ボタンをつくる
#openai APIかgoogle APIをapi listboxで選択
#apiの選択を変えたら、model listboxの選択肢を変更
#Roleを辞書型リストrole_list = [{"desc": "explanation", "prompt": "prompt"}]で設定し、comboboxにdescのリストを追加して選択
#role combobolxで選択したdescのpromptoをtext boxに表示
#promptを辞書型リストprompt_list = [{"desc": "explanation", "prompt": "prompt"}]で設定し、comboboxにdescのリストを追加して選択
#prompt combobolxで選択したdescのpromptoをtext boxに表示
#temperatureを入力するentry
#max_bytesを入力するentry
#queryボタンを押すと、入力ファイルをmax_bytesまで読み込み、apiに送信。response.txtをoutput_pathに保存するとともに、editorで表示
#query時、アプリ終了時に上記の設定を.iniファイルに保存。次回のアプリ記事同時に読み込む
#スクリプトディレクトリにroles.iniとprompt.iniがあると想定し、
#起動時にそれらを読み込んでrole_listとprompt_listに入れてください。
#これらを読み込む関数は独自実装し、desc="prompt"の形式で"prompt"部分は改行を含む複数行でも読めるようにし、desc=を見つけたらlistに追加してください。行頭が#で始まる場合はコメント行として無視します。

#pip install tkinter ttkthemes openai google-generativeai inifile chardet python-dotenv

import os
import re
import threading
import configparser

try:
    from dotenv import load_dotenv
except:
    print("\nImport error: dotenv")
    input("Install: pip install python-dotenv")
    exit()

try:
    import tkinter as tk
    import tkinter.font as tkfont
    from tkinter import ttk, filedialog, messagebox
except:
    print("\nImport error: tkinter")
    input("Install: tkinter (標準ライブラリ) が必要です")
    exit()

try:
    from ttkthemes import ThemedTk
except:
    print("\nImport error: ttkthemes")
    input("Install: pip install ttkthemes")
    exit()

try:
    import openai
except:
    print("\nWarning: openai is not installed")
    input("install by pip install openai if needed")
    openai = None

try:
    import google.generativeai as genai
except:
    print("\nWarning: google.generativeai is not installed")
    input("install by pip install google-generativeai if needed")
    genai = None

try:
    # 【追加】文字コード検出ライブラリ
    import chardet
except:
    print("\nImport error: chardet")
    input("Install: pip install chardet")
    exit()


PROGRAM_NAME = "AI Assistant"
editor_default = "code"

# GPT-5 系モデル: Responses API を使用（temperature は送らない）
openai5_models = [
    "gpt-5",
    "gpt-5-mini",
    "gpt-5-nano",
    "gpt-5-pro",
    "gpt-5-codex",
]
openai5_model_default = "gpt-5"

# GPT-4 / GPT-4o / 3.5 系モデル: Chat Completions API を使用
openai_models = [
    "gpt-4.1",
    "gpt-4.1-mini",
    "gpt-4o",
    "gpt-4o-mini",
    "o4-mini",
    "o4-mini-high",
    "gpt-3.5-turbo",
]
openai_model_default = "gpt-4o"

# UI で表示する OpenAI 全モデル
openai_all_models = openai5_models + openai_models

google_models = ["gemini-2.5-flash", "gemini-2.5-pro"]
google_model_default = "gemini-2.5-flash"

script_path = os.path.abspath(__file__)
config_ini = os.path.splitext(script_path)[0] + '.ini'
config_path = "translate.env"

if not os.path.isfile(config_ini):
    script_dir = os.path.dirname(os.path.abspath(__file__))
    config_path = os.path.join(script_dir, config_path)

print()
if os.path.isfile(config_path):
    print(f"config_path: {config_path}")
else:
    print(f"Warning: config_path {config_path} is not found")
load_dotenv(dotenv_path=config_path)

account_inf_path = os.getenv("account_inf_path", "accounts.env")
if os.path.isfile(account_inf_path):
    print(f"account_inf_path: {account_inf_path}")
else:
    print(f"Warning: account_inf_path {account_inf_path} is not found")
load_dotenv(dotenv_path=account_inf_path)

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
    print("\nWarning: OPENAI_API_KEY environment variable is not set.")

GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
if not GOOGLE_API_KEY:
    print("\nWarning: GOOGLE_API_KEY environment variable is not set.")

API_list = []
API_default = ""

if openai and OPENAI_API_KEY:
    API_list.append("OpenAI")
    API_default = "OpenAI"
else:
    openai_all_models = []
    openai5_models = []
    openai_models = []
    openai5_model_default = ""
    openai_model_default = ""

if genai and GOOGLE_API_KEY:
    API_list.append("Google")
    if API_default == "":
        API_default = "Google"
else:
    google_models = []
    google_model_default = ""


class AIEditorApp:
    def __init__(self, master):
        self.master = master
        master.title(PROGRAM_NAME)
        master.geometry("800x700")

        self.config_file = config_ini
        self.load_config()  # 【変更箇所】config.iniの読み込み

        # roles.iniとprompt.iniから読み込む
        self.role_list = self.load_ini_file("roles.ini")  # 【変更箇所】文字コード検出対応
        if not self.role_list:  # 読み込み失敗時のデフォルト設定
            self.role_list = [
                {"desc": "文章の要約", "prompt": "以下の文章を要約してください。\n\n{{text}}"},
                {"desc": "コードのレビュー", "prompt": "以下のPythonコードをレビューし、改善点を提案してください。\n\n{{text}}"},
                {"desc": "翻訳（日本語→英語）", "prompt": "以下の日本語を英語に翻訳してください。\n\n{{text}}"},
                {"desc": "キーワード抽出", "prompt": "以下の文章から主要なキーワードを5つ抽出してください。\n\n{{text}}"}
            ]

        self.prompt_list = self.load_ini_file("prompt.ini")  # 【変更箇所】文字コード検出対応
        if not self.prompt_list:  # 読み込み失敗時のデフォルト設定
            self.prompt_list = [
                {"desc": "丁寧な表現", "prompt": "常に丁寧な言葉遣いを心がけてください。"},
                {"desc": "専門的な表現", "prompt": "専門用語を適切に使用し、技術的な視点から記述してください。"},
                {"desc": "創造的な表現", "prompt": "創造的でユニークなアイデアを提案してください。"},
                {"desc": "簡潔な表現", "prompt": "可能な限り簡潔に、要点のみをまとめてください。"}
            ]

        # Initialize _line_height_pixels early
        self._line_height_pixels = 0

        self.create_widgets()

        # prompt_text_areaが作成された後にfont metricsを取得
        if self.prompt_text_area:
            self._line_height_pixels = tkfont.Font(
                font=self.prompt_text_area['font']
            ).metrics("linespace")

        self.load_initial_settings()

        master.protocol("WM_DELETE_WINDOW", self.on_closing)

    # 【修正箇所】INIファイルを読み込む独自関数 (chardetでエンコーディング検出)
    def load_ini_file(self, filename):
        file_path = os.path.join(os.path.dirname(script_path), filename)
        items = []
        current_desc = None
        current_prompt_lines = []

        if not os.path.exists(file_path):
            return []

        # 1. chardetでエンコーディングを検出
        try:
            with open(file_path, 'rb') as f:
                raw_data = f.read()
            det = chardet.detect(raw_data)
            detected_encoding = det['encoding']
            confidence = det.get('confidence', 0)

            if not detected_encoding or confidence < 0.8:
                detected_encoding = 'utf-8'

            if detected_encoding and 'gb' in detected_encoding.lower():
                detected_encoding = 'gbk'
            elif detected_encoding and 'windows-1252' in detected_encoding.lower():
                detected_encoding = 'utf-8'

            print(f"Detected encoding for {filename}: {detected_encoding}")
        except Exception as e:
            print(f"Warning: chardet failed for {filename}, defaulting to utf-8. Error: {e}")
            detected_encoding = 'utf-8'
            raw_data = b""

        # 2. 検出されたエンコーディングでファイルを読み込み
        try:
            if not raw_data:
                with open(file_path, 'rb') as f:
                    raw_data = f.read()

            content = raw_data.decode(detected_encoding, errors='replace')

            for line in content.splitlines():
                line_stripped = line.strip()

                # コメント行は無視
                if line_stripped.startswith('#'):
                    continue

                # desc="..." のパターンを探す
                match = re.match(r'desc\s*=\s*"([^"]*)"', line_stripped)
                if match:
                    # 以前の項目があればリストに追加
                    if current_desc is not None:
                        items.append({
                            "desc": current_desc,
                            "prompt": "\n".join(current_prompt_lines).strip()
                        })

                    # 新しい項目の開始
                    current_desc = match.group(1)
                    current_prompt_lines = []
                elif current_desc is not None:
                    # desc="..." の行以降の行を prompt として格納
                    current_prompt_lines.append(line)

            # 最後の項目を追加
            if current_desc is not None:
                items.append({
                    "desc": current_desc,
                    "prompt": "\n".join(current_prompt_lines).strip()
                })

        except Exception as e:
            messagebox.showerror("ファイル読み込みエラー", f"{filename} の読み込み中にエラーが発生しました: {e}")
            return []

        return items

    def create_widgets(self):
        # Editor Executable フレーム
        editor_frame = ttk.LabelFrame(self.master, text="Editor Settings")
        editor_frame.pack(padx=10, pady=5, fill="x")

        ttk.Label(editor_frame, text="Editor Executable:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.editor_executable_entry = ttk.Entry(editor_frame, width=50)
        self.editor_executable_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        ttk.Button(editor_frame, text="Browse", command=self.browse_editor_executable).grid(row=0, column=2, padx=5, pady=5)
        editor_frame.grid_columnconfigure(1, weight=1)

        # Input Text File フレーム
        input_file_frame = ttk.LabelFrame(self.master, text="Input File Settings")
        input_file_frame.pack(padx=10, pady=5, fill="x")

        ttk.Label(input_file_frame, text="Input Text File:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.input_file_entry = ttk.Entry(input_file_frame, width=50)
        self.input_file_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        ttk.Button(input_file_frame, text="Browse", command=self.browse_input_file).grid(row=0, column=2, padx=5, pady=5)
        ttk.Button(input_file_frame, text="View", command=lambda: self.view_file(self.input_file_entry.get())).grid(row=0, column=3, padx=5, pady=5)
        input_file_frame.grid_columnconfigure(1, weight=1)

        # Output File Settings フレーム
        output_file_frame = ttk.LabelFrame(self.master, text="Output File Settings")
        output_file_frame.pack(padx=10, pady=5, fill="x")

        ttk.Label(output_file_frame, text="Output File:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.output_file_entry = ttk.Entry(output_file_frame, width=50)
        self.output_file_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        ttk.Button(output_file_frame, text="Browse", command=self.browse_output_file).grid(row=0, column=2, padx=5, pady=5)
        ttk.Button(output_file_frame, text="View", command=lambda: self.view_file(self.output_file_entry.get())).grid(row=0, column=3, padx=5, pady=5)
        output_file_frame.grid_columnconfigure(1, weight=1)

        # API Settings フレーム
        api_frame = ttk.LabelFrame(self.master, text="API Settings")
        api_frame.pack(padx=10, pady=5, fill="x")

        ttk.Label(api_frame, text="API:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.api_selection = ttk.Combobox(api_frame, values=API_list, state="readonly")
        self.api_selection.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        self.api_selection.set(API_default)
        self.api_selection.bind("<<ComboboxSelected>>", self.update_model_list)

        ttk.Label(api_frame, text="Model:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
        self.model_selection = ttk.Combobox(api_frame, state="readonly")
        self.model_selection.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
        self.update_model_list()

        api_frame.grid_columnconfigure(1, weight=1)
        api_frame.grid_columnconfigure(3, weight=1)

        # Role and Prompt フレーム
        role_prompt_frame = ttk.LabelFrame(self.master, text="Role & Prompt Settings")
        role_prompt_frame.pack(padx=10, pady=5, fill="x")

        ttk.Label(role_prompt_frame, text="Role:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.role_combobox = ttk.Combobox(
            role_prompt_frame,
            values=[role["desc"] for role in self.role_list],
            state="readonly"
        )
        self.role_combobox.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        self.role_combobox.bind("<<ComboboxSelected>>", self.display_selected_role_prompt)
        role_prompt_frame.grid_columnconfigure(1, weight=1)

        ttk.Label(role_prompt_frame, text="Prompt Template:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.prompt_template_combobox = ttk.Combobox(
            role_prompt_frame,
            values=[prompt["desc"] for prompt in self.prompt_list],
            state="readonly"
        )
        self.prompt_template_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        self.prompt_template_combobox.bind("<<ComboboxSelected>>", self.display_selected_prompt_template)
        role_prompt_frame.grid_columnconfigure(1, weight=1)

        ttk.Label(role_prompt_frame, text="Current Prompt:").grid(row=2, column=0, padx=5, pady=5, sticky="nw")

        self.text_area_container = ttk.Frame(role_prompt_frame)
        self.text_area_container.grid(row=2, column=1, columnspan=2, padx=5, pady=5, sticky="nsew")

        self.prompt_text_area = tk.Text(self.text_area_container, height=8, width=70, wrap="word")
        self.prompt_text_area.pack(side="top", fill="both", expand=True)

        self.gripper = ttk.Frame(self.text_area_container, width=15, height=15,
                                 relief="raised", borderwidth=1)
        self.gripper.pack(side="bottom", anchor="se", padx=1, pady=1)
        self.gripper.bind("<ButtonPress-1>", self.on_gripper_press)
        self.gripper.bind("<B1-Motion>", self.on_gripper_drag)

        role_prompt_frame.grid_rowconfigure(2, weight=1)

        # Parameters フレーム
        params_frame = ttk.LabelFrame(self.master, text="Parameters")
        params_frame.pack(padx=10, pady=5, fill="x")

        ttk.Label(params_frame, text="Temperature:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.temperature_entry = ttk.Entry(params_frame, width=10)
        self.temperature_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        self.temperature_entry.insert(0, "0.7")

        ttk.Label(params_frame, text="Max Bytes:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
        self.max_bytes_entry = ttk.Entry(params_frame, width=10)
        self.max_bytes_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
        self.max_bytes_entry.insert(0, "4096")

        # GPT-5 用 Reasoning Effort コンボボックス
        ttk.Label(params_frame, text="Reasoning Effort (GPT-5):").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.reasoning_effort_combobox = ttk.Combobox(
            params_frame,
            state="readonly",
            values=["(auto)", "low", "medium", "high"]
        )
        self.reasoning_effort_combobox.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        self.reasoning_effort_combobox.set("(auto)")

        # Verbosity コンボボックス（プロンプトで制御）
        ttk.Label(params_frame, text="Verbosity:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
        self.verbosity_combobox = ttk.Combobox(
            params_frame,
            state="readonly",
            values=["(default)", "short", "normal", "long"]
        )
        self.verbosity_combobox.grid(row=1, column=3, padx=5, pady=5, sticky="ew")
        self.verbosity_combobox.set("(default)")

        params_frame.grid_columnconfigure(1, weight=1)
        params_frame.grid_columnconfigure(3, weight=1)

        # Query / Close ボタン
        button_frame = ttk.Frame(self.master)
        button_frame.pack(pady=10)

        self.query_button = ttk.Button(button_frame, text="Query AI", command=self.start_query_thread)
        self.query_button.pack(side="left", padx=5)

        self.close_button = ttk.Button(button_frame, text="Close", command=self.on_closing)
        self.close_button.pack(side="left", padx=5)

        # ステータスバー
        self.status_bar = ttk.Label(self.master, text="Ready", relief="sunken", anchor="w")
        self.status_bar.pack(side="bottom", fill="x", ipady=2)

    def browse_editor_executable(self):
        file_path = filedialog.askopenfilename(title="Select Editor Executable")
        if file_path:
            self.editor_executable_entry.delete(0, tk.END)
            self.editor_executable_entry.insert(0, file_path)

    def browse_input_file(self):
        file_path = filedialog.askopenfilename(
            title="Select Input Text File",
            filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
        )
        if file_path:
            self.input_file_entry.delete(0, tk.END)
            self.input_file_entry.insert(0, file_path)
            self.output_file_entry.delete(0, tk.END)
            self.output_file_entry.insert(0, self.generate_default_output_file_path(file_path))

    def browse_output_file(self):
        input_file_path = self.input_file_entry.get()
        initial_dir = os.path.dirname(input_file_path) if os.path.exists(input_file_path) else os.getcwd()
        initial_file = os.path.basename(
            self.generate_default_output_file_path(input_file_path)
        ) if input_file_path else "output.txt"

        file_path = filedialog.asksaveasfilename(
            title="Select Output File",
            initialdir=initial_dir,
            initialfile=initial_file,
            defaultextension=".txt",
            filetypes=[("Text files", "*.txt"), ("All files", "*.*")]
        )
        if file_path:
            self.output_file_entry.delete(0, tk.END)
            self.output_file_entry.insert(0, file_path)

    def view_file(self, file_path):
        editor_executable = self.editor_executable_entry.get()
        if not editor_executable or not os.path.exists(editor_executable):
            messagebox.showwarning("エディタ未設定", "エディタ実行可能ファイルが設定されていないか、存在しません。")
            return
        if not file_path or not os.path.exists(file_path):
            messagebox.showwarning("ファイルが見つかりません", f"指定されたファイルが見つかりません: {file_path}")
            return

        try:
            if os.name == 'nt':
                os.startfile(file_path)
            else:
                import subprocess
                if not os.path.exists(editor_executable):
                    messagebox.showerror("エディタエラー", f"エディタが見つかりません: {editor_executable}")
                    return
                subprocess.Popen([editor_executable, file_path])
        except Exception as e:
            messagebox.showerror("ファイルオープンエラー", f"ファイルをエディタで開けませんでした: {e}")

    def generate_default_output_file_path(self, input_file_path):
        if not input_file_path:
            return ""
        input_dir = os.path.dirname(input_file_path)
        file_body = os.path.splitext(os.path.basename(input_file_path))[0]
        return os.path.join(input_dir, f"{file_body}-output.txt")

    def update_model_list(self, event=None):
        selected_api = self.api_selection.get()
        if selected_api == "OpenAI":
            self.model_selection['values'] = openai_all_models
            current = self.model_selection.get()
            if current not in openai_all_models:
                default_model = openai5_model_default or openai_model_default
                if default_model:
                    self.model_selection.set(default_model)
                elif openai_all_models:
                    self.model_selection.set(openai_all_models[0])
        elif selected_api == "Google":
            self.model_selection['values'] = google_models
            if google_models:
                self.model_selection.set(google_model_default or google_models[0])
        else:
            self.model_selection['values'] = []
            self.model_selection.set("")

    def display_selected_role_prompt(self, event=None):
        selected_desc = self.role_combobox.get()
        for role in self.role_list:
            if role["desc"] == selected_desc:
                self.prompt_text_area.delete(1.0, tk.END)
                self.prompt_text_area.insert(tk.END, role["prompt"])
                break

    def display_selected_prompt_template(self, event=None):
        selected_desc = self.prompt_template_combobox.get()
        for prompt in self.prompt_list:
            if prompt["desc"] == selected_desc:
                current_prompt = self.prompt_text_area.get(1.0, tk.END).strip()
                if current_prompt:
                    self.prompt_text_area.insert(tk.END, "\n\n" + prompt["prompt"])
                else:
                    self.prompt_text_area.insert(tk.END, prompt["prompt"])
                break

    def on_gripper_press(self, event):
        self._start_y = event.y_root
        self._start_height_pixels = self.prompt_text_area.winfo_height()

    def on_gripper_drag(self, event):
        delta_y = event.y_root - self._start_y
        new_height_pixels = self._start_height_pixels + delta_y
        min_height_pixels = 50

        if self._line_height_pixels > 0 and new_height_pixels > min_height_pixels:
            new_height_lines = int(new_height_pixels / self._line_height_pixels)
            if new_height_lines < 1:
                new_height_lines = 1
            self.prompt_text_area.configure(height=new_height_lines)

    def start_query_thread(self):
        self.status_bar.config(text="Querying AI... Please wait.")
        self.query_button.config(state="disabled")
        self.master.update_idletasks()
        self.save_config()
        threading.Thread(target=self.run_query_logic).start()

    def extract_responses_text(self, response):
        """
        Responses API のレスポンスから人間向けテキストを抽出する。

        - output 内の ResponseOutputText / dict型 {"text": "..."} をすべて拾って連結
        - '\\n' のようなエスケープされた改行は実際の改行に戻す
        - 1文字も取れなければ空文字列を返す（呼び出し側でJSON保存に切り替える）
        """
        texts = []

        try:
            # pydantic オブジェクト or 同等構造を想定
            output = getattr(response, "output", None)

            # dict化して output を拾うパターンにも対応
            if output is None and hasattr(response, "model_dump"):
                data = response.model_dump()
                output = data.get("output")

            # さらに素のdictの場合にも一応対応
            if output is None and isinstance(response, dict):
                output = response.get("output")

            if not output:
                return ""

            for item in output:
                # item は ResponseReasoningItem / ResponseOutputMessage など
                contents = getattr(item, "content", None)
                if contents is None and isinstance(item, dict):
                    contents = item.get("content", [])
                if not contents:
                    continue

                for c in contents:
                    # c は ResponseOutputText 相当 or dict の想定
                    text = getattr(c, "text", None)
                    if text is None and isinstance(c, dict):
                        text = c.get("text")

                    if text:
                        # 念のためエスケープされた改行も実際の改行に
                        s = str(text).replace("\\n", "\n")
                        texts.append(s)

        except Exception:
            # 何か壊れていたら空文字扱い → 呼び出し側で JSON 保存にフォールバック
            return ""

        result = "\n".join(texts).strip()
        return result

    def apply_verbosity(self, base_prompt, verbosity):
        """
        Verbosity コンボボックスの指定をプロンプトに反映（APIパラメータではなく指示文）
        """
        v = (verbosity or "").lower()
        if v.startswith("(default)") or v == "":
            return base_prompt
        if v == "short":
            prefix = "回答はできるだけ簡潔に、要点のみを日本語で出力してください。\n\n"
        elif v == "normal":
            prefix = "回答は標準的な長さで、必要な説明のみを含めてください。\n\n"
        elif v == "long":
            prefix = "回答は詳しく、ステップを追って丁寧に説明してください。\n\n"
        else:
            return base_prompt
        return prefix + base_prompt

    def run_query_logic(self):
        input_file = self.input_file_entry.get()
        output_file_path = self.output_file_entry.get()

        selected_api = self.api_selection.get()
        selected_model = self.model_selection.get()
        prompt_content = self.prompt_text_area.get(1.0, tk.END).strip()

        reasoning_effort = self.reasoning_effort_combobox.get()
        verbosity = self.verbosity_combobox.get()

        try:
            temperature = float(self.temperature_entry.get())
            max_bytes = int(self.max_bytes_entry.get())
        except ValueError:
            self.master.after(0, lambda: self.status_bar.config(
                text="Error: Temperature and Max Bytes must be numbers."
            ))
            self.master.after(0, lambda: messagebox.showerror(
                "入力エラー", "TemperatureとMax Bytesは数値で入力してください。"
            ))
            self.master.after(0, lambda: self.query_button.config(state="normal"))
            return

        if not output_file_path:
            self.master.after(0, lambda: self.status_bar.config(
                text="Error: Output file path not specified."
            ))
            self.master.after(0, lambda: messagebox.showerror(
                "ファイルエラー", "出力ファイルパスが指定されていません。"
            ))
            self.master.after(0, lambda: self.query_button.config(state="normal"))
            return

        output_dir = os.path.dirname(output_file_path)
        if output_dir and not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir)
            except OSError as e:
                self.master.after(0, lambda: self.status_bar.config(
                    text=f"Error creating output directory: {e}"
                ))
                self.master.after(0, lambda: messagebox.showerror(
                    "ディレクトリエラー", f"出力ディレクトリの作成に失敗しました: {e}"
                ))
                self.master.after(0, lambda: self.query_button.config(state="normal"))
                return

        if not prompt_content:
            self.master.after(0, lambda: self.status_bar.config(
                text="Error: Prompt content is empty."
            ))
            self.master.after(0, lambda: messagebox.showerror(
                "プロンプトエラー", "プロンプトが入力されていません。"
            ))
            self.master.after(0, lambda: self.query_button.config(state="normal"))
            return

        file_content = ""
        full_prompt = prompt_content

        if "{{text}}" in prompt_content:
            if not input_file or not os.path.exists(input_file):
                self.master.after(0, lambda: self.status_bar.config(
                    text="Error: Input file not selected or does not exist."
                ))
                self.master.after(0, lambda: messagebox.showerror(
                    "ファイルエラー", "入力ファイルが選択されていないか、存在しません。"
                ))
                self.master.after(0, lambda: self.query_button.config(state="normal"))
                return

            self.master.after(0, lambda: self.status_bar.config(text="Reading input file..."))
            try:
                with open(input_file, 'rb') as f:
                    content_bytes = f.read(max_bytes)
                    file_content = content_bytes.decode('utf-8', errors='ignore')
            except Exception as e:
                self.master.after(0, lambda: self.status_bar.config(
                    text=f"Error reading input file: {e}"
                ))
                self.master.after(0, lambda: messagebox.showerror(
                    "ファイル読み込みエラー",
                    f"入力ファイルの読み込み中にエラーが発生しました: {e}"
                ))
                self.master.after(0, lambda: self.query_button.config(state="normal"))
                return

            full_prompt = prompt_content.replace("{{text}}", file_content)

        # Verbosity の指示を反映（GPT-5/4共通で使ってOK）
        full_prompt = self.apply_verbosity(full_prompt, verbosity)

        self.master.after(0, lambda: self.status_bar.config(text="Sending query to AI..."))

        try:
            ai_response = ""

            if selected_api == "OpenAI":
                if not OPENAI_API_KEY:
                    raise Exception("OpenAI API Key is not set.")
                if not openai:
                    raise Exception("openai library is not available.")

                client = openai.OpenAI(api_key=OPENAI_API_KEY)

                # GPT-5 系: Responses API
                if selected_model in openai5_models:
                    reasoning_param = {}
                    if reasoning_effort in ("low", "medium", "high"):
                        reasoning_param = {"effort": reasoning_effort}

                    # temperature は送らない
                    if reasoning_param:
                        response = client.responses.create(
                            model=selected_model,
                            input=full_prompt,
                            reasoning=reasoning_param,
                        )
                    else:
                        response = client.responses.create(
                            model=selected_model,
                            input=full_prompt,
                        )

                    # まずテキスト抽出を試みる
                    text = self.extract_responses_text(response)

                    if text:
                        ai_response = text
                    else:
                        # テキストが取れない場合は JSON 形式で保存
                        try:
                            if hasattr(response, "model_dump_json"):
                                ai_response = response.model_dump_json(
                                    indent=2,
                                    ensure_ascii=False
                                )
                            elif hasattr(response, "model_dump"):
                                import json
                                ai_response = json.dumps(
                                    response.model_dump(),
                                    indent=2,
                                    ensure_ascii=False
                                )
                            elif isinstance(response, dict):
                                import json
                                ai_response = json.dumps(
                                    response,
                                    indent=2,
                                    ensure_ascii=False
                                )
                            else:
                                # 最後の手段として repr
                                ai_response = repr(response)
                        except Exception:
                            ai_response = repr(response)

                # GPT-4 / 3.5 系: Chat Completions API
                elif selected_model in openai_models:
                    response = client.chat.completions.create(
                        model=selected_model,
                        messages=[{"role": "user", "content": full_prompt}],
                        temperature=temperature
                    )
                    ai_response = response.choices[0].message.content

                else:
                    raise Exception(f"Unsupported OpenAI model selected: {selected_model}")

            elif selected_api == "Google":
                if not GOOGLE_API_KEY:
                    raise Exception("Google API Key is not set.")
                if not genai:
                    raise Exception("google.generativeai library is not available.")

                genai.configure(api_key=GOOGLE_API_KEY)
                model = genai.GenerativeModel(selected_model)
                response = model.generate_content(
                    full_prompt,
                    generation_config=genai.types.GenerationConfig(
                        temperature=temperature
                    )
                )
                ai_response = response.text

            else:
                raise Exception(f"Unsupported API selected: {selected_api}")

            self.master.after(0, lambda: self.status_bar.config(text="Saving AI response..."))

            with open(output_file_path, 'w', encoding='utf-8') as f:
                f.write(ai_response)

            self.master.after(0, lambda: self.status_bar.config(
                text=f"Success! Response saved to {output_file_path}"
            ))
            self.master.after(0, lambda: messagebox.showinfo(
                "成功", f"AIからの応答が {output_file_path} に保存されました。"
            ))
            self.master.after(0, lambda: self.view_file(output_file_path))

        except Exception as e:
            self.master.after(0, lambda: self.status_bar.config(text=f"API Error: {e}"))
            self.master.after(0, lambda: messagebox.showerror(
                "APIエラー", f"API呼び出し中にエラーが発生しました: {e}"
            ))
        finally:
            self.master.after(0, lambda: self.query_button.config(state="normal"))
            if "Error" not in self.status_bar.cget("text"):
                self.master.after(0, lambda: self.status_bar.config(text="Ready"))

    def load_config(self):
        self.config = configparser.ConfigParser()
        if os.path.exists(self.config_file):
            try:
                with open(self.config_file, 'rb') as f:
                    raw_data = f.read()
                det = chardet.detect(raw_data)
                detected_encoding = det['encoding'] or 'utf-8'
                if det.get('confidence', 0) < 0.8:
                    detected_encoding = 'utf-8'
                print(f"Detected encoding for {self.config_file}: {detected_encoding}")
                self.config.read(self.config_file, encoding=detected_encoding)
            except Exception as e:
                print(f"Error reading {self.config_file}: {e}. Attempting utf-8.")
                try:
                    self.config.read(self.config_file, encoding='utf-8')
                except Exception:
                    print("Fatal error reading config. Starting empty.")
            if 'Settings' not in self.config:
                self.config['Settings'] = {}
        else:
            self.config['Settings'] = {}

    def save_config(self):
        self.config['Settings']['editor_executable'] = self.editor_executable_entry.get()
        self.config['Settings']['input_file'] = self.input_file_entry.get()
        self.config['Settings']['output_file'] = self.output_file_entry.get()
        self.config['Settings']['api_selection'] = self.api_selection.get()
        self.config['Settings']['model_selection'] = self.model_selection.get()
        self.config['Settings']['temperature'] = self.temperature_entry.get()
        self.config['Settings']['max_bytes'] = self.max_bytes_entry.get()
        self.config['Settings']['role_selection'] = self.role_combobox.get()
        self.config['Settings']['prompt_template_selection'] = self.prompt_template_combobox.get()
        self.config['Settings']['current_prompt_text'] = self.prompt_text_area.get(1.0, tk.END).strip()
        self.config['Settings']['reasoning_effort'] = self.reasoning_effort_combobox.get()
        self.config['Settings']['verbosity'] = self.verbosity_combobox.get()

        try:
            with open(self.config_file, 'w', encoding='utf-8') as configfile:
                self.config.write(configfile)
        except Exception as e:
            print(f"Warning: Failed to save config file: {e}")

    def load_initial_settings(self):
        settings = self.config['Settings']

        self.editor_executable_entry.delete(0, tk.END)
        self.editor_executable_entry.insert(0, settings.get('editor_executable', editor_default))

        self.input_file_entry.delete(0, tk.END)
        self.input_file_entry.insert(0, settings.get('input_file', ''))

        self.output_file_entry.delete(0, tk.END)
        self.output_file_entry.insert(0, settings.get('output_file', ''))

        api_sel = settings.get('api_selection', API_default)
        if api_sel in API_list:
            self.api_selection.set(api_sel)
        else:
            self.api_selection.set(API_default)
        self.update_model_list()

        model_values = list(self.model_selection['values'])
        model_sel = settings.get('model_selection', '')
        if model_sel in model_values:
            self.model_selection.set(model_sel)
        elif model_values:
            self.model_selection.set(model_values[0])

        self.temperature_entry.delete(0, tk.END)
        self.temperature_entry.insert(0, settings.get('temperature', '0.7'))

        self.max_bytes_entry.delete(0, tk.END)
        self.max_bytes_entry.insert(0, settings.get('max_bytes', '4096'))

        role_values = [role["desc"] for role in self.role_list]
        self.role_combobox['values'] = role_values
        role_sel = settings.get('role_selection', '')
        if role_sel in role_values:
            self.role_combobox.set(role_sel)
        elif role_values:
            self.role_combobox.set(role_values[0])

        prompt_temp_values = [prompt["desc"] for prompt in self.prompt_list]
        self.prompt_template_combobox['values'] = prompt_temp_values
        prompt_temp_sel = settings.get('prompt_template_selection', '')
        if prompt_temp_sel in prompt_temp_values:
            self.prompt_template_combobox.set(prompt_temp_sel)
        elif prompt_temp_values:
            self.prompt_template_combobox.set(prompt_temp_values[0])

        current_prompt_text = settings.get('current_prompt_text', '')
        self.prompt_text_area.delete(1.0, tk.END)
        if current_prompt_text:
            self.prompt_text_area.insert(tk.END, current_prompt_text)
        elif role_values:
            for role in self.role_list:
                if role["desc"] == self.role_combobox.get():
                    self.prompt_text_area.insert(tk.END, role["prompt"])
                    break

        # Reasoning Effort / Verbosity
        self.reasoning_effort_combobox.set(settings.get('reasoning_effort', "(auto)"))
        self.verbosity_combobox.set(settings.get('verbosity', "(default)"))

    def on_closing(self):
        self.save_config()
        self.master.destroy()


if __name__ == "__main__":
    root = ThemedTk(theme="plastik")
    app = AIEditorApp(root)
    root.mainloop()
