# Boot_Feature_Row_Adder_GUI.py
# ---------------------------------------------------------------------------
# Purpose:
#   Simple Tkinter GUI that reads the column headings from a markdown table
#   (ex: RW_Site_Scrape.md), prompts the user for text or 1/0 values, then
#   appends a new row to the bottom of that markdown file.
#
# Notes:
#   - "Text" fields are auto-detected (anything that isn't consistently 0/1 in
#     sample rows). Most feature columns will become 1/0 checkboxes.
#   - On Windows, if you keep the default file name "RW_Site_Scrape.md" inside
#     the Boot_Features folder, the script will auto-find it.
#   - Cross-platform: Windows/macOS/Linux supported.
#
# Run:
#   python Boot_Feature_Row_Adder_GUI.py
# ---------------------------------------------------------------------------

from __future__ import annotations

import os
import re
import subprocess
import sys
from dataclasses import dataclass
from typing import Dict, List, Tuple

import tkinter as tk
from tkinter import filedialog, messagebox, ttk


DEFAULT_FOLDER_WINDOWS = r"C:\Users\jasti\Documents\My_Sync\Shared\60_RedWing_Notes\PowerBi_Automation\Boot_Features"
DEFAULT_FILENAME = "RW_Site_Scrape.md"


@dataclass(frozen=True)
class ColumnSpec:
    name: str
    kind: str  # "bool" or "text"


def _is_bool_token(s: str) -> bool:
    s = s.strip()
    return s in {"0", "1", ""}


def _safe_cell_text(s: str) -> str:
    """
    Make user-entered text safe for a markdown table cell.
    - Replace literal pipe '|' to avoid breaking table formatting.
    """
    return s.replace("|", " / ").strip()


def _split_md_row(line: str) -> List[str]:
    # Markdown row like: | a | b | c |
    parts = [p.strip() for p in line.strip().strip("\n").split("|")]
    # Because the row typically starts/ends with '|', first/last are empty
    if parts and parts[0] == "":
        parts = parts[1:]
    if parts and parts[-1] == "":
        parts = parts[:-1]
    return parts


def find_first_markdown_table(path: str) -> Tuple[List[str], int, int]:
    """
    Returns (headers, header_line_index, separator_line_index)
    Raises ValueError if no markdown table header is found.
    """
    with open(path, "r", encoding="utf-8", errors="replace") as f:
        lines = f.readlines()

    # Find first line that looks like a header row and the next separator row
    for i, line in enumerate(lines):
        if not line.lstrip().startswith("|"):
            continue
        # Must have at least 2 columns
        cells = _split_md_row(line)
        if len(cells) < 2:
            continue
        if i + 1 >= len(lines):
            continue
        sep = lines[i + 1]
        if not sep.lstrip().startswith("|"):
            continue
        sep_cells = _split_md_row(sep)
        # Separator row is all dashes-ish
        if len(sep_cells) != len(cells):
            continue
        if all(re.fullmatch(r"[-:\s]+", c or "-") for c in sep_cells):
            headers = cells
            return headers, i, i + 1

    raise ValueError("Could not find a markdown table header row (| ... |) with a separator line (| --- |).")


def infer_column_specs(path: str, sample_rows: int = 50) -> List[ColumnSpec]:
    """
    Infer which columns are bool (0/1) vs text by sampling rows under the first table.
    """
    headers, _hdr_i, sep_i = find_first_markdown_table(path)

    with open(path, "r", encoding="utf-8", errors="replace") as f:
        lines = f.readlines()

    samples: List[List[str]] = []
    for line in lines[sep_i + 1 :]:
        if not line.lstrip().startswith("|"):
            # Stop when table ends
            break
        row = _split_md_row(line)
        if len(row) != len(headers):
            # Some files may have wrap/odd lines; skip them.
            continue
        samples.append(row)
        if len(samples) >= sample_rows:
            break

    specs: List[ColumnSpec] = []
    for col_idx, h in enumerate(headers):
        col_vals = [r[col_idx].strip() for r in samples]
        # If every observed token is 0/1/blank => bool
        if col_vals and all(_is_bool_token(v) for v in col_vals):
            specs.append(ColumnSpec(name=h, kind="bool"))
        else:
            specs.append(ColumnSpec(name=h, kind="text"))
    return specs


