import tkinter as tk
from tkinter import scrolledtext, messagebox, simpledialog
import requests
import json
import datetime
import webbrowser
import sys, os
import io
import time
import ctypes   # ← これを使います
import subprocess
import re
from urllib.parse import urlsplit
import ipaddress
from PIL import Image, ImageTk

# ★ 追加：DPI Awareness（Tk を作る前に呼ぶ）
def enable_dpi_awareness():
    if hasattr(ctypes, "windll"):
        try:
            # Windows 10 1607+ (Per-Monitor v2)
            ctypes.windll.user32.SetProcessDpiAwarenessContext(-4)
            return
        except Exception:
            pass
        try:
            # Windows 8.1+
            ctypes.windll.shcore.SetProcessDpiAwareness(2)  # PER_MONITOR_DPI_AWARE
            return
        except Exception:
            pass
        try:
            # Vista+
            ctypes.windll.user32.SetProcessDPIAware()
        except Exception:
            pass
enable_dpi_awareness()   # ← この1行を関数定義のすぐ下に入れる


try:
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("com.jigyodan.zaitaku.v4")
except Exception:
    pass


def resource_path(rel):
    if hasattr(sys, "_MEIPASS"):  # PyInstaller が一時展開する場所
        return os.path.join(sys._MEIPASS, rel)
    return os.path.join(os.path.abspath("."), rel)

CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".jigyodan_config.json")
ADMIN_PASSWORD = "admin"


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


