﻿#!/usr/bin/env python3
from __future__ import annotations

import ast
import os
import queue
import subprocess
import sys
import threading
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable

import tkinter as tk
from tkinter import messagebox, ttk
from tkinter.scrolledtext import ScrolledText


ROOT_DIR = Path(__file__).resolve().parent
BASE_DIR = ROOT_DIR / "Pulled_Info"
THIS_FILE = Path(__file__).resolve()
SKIP_DIRS = {
    "__pycache__",
    ".git",
    ".idea",
    ".vscode",
    ".venv",
    "venv",
    "node_modules",
}


@dataclass
class ArgOption:
    flags: list[str]
    display_flag: str
    arg_kind: str
    choices: list[str] = field(default_factory=list)
    help_text: str = ""
    selected: object | None = None


@dataclass
class ScriptItem:
    path: Path
    rel_path: str
    has_main_guard: bool
    options: list[ArgOption]
    selected: tk.BooleanVar
    summary_var: tk.StringVar

    def command(self) -> list[str]:
        cmd = [sys.executable, str(self.path)]
        for option in self.options:
            if option.arg_kind == "bool":
                if bool(option.selected.get()):  # type: ignore[union-attr]
                    cmd.append(option.display_flag)
            elif option.arg_kind == "choice":
                value = str(option.selected.get()).strip()  # type: ignore[union-attr]
                if value:
                    cmd.extend([option.display_flag, value])
        return cmd

    def selected_options_summary(self) -> str:
        chosen: list[str] = []
        for option in self.options:
            if option.arg_kind == "bool" and bool(option.selected.get()):  # type: ignore[union-attr]
                chosen.append(option.display_flag)
            elif option.arg_kind == "choice":
                value = str(option.selected.get()).strip()  # type: ignore[union-attr]
                if value:
                    chosen.append(f"{option.display_flag}={value}")
        return ", ".join(chosen) if chosen else "Default"


class ArgVisitor(ast.NodeVisitor):
    def __init__(self) -> None:
        self.has_main_guard = False
        self.options: list[ArgOption] = []

    def visit_If(self, node: ast.If) -> None:
        if self._is_main_guard(node.test):
            self.has_main_guard = True
        self.generic_visit(node)

    def visit_Call(self, node: ast.Call) -> None:
        if isinstance(node.func, ast.Attribute) and node.func.attr == "add_argument":
            option = self._parse_add_argument(node)
            if option is not None:
                self.options.append(option)
        self.generic_visit(node)

    def _is_main_guard(self, node: ast.AST) -> bool:
        if not isinstance(node, ast.Compare):
            return False
        if not isinstance(node.left, ast.Name) or node.left.id != "__name__":
            return False
        if len(node.ops) != 1 or not isinstance(node.ops[0], ast.Eq):
            return False
        if len(node.comparators) != 1:
            return False
        comp = node.comparators[0]
        return isinstance(comp, ast.Constant) and comp.value == "__main__"

    def _parse_add_argument(self, node: ast.Call) -> ArgOption | None:
        flags: list[str] = []
        for arg in node.args:
            if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
                flags.append(arg.value)
        if not flags:
            return None

        keyword_map = {kw.arg: kw.value for kw in node.keywords if kw.arg}
        action = self._const_str(keyword_map.get("action"))
        choices = self._const_str_list(keyword_map.get("choices"))
        help_text = self._const_str(keyword_map.get("help")) or ""
        display_flag = self._pick_display_flag(flags)

        if not display_flag.startswith("-"):
            return None
        if action in {"store_true", "store_false"}:
            return ArgOption(flags=flags, display_flag=display_flag, arg_kind="bool", help_text=help_text)
        if choices:
            return ArgOption(
                flags=flags,
                display_flag=display_flag,
                arg_kind="choice",
                choices=choices,
                help_text=help_text,
            )
        return None

    def _pick_display_flag(self, flags: list[str]) -> str:
        long_flags = [flag for flag in flags if flag.startswith("--")]
        if long_flags:
            return long_flags[0]
        return flags[0]

    def _const_str(self, node: ast.AST | None) -> str | None:
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            return node.value
        return None

    def _const_str_list(self, node: ast.AST | None) -> list[str]:
        if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
            values: list[str] = []
            for item in node.elts:
                if isinstance(item, ast.Constant) and item.value is not None:
                    values.append(str(item.value))
            return values
        return []


def iter_python_scripts(base_dir: Path) -> Iterable[Path]:
    for path in base_dir.rglob("*.py"):
        if path.resolve() == THIS_FILE:
            continue
        if path.name == "__init__.py":
            continue
        if any(part in SKIP_DIRS or part.startswith(".") for part in path.parts):
            continue
        yield path