def append_markdown_row(path: str, values: List[str]) -> None:
    row_line = "| " + " | ".join(values) + " |\n"

    # Ensure file ends with a newline before appending
    need_newline = False
    try:
        if os.path.exists(path) and os.path.getsize(path) > 0:
            with open(path, "rb") as fb:
                fb.seek(-1, os.SEEK_END)
                last = fb.read(1)
                need_newline = last not in {b"\n", b"\r"}
    except OSError:
        need_newline = False

    with open(path, "a", encoding="utf-8", newline="") as f:
        if need_newline:
            f.write("\n")
        f.write(row_line)


def open_in_default_app(path: str) -> None:
    try:
        if sys.platform.startswith("win"):
            os.startfile(path)  # type: ignore[attr-defined]
        elif sys.platform == "darwin":
            subprocess.run(["open", path], check=False)
        else:
            subprocess.run(["xdg-open", path], check=False)
    except Exception:
        # Non-fatal
        pass


class App(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.configure(bg="#000000")
        self._apply_dark_theme()
        self.title("Boot Features — Append Row")
        self.geometry("900x700")

        self.file_path_var = tk.StringVar(value="")
        self.open_after_var = tk.IntVar(value=1)

        self.specs: List[ColumnSpec] = []
        self.widgets: Dict[str, object] = {}  # header -> (StringVar or IntVar)

        self._build_ui()
        self._auto_pick_default_file()

    def _apply_dark_theme(self) -> None:
        """Apply a simple dark theme (black background) for ttk + canvas."""
        style = ttk.Style(self)
        try:
            style.theme_use("clam")  # best cross-platform theme for color control
        except Exception:
            pass

        bg = "#000000"
        fg = "#FFFFFF"
        widget_bg = "#111111"
        widget_bg_active = "#222222"
        sep = "#333333"

        style.configure("TFrame", background=bg)
        style.configure("TLabel", background=bg, foreground=fg)
        style.configure("TCheckbutton", background=bg, foreground=fg)
        style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)])

        style.configure("TButton", background=widget_bg, foreground=fg)
        style.map("TButton", background=[("active", widget_bg_active), ("pressed", widget_bg_active)])

        # Entry field colors
        style.configure(
            "TEntry",
            fieldbackground=widget_bg,
            foreground=fg,
            background=widget_bg,
            insertcolor=fg,
        )

        style.configure("TSeparator", background=sep)

        # Scrollbar (may vary by platform/theme)
        style.configure(
            "Vertical.TScrollbar",
            background=widget_bg,
            troughcolor=bg,
            arrowcolor=fg,
        )

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

        ttk.Label(top, text="Target markdown file:").pack(side="left")
        entry = ttk.Entry(top, textvariable=self.file_path_var)
        entry.pack(side="left", fill="x", expand=True, padx=(8, 8))

        ttk.Button(top, text="Choose…", command=self._choose_file).pack(side="left")

        ttk.Checkbutton(
            top,
            text="Open file after append",
            variable=self.open_after_var,
        ).pack(side="left", padx=(10, 0))

        ttk.Separator(self).pack(fill="x", pady=(0, 5))

        # Scroll area
        container = ttk.Frame(self)
        container.pack(fill="both", expand=True)

        self.canvas = tk.Canvas(container, highlightthickness=0, bg="#000000")
        self.canvas.pack(side="left", fill="both", expand=True)

        scrollbar = ttk.Scrollbar(container, orient="vertical", command=self.canvas.yview)
        scrollbar.pack(side="right", fill="y")
        self.canvas.configure(yscrollcommand=scrollbar.set)

        self.form_frame = ttk.Frame(self.canvas, padding=10)
        self.form_window = self.canvas.create_window((0, 0), window=self.form_frame, anchor="nw")

        self.form_frame.bind("<Configure>", self._on_frame_configure)
        self.canvas.bind("<Configure>", self._on_canvas_configure)

        # Bottom buttons
        bottom = ttk.Frame(self, padding=10)
        bottom.pack(fill="x")

        ttk.Button(bottom, text="Clear", command=self._clear_form).pack(side="left")
        ttk.Button(bottom, text="Append Row", command=self._append).pack(side="right")

        # Mouse wheel scroll (Windows/macOS/Linux)
        self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)       # Windows/macOS
        self.canvas.bind_all("<Button-4>", self._on_mousewheel_linux)   # Linux up
        self.canvas.bind_all("<Button-5>", self._on_mousewheel_linux)   # Linux down

    def _on_frame_configure(self, _event: tk.Event) -> None:
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def _on_canvas_configure(self, event: tk.Event) -> None:
        # Keep the inner frame width matched to the canvas width
        self.canvas.itemconfigure(self.form_window, width=event.width)

    def _on_mousewheel(self, event: tk.Event) -> None:
        delta = getattr(event, "delta", 0)
        if delta:
            self.canvas.yview_scroll(int(-delta / 120), "units")

    def _on_mousewheel_linux(self, event: tk.Event) -> None:
        num = getattr(event, "num", 0)
        if num == 4:
            self.canvas.yview_scroll(-3, "units")
        elif num == 5:
            self.canvas.yview_scroll(3, "units")

    def _auto_pick_default_file(self) -> None:
        # Windows default folder (user asked for this path)
        candidate = os.path.join(DEFAULT_FOLDER_WINDOWS, DEFAULT_FILENAME)
        if os.path.exists(candidate):
            self.file_path_var.set(candidate)
            self._load_file(candidate)
            return

        # If script is placed beside the markdown file
        here = os.path.dirname(os.path.abspath(__file__))
        candidate2 = os.path.join(here, DEFAULT_FILENAME)
        if os.path.exists(candidate2):
            self.file_path_var.set(candidate2)
            self._load_file(candidate2)

    def _choose_file(self) -> None:
        initialdir = DEFAULT_FOLDER_WINDOWS if os.path.isdir(DEFAULT_FOLDER_WINDOWS) else os.getcwd()
        path = filedialog.askopenfilename(
            title="Choose markdown file",
            initialdir=initialdir,
            filetypes=[("Markdown", "*.md"), ("All files", "*.*")],
        )
        if not path:
            return
        self.file_path_var.set(path)
        self._load_file(path)

    def _load_file(self, path: str) -> None:
        try:
            self.specs = infer_column_specs(path)
        except Exception as e:
            messagebox.showerror("Could not read headings", f"{e}")
            self.specs = []
            return

        # Build form
        for child in list(self.form_frame.winfo_children()):
            child.destroy()
        self.widgets.clear()

        ttk.Label(
            self.form_frame,
            text="Enter values for each column, then click “Append Row”.",
            font=("TkDefaultFont", 10, "bold"),
        ).pack(anchor="w", pady=(0, 10))

        grid = ttk.Frame(self.form_frame)
        grid.pack(fill="both", expand=True)

        for r, spec in enumerate(self.specs):
            ttk.Label(grid, text=spec.name).grid(row=r, column=0, sticky="w", padx=(0, 10), pady=4)

            if spec.kind == "bool":
                var = tk.IntVar(value=0)
                cb = ttk.Checkbutton(grid, text="1 (checked) / 0 (unchecked)", variable=var)
                cb.grid(row=r, column=1, sticky="w", pady=4)
                self.widgets[spec.name] = var
            else:
                var = tk.StringVar(value="")
                ent = ttk.Entry(grid, textvariable=var)
                ent.grid(row=r, column=1, sticky="ew", pady=4)
                self.widgets[spec.name] = var

        grid.columnconfigure(1, weight=1)

    def _clear_form(self) -> None:
        for spec in self.specs:
            v = self.widgets.get(spec.name)
            if isinstance(v, tk.StringVar):
                v.set("")
            elif isinstance(v, tk.IntVar):
                v.set(0)

    def _append(self) -> None:
        path = self.file_path_var.get().strip()
        if not path:
            messagebox.showinfo("No file selected", "Choose the markdown file first.")
            return
        if not os.path.exists(path):
            messagebox.showerror("File not found", f"File does not exist:\n{path}")
            return
        if not self.specs:
            messagebox.showinfo("No headings loaded", "Could not load headings from the selected file.")
            return

        values: List[str] = []
        for spec in self.specs:
            v = self.widgets.get(spec.name)
            if spec.kind == "bool":
                if not isinstance(v, tk.IntVar):
                    values.append("0")
                else:
                    values.append("1" if int(v.get()) else "0")
            else:
                if not isinstance(v, tk.StringVar):
                    values.append("")
                else:
                    values.append(_safe_cell_text(v.get()))

        if len(values) != len(self.specs):
            messagebox.showerror("Internal error", "Column mismatch while building the row.")
            return

        try:
            append_markdown_row(path, values)
        except PermissionError:
            messagebox.showerror("Permission denied", "Close the markdown file if it's open in another program and try again.")
            return
        except Exception as e:
            messagebox.showerror("Failed to append", f"{e}")
            return

        messagebox.showinfo("Done", "Row appended successfully.")
        if int(self.open_after_var.get()):
            open_in_default_app(path)


def main() -> int:
    App().mainloop()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
