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
from PIL import Image, ImageTk
from tkinter import scrolledtext, messagebox, simpledialog

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

        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

        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.configure(bg="#bfcddb")

        # 固定画像サイズ
        self.IMG_W = 160
        self.IMG_H = 120

        # ボタン共通サイズ
        self.BTN_WIDE = 16
        self.BTN_HEIGHT = 2

        # 変更後（縦だけ少し広くして、ユーザーが縦は調整できるようにする）
        self.geometry("900x450")
        self.resizable(False, True)

        # 状態
        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
        self.last_capture_photo = None
        self.capture_display_label = None   # ← これを追加

        # 2カラム
        self.columnconfigure(0, weight=6, minsize=450)
        self.columnconfigure(1, weight=5, minsize=450)
        self.rowconfigure(0, weight=1)
        self.rowconfigure(1, weight=0)   # フッター用の最下段

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

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

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

        links_header = tk.Frame(link_frame, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        links_header.grid(row=0, column=0, sticky="ew", padx=2, pady=(0, 4))
        links_header.columnconfigure(0, weight=1)
        tk.Label(links_header, text="参考外部リンク", font=("Meiryo", 12, "bold"),
                 bg="#bfcddb", anchor="w").grid(row=0, column=0, sticky="w")
        tk.Button(links_header, text="日報", width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                  command=self.open_report).grid(row=0, column=1, padx=(10, 0))

        links_box = tk.Frame(link_frame, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        links_box.grid(row=1, column=0, sticky="nsew")
        link_frame.rowconfigure(1, weight=1)

        self.links_list = tk.Listbox(links_box, height=5, bd=1)
        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", bd=0, relief="flat", highlightthickness=0)
        right.grid(row=0, column=1, sticky="nsew", padx=0, pady=0)  # 外側余白ゼロ
        right.columnconfigure(0, weight=1)
        right.rowconfigure(0, weight=1, minsize=260)
        # 変更後（余白を少し削る）
        right.rowconfigure(1, weight=0, minsize=self.IMG_H + 40)

        # 上段：チャット
        upper_right = tk.Frame(right, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        upper_right.grid(row=0, column=0, sticky="nsew")
        upper_right.columnconfigure(0, weight=1)
        upper_right.rowconfigure(0, weight=0)
        # 変更後（下段優先）
        right.rowconfigure(0, weight=0, minsize=220)                 # 上段は固定ぎみ
        right.rowconfigure(1, weight=1, minsize=self.IMG_H + 40)     # 下段に余りを渡す

        header = tk.Frame(upper_right, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        header.grid(row=0, column=0, sticky="ew", padx=4, pady=(8, 6))
        WIDE_L = self.BTN_WIDE + 1
        WIDE_S = max(8, self.BTN_WIDE - 10)
        self.lbl_login = tk.Button(header, text="未ログイン", width=WIDE_L, height=self.BTN_HEIGHT,
                                   state="disabled", relief="raised")
        self.btn_login = tk.Button(header, text="ログイン", width=WIDE_L, height=self.BTN_HEIGHT,
                                   command=self.toggle_login)
        self.btn_setting = tk.Button(header, text="設定", width=WIDE_S, height=self.BTN_HEIGHT,
                                     command=self.open_settings)
        self.update_button = tk.Button(header, text="更新", width=WIDE_S, height=self.BTN_HEIGHT,
                                       command=self.fetch_messages_once)
        self.lbl_login.grid(row=0, column=0, padx=6)
        self.btn_login.grid(row=0, column=1, padx=6)
        self.btn_setting.grid(row=0, column=2, padx=6)
        self.update_button.grid(row=0, column=3, padx=6)

        # 変更後（例：8行）
        self.chat_display = scrolledtext.ScrolledText(
        upper_right,
        wrap='word',
        height=12   # ← デフォルト20を12くらいにする
)

        self.chat_display.grid(row=1, column=0, sticky="nsew")
        self.chat_display.bind("<KeyPress>", lambda e: "break")
        self.chat_display.bind("<Tab>", lambda e: (e.widget.tk_focusNext(), "break")[1])
        self.chat_display.bind("<Shift-Tab>", lambda e: (e.widget.tk_focusPrev(), "break")[1])

        input_row = tk.Frame(upper_right, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        input_row.grid(row=2, 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)

        # 下段：画像＆ボタン
        lower_right = tk.Frame(right, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        lower_right.grid(row=1, column=0, sticky="nsew", padx=0, pady=0)  # 外側余白ゼロ
        lower_right.columnconfigure(0, weight=1)
        lower_right.rowconfigure(0, weight=0)
        lower_right.rowconfigure(1, weight=0)

        # ====== ここが重要：左端に“ガター列”を作る ======
        # ====== 下段：画像＆ボタン ======
        # ====== 下段：画像＆ボタン ======
        biz = tk.Frame(lower_right, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        biz.grid(row=0, column=0, sticky="nsew")

        # 列構成：col=0 画像ラッパー / col=1 操作列
        biz.columnconfigure(0, weight=0, minsize=self.IMG_W + 8)   # ← 左右に8pxぶんラッパーの幅を確保
        biz.columnconfigure(1, weight=0, minsize=220)              # 操作列の最低幅で潰れ防止
        biz.rowconfigure(0,  weight=0, minsize=self.IMG_H)

        # 親色ラッパー（ここに白い画像箱を少し右に“置く”）
        img_wrap = tk.Frame(
            biz,
            width=self.IMG_W + 8,   # ← 左右余白ぶん広げる
            height=self.IMG_H,
            bg="#bfcddb",
            bd=0, relief="flat", highlightthickness=0
        )
        img_wrap.grid(row=0, column=0, sticky="nw", padx=(0, 8))   # 右側との間は8px
        img_wrap.grid_propagate(False)

        # 白い画像エリア本体（左に4pxの親色を常設するため、placeでx=4に置く）
        img_box = tk.Frame(
            img_wrap,
            bg="white",
            bd=0, relief="flat", highlightthickness=0
        )
        img_box.place(x=4, y=0, width=self.IMG_W, height=self.IMG_H)  # ← ココがポイント（左4pxオフセット）

        # 画像ラベル（サイズ指定しない／fillで埋める）
        self.capture_display_label = tk.Label(img_box, bg="white", bd=0, relief="flat", highlightthickness=0)
        self.capture_display_label.pack(fill="both", expand=True)

        # 右側の操作列
        side = tk.Frame(biz, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        side.grid(row=0, column=1, sticky="nsew")
        side.columnconfigure(0, weight=1)

        btn_row = tk.Frame(side, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        btn_row.grid(row=0, column=0, sticky="ew", pady=(2, 12))
        btn_row.columnconfigure(0, weight=1)
        btn_row.columnconfigure(1, weight=1)

        self.training_button = tk.Button(btn_row, text="作業開始",
                                        width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                                        command=self.toggle_training)
        self.training_button.grid(row=0, column=0, padx=(0, 8), sticky="ew")

        inquiry_btn = tk.Button(btn_row, text="連絡要求",
                                width=self.BTN_WIDE, height=self.BTN_HEIGHT,
                                command=self.send_inquiry)
        inquiry_btn.grid(row=0, column=1, sticky="ew")

        status = tk.Frame(side, bg="#bfcddb", bd=0, relief="flat")
        status.grid(row=1, column=0, sticky="ew")
        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)

        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)

        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)

        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(lower_right, bg="#bfcddb", bd=0, relief="flat", highlightthickness=0)
        close_row.grid(row=1, column=0, sticky="e")
        tk.Button(
            close_row,
            text="閉じる",
            width=8,
            command=self.on_close
        ).pack(side="right", padx=(0, 6), pady=(6, 0))  # ← 右側に12pxの余白




        # ▼ ここからフッターを追加
        footer = tk.Frame(self, bg="#bfcddb")
        footer.grid(row=1, column=0, columnspan=2, sticky="ew")  # 2カラムぶち抜きで最下段
        footer.columnconfigure(0, weight=1)

        tk.Label(
            footer,
            text="COPYRIGHT©NPO在宅就労支援事業団 All RIGHTS RESERVED.",
            font=("Meiryo", 9),
            bg="#bfcddb",
            anchor="center"
        ).grid(row=0, column=0, pady=(4, 6))
        # ▲ フッターここまで





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

        # 必要寸法を実測して高さを確保
        # self.update_idletasks()
        # need_w = max(self.winfo_reqwidth(), 900)
        # need_h = max(self.winfo_reqheight(), self.IMG_H + 420)
        # self.geometry(f"{need_w}x{need_h}")

    # ---------------- 通信 ----------------
    def post_multipart(self, url: str, fields: dict, files: dict | None = None):
        files_param = {k: (None, "" if v is None else str(v)) for k, v in fields.items()}
        if files:
            files_param.update(files)
        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, 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, self.IMG_W)

            cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.IMG_H)
            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, None)
            ok2, buf = cv2.imencode('.jpg', frame)
            if not ok2:
                self.display_message("[注意] 画像のエンコードに失敗しました。")
                return (None, None)
            bio = io.BytesIO(buf.tobytes())
            bio.seek(0)
            return (("camera.jpg", bio, "image/jpeg"), frame)
        except Exception as e:
            self.display_message(f"[注意] カメラ撮影に失敗: {e}")
            return (None, None)

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

    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.rstrip('/') + "/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.rstrip('/') + "/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')
        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.rstrip('/') + "/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
        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.rstrip('/') + "/training_inquiry.php"
            data = {"value": self.token}
            cam_data, frame = self.capture_camera()
            if frame is not None:
                self.display_captured_image(frame)
            files = {"file": cam_data} if cam_data 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.rstrip('/') + "/training_start.php"
                data = {"value": self.token}
                cam_data, frame = self.capture_camera()
                if frame is not None:
                    self.display_captured_image(frame)
                files = {"file": cam_data} if cam_data 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.rstrip('/') + "/training_end.php"
                data = {"value": self.token}
                cam_data, frame = self.capture_camera()
                if frame is not None:
                    self.display_captured_image(frame)
                files = {"file": cam_data} if cam_data 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.rstrip('/') + "/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.rstrip('/') + "/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 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.rstrip('/') + "/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.rstrip('/') + "/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.rstrip('/') + "/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 display_captured_image(self, frame):
        # 画像エリア未初期化なら何もしない（起動直後の呼び出し対策）
        if not isinstance(getattr(self, "capture_display_label", None), tk.Label):
            self.display_message("[注意] 画像エリアが未初期化のため表示をスキップしました。")
            return
        try:
            import cv2
            img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            pil_img = Image.fromarray(img_rgb).resize((self.IMG_W, self.IMG_H), Image.Resampling.LANCZOS)
            self.last_capture_photo = ImageTk.PhotoImage(pil_img)
            self.capture_display_label.config(image=self.last_capture_photo, bg="white")
            # 参照保持の保険（ガベージコレクタ対策）
            self.capture_display_label.image = self.last_capture_photo
        except Exception as e:
            self.display_message(f"[表示エラー] {e}")


    def clear_image(self):
        if isinstance(getattr(self, "capture_display_label", None), tk.Label):
            self.capture_display_label.config(image="", bg="white")
            self.capture_display_label.image = None
        self.last_capture_photo = None


    # 画面キャプチャ（使っていない場合は削除可）
    def capture_screen_file(self):
        try:
            import mss
            import mss.tools
            from PIL import Image as PILImage
        except Exception:
            try:
                from PIL import ImageGrab, Image as PILImage
            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.mss.monitors[0] if hasattr(sct, "monitors") else sct.monitors[0]
                img = sct.grab(monitor)
                from PIL import Image as PILImage2
                im = PILImage2.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 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()