def discover_script(path: Path, root: tk.Tk) -> ScriptItem | None:
    try:
        source = path.read_text(encoding="utf-8", errors="ignore")
        tree = ast.parse(source, filename=str(path))
    except Exception:
        return None

    visitor = ArgVisitor()
    visitor.visit(tree)

    imports_tk = "tkinter" in source
    imports_argparse = "argparse" in source
    runnable = visitor.has_main_guard or imports_tk or imports_argparse
    if not runnable:
        return None

    deduped: list[ArgOption] = []
    seen = set()
    for option in visitor.options:
        key = (option.display_flag, option.arg_kind)
        if key in seen:
            continue
        seen.add(key)
        if option.arg_kind == "bool":
            option.selected = tk.BooleanVar(master=root, value=False)
        else:
            option.selected = tk.StringVar(master=root, value="")
        deduped.append(option)

    return ScriptItem(
        path=path.resolve(),
        rel_path=path.resolve().relative_to(BASE_DIR).as_posix(),
        has_main_guard=visitor.has_main_guard,
        options=deduped,
        selected=tk.BooleanVar(master=root, value=False),
        summary_var=tk.StringVar(master=root, value="Default"),
    )


class OptionDialog(tk.Toplevel):
    def __init__(self, master: tk.Misc, script: ScriptItem) -> None:
        super().__init__(master)
        self.script = script
        self.title(f"Options: {script.rel_path}")
        self.transient(master)
        self.grab_set()
        self.resizable(True, True)

        body = ttk.Frame(self, padding=12)
        body.pack(fill="both", expand=True)

        ttk.Label(body, text=script.rel_path, font=("", 10, "bold")).pack(anchor="w")
        ttk.Label(body, text="Enable any discovered mode flags or option values for this script.").pack(
            anchor="w", pady=(4, 10)
        )

        if not script.options:
            ttk.Label(body, text="No CLI mode flags were detected. This script will run with its default behavior.").pack(
                anchor="w"
            )
        else:
            for option in script.options:
                group = ttk.LabelFrame(body, text=option.display_flag, padding=8)
                group.pack(fill="x", expand=True, pady=(0, 8))
                if option.help_text:
                    ttk.Label(group, text=option.help_text, wraplength=560).pack(anchor="w", pady=(0, 6))
                if option.arg_kind == "bool":
                    ttk.Checkbutton(group, text="Enabled", variable=option.selected).pack(anchor="w")
                elif option.arg_kind == "choice":
                    values = [""] + option.choices
                    combo = ttk.Combobox(
                        group,
                        textvariable=option.selected,
                        values=values,
                        state="readonly",
                        width=28,
                    )
                    combo.pack(anchor="w")
                    if not str(option.selected.get()).strip():
                        combo.set("")

        btns = ttk.Frame(body)
        btns.pack(fill="x", pady=(10, 0))
        ttk.Button(btns, text="Reset", command=self._reset).pack(side="left")
        ttk.Button(btns, text="Close", command=self._close).pack(side="right")

        self.bind("<Escape>", lambda _e: self._close())
        self.wait_visibility()
        self.focus_set()

    def _reset(self) -> None:
        for option in self.script.options:
            if option.arg_kind == "bool":
                option.selected.set(False)  # type: ignore[union-attr]
            elif option.arg_kind == "choice":
                option.selected.set("")  # type: ignore[union-attr]
        self.script.summary_var.set(self.script.selected_options_summary())

    def _close(self) -> None:
        self.script.summary_var.set(self.script.selected_options_summary())
        self.destroy()


class ScriptRow(ttk.Frame):
    def __init__(self, master: tk.Misc, app: "AllToolsApp", script: ScriptItem) -> None:
        super().__init__(master, padding=(4, 3))
        self.app = app
        self.script = script

        ttk.Checkbutton(self, variable=script.selected, command=self.app.refresh_selection_summary).grid(
            row=0, column=0, sticky="w"
        )
        ttk.Label(self, text=script.rel_path).grid(row=0, column=1, sticky="w")
        ttk.Label(self, textvariable=script.summary_var, width=28).grid(row=0, column=2, sticky="w", padx=(10, 8))
        ttk.Button(self, text="Modes", width=8, command=self.open_modes).grid(row=0, column=3, padx=(0, 4))
        ttk.Button(self, text="Up", width=5, command=lambda: self.app.move_script(script, -1)).grid(
            row=0, column=4, padx=(0, 2)
        )
        ttk.Button(self, text="Down", width=5, command=lambda: self.app.move_script(script, 1)).grid(row=0, column=5)

        self.columnconfigure(1, weight=1)

    def open_modes(self) -> None:
        dialog = OptionDialog(self, self.script)
        self.wait_window(dialog)
        self.script.summary_var.set(self.script.selected_options_summary())
        self.app.refresh_selection_summary()


