"""Local development workbench. Every control invokes real offline behavior."""
import json
import os
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox
import uuid
import threading
import queue
from . import DISPLAY_VERSION, REVISION
from .catalog import read_json
from .engine import Executor
from .paths import user_data
from .paths import root


class Workbench:
    def __init__(self, window, catalog, *, storage=None, offline_smoke=False):
        self.window, self.catalog = window, catalog
        self.storage = Path(storage) if storage else user_data()
        self.offline_smoke = offline_smoke
        self.storage.mkdir(parents=True, exist_ok=True)
        self.engine, self.running = None, False
        window.title("마숙자 27호 " + DISPLAY_VERSION + " · " + REVISION)
        window.geometry("1160x780")
        window.minsize(860, 600)
        style = ttk.Style(window)
        style.theme_use("clam")
        style.configure("TLabel", font=("맑은 고딕", 10))
        style.configure("TButton", padding=6)
        ttk.Label(window, text="마숙자 27호  |  출처에서 행동으로", font=("맑은 고딕", 18, "bold")).pack(anchor="w", padx=18, pady=(14, 3))
        ttk.Label(window, text="테스트본 · 입력 기본 OFF · 사용자 테스트 시작으로만 활성화 · 중지 / F8 · 미지원 경로는 대기", foreground="#9a3a15").pack(anchor="w", padx=18, pady=(0, 12))
        tabs = ttk.Notebook(window)
        tabs.pack(fill="both", expand=True, padx=14, pady=(0, 14))
        self.arbeit_page = ttk.Frame(tabs, padding=10)
        tabs.add(self.arbeit_page, text="아르바이트 실행 준비")
        self.build_arbeit_page()
        self.run_page = ttk.Frame(tabs, padding=10)
        self.catalog_page = ttk.Frame(tabs, padding=10)
        self.coverage_page = ttk.Frame(tabs, padding=10)
        self.source_page = ttk.Frame(tabs, padding=10)
        for page, title in ((self.run_page, "dev1 합성 회귀 검사"), (self.catalog_page, "자료 목록"), (self.coverage_page, "지원 범위"), (self.source_page, "출처 / 변경 영향")):
            tabs.add(page, text=title)
        row = ttk.Frame(self.run_page)
        row.pack(fill="x")
        self.scenario = tk.StringVar(value="bag-resume")
        ttk.Combobox(row, textvariable=self.scenario, values=sorted(p.stem for p in (catalog.root / "data/fixtures").glob("*.json")), state="readonly", width=25).pack(side="left")
        for title, command in (("새 합성 실행", self.new_run), ("한 단계", self.step), ("계속", self.run), ("일시정지", self.pause), ("재개", self.resume), ("취소", self.cancel), ("최근 기록 열기", self.load_last)):
            ttk.Button(row, text=title, command=lambda f=command: self.safe(f)).pack(side="left", padx=3)
        self.status = tk.StringVar(value="실행 전 · 현재 게임 상태는 알 수 없습니다.")
        ttk.Label(self.run_page, textvariable=self.status, wraplength=1000).pack(anchor="w", pady=10)
        self.states = self.table(self.run_page, ("상태", "값", "출처", "관측 시각", "확실성"), height=11)
        self.log = tk.Text(self.run_page, height=12, wrap="word", font=("맑은 고딕", 10))
        self.log.pack(fill="both", expand=True, pady=(10, 0))
        self.log.configure(state="disabled")
        ttk.Label(self.catalog_page, text="dev1에서 보존한 일반 자료 목록입니다. 전체 아르바이트 정의·행동 그래프는 첫 번째 탭에서 확인합니다. 정적 정의는 현재 수락 목록이 아닙니다.", wraplength=1000).pack(anchor="w")
        self.query = tk.StringVar()
        search = ttk.Entry(self.catalog_page, textvariable=self.query)
        search.pack(fill="x", pady=8)
        search.bind("<KeyRelease>", lambda _: self.search())
        self.catalog_table = self.table(self.catalog_page, ("종류", "ID", "이름", "출처", "범위"), height=13)
        self.catalog_table.bind("<<TreeviewSelect>>", self.show_definition)
        self.definition = tk.Text(self.catalog_page, height=10, wrap="word", font=("맑은 고딕", 10))
        self.definition.pack(fill="both", expand=True)
        self.definition.configure(state="disabled")
        self.search()
        ttk.Label(self.coverage_page, text="요구 기준표 · 목록 존재 / 좌표 / 입력 / 효과 / 복구를 별도로 표시합니다. 게임 실기 확인: 0").pack(anchor="w", pady=(0, 10))
        coverage = self.table(self.coverage_page, ("ID / 요구", "목록", "좌표", "입력", "효과", "복구"), height=20)
        current_coverage = root() / "data/arbeit-coverage.json"
        requirements = read_json(current_coverage)["requirements"] if current_coverage.exists() else catalog.coverage["requirements"]
        for r in requirements:
            coverage.insert("", "end", values=(r["id"] + " " + r["requirement"], r["listed"], r["coordinates"], r["input"], r["effect"], r["recovery"]))
        ttk.Label(self.source_page, text="원본 경로는 이력 정보입니다. 실행은 이 설치 폴더의 해시 검증 스냅샷만 읽습니다.").pack(anchor="w", pady=(0, 10))
        self.sources = self.table(self.source_page, ("출처 ID", "원본 버전", "스냅샷", "SHA256"), height=12)
        for source in catalog.sources:
            self.sources.insert("", "end", iid=source["id"], values=(source["id"], source["project_version"], source["snapshot"], source["sha256"]))
        self.sources.bind("<<TreeviewSelect>>", self.show_impact)
        self.impact = tk.Text(self.source_page, height=10, wrap="word", font=("맑은 고딕", 10))
        self.impact.pack(fill="both", expand=True)
        self.impact.configure(state="disabled")
        window.protocol("WM_DELETE_WINDOW", self.close)

    def build_arbeit_page(self):
        from .arbeit_runtime import PreparationSession, readiness
        self.preparation = PreparationSession(self.storage, allow_native_observation=not self.offline_smoke)
        self.arbeit_runner = None
        if not self.offline_smoke:
            from .continuous_runner import ContinuousRunner
            self.arbeit_runner = ContinuousRunner(self.preparation)
            self.window.after(100, self.arbeit_run_result)
        self.probe_results = queue.Queue()
        self.probing = False
        from .arbeit_graph import load_catalog
        self.arbeit_catalog = load_catalog()
        self.arbeit_choices = {str(q['id']) + " · " + q['title']: q['id'] for q in self.arbeit_catalog['quests']}
        self.arbeit_choice = tk.StringVar(value=next(iter(self.arbeit_choices)))
        ttk.Label(self.arbeit_page, text=f"전체 아르바이트 수행 엔진 · 현재 자료 {len(self.arbeit_choices)}개 정의", font=("맑은 고딕", 12, "bold")).pack(anchor="w")
        ttk.Combobox(self.arbeit_page, textvariable=self.arbeit_choice, values=list(self.arbeit_choices), state="readonly", width=85).pack(anchor="w", pady=6)
        self.arbeit_progress=tk.StringVar(value='대기 · 입력 OFF · 게임 실행 미검증')
        ttk.Label(self.arbeit_page,textvariable=self.arbeit_progress,wraplength=1000).pack(anchor='w',pady=4)
        ttk.Label(self.arbeit_page, text="정적 정의는 현재 수락 가능·수락 완료를 뜻하지 않습니다. 실상태 관측이 없는 단계는 실행하지 않습니다.", wraplength=1000).pack(anchor="w", pady=(4, 8))
        row = ttk.Frame(self.arbeit_page)
        row.pack(fill="x")
        for title, callback in (("준비 상태", self.arbeit_status), ("전체 정의 검사", self.arbeit_audit), ("목표 / 출처", self.arbeit_graph), ("남은 지원 범위", self.arbeit_gaps), ("드라이 사전검사", self.arbeit_start), ("사용자 테스트 시작", self.arbeit_test_start), ("중지 / F8", self.arbeit_stop)):
            ttk.Button(row, text=title, command=lambda f=callback: self.safe(f)).pack(side="left", padx=3)
        ttk.Button(row, text="일시정지", command=lambda: self.safe(self.arbeit_pause)).pack(side="left", padx=3)
        ttk.Button(row, text="재개", command=lambda: self.safe(self.arbeit_test_start)).pack(side="left", padx=3)
        support = ttk.Frame(self.arbeit_page)
        support.pack(fill="x", pady=4)
        ttk.Button(support, text="연결 확인 · 읽기 전용", command=lambda: self.safe(self.arbeit_probe)).pack(side="left", padx=3)
        ttk.Button(support, text="로그 묶기", command=lambda: self.safe(self.arbeit_export_logs)).pack(side="left", padx=3)
        ttk.Label(support, text="중지 후 현재 버전의 로그만 로컬 ZIP으로 저장합니다. 업로드하지 않습니다.").pack(side="left", padx=6)
        self.window.bind('<F8>', lambda _: self.arbeit_stop())
        self.arbeit_log = tk.Text(self.arbeit_page, wrap="word", font=("맑은 고딕", 10))
        self.arbeit_log.pack(fill="both", expand=True, pady=(10, 0))
        self.arbeit_status()

    def arbeit_status(self):
        from .arbeit_runtime import readiness
        self.set_text(self.arbeit_log, json.dumps(readiness(), ensure_ascii=False, indent=2))

    def arbeit_audit(self):
        from .arbeit_graph import audit
        result = audit()
        self.set_text(self.arbeit_log, json.dumps({k:v for k,v in result.items() if k != "nodes"}, ensure_ascii=False, indent=2))

    def arbeit_gaps(self):
        from .arbeit_gap_matrix import load_gap_matrix, filter_quest
        selected = self.arbeit_choices[self.arbeit_choice.get()]
        result = filter_quest(load_gap_matrix(), selected)
        self.set_text(self.arbeit_log, json.dumps(result, ensure_ascii=False, indent=2))

    def arbeit_start(self, *, enable_input=False):
        quest_id = self.arbeit_choices[self.arbeit_choice.get()]
        if self.arbeit_runner is not None:
            accepted=self.arbeit_runner.start(quest_id, enable_input=enable_input)
            if hasattr(self,'arbeit_progress'):
                self.arbeit_progress.set('기존 시작 요청 유지 · 중복 시작 무시 · 중지 / F8로 취소' if accepted is False
                    else '시작 준비 중 · 중지 / F8로 취소 가능 · 입력 상태 확인 중')
        else:
            result = (self.preparation.request_start(quest_id, enable_input=True) if enable_input
                else self.preparation.request_start(quest_id))
            self.set_text(self.arbeit_log, json.dumps(result, ensure_ascii=False, indent=2))

    def arbeit_test_start(self):
        self.arbeit_start(enable_input=True)

    def arbeit_export_logs(self):
        if self.arbeit_runner is not None:
            self.arbeit_runner.export_logs()
        else:
            from .support_bundle import export_support_bundle
            self.preparation.signal_stop()
            result = {'execution':self.preparation.stop(),'input_enabled':False,
                'support_bundle':export_support_bundle(self.storage)}
            self.set_text(self.arbeit_log,json.dumps(result,ensure_ascii=False,indent=2))

    def arbeit_run_result(self):
        if self.arbeit_runner is None or self.arbeit_runner.closed.is_set():
            return
        latest = None
        while True:
            try:
                latest = self.arbeit_runner.results.get_nowait()
            except queue.Empty:
                break
        if latest is not None:
            execution=latest.get('execution',latest)
            states={'running':'진행','waiting':'관측 대기','paused':'일시정지','blocked':'준비 필요',
                'complete':'현재 효과 확인 완료','offline_contract_passed':'오프라인 검증 종료'}
            input_state = 'ON' if latest.get('input_enabled') is True else 'OFF'
            progress=f"{execution.get('title','아르바이트')} · {states.get(execution.get('status'),'상태 확인')} · {execution.get('cursor',0)}/{execution.get('node_count',0)} · 입력 {input_state}"
            material=execution.get('material_supply') or {}
            if material:
                stages={'return_to_field':'재료 보충을 위한 화면 복귀','supplying':'재료 보충',
                    'return_to_parent':'원래 목표로 복귀','resume_parent':'원래 목표 재계획'}
                progress+=' · '+stages.get(material.get('phase'),'재료 상태 확인')
                progress+=f" ({material.get('index',0)}/{len(material.get('goals',[]))})"
            self.arbeit_progress.set(progress)
            self.set_text(self.arbeit_log, json.dumps(latest, ensure_ascii=False, indent=2))
        self.window.after(100, self.arbeit_run_result)

    def arbeit_pause(self):
        if self.arbeit_runner is not None:
            self.arbeit_runner.pause()
        else:
            self.preparation.pause()
        if hasattr(self,'arbeit_progress'):
            self.arbeit_progress.set('일시정지 요청 · 입력 OFF')

    def arbeit_graph(self):
        from .arbeit_graph import compile_quest
        selected = self.arbeit_choices[self.arbeit_choice.get()]
        quest = next(q for q in self.arbeit_catalog["quests"] if q["id"] == selected)
        self.set_text(self.arbeit_log, json.dumps(compile_quest(quest), ensure_ascii=False, indent=2))

    def arbeit_stop(self):
        if self.arbeit_runner is not None:
            self.arbeit_runner.stop()
        else:
            self.preparation.signal_stop()
            self.set_text(self.arbeit_log, json.dumps(self.preparation.stop(), ensure_ascii=False, indent=2))
        if hasattr(self,'arbeit_progress'):
            self.arbeit_progress.set('중지 요청 · 입력 OFF')

    def arbeit_probe(self):
        if self.probing:
            return
        from .arbeit_runtime import probe
        self.probing = True
        self.set_text(self.arbeit_log, "공식 CLI 읽기 조회 중입니다. 게임 입력을 보내지 않습니다.")
        def work():
            try:
                result = probe(Path(r"C:\Nexon\MabinogiMobile\MabinogiMobile_CLI.exe"), self.storage / "readonly-evidence")
            except Exception as exc:
                result = {"error": str(exc)}
            self.probe_results.put(result)
        threading.Thread(target=work, daemon=True).start()
        self.window.after(100, self.arbeit_probe_result)

    def arbeit_probe_result(self):
        try:
            result = self.probe_results.get_nowait()
        except queue.Empty:
            self.window.after(100, self.arbeit_probe_result)
            return
        self.probing = False
        self.set_text(self.arbeit_log, json.dumps(result, ensure_ascii=False, indent=2))

    def table(self, parent, columns, height):
        frame = ttk.Frame(parent)
        frame.pack(fill="both", expand=True)
        table = ttk.Treeview(frame, columns=columns, show="headings", height=height)
        for column in columns:
            table.heading(column, text=column)
            table.column(column, width=160, minwidth=80, stretch=True)
        scroll = ttk.Scrollbar(frame, orient="vertical", command=table.yview)
        horizontal = ttk.Scrollbar(frame, orient="horizontal", command=table.xview)
        table.configure(yscrollcommand=scroll.set, xscrollcommand=horizontal.set)
        table.grid(row=0, column=0, sticky="nsew")
        scroll.grid(row=0, column=1, sticky="ns")
        horizontal.grid(row=1, column=0, sticky="ew")
        frame.rowconfigure(0, weight=1)
        frame.columnconfigure(0, weight=1)
        return table

    def safe(self, callback):
        try:
            callback()
        except Exception as exc:
            self.running = False
            messagebox.showerror("로컬 실행 오류", str(exc), parent=self.window)

    @staticmethod
    def set_text(widget, text):
        widget.configure(state="normal")
        widget.delete("1.0", "end")
        widget.insert("1.0", text)
        widget.configure(state="disabled")

    def new_run(self):
        self.running = False
        if self.engine and self.engine.status not in ("stopped", "simulated_complete"):
            self.engine.pause()
        name = self.scenario.get()
        fixture = read_json(self.catalog.root / "data/fixtures" / (name + ".json"))
        journal = self.storage / ("run-" + uuid.uuid4().hex + ".json")
        self.engine = Executor(self.catalog, fixture, journal)
        (self.storage / "last-run.txt").write_text(journal.name, encoding="utf8")
        self.refresh()

    def load_last(self):
        self.running = False
        name = (self.storage / "last-run.txt").read_text(encoding="utf8")
        if Path(name).name != name or not name.startswith("run-"):
            raise ValueError("Invalid journal name")
        self.engine = Executor.load(self.catalog, self.storage / name)
        self.refresh()

    def step(self):
        if self.engine is None:
            self.new_run()
        self.engine.tick()
        self.refresh()

    def run(self):
        if self.engine is None:
            self.new_run()
        if self.running:
            return
        self.running = True
        self.pump()

    def pump(self):
        if not self.running:
            return
        self.step()
        if self.engine.status in ("waiting", "stopped", "simulated_complete", "paused"):
            self.running = False
        else:
            self.window.after(150, lambda: self.safe(self.pump))

    def pause(self):
        self.running = False
        if self.engine:
            self.engine.pause()
            self.refresh()

    def resume(self):
        if self.engine and self.engine.resume():
            self.refresh()

    def cancel(self):
        self.running = False
        if self.engine:
            self.engine.cancel()
            self.refresh()

    def refresh(self):
        e = self.engine
        self.status.set(f"{e.goal['title']} · 목표 달걀 {e.goal['parameters']['quantity']}개 · {e.cursor}/{len(e.goal['steps'])} · {e.status} · {e.current_action or '끝'} · {e.reason} · 게임 완료 아님")
        self.states.delete(*self.states.get_children())
        for key, o in e.state.latest.items():
            self.states.insert("", "end", values=(key, o.value, o.source, o.observed_at, o.uncertainty or ("충돌" if key in e.state.conflicts else o.confidence)))
        self.set_text(self.log, "\n".join(f"{x['sequence']:02d}  {x['kind']}  {x['reason']}" for x in e.events) + "\n\n로컬 기록: " + str(e.journal))
        self.log.see("end")

    def search(self):
        self.catalog_table.delete(*self.catalog_table.get_children())
        for index, row in enumerate(self.catalog.search(self.query.get())):
            self.catalog_table.insert("", "end", iid=str(index), values=(row["kind"], row["id"], row["name"], row["source"], row["status"]))

    def show_definition(self, _=None):
        selected = self.catalog_table.selection()
        if selected:
            values = self.catalog_table.item(selected[0], "values")
            definition = self.catalog.quest(values[1]) if values[0] == "quest" else dict(zip(("kind", "id", "name", "source", "status"), values))
            self.set_text(self.definition, json.dumps(definition, ensure_ascii=False, indent=2))

    def show_impact(self, _=None):
        selected = self.sources.selection()
        if selected:
            source = next(s for s in self.catalog.sources if s["id"] == selected[0])
            self.set_text(self.impact, "변경 시 재검토/무효화 대상:\n" + " → ".join(self.catalog.impact(selected)) + "\n\n" + json.dumps(source, ensure_ascii=False, indent=2))

    def close(self):
        self.pause()
        if self.arbeit_runner is not None:
            self.arbeit_runner.close()
            self.window.after(30, self.finish_close)
        else:
            self.preparation.close()
            self.window.destroy()

    def finish_close(self):
        if self.arbeit_runner.closed.is_set():
            self.window.destroy()
        else:
            self.window.after(30, self.finish_close)