def get_system_volume_serial():
    drive = os.getenv("SystemDrive", "C:") + "\\"
    if hasattr(ctypes, "windll"):
        try:
            volume_name_buffer = ctypes.create_unicode_buffer(256)
            filesystem_name_buffer = ctypes.create_unicode_buffer(256)
            serial_number = ctypes.c_uint()
            max_component_length = ctypes.c_uint()
            file_system_flags = ctypes.c_uint()
            result = ctypes.windll.kernel32.GetVolumeInformationW(
                drive,
                volume_name_buffer,
                len(volume_name_buffer),
                ctypes.byref(serial_number),
                ctypes.byref(max_component_length),
                ctypes.byref(file_system_flags),
                filesystem_name_buffer,
                len(filesystem_name_buffer),
            )
            if result:
                return f"{serial_number.value:08X}"
        except Exception:
            pass

    try:
        completed = subprocess.run(
            ["cmd", "/c", "vol", drive],
            capture_output=True,
            text=True,
            timeout=5,
        )
        if completed.returncode == 0 and completed.stdout:
            match = re.search(r"([0-9A-Fa-f]{4}-[0-9A-Fa-f]{4})", completed.stdout)
            if match:
                return match.group(1).replace("-", "").upper()
    except Exception:
        pass

    return ""




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)
        volume_value = cfg.get("volume_no", "")
        auto_volume = get_system_volume_serial()
        if auto_volume:
            volume_value = auto_volume
        self.ent_vol.insert(0, volume_value)

        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")


        # --- アイコン設定 ---
        try:
            # 1) .ico（マルチサイズ ICO 推奨：16,24,32,48,64,128,256 を内包）
            self.iconbitmap(resource_path("zaitaku.ico"))
        except Exception as e:
            print("ICO読み込み失敗:", e)

        # 2) 追加：PNGも登録（タスクバー・Alt+Tab でのぼやけ対策）
        try:
            # ここでは 16 / 32 / 48 / 256px を例示（必要に応じて増減OK）
            png16  = ImageTk.PhotoImage(Image.open(resource_path("icon_16.png")))
            png32  = ImageTk.PhotoImage(Image.open(resource_path("icon_32.png")))
            png48  = ImageTk.PhotoImage(Image.open(resource_path("icon_48.png")))
            png256 = ImageTk.PhotoImage(Image.open(resource_path("icon_256.png")))
            # 複数渡すと OS/Tk が最適サイズを選ぶ
            self.wm_iconphoto(True, png256, png48, png32, png16)
            # 参照キープ（ガベコレ防止）
            self._icon_refs = (png16, png32, png48, png256)
        except Exception as e:
            print("PNGアイコン設定失敗:", e)




        # 固定画像サイズ/ボタンサイズ
        self.IMG_W = 160
        self.IMG_H = 120
        self.BTN_WIDE = 16
        self.BTN_HEIGHT = 2

        # 画面サイズは横固定・縦のみ可変
        self.geometry("900x470")
        self.resizable(False, False)

        # 状態
        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 = 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=230)
        left.rowconfigure(1, weight=0, minsize=147)

        # 作業メモ（読み専用風のText）— 外枠も白、枠なし
        workspace_outer = tk.Frame(
            left,
            bg="white",
            bd=0,                # ← 枠線なし
            relief="flat",       # ← 立体効果なし
            highlightthickness=0 # ← フォーカス枠なし
        )
        workspace_outer.grid(row=0, column=0, sticky="nsew", padx=4, pady=(4, 8))

        self.workspace = tk.Text(
            workspace_outer,
            bg="white",
            height=14,
            bd=0,
            relief="flat",       # ← 念のため追加
            highlightthickness=0,
            takefocus=True
        )
        # 内側余白 5px（白が見える）
        self.workspace.pack(fill="both", expand=True, padx=5, pady=5)
        self.workspace.bind("<KeyPress>", self.on_readonly_text_keypress)
        self.workspace.bind("<Tab>", self.focus_next_widget)
        self.workspace.bind("<Shift-Tab>", self.focus_prev_widget)





        # 外部リンク
        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)
        link_frame.rowconfigure(1, 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_panel = tk.Frame(link_frame, bg="#bfcddb", bd=0, highlightthickness=0)
        links_panel.grid(row=1, column=0, sticky="nsew")

        # 外枠も白にする（薄い枠線が欲しければ bd=1, relief="solid" を維持）
        outer = tk.Frame(links_panel, bg="white", bd=0, highlightthickness=0, relief="flat")
        outer.pack(fill="both", expand=True)
        # 内枠（余白を白で表示）
        inner = tk.Frame(outer, bg="white")
        inner.pack(fill="both", expand=True, padx=8, pady=8)

        # Listbox も白に
        self.links_list = tk.Listbox(inner, height=5, bd=0, highlightthickness=0, bg="white")
        self.links_list.pack(fill="both", expand=True)
        self.links_list.bind("<Double-Button-1>", self.open_selected_link)
        self._links_map = {}


        # ===== 右側 =====
        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=0, minsize=220)  # 上段やや固定
        right.rowconfigure(1, weight=1, 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)

        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)

        # チャット表示欄 — 外枠も内側も白でフラット
        chat_frame_outer = tk.Frame(
            upper_right,
            bg="white",
            bd=0,                 # 枠線なし
            relief="flat",        # フラットにする
            highlightthickness=0  # フォーカス枠なし
        )
        chat_frame_outer.grid(row=1, column=0, sticky="nsew", padx=4, pady=4)

        self.chat_display = scrolledtext.ScrolledText(
            chat_frame_outer,
            wrap="word",
            height=12,
            bd=0,                 # 枠線なし
            relief="flat",        # フラットにする
            highlightthickness=0, # フォーカス枠なし
            bg="white"            # 背景を白に統一
        )
        self.chat_display.configure(state="normal", takefocus=True)
        self.chat_display.pack(fill="both", expand=True, padx=5, pady=5)



        self.chat_display.tag_configure("chat_admin", foreground="red", spacing3=8)
        self.chat_display.tag_configure("chat_user", foreground="blue", spacing3=8)
        self.chat_display.tag_configure("chat_default", spacing3=8)
        self.chat_display.bind("<KeyPress>", self.on_readonly_text_keypress)
        self.chat_display.bind("<Tab>", self.focus_next_widget)
        self.chat_display.bind("<Shift-Tab>", self.focus_prev_widget)


        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), padx=(4, 0))
        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")
        biz.columnconfigure(0, weight=0, minsize=self.IMG_W + 8)
        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))
        img_wrap.grid_propagate(False)

        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)

        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)

        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))

        # フッター
        footer = tk.Frame(self, bg="#bfcddb")
        footer.grid(row=1, column=0, columnspan=2, sticky="ew")
        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)

    # ---------------- 通信 ----------------
    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)  # IP直アクセスならHost差し替え
                    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):
        admin_pass = simpledialog.askstring("管理者パスワード", "設定を開くにはパスワードを入力してください。", show="*")
        if admin_pass is None:
            return
        if admin_pass != ADMIN_PASSWORD:
            messagebox.showerror("エラー", "パスワードが違います。")
            return
        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))
            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:
                    self.display_message("[エラー] 作業開始に失敗しました。")
            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()
                else:
                    self.display_message("[エラー] 作業終了に失敗しました。")
        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")

                        author_name = user_name or admin_name
                        if author_name and "\u30b7\u30b9\u30c6\u30e0" in author_name:
                            continue

                        role = None
                        if user_name:
                            role = "user"
                        elif admin_name:
                            role = "admin"

                        display_text = ""
                        if author_name:
                            display_text += f"{author_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, role=role)
            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")

                    author_name = user_name or admin_name
                    if author_name and "\u30b7\u30b9\u30c6\u30e0" in author_name:
                        continue

                    role = None
                    if user_name:
                        role = "user"
                    elif admin_name:
                        role = "admin"

                    display_text = ""
                    if author_name:
                        display_text += f"{author_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, role=role)
        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 focus_next_widget(self, event):
        widget = event.widget.tk_focusNext()
        if widget:
            widget.focus_set()
        return "break"

    def focus_prev_widget(self, event):
        widget = event.widget.tk_focusPrev()
        if widget:
            widget.focus_set()
        return "break"

    def on_readonly_text_keypress(self, event):
        # 読み取り専用風：タブ移動・コピーなどは許可
        if event.keysym in ("Tab", "ISO_Left_Tab"):
            return
        if (event.state & 0x4) and event.keysym.lower() in ("a", "c", "insert"):
            return
        if event.char == "":
            return
        return "break"

    def display_message(self, message, role=None):
        text_value = str(message)
        blocked_phrases = ("[\u30b7\u30b9\u30c6\u30e0] \u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002", "[\u30b7\u30b9\u30c6\u30e0] \u30ed\u30b0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "\u30b7\u30b9\u30c6\u30e0] \u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3057\u305f\u3002", "\u30b7\u30b9\u30c6\u30e0] \u30ed\u30b0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002")
        if any(phrase in text_value for phrase in blocked_phrases) or "[システム]" in text_value:
            return
        self._append_chat_line(text_value, role)

    def _append_chat_line(self, message, role=None):
        widget = getattr(self, "chat_display", None)
        if widget is None:
            return

        tag = None
        if role == "admin":
            tag = "chat_admin"
        elif role == "user":
            tag = "chat_user"

        if not tag:
            tag = "chat_default"

        text_value = message.rstrip("\n") + "\n"
        widget.insert("1.0", text_value, tag)
        widget.yview_moveto(0.0)

    def notice_clear(self):
        try:
            self.workspace.config(state="normal")
            self.workspace.delete("1.0", tk.END)
        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

    # 画面キャプチャ（可能ならImageGrab→mssの順で試す）
    def capture_screen_file(self):
        # まず PIL.ImageGrab
        try:
            from PIL import ImageGrab

            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_grab:
            # 次に mss
            try:
                import mss

                with mss.mss() as sct:
                    monitor = sct.monitors[0]  # 全画面
                    shot = sct.grab(monitor)
                    from PIL import Image as PILImage2

                    im = PILImage2.frombytes("RGB", shot.size, shot.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_mss:
                self.display_message(f"[注意] 画面キャプチャ未対応: PIL.ImageGrab または mss をインストール/許可してください ({e_grab} / {e_mss})")
                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()