class AllToolsApp:
    def __init__(self, root: tk.Tk) -> None:
        self.root = root
        self.root.title("All_Tools")
        self.root.geometry("1200x820")

        self.scripts: list[ScriptItem] = []
        self.rows: list[ScriptRow] = []
        self.log_queue: queue.Queue[tuple[str, str]] = queue.Queue()
        self.runner_thread: threading.Thread | None = None
        self.stop_requested = threading.Event()
        self.current_proc: subprocess.Popen[str] | None = None

        self.status_var = tk.StringVar(value="Scanning scripts...")
        self.selection_var = tk.StringVar(value="No scripts selected.")

        self._build_ui()
        self.refresh_scripts()
        self._pump_log_queue()

    def _build_ui(self) -> None:
        top = ttk.Frame(self.root, padding=10)
        top.pack(fill="x")

        ttk.Label(top, text="All_Tools", font=("", 14, "bold")).pack(anchor="w")
        ttk.Label(
            top,
            text="Select scripts from this folder tree, configure detected mode flags, then run them one-by-one.",
        ).pack(anchor="w", pady=(2, 8))

        btns = ttk.Frame(top)
        btns.pack(fill="x")
        ttk.Button(btns, text="Refresh Scripts", command=self.refresh_scripts).pack(side="left")
        ttk.Button(btns, text="Select All", command=lambda: self.set_all_selected(True)).pack(side="left", padx=(8, 0))
        ttk.Button(btns, text="Select None", command=lambda: self.set_all_selected(False)).pack(side="left", padx=(8, 0))
        self.run_btn = ttk.Button(btns, text="Run Selected", command=self.run_selected)
        self.run_btn.pack(side="right")
        self.stop_btn = ttk.Button(btns, text="Stop", command=self.stop_run, state="disabled")
        self.stop_btn.pack(side="right", padx=(0, 8))

        ttk.Label(top, textvariable=self.status_var).pack(anchor="w", pady=(8, 0))
        ttk.Label(top, textvariable=self.selection_var).pack(anchor="w", pady=(2, 0))

        main = ttk.Panedwindow(self.root, orient="vertical")
        main.pack(fill="both", expand=True, padx=10, pady=(0, 10))

        scripts_frame = ttk.Frame(main, padding=6)
        logs_frame = ttk.Frame(main, padding=6)
        main.add(scripts_frame, weight=3)
        main.add(logs_frame, weight=2)

        ttk.Label(scripts_frame, text="Scripts").pack(anchor="w")
        canvas_holder = ttk.Frame(scripts_frame)
        canvas_holder.pack(fill="both", expand=True, pady=(6, 0))

        self.canvas = tk.Canvas(canvas_holder, highlightthickness=0)
        self.scrollbar = ttk.Scrollbar(canvas_holder, orient="vertical", command=self.canvas.yview)
        self.rows_frame = ttk.Frame(self.canvas)
        self.rows_frame.bind(
            "<Configure>",
            lambda _e: self.canvas.configure(scrollregion=self.canvas.bbox("all")),
        )
        self.canvas.create_window((0, 0), window=self.rows_frame, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        self.canvas.pack(side="left", fill="both", expand=True)
        self.scrollbar.pack(side="right", fill="y")

        ttk.Label(logs_frame, text="Run Log").pack(anchor="w")
        self.log_text = ScrolledText(logs_frame, wrap="word", height=16)
        self.log_text.pack(fill="both", expand=True, pady=(6, 0))
        self.log_text.configure(state="disabled")

    def refresh_scripts(self) -> None:
        if self.runner_thread and self.runner_thread.is_alive():
            messagebox.showinfo("Busy", "Wait for the current run to finish before refreshing the list.")
            return

        previous: dict[str, tuple[bool, dict[str, object]]] = {}
        for script in self.scripts:
            options_state: dict[str, object] = {}
            for option in script.options:
                options_state[option.display_flag] = option.selected.get()  # type: ignore[union-attr]
            previous[script.rel_path] = (bool(script.selected.get()), options_state)

        discovered: list[ScriptItem] = []
        for path in sorted(iter_python_scripts(BASE_DIR), key=lambda p: p.relative_to(BASE_DIR).as_posix().lower()):
            script = discover_script(path, self.root)
            if script is None:
                continue
            if script.rel_path in previous:
                was_selected, option_state = previous[script.rel_path]
                script.selected.set(was_selected)
                for option in script.options:
                    if option.display_flag in option_state:
                        option.selected.set(option_state[option.display_flag])  # type: ignore[union-attr]
            script.summary_var.set(script.selected_options_summary())
            discovered.append(script)

        self.scripts = discovered
        self._render_rows()
        self.refresh_selection_summary()
        self.status_var.set(f"Found {len(self.scripts)} runnable scripts under {BASE_DIR}")

    def _render_rows(self) -> None:
        for row in self.rows:
            row.destroy()
        self.rows.clear()

        for script in self.scripts:
            row = ScriptRow(self.rows_frame, self, script)
            row.pack(fill="x", expand=True, anchor="n")
            self.rows.append(row)

    def move_script(self, script: ScriptItem, delta: int) -> None:
        idx = self.scripts.index(script)
        new_idx = idx + delta
        if new_idx < 0 or new_idx >= len(self.scripts):
            return
        self.scripts[idx], self.scripts[new_idx] = self.scripts[new_idx], self.scripts[idx]
        self._render_rows()
        self.refresh_selection_summary()

    def set_all_selected(self, value: bool) -> None:
        for script in self.scripts:
            script.selected.set(value)
        self.refresh_selection_summary()

    def refresh_selection_summary(self) -> None:
        selected = [script for script in self.scripts if bool(script.selected.get())]
        if not selected:
            self.selection_var.set("No scripts selected.")
            return
        self.selection_var.set(f"Selected: {len(selected)} scripts. Run order follows the list shown above.")

    def run_selected(self) -> None:
        if self.runner_thread and self.runner_thread.is_alive():
            return

        selected = [script for script in self.scripts if bool(script.selected.get())]
        if not selected:
            messagebox.showerror("No Selection", "Select at least one script to run.")
            return

        self.stop_requested.clear()
        self._set_running(True)
        self._append_log("Starting queued run.")
        self.runner_thread = threading.Thread(target=self._run_queue, args=(selected,), daemon=True)
        self.runner_thread.start()

    def stop_run(self) -> None:
        self.stop_requested.set()
        proc = self.current_proc
        if proc and proc.poll() is None:
            try:
                if os.name == "nt":
                    subprocess.run(
                        ["taskkill", "/PID", str(proc.pid), "/T", "/F"],
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL,
                        check=False,
                    )
                else:
                    proc.terminate()
            except Exception as exc:
                self._append_log(f"Stop request error: {exc}")
        self._append_log("Stop requested.")

    def _run_queue(self, selected: list[ScriptItem]) -> None:
        failures = 0
        completed = 0

        for index, script in enumerate(selected, start=1):
            if self.stop_requested.is_set():
                break

            cmd = script.command()
            self.log_queue.put(("line", ""))
            self.log_queue.put(("line", f"[{index}/{len(selected)}] Running {script.rel_path}"))
            self.log_queue.put(("line", "Command: " + " ".join(f'"{part}"' if " " in part else part for part in cmd)))

            try:
                self.current_proc = subprocess.Popen(
                    cmd,
                    cwd=str(script.path.parent),
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    text=True,
                    encoding="utf-8",
                    errors="replace",
                    bufsize=1,
                )
                assert self.current_proc.stdout is not None
                for line in self.current_proc.stdout:
                    self.log_queue.put(("line", line.rstrip()))
                rc = self.current_proc.wait()
            except Exception as exc:
                failures += 1
                self.log_queue.put(("line", f"Failed to start {script.rel_path}: {exc}"))
                continue
            finally:
                self.current_proc = None

            completed += 1
            if rc != 0:
                failures += 1
                self.log_queue.put(("line", f"Finished with exit code {rc}: {script.rel_path}"))
            else:
                self.log_queue.put(("line", f"Finished successfully: {script.rel_path}"))

        if self.stop_requested.is_set():
            self.log_queue.put(("done", f"Run stopped. Completed {completed} script(s), failures: {failures}."))
        else:
            self.log_queue.put(("done", f"Run complete. Completed {completed} script(s), failures: {failures}."))

    def _append_log(self, message: str) -> None:
        self.log_text.configure(state="normal")
        self.log_text.insert("end", message + "\n")
        self.log_text.see("end")
        self.log_text.configure(state="disabled")

    def _set_running(self, running: bool) -> None:
        self.run_btn.configure(state="disabled" if running else "normal")
        self.stop_btn.configure(state="normal" if running else "disabled")

    def _pump_log_queue(self) -> None:
        try:
            while True:
                kind, payload = self.log_queue.get_nowait()
                if kind == "line":
                    self._append_log(payload)
                elif kind == "done":
                    self._append_log(payload)
                    self._set_running(False)
        except queue.Empty:
            pass
        self.root.after(150, self._pump_log_queue)


def main() -> None:
    root = tk.Tk()
    app = AllToolsApp(root)
    root.minsize(900, 650)
    root.mainloop()


if __name__ == "__main__":
    main()