def launch(catalog, *, smoke=False):
    # The selected Python runtime's external Tcl lookup failed even though its
    # scripts were readable. Use our copied, licensed runtime in source/frozen
    # builds. This changes this process only, never the user's environment.
    os.environ["TCL_LIBRARY"] = str(root() / "runtime/tcl8.6")
    os.environ["TK_LIBRARY"] = str(root() / "runtime/tk8.6")
    window = tk.Tk()
    # Each smoke is evidence for this exact source set, never an implicit resume
    # of a journal created before a source-lock update.
    import uuid
    app = Workbench(window, catalog, storage=user_data() / ("ui-smoke-" + uuid.uuid4().hex) if smoke else None, offline_smoke=smoke)
    if smoke:
        window.withdraw()
        app.arbeit_status()
        app.arbeit_gaps()
        assert "filtered_quest_id" in app.arbeit_log.get("1.0", "end")
        app.arbeit_start()
        assert app.preparation.engine.data["status"] == "blocked"
        from unittest.mock import patch
        with patch.object(app.preparation,'request_start',return_value={'execution':{'status':'paused'},'input_enabled':False}) as started:
            app.arbeit_test_start()
            started.assert_called_once_with(app.arbeit_choices[app.arbeit_choice.get()],enable_input=True)
        app.arbeit_stop()
        app.new_run()
        app.step()
        app.pause()
        app.resume()
        app.engine.run_until_boundary()
        app.refresh()
        app.query.set("싱싱한 달걀")
        app.search()
        app.sources.selection_set("quest-catalog")
        app.show_impact()
        window.update_idletasks()
        assert app.engine.status == "simulated_complete"
        assert len(app.catalog_table.get_children()) == 1
        app.close()
        print("UI_SMOKE_OK: Arbeit preflight/blocked journal/stop, catalog search, offline regression; no game input or live query")
        return 0
    window.mainloop()
    return 0
