import tkinter as tk
from tkinter import scrolledtext, messagebox
import requests
import json
import datetime
import webbrowser
import os
import io
import time
from urllib.parse import urlsplit
import ipaddress

# ------------------------------
# 設定ファイル
# ------------------------------
CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".jigyodan_config.json")


def load_config():
    try:
        if os.path.exists(CONFIG_PATH):
            with open(CONFIG_PATH, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception:
        pass
    return {}


def save_config(cfg):
    try:
        with open(CONFIG_PATH, "w", encoding="utf-8") as f:
            json.dump(cfg, f, ensure_ascii=False, indent=2)
    except Exception:
        pass


# ------------------------------
# 設定ダイアログ
# ------------------------------
class SettingsDialog(tk.Toplevel):
    def __init__(self, master, cfg):
        super().__init__(master)
        self.title("設定")
        self.resizable(False, False)
        self.grab_set()
        self.cfg = cfg.copy()

        frm = tk.Frame(self)
        frm.pack(padx=12, pady=12)

        row = 0
        # 接続先
        tk.Label(frm, text="接続先").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.base_choices = [
            "https://jigyodan.sakura.ne.jp/zaitakukanri_honbu/",
            "http://49.212.200.138/zaitakukanri_honbu/public/",
            "http://localhost/zaitakukanri_honbu/",
        ]
        self.var_base = tk.StringVar(value=self.cfg.get("base_url", self.base_choices[0]))
        self.opt_base = tk.OptionMenu(frm, self.var_base, *self.base_choices)
        self.opt_base.config(width=38)
        self.opt_base.grid(row=row, column=1, padx=6, pady=6, sticky='w')
        row += 1

        tk.Label(frm, text="ユーザー名").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.ent_user = tk.Entry(frm, width=28)
        self.ent_user.grid(row=row, column=1, padx=6, pady=6)
        self.ent_user.insert(0, cfg.get("user_id", ""))
        row += 1

        tk.Label(frm, text="パスワード").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.ent_pass = tk.Entry(frm, show='*', width=28)
        self.ent_pass.grid(row=row, column=1, padx=6, pady=6)
        self.ent_pass.insert(0, cfg.get("password", ""))
        row += 1

        tk.Label(frm, text="ボリューム番号").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.ent_vol = tk.Entry(frm, width=28)
        self.ent_vol.grid(row=row, column=1, padx=6, pady=6)
        self.ent_vol.insert(0, cfg.get("volume_no", ""))
        row += 1

        tk.Label(frm, text="シリアル番号").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.ent_ser = tk.Entry(frm, width=28)
        self.ent_ser.grid(row=row, column=1, padx=6, pady=6)
        self.ent_ser.insert(0, cfg.get("serial_no", ""))
        row += 1

        # 任意: ホストヘッダ（IP直指定時に仮想ホストを指定するため）
        tk.Label(frm, text="Hostヘッダ(任意)").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.ent_host = tk.Entry(frm, width=28)
        self.ent_host.grid(row=row, column=1, padx=6, pady=6)
        self.ent_host.insert(0, cfg.get("host_header", ""))
        row += 1

        # 任意: カメラ番号（既定: 0）
        tk.Label(frm, text="カメラ番号(任意)").grid(row=row, column=0, sticky='e', padx=6, pady=6)
        self.ent_cam = tk.Entry(frm, width=28)
        self.ent_cam.grid(row=row, column=1, padx=6, pady=6)
        self.ent_cam.insert(0, str(cfg.get("camera_index", 0)))
        row += 1

        btns = tk.Frame(self)
        btns.pack(padx=12, pady=(0, 12), fill='x')
        tk.Button(btns, text="保存", command=self.on_save).pack(side='right', padx=4)
        tk.Button(btns, text="閉じる", command=self.destroy).pack(side='right', padx=4)

    def on_save(self):
        self.cfg["base_url"] = self.var_base.get().strip()
        self.cfg["user_id"] = self.ent_user.get().strip()
        self.cfg["password"] = self.ent_pass.get().strip()
        self.cfg["volume_no"] = self.ent_vol.get().strip()
        self.cfg["serial_no"] = self.ent_ser.get().strip()
        self.cfg["host_header"] = self.ent_host.get().strip()
        try:
            self.cfg["camera_index"] = int(self.ent_cam.get().strip())
        except Exception:
            self.cfg["camera_index"] = 0
        # 即時反映
        self.master._temp_password = self.cfg["password"]
        self.master.base_url = self.cfg["base_url"]
        self.master.host_header = self.cfg.get("host_header", "")
        self.master.camera_index = self.cfg.get("camera_index", 0)
        save_config(self.cfg)
        self.destroy()


# ------------------------------
# メインウィンドウ（レイアウト）
# ------------------------------
class MainWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("在宅就労管理システム Ver.4.0")
        self.geometry("1220x760")
        self.minsize(1000, 650)

        # 共通サイズ（ここを変えると一括で調整できます）
        self.BTN_WIDE = 16    # 横幅（文字数単位）
        self.BTN_HEIGHT = 2   # 高さ（行数単位）

        # 状態
        cfg0 = load_config()
        self.base_url = cfg0.get("base_url", "https://jigyodan.sakura.ne.jp/zaitakukanri_honbu/")
        self.token = None
        self.last_chat_date = ""
        self.logged_in = False
        self._poll_after_id = None
        self._temp_password = ""
        self.host_header = cfg0.get("host_header", "")
        self.camera_index = int(cfg0.get("camera_index", 0))
        # 中間キャプチャ間隔（分）
        self.middle_interval_minutes = 30
        self._middle_after_id = None
        # キーボード/マウス反応監視
        self.keyboard_interval_minutes = 30
        self.mouse_interval_minutes = 30
        self._kbd_after_id = None
        self._mouse_after_id = None
        self.keyboard_flag = '2'
        self.mouse_flag = '2'
        self._kbd_listener = None
        self._mouse_listener = None

        # 2カラム構成
        self.columnconfigure(0, weight=6, minsize=600)
        self.columnconfigure(1, weight=5, minsize=500)
        self.rowconfigure(0, weight=1)

        # -------- LEFT --------
        left = tk.Frame(self, bg="#bfcddb")
        left.grid(row=0, column=0, sticky="nsew", padx=(10, 6), pady=10)
        left.rowconfigure(0, weight=1)
        left.rowconfigure(1, weight=0)
        left.columnconfigure(0, weight=1)

        self.workspace = tk.Text(left, bg="white", state="disabled")
        self.workspace.grid(row=0, column=0, sticky="nsew", padx=4, pady=(4, 8))

        link_frame = tk.Frame(left, bg="#bfcddb")
        link_frame.grid(row=1, column=0, sticky="nsew")
        link_frame.columnconfigure(0, weight=1)

        cap = tk.Frame(link_frame, bg="#bfcddb")
        cap.grid(row=0, column=0, sticky="w", padx=2, pady=(0, 4))
        tk.Label(cap, text="参考外部リンク", font=("Meiryo", 12, "bold"), bg="#bfcddb").pack(side="left", padx=(4, 10))

        btns = tk.Frame(link_frame, bg="#bfcddb")
        btns.grid(row=1, column=0, sticky="w", padx=0, pady=(0, 6))
        # ← 「日報」だけ残し、サイズを広げる
        tk.Button(btns, text="日報", width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                  command=self.open_report).pack(side="left", padx=6)

        links_box = tk.Frame(link_frame, bg="#bfcddb")
        links_box.grid(row=2, column=0, sticky="nsew")
        link_frame.rowconfigure(2, weight=1)
        self.links_list = tk.Listbox(links_box, height=7)
        self.links_list.pack(fill="both", expand=True, padx=4, pady=(0, 4))
        self.links_list.bind('<Double-Button-1>', self.open_selected_link)
        self._links_map = {}

        # -------- RIGHT --------
        right = tk.Frame(self, bg="#bfcddb")
        right.grid(row=0, column=1, sticky="nsew", padx=(6, 10), pady=10)
        right.rowconfigure(0, weight=0)
        right.rowconfigure(1, weight=0)
        right.rowconfigure(2, weight=1)
        right.rowconfigure(3, weight=0)
        right.rowconfigure(4, weight=1)
        right.rowconfigure(5, weight=0)
        right.columnconfigure(0, weight=1)

        header = tk.Frame(right, bg="#bfcddb")
        header.grid(row=0, column=0, sticky="ew", pady=(0, 6))

        # ログイン/設定/未ログイン（サイズ拡大）
        self.btn_login = tk.Button(header, text="ログイン",
                                   width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                                   command=self.toggle_login)
        self.btn_login.grid(row=0, column=0, padx=4)

        self.btn_setting = tk.Button(header, text="設定",
                                     width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                                     command=self.open_settings)
        self.btn_setting.grid(row=0, column=1, padx=4)

        self.lbl_login = tk.Label(header, text="未ログイン",
                                  relief='solid', width=self.BTN_WIDE, height=self.BTN_HEIGHT, anchor="center")
        self.lbl_login.grid(row=0, column=2, padx=(8, 0))

        chat_head = tk.Frame(right, bg="#bfcddb")
        chat_head.grid(row=1, column=0, sticky="ew")
        tk.Label(chat_head, text="メッセージ（チャット）", font=("Meiryo", 12, "bold"), bg="#bfcddb").pack(side='left')
        self.update_button = tk.Button(chat_head, text="更新", command=self.fetch_messages_once, width=6)
        self.update_button.pack(side='right')

        self.chat_display = scrolledtext.ScrolledText(right, state='disabled', wrap='word', height=12)
        self.chat_display.grid(row=2, column=0, sticky="nsew", pady=(2, 6))

        input_row = tk.Frame(right, bg="#bfcddb")
        input_row.grid(row=3, column=0, sticky="ew", pady=(0, 6))
        input_row.columnconfigure(0, weight=1)
        self.message_entry = tk.Entry(input_row, state='disabled')
        self.message_entry.grid(row=0, column=0, sticky="ew")
        self.message_entry.bind("<Return>", self.send_message_event)
        self.send_button = tk.Button(input_row, text="送信", command=self.send_message, state='disabled', width=6)
        self.send_button.grid(row=0, column=1, padx=6)

        biz = tk.Frame(right, bg="#bfcddb")
        biz.grid(row=4, column=0, sticky="nsew")
        biz.columnconfigure(0, weight=1)
        biz.columnconfigure(1, weight=0)
        biz.rowconfigure(0, weight=1)

        self.biz_panel = tk.Text(biz, bg="#a9bfd3", state="disabled")
        self.biz_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 8))

        side = tk.Frame(biz, bg="#bfcddb")
        side.grid(row=0, column=1, sticky="ns")

        # --- 作業開始/問い合せ を横並び（サイズ拡大） ---
        btn_row = tk.Frame(side, bg="#bfcddb")
        btn_row.pack(fill="x", pady=(2, 12))
        self.training_button = tk.Button(btn_row, text="作業開始",
                                         width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                                         command=self.toggle_training)
        self.training_button.pack(side="left", padx=(0, 8), fill="x")

        inquiry_btn = tk.Button(btn_row, text="問い合せ",
                                width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                                command=self.send_inquiry)
        inquiry_btn.pack(side="left", fill="x")

        status = tk.Frame(side, bg="#bfcddb", bd=1, relief="flat")
        status.pack(fill="x")
        tk.Label(status, text="開始時刻", anchor="w", bg="#bfcddb").grid(row=0, column=0, sticky="w", padx=2, pady=2)
        self.lbl_start_val = tk.Label(status, text="-", anchor="w", bg="#bfcddb")
        self.lbl_start_val.grid(row=0, column=1, sticky="w", padx=6, pady=2)

        tk.Label(status, text="終了時刻", anchor="w", bg="#bfcddb").grid(row=1, column=0, sticky="w", padx=2, pady=2)
        self.lbl_end_val = tk.Label(status, text="-", anchor="w", bg="#bfcddb")
        self.lbl_end_val.grid(row=1, column=1, sticky="w", padx=6, pady=2)

        tk.Label(status, text="経過時間", anchor="w", bg="#bfcddb").grid(row=2, column=0, sticky="w", padx=2, pady=2)
        self.lbl_time_val = tk.Label(status, text="-", anchor="w", bg="#bfcddb")
        self.lbl_time_val.grid(row=2, column=1, sticky="w", padx=6, pady=2)

        close_row = tk.Frame(right, bg="#bfcddb")
        close_row.grid(row=5, column=0, sticky="e")
        tk.Button(close_row, text="閉じる", width=8, command=self.on_close).pack(pady=(6, 0))

        self.set_logged_out_state()
        self.protocol("WM_DELETE_WINDOW", self.on_close)

    # ---------------- 通信 ----------------
    def post_multipart(self, url: str, fields: dict, files: dict | None = None):
        files_param = {}
        for k, v in fields.items():
            files_param[k] = (None, "" if v is None else str(v))
        if files:
            for k, v in files.items():
                files_param[k] = v
        headers = {}
        try:
            hostname = urlsplit(self.base_url).hostname or ""
            if self.host_header:
                headers["Host"] = self.host_header
            else:
                try:
                    ipaddress.ip_address(hostname)
                    headers["Host"] = "jigyodan.sakura.ne.jp"
                except Exception:
                    pass
        except Exception:
            pass
        return requests.post(url, files=files_param, headers=headers)

    # カメラ1枚撮影
    def capture_camera(self):
        try:
            import cv2
        except Exception as e:
            self.display_message(f"[注意] カメラ未対応: opencv-python をインストールしてください ({e})")
            return None
        try:
            idx = int(self.camera_index) if isinstance(self.camera_index, int) or str(self.camera_index).isdigit() else 0
            cap = cv2.VideoCapture(idx, cv2.CAP_DSHOW)
            if not cap.isOpened():
                cap = cv2.VideoCapture(idx)
            cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
            cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
            ok, frame = cap.read()
            for _ in range(10):
                if ok and frame is not None:
                    break
                time.sleep(0.2)
                ok, frame = cap.read()
            cap.release()
            if not ok or frame is None:
                self.display_message("[注意] カメラから画像を取得できませんでした。")
                return None
            ok2, buf = cv2.imencode('.jpg', frame)
            if not ok2:
                self.display_message("[注意] 画像のエンコードに失敗しました。")
                return None
            bio = io.BytesIO(buf.tobytes())
            bio.seek(0)
            return ("camera.jpg", bio, "image/jpeg")
        except Exception as e:
            self.display_message(f"[注意] カメラ撮影に失敗: {e}")
            return None

    # UI / 動作
    def open_settings(self):
        cfg = load_config()
        SettingsDialog(self, cfg)

    def toggle_login(self):
        if not self.logged_in:
            self.login()
        else:
            self.logout()

    def login(self):
        cfg = load_config()
        user_id = (cfg.get("user_id") or "").strip()
        volume_no = (cfg.get("volume_no") or "").strip()
        serial_no = (cfg.get("serial_no") or "").strip()
        password = (self._temp_password or cfg.get("password") or "").strip()
        if not user_id or not password or not volume_no or not serial_no:
            self.display_message("設定からログイン情報を入力してください。")
            return
        try:
            url = self.base_url + "training_login.php"
            data = {"user_id": user_id, "user_password": password, "volume_no": volume_no, "serial_no": serial_no}
            res = self.post_multipart(url, data)
            res.raise_for_status()
            js = res.json()
            if isinstance(js, dict) and isinstance(js.get("res"), str) and len(js["res"]) == 32:
                self.token = js["res"]
                try:
                    if 's' in js: self.middle_interval_minutes = max(1, int(str(js['s'])))
                    if 'k' in js: self.keyboard_interval_minutes = max(1, int(str(js['k'])))
                    if 'm' in js: self.mouse_interval_minutes = max(1, int(str(js['m'])))
                except Exception:
                    pass
                self.logged_in = True
                self.lbl_login.config(text="ログイン中")
                self.btn_login.config(text="ログアウト")
                self.load_first()
                self.message_entry.config(state='normal')
                self.send_button.config(state='normal')
                self.start_polling()
                self.display_message("[システム] ログインしました。")
                self.last_chat_date = ""
            else:
                err = js.get("error", "ログインに失敗しました。") if isinstance(js, dict) else "ログインに失敗しました。"
                self.display_message(f"[エラー] {err}")
        except requests.exceptions.RequestException as e:
            self.display_message(f"[エラー] ログイン通信失敗: {e}")
        except json.JSONDecodeError:
            self.display_message("[エラー] 応答が不正です。")

    def logout(self):
        try:
            if self.token:
                url = self.base_url + "training_logout.php"
                self.post_multipart(url, {"value": self.token})
        except Exception:
            pass
        self.token = None
        self.logged_in = False
        self.lbl_login.config(text="未ログイン")
        self.btn_login.config(text="ログイン")
        self.notice_clear()
        self.links_list.delete(0, tk.END)
        self._links_map.clear()
        self.last_chat_date = ""
        self.stop_polling()
        self.message_entry.config(state='disabled')
        self.send_button.config(state='disabled')
        # 作業状態リセット
        self.training_started = False
        self.lbl_start_val.config(text="-")
        self.lbl_end_val.config(text="-")
        self.lbl_time_val.config(text="-")
        if hasattr(self, "training_button"):
            self.training_button.config(text="作業開始")
        self.display_message("[システム] ログアウトしました。")
        # タイマ停止
        self.stop_middle_timer()
        self.stop_keyboard_timer()
        self.stop_mouse_timer()
        self.stop_kbd_mouse_listeners()

    def set_logged_out_state(self):
        self.token = None
        self.logged_in = False
        self.lbl_login.config(text="未ログイン")
        self.btn_login.config(text="ログイン")
        self.message_entry.config(state='disabled')
        self.send_button.config(state='disabled')
        # 作業UI初期化
        self.training_started = False
        self.training_start_time = None
        self.training_end_time = None

    # 初期表示（通知/リンク）
    def load_first(self):
        if not self.token:
            return
        try:
            url = self.base_url + "first.php"
            data = {"value": self.token}
            res = self.post_multipart(url, data)
            try:
                res.raise_for_status()
            except requests.exceptions.HTTPError:
                if res.status_code == 404:
                    return
                raise
            js = res.json()
            # お知らせは左上のワークスペースに表示
            notice = js.get("notice")
            try:
                self.workspace.config(state='normal')
                self.workspace.delete("1.0", tk.END)
                if notice:
                    self.workspace.insert(tk.END, str(notice))
                self.workspace.config(state='disabled')
            except Exception:
                pass
            # 外部リンク
            self.links_list.delete(0, tk.END)
            self._links_map.clear()
            links = js.get("link")
            if isinstance(links, list):
                for idx, item in enumerate(links):
                    name = item.get("link_name") if isinstance(item, dict) else None
                    url2 = item.get("link_url") if isinstance(item, dict) else None
                    if name and url2:
                        self.links_list.insert(tk.END, name)
                        self._links_map[idx] = url2
        except requests.exceptions.RequestException as e:
            self.display_message(f"エラー（初期表示）: {e}")
        except json.JSONDecodeError:
            self.display_message("エラー: 初期データの応答が不正です。")

    def open_report(self):
        if not self.token:
            self.display_message("[注意] ログインしてください。")
            return
        # 旧プロジェクト互換の URL（/report_edit.php?t=...）を開く
        base = self.base_url.rstrip('/') + '/'
        webbrowser.open(base + f"report_edit.php?t={self.token}")

    def open_selected_link(self, event=None):
        sel = self.links_list.curselection()
        if not sel:
            return
        idx = sel[0]
        url = self._links_map.get(idx)
        if url:
            webbrowser.open(url)
            self.links_list.selection_clear(0, tk.END)

    def send_inquiry(self):
        if not self.token:
            self.display_message("[注意] ログインしてください。")
            return
        try:
            url = self.base_url + "training_inquiry.php"
            data = {"value": self.token}
            cam = self.capture_camera()
            files = {"file": cam} if cam else None
            res = self.post_multipart(url, data, files)
            res.raise_for_status()
            self.display_message("[システム] 問い合せを送信しました。")
        except requests.exceptions.RequestException as e:
            self.display_message(f"エラー（問い合せ）: {e}")

    def toggle_training(self):
        if not self.token:
            self.display_message("[注意] ログインしてください。")
            return
        try:
            if not getattr(self, "training_started", False):
                url = self.base_url + "training_start.php"
                data = {"value": self.token}
                cam = self.capture_camera()
                files = {"file": cam} if cam else None
                res = self.post_multipart(url, data, files)
                res.raise_for_status()
                js = {}
                try:
                    js = res.json()
                except Exception:
                    pass
                if isinstance(js, dict) and isinstance(js.get("res"), str) and len(js["res"]) == 32:
                    self.token = js["res"]
                self.training_started = True
                self.training_start_time = datetime.datetime.now()
                self.lbl_start_val.config(text=self.training_start_time.strftime("%Y-%m-%d %H:%M:%S"))
                self.lbl_end_val.config(text="-")
                self.lbl_time_val.config(text="-")
                # トグル：作業終了へ
                self.training_button.config(text="作業終了")
                # 定期送信/監視開始
                self.start_middle_timer()
                self.start_kbd_mouse_listeners()
                self.start_keyboard_timer()
                self.start_mouse_timer()
            else:
                url = self.base_url + "training_end.php"
                data = {"value": self.token}
                cam = self.capture_camera()
                files = {"file": cam} if cam else None
                res = self.post_multipart(url, data, files)
                res.raise_for_status()
                js = {}
                try:
                    js = res.json()
                except Exception:
                    pass
                if isinstance(js, dict) and isinstance(js.get("res"), str) and len(js["res"]) == 32:
                    self.token = js["res"]
                self.training_started = False
                self.training_end_time = datetime.datetime.now()
                self.lbl_end_val.config(text=self.training_end_time.strftime("%Y-%m-%d %H:%M:%S"))
                if self.training_start_time and self.training_end_time:
                    delta = self.training_end_time - self.training_start_time
                    mins = int(delta.total_seconds() // 60)
                    self.lbl_time_val.config(text=f"{mins} 分")
                # トグル：作業開始へ
                self.training_button.config(text="作業開始")
                # 停止
                self.stop_middle_timer()
                self.stop_keyboard_timer()
                self.stop_mouse_timer()
                self.stop_kbd_mouse_listeners()
        except requests.exceptions.RequestException as e:
            self.display_message(f"エラー（業務）: {e}")

    def send_message_event(self, event):
        self.send_message()

    def send_message(self):
        message = self.message_entry.get()
        if not message:
            return
        self.message_entry.delete(0, tk.END)
        if not self.token:
            self.display_message("[注意] ログインしてください。")
            return
        try:
            url = self.base_url + "training_insert.php"
            data = {"value": self.token, "value2": self.last_chat_date, "value3": message}
            res = self.post_multipart(url, data)
            try:
                res.raise_for_status()
            except requests.exceptions.HTTPError as e:
                self.display_message(f"[HTTP] 送信失敗: {e} body={res.text[:200]}")
                raise
            try:
                response_data = res.json()
                if isinstance(response_data, list):
                    for item in response_data:
                        user_name = item.get("user_name")
                        admin_name = item.get("admin_name")
                        insert_date = item.get("insert_date")
                        chat_text = item.get("chat_text")
                        display_text = ""
                        if user_name:
                            display_text += f"{user_name}>"
                        elif admin_name:
                            display_text += f"{admin_name}>"
                        if insert_date:
                            display_text += f"{insert_date}\n"
                        if chat_text:
                            display_text += f"{chat_text}"
                        if display_text:
                            self.display_message(display_text)
            except json.JSONDecodeError:
                self.fetch_messages_once()
        except requests.exceptions.RequestException as e:
            self.display_message(f"エラー（送信）: {e}")

    def fetch_messages_once(self):
        if not self.token:
            return
        try:
            url = self.base_url + "training_update.php"
            data = {"value": self.token, "value2": self.last_chat_date, "value3": ""}
            res = self.post_multipart(url, data)
            try:
                res.raise_for_status()
            except requests.exceptions.HTTPError as e:
                self.display_message(f"[HTTP] {e} body={res.text[:200]}")
                raise
            response_data = res.json()
            if isinstance(response_data, list):
                for item in response_data:
                    user_name = item.get("user_name")
                    admin_name = item.get("admin_name")
                    insert_date = item.get("insert_date")
                    chat_text = item.get("chat_text")
                    display_text = ""
                    if user_name:
                        display_text += f"{user_name}>"
                    elif admin_name:
                        display_text += f"{admin_name}>"
                    if insert_date:
                        display_text += f"{insert_date}\n"
                    if chat_text:
                        display_text += f"{chat_text}"
                    if display_text:
                        self.display_message(display_text)
        except requests.exceptions.RequestException as e:
            self.display_message(f"エラー（受信）: {e}")
        except json.JSONDecodeError:
            self.display_message("エラー: サーバーからの応答が不正です。")

    def start_polling(self):
        self.stop_polling()
        def _poll():
            self.fetch_messages_once()
            self._poll_after_id = self.after(5000, _poll)
        _poll()

    def stop_polling(self):
        if self._poll_after_id is not None:
            try:
                self.after_cancel(self._poll_after_id)
            except Exception:
                pass
            self._poll_after_id = None

    def display_message(self, message):
        self.chat_display.config(state='normal')
               # 既存の末尾に追加
        self.chat_display.insert(tk.END, message + "\n")
        self.chat_display.yview(tk.END)
        self.chat_display.config(state='disabled')

    def notice_clear(self):
        # お知らせの表示領域（左上ワークスペース）をクリア
        try:
            self.workspace.config(state='normal')
            self.workspace.delete("1.0", tk.END)
            self.workspace.config(state='disabled')
        except Exception:
            pass

    # --- 画面キャプチャ（中間） ---
    def capture_screen_file(self):
        try:
            import mss
            import mss.tools
            from PIL import Image
        except Exception:
            try:
                from PIL import ImageGrab, Image
            except Exception as e:
                self.display_message(f"[注意] 画面キャプチャ未対応: mss または pillow をインストールしてください ({e})")
                return None
            try:
                img = ImageGrab.grab()
                img = img.resize((320, 240))
                bio = io.BytesIO()
                img.save(bio, format='JPEG')
                bio.seek(0)
                return ("capture.jpg", bio, "image/jpeg")
            except Exception as e:
                self.display_message(f"[注意] 画面キャプチャ失敗: {e}")
                return None
        try:
            with mss.mss() as sct:
                monitor = sct.monitors[0]
                img = sct.grab(monitor)
                from PIL import Image
                im = Image.frombytes('RGB', img.size, img.rgb)
                im = im.resize((320, 240))
                bio = io.BytesIO()
                im.save(bio, format='JPEG')
                bio.seek(0)
                return ("capture.jpg", bio, "image/jpeg")
        except Exception as e:
            self.display_message(f"[注意] 画面キャプチャ失敗(mss): {e}")
            return None

    def tick_middle(self):
        if not self.logged_in or not getattr(self, "training_started", False) or not self.token:
            return
        try:
            url = self.base_url + "training_middle.php"
            cap = self.capture_screen_file()
            files = {"file": cap} if cap else None
            self.post_multipart(url, {"value": self.token}, files)
        except Exception:
            pass
        finally:
            millis = max(1, int(self.middle_interval_minutes)) * 60 * 1000
            self._middle_after_id = self.after(millis, self.tick_middle)

    def start_middle_timer(self):
        self.stop_middle_timer()
        self.tick_middle()

    def stop_middle_timer(self):
        if self._middle_after_id is not None:
            try:
                self.after_cancel(self._middle_after_id)
            except Exception:
                pass
            self._middle_after_id = None

    # --- 入力反応監視（キーボード/マウス） ---
    def start_kbd_mouse_listeners(self):
        try:
            from pynput import keyboard, mouse
        except Exception as e:
            self.display_message(f"[注意] 入力監視未対応: pynput をインストールしてください ({e})")
            return

        def on_key_press(key): self.keyboard_flag = '1'
        def on_key_release(key): self.keyboard_flag = '1'
        def on_move(x, y): self.mouse_flag = '1'
        def on_click(x, y, button, pressed): self.mouse_flag = '1'
        def on_scroll(x, y, dx, dy): self.mouse_flag = '1'

        try:
            self._kbd_listener = keyboard.Listener(on_press=on_key_press, on_release=on_key_release)
            self._mouse_listener = mouse.Listener(on_move=on_move, on_click=on_click, on_scroll=on_scroll)
            self._kbd_listener.daemon = True
            self._mouse_listener.daemon = True
            self._kbd_listener.start()
            self._mouse_listener.start()
        except Exception as e:
            self.display_message(f"[注意] 入力監視開始に失敗: {e}")

    def stop_kbd_mouse_listeners(self):
        try:
            if self._kbd_listener is not None:
                self._kbd_listener.stop()
        except Exception:
            pass
        try:
            if self._mouse_listener is not None:
                self._mouse_listener.stop()
        except Exception:
            pass
        self._kbd_listener = None
        self._mouse_listener = None

    def tick_keyboard(self):
        if not self.logged_in or not getattr(self, "training_started", False) or not self.token:
            return
        try:
            url = self.base_url + "training_keyboard.php"
            self.post_multipart(url, {"value": self.token, "value2": self.keyboard_flag})
        except Exception:
            pass
        finally:
            self.keyboard_flag = '2'
            millis = max(1, int(self.keyboard_interval_minutes)) * 60 * 1000
            self._kbd_after_id = self.after(millis, self.tick_keyboard)

    def start_keyboard_timer(self):
        self.stop_keyboard_timer()
        self.tick_keyboard()

    def stop_keyboard_timer(self):
        if self._kbd_after_id is not None:
            try:
                self.after_cancel(self._kbd_after_id)
            except Exception:
                pass
            self._kbd_after_id = None

    def tick_mouse(self):
        if not self.logged_in or not getattr(self, "training_started", False) or not self.token:
            return
        try:
            url = self.base_url + "training_mouse.php"
            self.post_multipart(url, {"value": self.token, "value2": self.mouse_flag})
        except Exception:
            pass
        finally:
            self.mouse_flag = '2'
            millis = max(1, int(self.mouse_interval_minutes)) * 60 * 1000
            self._mouse_after_id = self.after(millis, self.tick_mouse)

    def start_mouse_timer(self):
        self.stop_mouse_timer()
        self.tick_mouse()

    def stop_mouse_timer(self):
        if self._mouse_after_id is not None:
            try:
                self.after_cancel(self._mouse_after_id)
            except Exception:
                pass
            self._mouse_after_id = None

    # 終了処理
    def on_close(self):
        if self.logged_in:
            if not messagebox.askokcancel("確認", "ログアウトせずに終了しますか？"):
                return
        try:
            self.stop_polling()
            self.stop_middle_timer()
            self.stop_keyboard_timer()
            self.stop_mouse_timer()
            self.stop_kbd_mouse_listeners()
        finally:
            self.destroy()


if __name__ == "__main__":
    app = MainWindow()
    app.mainloop()
