# coding: utf-8
# python 2 only

# Copyright (c) 2026 TormachTips.com. All rights reserved.
# Licensed under the TormachTips Personal Use License.
# Permission is granted only for private personal use and private personal modification.
# No sharing, publication, distribution, resale, sublicensing, screenshots, code excerpts,
# benchmarks, or videos are permitted without prior written permission.
# Requests:         tormach.1100m@gmail.com
# Information page: https://tormachtips.com/plugins.htm

#############################################
##                                         ##
##       Tool Life DB Viewer v1.01         ##
##          www.tormachtips.com            ##
##                                         ##
#############################################

# 1.01 - public beta - 4/2/26

import Tkinter as tk
import tkMessageBox
import sqlite3
import os
import datetime
import tkSimpleDialog

DB_FILE         = "/home/operator/gcode/python/hobbs/hobbs_tool_life.db"
APPROACH_OFFSET = 10
CURRENT_VER     = "1.01"
SCRIPT_NAME     = "Hobbs Tool Life Manager"
DESCRIPTION     = "The main Tool Life Manager utility."

class ToolLifeRepository(object):
    def __init__(self, db_file):
        self.db_file = db_file
        self.conn = sqlite3.connect(db_file)
        self.ensure_schema()   

    def has_history(self, tool):
        c = self.conn.cursor()
        c.execute("SELECT 1 FROM tool_history WHERE tool = ? LIMIT 1", (tool,))
        return c.fetchone() is not None

    def has_reminders(self, tool):
        c = self.conn.cursor()
        c.execute("SELECT 1 FROM tool_reminders WHERE tool = ? LIMIT 1", (tool,))
        return c.fetchone() is not None

    def delete_tool_and_history(self, tool):
        c = self.conn.cursor()
        c.execute("DELETE FROM tool_history WHERE tool = ?", (tool,))
        c.execute("DELETE FROM tool_usage WHERE tool = ?", (tool,))
        self.conn.commit()

    def add_tool(self, tool, installed_date, total_seconds=0):
        c = self.conn.cursor()
        c.execute("INSERT INTO tool_usage (tool, installed_date, total_seconds) VALUES (?, ?, ?)",(tool, installed_date, total_seconds))
        self.conn.commit()

    def list_tool_reminders(self, tool):
        c = self.conn.cursor()
        c.execute(
            """SELECT id, tool, remind_after_minutes, message, is_active
               FROM tool_reminders
               WHERE tool = ?
               ORDER BY remind_after_minutes ASC, id ASC""",
            (tool,)        )
        return c.fetchall()

    def list_all_tool_reminders(self):
        c = self.conn.cursor()
        c.execute(
            """SELECT r.id,
                      r.tool,
                      r.remind_after_minutes,
                      r.message,
                      r.is_active,
                      COALESCE(u.total_seconds, 0)
               FROM tool_reminders r
               LEFT JOIN tool_usage u ON u.tool = r.tool
               ORDER BY r.tool ASC, r.remind_after_minutes ASC, r.id ASC"""        )
        return c.fetchall()

    def delete_tool_reminder(self, reminder_id):
        c = self.conn.cursor()
        c.execute("DELETE FROM tool_reminders WHERE id = ?", (reminder_id,))
        self.conn.commit()

    def delete_all_tool_reminders(self, tool):
        c = self.conn.cursor()
        c.execute("DELETE FROM tool_reminders WHERE tool = ?", (tool,))
        self.conn.commit()        

    def ensure_schema(self):
        c = self.conn.cursor()
        c.execute('''
            CREATE TABLE IF NOT EXISTS tool_usage (
                tool INTEGER PRIMARY KEY,
                installed_date TEXT,
                total_seconds INTEGER DEFAULT 0
            )        ''')
        c.execute('''
            CREATE TABLE IF NOT EXISTS tool_history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                tool INTEGER NOT NULL,
                installed_date TEXT,
                reset_date TEXT NOT NULL,
                elapsed_seconds INTEGER DEFAULT 0,
                total_seconds INTEGER DEFAULT 0
            )        ''')           
        c.execute('''
            CREATE TABLE IF NOT EXISTS tool_reminders (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                tool INTEGER NOT NULL,
                remind_after_minutes INTEGER NOT NULL,
                message TEXT,
                is_active INTEGER DEFAULT 1,
                last_notified_at TEXT
            )''')            
        self.conn.commit()

    def add_tool_reminder(self, tool, remind_after_minutes, message):
        c = self.conn.cursor()
        c.execute(
            """INSERT INTO tool_reminders
               (tool, remind_after_minutes, message, is_active, last_notified_at)
               VALUES (?, ?, ?, 1, NULL)""",
            (tool, remind_after_minutes, message)        )
        self.conn.commit()

    def close(self):
        try:
            self.conn.close()
        except:
            pass

    def list_tools(self):
        c = self.conn.cursor()
        c.execute("SELECT tool, installed_date, total_seconds FROM tool_usage")
        return c.fetchall()

    def get_tool(self, tool):
        c = self.conn.cursor()
        c.execute("SELECT tool, installed_date, total_seconds FROM tool_usage WHERE tool = ?",(tool,))
        return c.fetchone()

    def update_tool_installed_date(self, tool, installed_date):
        c = self.conn.cursor()
        c.execute("UPDATE tool_usage SET installed_date = ? WHERE tool = ?",(installed_date, tool))
        self.conn.commit()

    def update_tool_total_seconds(self, tool, total_seconds):
        c = self.conn.cursor()
        c.execute("UPDATE tool_usage SET total_seconds = ? WHERE tool = ?",(total_seconds, tool))
        self.conn.commit()

    def delete_tool(self, tool):
        c = self.conn.cursor()
        c.execute("DELETE FROM tool_usage WHERE tool = ?", (tool,))
        self.conn.commit()

    def archive_and_reset_tool(self, tool, reset_date, elapsed_seconds):
        c = self.conn.cursor()
        c.execute("SELECT installed_date, total_seconds FROM tool_usage WHERE tool = ?",(tool,))
        row = c.fetchone()
        if not row:
            return False
        installed_date, total_seconds = row
        c.execute(
            """INSERT INTO tool_history
               (tool, installed_date, reset_date, elapsed_seconds, total_seconds)
               VALUES (?, ?, ?, ?, ?)""",
            (tool, installed_date, reset_date, elapsed_seconds, total_seconds)        )
        c.execute(
            """UPDATE tool_usage
               SET installed_date = ?, total_seconds = 0
               WHERE tool = ?""",
            (reset_date, tool)        )
        self.conn.commit()
        return True

    def list_history(self, tool):
        c = self.conn.cursor()
        c.execute(
            """SELECT id, tool, installed_date, reset_date, elapsed_seconds, total_seconds
               FROM tool_history
               WHERE tool = ?
               ORDER BY id DESC""",
            (tool,)        )
        return c.fetchall()

    def get_history_row(self, history_id):
        c = self.conn.cursor()
        c.execute(
            """SELECT id, tool, installed_date, reset_date, elapsed_seconds, total_seconds
               FROM tool_history
               WHERE id = ?""",
            (history_id,)        )
        return c.fetchone()

    def update_history_dates_and_elapsed(self, history_id, installed_date, reset_date, elapsed_seconds):
        c = self.conn.cursor()
        c.execute(
            """UPDATE tool_history
               SET installed_date = ?, reset_date = ?, elapsed_seconds = ?
               WHERE id = ?""",
            (installed_date, reset_date, elapsed_seconds, history_id)        )
        self.conn.commit()

    def update_history_total_seconds(self, history_id, total_seconds):
        c = self.conn.cursor()
        c.execute(            "UPDATE tool_history SET total_seconds = ? WHERE id = ?",            (total_seconds, history_id)        )
        self.conn.commit()

    def update_history_life_and_reset(self, history_id, elapsed_seconds, reset_date):
        c = self.conn.cursor()
        c.execute(
            """UPDATE tool_history
               SET elapsed_seconds = ?, reset_date = ?
               WHERE id = ?""",
            (elapsed_seconds, reset_date, history_id)        )
        self.conn.commit()

    def delete_history_row(self, history_id):
        c = self.conn.cursor()
        c.execute("DELETE FROM tool_history WHERE id = ?", (history_id,))
        self.conn.commit()

    def delete_all_history_for_tool(self, tool):
        c = self.conn.cursor()
        c.execute("DELETE FROM tool_history WHERE tool = ?", (tool,))
        self.conn.commit()

class ToolLifeService(object):    
    DATETIME_FORMATS = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"]

    def get_reminder_status_text(self, total_seconds, remind_after_minutes):
        try:
            total_seconds = int(total_seconds or 0)
            remind_after_minutes = int(remind_after_minutes or 0)
        except:
            return ""
        if remind_after_minutes <= 0:
            return ""
        threshold_seconds = remind_after_minutes * 60
        approach_seconds = max(0, threshold_seconds - (APPROACH_OFFSET * 60))
        if total_seconds >= threshold_seconds:
            return "exceeded"
        if total_seconds >= approach_seconds:
            return "approaching"
        return ""    

    def parse_endurance_to_seconds(self, text_value):
        if text_value is None:
            raise ValueError(
                "Enter custom endurance as hours or minutes, for example:\n\n"
                "2.5\n"
                "2.5h\n"
                "150m\n"
                "2h 30m\n")
        text = text_value.strip().lower()
        if not text:
            raise ValueError(
                "Enter custom endurance as hours or minutes, for example:\n\n"
                "2.5\n"
                "2.5h\n"
                "150m\n"
                "2h 30m\n"
            )
        compact = text.replace(" ", "")
        if compact.endswith("m") and "h" not in compact:
            return self.minutes_to_seconds(compact[:-1])
        if compact.endswith("h") and "m" not in compact:
            return self.hours_to_seconds(compact[:-1])
        if "h" in compact or "m" in compact:
            hours = 0.0
            minutes = 0.0
            h_pos = compact.find("h")
            if h_pos >= 0:
                hours_text = compact[:h_pos]
                if not hours_text:
                    raise ValueError("Invalid endurance format.")
                hours = self.parse_nonnegative_float(hours_text, "Hours")
                compact = compact[h_pos + 1:]
            m_pos = compact.find("m")
            if m_pos >= 0:
                minutes_text = compact[:m_pos]
                if not minutes_text:
                    raise ValueError("Invalid endurance format.")
                minutes = self.parse_nonnegative_float(minutes_text, "Minutes")
                compact = compact[m_pos + 1:]
            if compact:
                raise ValueError("Invalid endurance format.")
            return int(round((hours * 3600) + (minutes * 60)))
        return self.hours_to_seconds(text)

    def parse_datetime(self, text_value):
        if not text_value:
            return None
        text_value = text_value.strip()
        for fmt in self.DATETIME_FORMATS:
            try:
                return datetime.datetime.strptime(text_value, fmt)
            except:
                pass
        return None

    def normalize_datetime_text(self, text_value):
        dt = self.parse_datetime(text_value)
        if not dt:
            raise ValueError("Enter date/time as:\n\nYYYY-MM-DD HH:MM:SS\nor\nYYYY-MM-DD HH:MM")
        return dt.strftime("%Y-%m-%d %H:%M:%S")

    def parse_nonnegative_float(self, text_value, label_name):
        try:
            value = float(text_value.strip())
        except:
            raise ValueError("Enter %s as a number, for example:\n\n1\n1.5\n0.25" % label_name.lower())
        if value < 0:
            raise ValueError("%s cannot be negative." % label_name)
        return value

    def hours_to_seconds(self, hours_text):
        hours = self.parse_nonnegative_float(hours_text, "Hours")
        return int(round(hours * 3600))

    def minutes_to_seconds(self, minutes_text):
        minutes = self.parse_nonnegative_float(minutes_text, "Minutes")
        return int(round(minutes * 60))

    def life_hours_to_seconds(self, life_text):
        hours = self.parse_nonnegative_float(life_text, "Life")
        return int(round(hours * 3600))

    def seconds_to_hours_text(self, total_seconds):
        if total_seconds is None:
            return ""
        return "%.2f" % (total_seconds / 3600.0)

    def seconds_to_minutes_text(self, total_seconds):
        if total_seconds is None:
            return ""
        return "%.1f" % (total_seconds / 60.0)

    def format_endurance_compact(self, total_seconds):
        if total_seconds is None or total_seconds < 0:
            return ""
        total_minutes = int(round(total_seconds / 60.0))
        hours = total_minutes / 60
        minutes = total_minutes % 60
        if minutes == 0:
            hours_text = "%dh" % hours
        elif hours == 0:
            hours_text = "%.1fh" % (total_seconds / 3600.0)
        else:
            hours_text = "%dh %dm" % (hours, minutes)
        return "%s (%dm)" % (hours_text, total_minutes)        

    def get_elapsed_since_install(self, installed_date_text):
        installed_dt = self.parse_datetime(installed_date_text)
        if not installed_dt:
            return None
        now = datetime.datetime.now()
        total_seconds = int((now - installed_dt).total_seconds())
        if total_seconds < 0:
            return -1
        return total_seconds

    def get_seconds_between_dates(self, start_text, end_text):
        start_dt = self.parse_datetime(start_text)
        end_dt = self.parse_datetime(end_text)
        if not start_dt or not end_dt:
            return None
        total_seconds = int((end_dt - start_dt).total_seconds())
        if total_seconds < 0:
            return -1
        return total_seconds

    def add_seconds_to_datetime_text(self, base_text, total_seconds):
        base_dt = self.parse_datetime(base_text)
        if not base_dt:
            raise ValueError("Install date must be valid before editing life.")
        result = base_dt + datetime.timedelta(seconds=total_seconds)
        return result.strftime("%Y-%m-%d %H:%M:%S")

    def format_elapsed_from_install(self, installed_date_text):
        total_seconds = self.get_elapsed_since_install(installed_date_text)
        if total_seconds is None:
            return "Invalid date"
        if total_seconds < 0:
            return "Future date"
        return self.format_duration_from_seconds(total_seconds)

    def format_duration_from_seconds(self, total_seconds):
        if total_seconds is None or total_seconds < 0:
            return ""
        month_seconds = 30 * 86400
        day_seconds = 86400
        hour_seconds = 3600
        minute_seconds = 60
        months = total_seconds / month_seconds
        rem = total_seconds % month_seconds
        days = rem / day_seconds
        rem = rem % day_seconds
        hours = rem / hour_seconds
        rem = rem % hour_seconds
        minutes = rem / minute_seconds
        parts = []
        if months > 0:
            parts.append("%dmo" % months)
        if months > 0 or days > 0:
            parts.append("%dd" % days)
        if months > 0 or days > 0 or hours > 0:
            parts.append("%dh" % hours)
        parts.append("%dm" % minutes)
        return " ".join(parts)

class UiMixin(object):
    BG_APP    = "#e9e9e9"
    BG_HEADER = "#d8d8d8"
    BG_ROW_1  = "#f7f7f7"
    BG_ROW_2  = "#ededed"
    BG_TABLE  = "#cfcfcf"

    def show_error(self, title, message):
        tkMessageBox.showerror(title, message)

    def ask_yes_no(self, title, message):
        return tkMessageBox.askyesno(title, message)

    def alternating_bg(self, row_index):
        return self.BG_ROW_1 if (row_index % 2 == 1) else self.BG_ROW_2

    def make_label_cell(self, parent, row, col, text, width, bg, anchor="center", bold=False):
        font = ("Courier", 10, "bold") if bold else ("Courier", 10)
        lbl = tk.Label(parent,text=text,width=width,font=font,bg=bg,anchor=anchor,bd=0,padx=6,pady=7)
        lbl.grid(row=row, column=col, sticky="nsew", padx=0, pady=0)
        return lbl

    def make_entry_cell(self, parent, row, col, textvariable, width, bg):
        holder = tk.Frame(parent, bg=bg, bd=0, highlightthickness=0)
        holder.grid(row=row, column=col, sticky="nsew", padx=0, pady=0)
        entry = tk.Entry(
            holder,
            textvariable=textvariable,
            width=width,
            justify="center",
            font=("Courier", 10),
            bd=1,
            relief="sunken",
            highlightthickness=1,
            highlightbackground="#bcbcbc",
            highlightcolor="#4a90e2",
            bg="white"        )
        entry.pack(fill="x", padx=6, pady=4)
        return entry

    def bind_save(self, entry, callback):
        entry.bind("<Return>", callback)
        entry.bind("<KP_Enter>", callback)

    def make_centered_button_group(self, parent, row, col, bg):
        holder = tk.Frame(parent, bg=bg, bd=0, highlightthickness=0)
        holder.grid(row=row, column=col, sticky="nsew", padx=0, pady=0)
        inner = tk.Frame(holder, bg=bg, bd=0, highlightthickness=0)
        inner.pack(anchor="center", pady=4)
        return inner

    def make_button(self, parent, text, command, width=8, bg=None, activebackground=None, fg="black", font=None):
        kwargs = {
            "text": text,
            "command": command,
            "width": width,
            "relief": "raised",
            "bd": 1,
            "padx": 2,
            "pady": 2,
            "fg": fg
        }
        if bg is not None:
            kwargs["bg"] = bg
        if activebackground is not None:
            kwargs["activebackground"] = activebackground
        if font is not None:
            kwargs["font"] = font
        btn = tk.Button(parent, **kwargs)
        return btn

    def zoom_root_window(self, root):
        try:
            root.state("zoomed")
            return
        except:
            pass
        try:
            root.attributes("-zoomed", True)
            return
        except:
            pass
        w = root.winfo_screenwidth()
        h = root.winfo_screenheight()
        root.geometry("%dx%d+0+0" % (w, h))

    def center_window_on_screen(self, win):
        win.update_idletasks()
        w = win.winfo_width()
        h = win.winfo_height()
        sw = win.winfo_screenwidth()
        sh = win.winfo_screenheight()
        x = (sw - w) / 2
        y = (sh - h) / 2
        win.geometry("+%d+%d" % (x, y))

    def ask_tool_number_centered(self, parent, title, prompt, initial_value=""):
        dialog = tk.Toplevel(parent)
        dialog.title(title)
        dialog.configure(bg=self.BG_APP)
        dialog.transient(parent)
        dialog.resizable(False, False)
        result = {"value": None}
        outer = tk.Frame(dialog, bg=self.BG_APP)
        outer.pack(fill="both", expand=True, padx=16, pady=16)
        tk.Label(
            outer,
            text=prompt,
            justify="left",
            anchor="w",
            bg=self.BG_APP
        ).pack(fill="x", pady=(0, 8))
        value_var = tk.StringVar()
        value_var.set(initial_value)
        entry = tk.Entry(
            outer,
            textvariable=value_var,
            width=16,
            justify="center",
            font=("Courier", 11),
            bd=1,
            relief="sunken"
        )
        entry.pack(pady=(0, 10))
        entry.selection_range(0, tk.END)
        entry.focus_set()
        btn_frame = tk.Frame(outer, bg=self.BG_APP)
        btn_frame.pack()

        def submit():
            text = value_var.get().strip()
            if not text:
                self.show_error("Invalid Tool Number", "Enter a tool number.")
                return
            try:
                value = int(text)
            except:
                self.show_error("Invalid Tool Number", "Tool number must be an integer.")
                return
            if value < 1:
                self.show_error("Invalid Tool Number", "Tool number must be 1 or greater.")
                return
            result["value"] = value
            dialog.destroy()

        def cancel():
            dialog.destroy()
        ok_btn = tk.Button(btn_frame, text="OK", command=submit, width=10)
        ok_btn.pack(side="left", padx=4)
        cancel_btn = tk.Button(btn_frame, text="Cancel", command=cancel, width=10)
        cancel_btn.pack(side="left", padx=4)
        dialog.bind("<Return>", lambda event: submit())
        dialog.bind("<KP_Enter>", lambda event: submit())
        dialog.bind("<Escape>", lambda event: cancel())
        dialog.protocol("WM_DELETE_WINDOW", cancel)
        dialog.grab_set()
        dialog.lift()
        dialog.attributes("-topmost", True)
        self.center_window_on_screen(dialog)
        dialog.after(200, lambda: dialog.attributes("-topmost", False))
        parent.wait_window(dialog)
        return result["value"]        

class HistoryWindow(UiMixin):
    def __init__(self, parent_root, repo, service, tool, refresh_main_callback=None):        
        self.repo = repo
        self.service = service
        self.tool = tool
        self.refresh_main_callback = refresh_main_callback
        self.win = tk.Toplevel(parent_root)
        self.win.title("Tool %s History" % tool)
        self.win.configure(bg=self.BG_APP)        
        self.refresh()

    def refresh(self):
        for widget in self.win.winfo_children():
            widget.destroy()
        outer = tk.Frame(self.win, bg=self.BG_APP)
        outer.pack(fill="both", expand=True, padx=12, pady=12)
        top_bar = tk.Frame(outer, bg=self.BG_APP)
        top_bar.pack(fill="x", pady=(0, 8))
        tk.Label(
            top_bar,
            text="Tool %s Cutter History" % self.tool,
            font=("Helvetica", 11, "bold"),
            bg=self.BG_APP,
            anchor="w"
        ).pack(side="left")
        delete_all_btn = self.make_button(
            top_bar,
            text="Delete All History",
            command=self.on_delete_all,
            width=16,
            bg="#d9534f",
            activebackground="#c9302c",
            fg="white"        )
        delete_all_btn.pack(side="right")
        table = tk.Frame(outer, bg=self.BG_TABLE, bd=1, relief="solid")
        table.pack(fill="both", expand=True)
        headers = [
            ("Installed", 20),
            ("Reset", 20),
            ("Age (hrs)", 12),
            ("Usage", 16),
            ("Action", 10),        ]
        for col, (title, width) in enumerate(headers):
            self.make_label_cell(
                table,
                0,
                col,
                title,
                width,
                self.BG_HEADER,
                anchor="center",
                bold=True            )
        rows = self.repo.list_history(self.tool)
        if not rows:
            no_data = tk.Label(
                table,
                text="No history for Tool %s yet." % self.tool,
                font=("Helvetica", 10),
                bg=self.BG_ROW_1,
                padx=10,
                pady=14            )
            no_data.grid(row=1, column=0, columnspan=5, sticky="ew")
        else:
            for i, row in enumerate(rows, start=1):
                self._render_history_row(table, i, row)
        for col in range(len(headers)):
            table.grid_columnconfigure(col, weight=1)

    def _render_history_row(self, table, row_index, row):
        history_id, tool, installed_date, reset_date, elapsed_seconds, total_seconds = row
        bg = self.alternating_bg(row_index)
        installed_var = tk.StringVar()
        installed_var.set(installed_date[:19] if installed_date else "")
        installed_entry = self.make_entry_cell(table, row_index, 0, installed_var, 20, bg)
        self.bind_save(
            installed_entry,
            lambda event, hid=history_id, v=installed_var: self.on_change_history_date(hid, "installed_date", v)
        )
        reset_var = tk.StringVar()
        reset_var.set(reset_date[:19] if reset_date else "")
        reset_entry = self.make_entry_cell(table, row_index, 1, reset_var, 20, bg)
        self.bind_save(
            reset_entry,
            lambda event, hid=history_id, v=reset_var: self.on_change_history_date(hid, "reset_date", v)
        )
        life_var = tk.StringVar()
        life_var.set(self.service.seconds_to_hours_text(elapsed_seconds))
        life_entry = self.make_entry_cell(table, row_index, 2, life_var, 12, bg)
        self.bind_save(
            life_entry,
            lambda event, hid=history_id, v=life_var: self.on_change_history_life(hid, v)
        )
        endurance_lbl = self.make_label_cell(
            table,
            row_index,
            3,
            self.service.format_endurance_compact(total_seconds),
            20,
            bg,
            anchor="center"
        )
        endurance_lbl.bind(
            "<Button-1>",
            lambda event, hid=history_id, secs=total_seconds: self.on_edit_history_endurance(hid, secs)
        )
        action_holder = tk.Frame(table, bg=bg, bd=0, highlightthickness=0)
        action_holder.grid(row=row_index, column=4, sticky="nsew", padx=0, pady=0)
        delete_btn = self.make_button(
            action_holder,
            text="Delete",
            command=lambda hid=history_id: self.on_delete_history_row(hid),
            width=8,
            bg="#d9534f",
            activebackground="#c9302c",
            fg="white"
        )
        delete_btn.pack(anchor="center", padx=4, pady=4)

    def on_edit_history_endurance(self, history_id, current_seconds):
        initial_value = "%dm" % int(round(current_seconds / 60.0))
        value = tkSimpleDialog.askstring(
            "Edit Endurance",
            "Enter custom endurance as hours or minutes.\n\nExamples:\n  2.5\n  2.5h\n  150m\n  2h 30m\n",
            parent=self.win,
            initialvalue=initial_value
        )
        if value is None:
            return
        try:
            total_seconds = self.service.parse_endurance_to_seconds(value)
            self.repo.update_history_total_seconds(history_id, total_seconds)
            self.refresh()
        except ValueError as e:
            self.show_error("Invalid Endurance", str(e))        

    def on_change_history_date(self, history_id, column_name, value_var):
        try:
            new_value = self.service.normalize_datetime_text(value_var.get())
            row = self.repo.get_history_row(history_id)
            if not row:
                return "break"
            existing_installed = row[2]
            existing_reset = row[3]
            if column_name == "installed_date":
                installed_date = new_value
                reset_date = existing_reset
            else:
                installed_date = existing_installed
                reset_date = new_value
            elapsed_seconds = self.service.get_seconds_between_dates(installed_date, reset_date)
            if elapsed_seconds is None:
                elapsed_seconds = 0
            elif elapsed_seconds < 0:
                raise ValueError("Reset date cannot be earlier than install date.")
            self.repo.update_history_dates_and_elapsed(                history_id,                installed_date,                reset_date,                elapsed_seconds            )
            self.refresh()
        except ValueError as e:
            self.show_error("Invalid Date/Time", str(e))
        return "break"

    def on_change_history_life(self, history_id, value_var):
        try:
            elapsed_seconds = self.service.life_hours_to_seconds(value_var.get())
            row = self.repo.get_history_row(history_id)
            if not row:
                return "break"
            installed_date = row[2]
            if not installed_date:
                raise ValueError("Set a valid install date before editing life.")
            reset_date = self.service.add_seconds_to_datetime_text(installed_date, elapsed_seconds)
            self.repo.update_history_life_and_reset(history_id, elapsed_seconds, reset_date)
            self.refresh()
        except ValueError as e:
            self.show_error("Invalid Life", str(e))
        return "break"

    def on_delete_history_row(self, history_id):
        if self.ask_yes_no("Delete History Row","Delete this history row?\n\nThis permanently removes the selected record."):
            self.repo.delete_history_row(history_id)
            self.refresh()
            if self.refresh_main_callback:
                self.refresh_main_callback()

    def on_delete_all(self):
        if self.ask_yes_no("Delete All History","Delete all history for Tool %s?\n\nThis permanently removes all archived records for this tool." % self.tool):
            self.repo.delete_all_history_for_tool(self.tool)
            self.refresh()
            if self.refresh_main_callback:
                self.refresh_main_callback()

class MainWindow(UiMixin):
    def __init__(self, repo, service):
        self.repo = repo
        self.service = service
        self.sort_column = "tool"
        self.sort_reverse = False
        self.root = tk.Tk()
        self.root.title("Tool Manager v%s - tormachtips.com" % CURRENT_VER)
        self.root.configure(bg=self.BG_APP)
        self.zoom_root_window(self.root)
        self.table_frame = None
        self.build_ui()
        self.root.bind_all("<MouseWheel>", self._on_mousewheel)
        self.root.bind_all("<Button-4>", self._on_mousewheel)
        self.root.bind_all("<Button-5>", self._on_mousewheel)
        self.root.bind_all("<Up>", self._on_keyboard_scroll)
        self.root.bind_all("<Down>", self._on_keyboard_scroll)
        self.root.bind_all("<Home>", self._on_keyboard_scroll)
        self.root.bind_all("<End>", self._on_keyboard_scroll)
        self.root.bind_all("<Prior>", self._on_keyboard_scroll)
        self.root.bind_all("<Next>", self._on_keyboard_scroll)
        self.refresh_table()

    def on_show_help(self):
        help_text = (
            '"Tool Number" means the tool holder number from your PathPilot tool table.\n\n'
            '"Cutter" means the consumable tool currently installed in that holder.\n\n'
            'A tool holder can be used with many cutters over its life. This script tracks both the holder and the current cutter in that holder.\n\n'
            '"Current Cutter Since" is the date the current cutter was installed in the holder. It will update when your PathPilot tool table is updated. You may adjust this manually.\n\n'
            '"Cutter Age" is simply the time elapsed since "Current Cutter Since."\n\n'
            '"Cutter Usage" is the amount of work the current cutter has performed, as tracked by the related monitoring scripts. You may also adjust this manually.\n\n'
            '"History" shows past cutter changes for each tool holder and lets you review or edit those records.\n\n'
            '"Reminders" lets you create an in-PathPilot reminder when a cutter exceeds a user-defined wear limit. Notifications appear in the Status Window.\n\n'
            '"New Cutter" installs a new cutter in the selected holder. Use this for manual changes or if the monitoring service misses a PathPilot update. The script watches Z-offset changes and assumes a new cutter will normally not be exactly the same length as the previous one. You would have to be accurate to a tenth (0.0001") for it not to catch.\n\n'
            '"Delete" removes the current cutter record or the entire holder record from this script only. It does not change anything in PathPilot.\n\n'
            'Click any column header to sort.\n\n'
            'A tool will not appear here until it has first been rotated by the spindle (M3, etc.).')            
        dialog = tk.Toplevel(self.root)
        dialog.title("Help")
        dialog.configure(bg=self.BG_APP)
        dialog.transient(self.root)
        dialog.grab_set()
        dialog.resizable(False, False)
        outer = tk.Frame(dialog, bg=self.BG_APP)
        outer.pack(fill="both", expand=True, padx=16, pady=16)
        help_box = tk.Text(
            outer,
            width=120,
            height=35,
            wrap="word",
            font=("DejaVu Sans Mono", 9),
            bg="#ffff99",
            fg="black",
            relief="sunken",
            bd=1,
            padx=8,
            pady=8
        )
        help_box.pack(fill="both", expand=True, pady=(0, 12))
        help_box.insert("1.0", help_text)
        help_box.config(state="disabled")
        close_btn = self.make_button(
            outer,
            text="Close",
            command=dialog.destroy,
            width=10
        )
        close_btn.pack()
        dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
        dialog.update_idletasks()
        x = self.root.winfo_rootx() + (self.root.winfo_width() / 2) - (dialog.winfo_width() / 2)
        y = self.root.winfo_rooty() + (self.root.winfo_height() / 2) - (dialog.winfo_height() / 2)
        dialog.geometry("+%d+%d" % (x, y))
        self.root.wait_window(dialog)

    def on_edit_tool_endurance(self, tool, current_seconds):
        initial_value = "%dm" % int(round(current_seconds / 60.0))
        value = tkSimpleDialog.askstring(
            "Edit Endurance",
            "Enter custom endurance as hours or minutes.\n\nExamples:\n  2.5\n  2.5h\n  150m\n  2h 30m\n",
            parent=self.root,
            initialvalue=initial_value
        )
        if value is None:
            return
        try:
            total_seconds = self.service.parse_endurance_to_seconds(value)
            self.repo.update_tool_total_seconds(tool, total_seconds)
            self.refresh_table()
        except ValueError as e:
            self.show_error("Invalid Endurance", str(e))

    def _on_keyboard_scroll(self, event):
        try:
            focus = self.root.focus_get()
            if isinstance(focus, tk.Entry):
                if event.keysym in ("Home", "End"):
                    return None
            if event.keysym == "Up":
                self.table_canvas.yview_scroll(-1, "units")
                return "break"
            if event.keysym == "Down":
                self.table_canvas.yview_scroll(1, "units")
                return "break"
            if event.keysym == "Prior":
                self.table_canvas.yview_scroll(-1, "pages")
                return "break"
            if event.keysym == "Next":
                self.table_canvas.yview_scroll(1, "pages")
                return "break"
            if event.keysym == "Home":
                self.table_canvas.yview_moveto(0.0)
                return "break"
            if event.keysym == "End":
                self.table_canvas.yview_moveto(1.0)
                return "break"
        except:
            pass
        return None

    def _on_mousewheel(self, event):
        try:
            if event.num == 4:
                self.table_canvas.yview_scroll(-1, "units")
            elif event.num == 5:
                self.table_canvas.yview_scroll(1, "units")
            else:
                self.table_canvas.yview_scroll(-1 * (event.delta / 120), "units")
        except:
            pass

    def on_add_tool(self):
        tool = tkSimpleDialog.askinteger("Add Tool","Enter tool number:",parent=self.root,minvalue=1)
        if tool is None:
            return
        existing = self.repo.get_tool(tool)
        if existing:
            self.show_error("Tool Already Exists","Tool %s is already in the table." % tool)
            return
        installed_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.repo.add_tool(tool, installed_date, 0)
        self.refresh_table()

    def on_view_all_reminders(self):
        try:
            reminders = self.repo.list_all_tool_reminders()
            dialog = tk.Toplevel(self.root)
            dialog.title("Current Cutter Reminders")
            dialog.configure(bg=self.BG_APP)
            dialog.transient(self.root)
            dialog.grab_set()
            dialog.resizable(False, False)
            outer = tk.Frame(dialog, bg=self.BG_APP)
            outer.pack(fill="both", expand=True, padx=16, pady=16)
            if not reminders:
                tk.Label(
                    outer,
                    text="No reminders found.",
                    justify="left",
                    anchor="w",
                    bg=self.BG_APP
                ).pack(fill="x", pady=8)
            else:
                listbox = tk.Listbox(
                    dialog,
                    width=100,
                    height=min(len(reminders) + 2, 18),
                    font=("Courier", 10)
                )
                listbox.pack(padx=16, pady=8, fill="both", expand=True)
                listbox.insert(tk.END, "Tool  Limit    State        Message")
                listbox.insert(tk.END, "----  -------  -----------  ------------------------------")
                for reminder_id, tool, remind_after_minutes, message, is_active, total_seconds in reminders:
                    reminder_state = self.service.get_reminder_status_text(total_seconds, remind_after_minutes)
                    if not reminder_state:
                        reminder_state = ""
                    text = "%-4s  %-7s  %-11s  %s" % (
                        tool,
                        ("%s min" % remind_after_minutes),
                        reminder_state,
                        (message or "(no message)")
                    )
                    listbox.insert(tk.END, text)
            btn_frame = tk.Frame(dialog, bg=self.BG_APP)
            btn_frame.pack(pady=(0, 8))
            tk.Button(
                btn_frame,
                text="Close",
                command=dialog.destroy,
                width=10
            ).pack(side="left", padx=4)
            dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
            dialog.update_idletasks()
            x = self.root.winfo_rootx() + (self.root.winfo_width() / 2) - (dialog.winfo_width() / 2)
            y = self.root.winfo_rooty() + (self.root.winfo_height() / 2) - (dialog.winfo_height() / 2)
            dialog.geometry("+%d+%d" % (x, y))
            self.root.wait_window(dialog)
        except Exception as e:
            self.show_error("Reminder Error", str(e))

    def on_add_reminder(self, tool):
        minutes = tkSimpleDialog.askinteger(
            "Add Reminder",
            "Remind when Tool %s exceeds how many minutes of use?" % tool,
            parent=self.root,
            minvalue=1        )
        if minutes is None:
            return
        message = tkSimpleDialog.askstring(
            "Reminder Message",
            "Optional reminder message for Tool %s:" % tool,
            parent=self.root        )
        if message is None:
            message = ""
        self.repo.add_tool_reminder(tool, minutes, message)
        tkMessageBox.showinfo(
            "Reminder Added",
            "Reminder saved for Tool %s at %s minutes." % (tool, minutes)        )

    def on_delete_reminders(self, tool):
        try:
            reminders = self.repo.list_tool_reminders(tool)
            if not reminders:
                tkMessageBox.showinfo(
                    "No Reminders",
                    "Tool %s has no reminders to delete." % tool                )
                return
            dialog = tk.Toplevel(self.root)
            dialog.title("Delete Reminders - Tool %s" % tool)
            dialog.configure(bg=self.BG_APP)
            dialog.transient(self.root)
            dialog.grab_set()
            dialog.resizable(False, False)
            tk.Label(
                dialog,
                text="Select a reminder to delete for Tool %s:" % tool,
                justify="left",
                anchor="w",
                bg=self.BG_APP,
                padx=16,
                pady=8
            ).pack(fill="x")
            listbox = tk.Listbox(dialog, width=60, height=min(len(reminders), 10))
            listbox.pack(padx=16, pady=8, fill="both", expand=True)
            for reminder_id, _tool, remind_after_minutes, message, is_active in reminders:
                status = "Active" if is_active else "Inactive"
                text = "%s min  |  %s  |  %s" % (
                    remind_after_minutes,
                    status,
                    message or "(no message)"                )
                listbox.insert(tk.END, text)
            btn_frame = tk.Frame(dialog, bg=self.BG_APP)
            btn_frame.pack(pady=8)

            def delete_selected():
                selection = listbox.curselection()
                if not selection:
                    self.show_error("No Selection", "Select a reminder to delete.")
                    return
                idx = int(selection[0])
                reminder_id = reminders[idx][0]
                if self.ask_yes_no("Delete Reminder", "Delete the selected reminder?"):
                    self.repo.delete_tool_reminder(reminder_id)
                    dialog.destroy()

            def delete_all():
                if self.ask_yes_no(
                    "Delete All Reminders",
                    "Delete all reminders for Tool %s?" % tool                ):
                    self.repo.delete_all_tool_reminders(tool)
                    dialog.destroy()
            tk.Button(
                btn_frame,
                text="Delete Selected",
                command=delete_selected,
                width=16,
                bg="#d9534f",
                activebackground="#c9302c",
                fg="white"
            ).pack(side="left", padx=4)
            tk.Button(
                btn_frame,
                text="Delete All",
                command=delete_all,
                width=12,
                bg="#f0d6d6",
                activebackground="#e6c3c3"
            ).pack(side="left", padx=4)
            tk.Button(
                btn_frame,
                text="Close",
                command=dialog.destroy,
                width=10
            ).pack(side="left", padx=4)
            dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
            dialog.update_idletasks()
            x = self.root.winfo_rootx() + (self.root.winfo_width() / 2) - (dialog.winfo_width() / 2)
            y = self.root.winfo_rooty() + (self.root.winfo_height() / 2) - (dialog.winfo_height() / 2)
            dialog.geometry("+%d+%d" % (x, y))
            self.root.wait_window(dialog)
        except Exception as e:
            self.show_error("Reminder Error", str(e))

    def on_manage_reminders(self, tool):
        reminders = self.repo.list_tool_reminders(tool)
        dialog = tk.Toplevel(self.root)
        dialog.title("Tool %s Reminders" % tool)
        dialog.configure(bg=self.BG_APP)
        dialog.transient(self.root)
        dialog.resizable(False, False)
        dialog.update_idletasks()
        dialog.lift()
        dialog.focus_force()
        dialog.grab_set()
        dialog.focus_force()
        dialog.after(200, lambda: dialog.attributes("-topmost", False))
        result = {"choice": None}
        outer = tk.Frame(dialog, bg=self.BG_APP)
        outer.pack(fill="both", expand=True, padx=16, pady=16)
        tk.Label(
            outer,
            text="Manage reminders for Tool %s." % tool,
            justify="left",
            anchor="w",
            bg=self.BG_APP
        ).pack(fill="x")
        if reminders:
            tk.Label(
                outer,
                text="Existing reminders:",
                justify="left",
                anchor="w",
                bg=self.BG_APP,
                font=("Helvetica", 10, "bold")
            ).pack(fill="x", pady=8)
            listbox = tk.Listbox(outer, width=60, height=min(len(reminders), 8))
            listbox.pack(fill="both", expand=True, pady=4)
            for reminder_id, _tool, remind_after_minutes, message, is_active in reminders:
                status = "Active" if is_active else "Inactive"
                text = "%s min  |  %s  |  %s" % (
                    remind_after_minutes,
                    status,
                    message or "(no message)"                )
                listbox.insert(tk.END, text)
        else:
            tk.Label(
                outer,
                text="No reminders exist yet for this tool.",
                justify="left",
                anchor="w",
                bg=self.BG_APP
            ).pack(fill="x", pady=8)
        tk.Label(
            outer,
            text=(
                "\nAdd Reminder:\n"
                "   Create a new reminder for this tool.\n\n"
                "Delete Reminder(s):\n"
                "   Remove one or more existing reminders for this tool."
            ),
            justify="left",
            anchor="w",
            bg=self.BG_APP
        ).pack(fill="x", pady=8)
        btn_frame = tk.Frame(outer, bg=self.BG_APP)
        btn_frame.pack(pady=8)

        def choose(value):
            result["choice"] = value
            dialog.destroy()
        tk.Button(
            btn_frame,
            text="Add Reminder",
            command=lambda: choose("add"),
            width=16
        ).pack(side="left", padx=4)
        tk.Button(
            btn_frame,
            text="Delete Reminder(s)",
            command=lambda: choose("delete"),
            width=18,
            bg="#f0d6d6",
            activebackground="#e6c3c3"
        ).pack(side="left", padx=4)
        tk.Button(
            btn_frame,
            text="Cancel",
            command=lambda: choose(None),
            width=10
        ).pack(side="left", padx=4)
        dialog.protocol("WM_DELETE_WINDOW", lambda: choose(None))
        dialog.update_idletasks()
        x = self.root.winfo_rootx() + (self.root.winfo_width() / 2) - (dialog.winfo_width() / 2)
        y = self.root.winfo_rooty() + (self.root.winfo_height() / 2) - (dialog.winfo_height() / 2)
        dialog.geometry("+%d+%d" % (x, y))
        dialog.lift()
        dialog.focus_force()
        self.root.wait_window(dialog)               
        if result["choice"] == "add":
            self.on_add_reminder(tool)
            self.refresh_table()
        elif result["choice"] == "delete":
            self.on_delete_reminders(tool)
            self.refresh_table()            

    def build_ui(self):
        outer = tk.Frame(self.root, bg=self.BG_APP)
        outer.pack(fill="both", expand=True, padx=18, pady=18)
        table_outer = tk.Frame(outer, bg=self.BG_APP)
        table_outer.pack(fill="both", expand=True)
        self.table_canvas = tk.Canvas(
            table_outer,
            bg=self.BG_APP,
            highlightthickness=0,
            bd=0
        )
        self.table_canvas.pack(side="left", fill="both", expand=True)
        v_scroll = tk.Scrollbar(table_outer, orient="vertical", command=self.table_canvas.yview)
        v_scroll.pack(side="right", fill="y")
        self.table_canvas.configure(yscrollcommand=v_scroll.set)
        self.table_frame = tk.Frame(
            self.table_canvas,
            bg=self.BG_TABLE,
            bd=1,
            relief="solid"
        )
        self.table_canvas_window = self.table_canvas.create_window(
            (0, 0),
            window=self.table_frame,
            anchor="nw"
        )

        def _on_table_configure(event):
            self.table_canvas.configure(scrollregion=self.table_canvas.bbox("all"))

        def _on_canvas_configure(event):
            self.table_canvas.itemconfigure(self.table_canvas_window, width=event.width)
        self.table_frame.bind("<Configure>", _on_table_configure)
        self.table_canvas.bind("<Configure>", _on_canvas_configure)
        button_frame = tk.Frame(outer, bg=self.BG_APP)
        button_frame.pack(side="bottom", pady=18)
        refresh_btn = self.make_button(button_frame, text="Refresh", command=self.refresh_table, width=10)
        refresh_btn.pack(side="left", padx=6)
        add_btn = self.make_button(button_frame, text="Add Tool", command=self.on_add_tool, width=10)
        add_btn.pack(side="left", padx=6)
        all_reminders_btn = self.make_button(
            button_frame,
            text="View Reminders",
            command=self.on_view_all_reminders,
            width=14
        )
        all_reminders_btn.pack(side="left", padx=6)
        help_btn = self.make_button(
            button_frame,
            text="Help",
            command=self.on_show_help,
            width=10
        )
        help_btn.pack(side="left", padx=6)

    def on_close(self):
        try:
            self.root.unbind_all("<MouseWheel>")
            self.root.unbind_all("<Button-4>")
            self.root.unbind_all("<Button-5>")
            self.root.unbind_all("<Up>")
            self.root.unbind_all("<Down>")
            self.root.unbind_all("<Home>")
            self.root.unbind_all("<End>")
            self.root.unbind_all("<Prior>")
            self.root.unbind_all("<Next>")
        except:
            pass
        self.repo.close()
        self.root.destroy()

    def run(self):
        self.root.mainloop()

    def clear_table(self):
        for widget in self.table_frame.winfo_children():
            widget.destroy()

    def get_header_text(self, column_name, label):
        if self.sort_column != column_name:
            return label
        if self.sort_reverse:
            return label + " ▼"
        return label + " ▲"

    def sort_by(self, column_name):
        if self.sort_column == column_name:
            self.sort_reverse = not self.sort_reverse
        else:
            self.sort_column = column_name
            self.sort_reverse = False
        self.refresh_table()

    def get_sort_value(self, row):
        tool, installed_date, total_seconds = row
        if self.sort_column == "tool":
            return tool
        if self.sort_column == "installed_date":
            dt = self.service.parse_datetime(installed_date)
            return dt or datetime.datetime.min
        if self.sort_column == "endurance":
            return total_seconds / 60.0
        if self.sort_column == "elapsed":
            elapsed = self.service.get_elapsed_since_install(installed_date)
            if elapsed is None:
                return -2
            return elapsed
        return tool

    def refresh_table(self):
        self.clear_table()
        headers = [
            ("tool", "Tool", 8),
            ("installed_date", "Current Cutter Since", 20),
            ("elapsed", "Cutter Age", 16),
            ("endurance", "Cutter Usage", 16),
            ("action", "Actions", 42),
        ]
        for col, (column_name, label, width) in enumerate(headers):
            btn = tk.Button(
                self.table_frame,
                text=self.get_header_text(column_name, label),
                width=width,
                font=("Helvetica", 10, "bold"),
                bg=self.BG_HEADER,
                activebackground="#c8c8c8",
                relief="flat",
                command=lambda c=column_name: self.sort_by(c)
            )
            btn.grid(row=0, column=col, sticky="nsew", padx=0, pady=0)
        rows = self.repo.list_tools()
        rows = sorted(rows, key=self.get_sort_value, reverse=self.sort_reverse)
        if not rows:
            no_data = tk.Label(
                self.table_frame,
                text="No tools found yet.",
                font=("Helvetica", 10),
                bg=self.BG_ROW_1,
                padx=10,
                pady=14
            )
            no_data.grid(row=1, column=0, columnspan=len(headers), sticky="ew")
        else:
            for i, row in enumerate(rows, start=1):
                self.render_tool_row(i, row)
        for col in range(len(headers)):
            self.table_frame.grid_columnconfigure(col, weight=1)

    def render_tool_row(self, row_index, row):
        tool, installed_date, total_seconds = row
        bg = self.alternating_bg(row_index)
        elapsed_text = self.service.format_elapsed_from_install(installed_date)
        self.make_label_cell(
            self.table_frame,
            row_index,
            0,
            str(tool),
            8,
            bg,
            anchor="center"
        )
        installed_var = tk.StringVar()
        installed_var.set(installed_date[:19] if installed_date else "")
        installed_entry = self.make_entry_cell(self.table_frame, row_index, 1, installed_var, 20, bg)
        self.bind_save(
            installed_entry,
            lambda event, t=tool, v=installed_var: self.on_update_tool_installed(t, v)
        )
        self.make_label_cell(
            self.table_frame,
            row_index,
            2,
            elapsed_text,
            16,
            bg,
            anchor="center"
        )
        endurance_lbl = self.make_label_cell(
            self.table_frame,
            row_index,
            3,
            self.service.format_endurance_compact(total_seconds),
            16,
            bg,
            anchor="center"
        )
        endurance_lbl.bind(
            "<Button-1>",
            lambda event, t=tool, secs=total_seconds: self.on_edit_tool_endurance(t, secs)
        )
        has_history = self.repo.has_history(tool)
        has_reminders = self.repo.has_reminders(tool)
        history_bg = "#fff2a8" if has_history else None
        history_active = "#f0e08a" if has_history else None
        history_text = "History" if has_history else "History"
        reminder_bg = "#fff2a8" if has_reminders else None
        reminder_active = "#f0e08a" if has_reminders else "#ec971f"
        reminder_text = "Reminder" if has_reminders else "Reminder"
        action_group = self.make_centered_button_group(self.table_frame, row_index, 4, bg)
        history_btn = self.make_button(
            action_group,
            text=history_text,
            command=lambda t=tool: self.on_view_history(t),
            width=9,
            bg=history_bg,
            activebackground=history_active
        )
        history_btn.pack(side="left", padx=2)
        reset_btn = self.make_button(
            action_group,
            text="Reset",
            command=lambda t=tool: self.on_reset_tool(t),
            width=8,
            bg="#f0d6d6",
            activebackground="#e6c3c3"
        )
        reset_btn.pack(side="left", padx=2)
        reminder_btn = self.make_button(
            action_group,
            text=reminder_text,
            command=lambda t=tool: self.on_manage_reminders(t),
            width=10,
            bg=reminder_bg,
            activebackground=reminder_active,
            fg="black"
        )
        reminder_btn.pack(side="left", padx=2)
        delete_btn = self.make_button(
            action_group,
            text="Delete",
            command=lambda t=tool: self.on_delete_tool(t),
            width=8,
            bg="#d9534f",
            activebackground="#c9302c",
            fg="white"
        )
        delete_btn.pack(side="left", padx=2)        

    def on_update_tool_installed(self, tool, value_var):
        try:
            normalized = self.service.normalize_datetime_text(value_var.get())
            self.repo.update_tool_installed_date(tool, normalized)
            self.refresh_table()
        except ValueError as e:
            self.show_error("Invalid Date/Time", str(e))
        return "break"

    def ask_delete_tool_choice(self, tool):
        dialog = tk.Toplevel(self.root)
        dialog.title("Delete Tool")
        dialog.configure(bg=self.BG_APP)
        dialog.transient(self.root)
        dialog.grab_set()
        dialog.resizable(False, False)
        result = {"choice": None}
        msg = (
            "Choose what to delete for Tool %s.\n\n"
            "Delete Current Run Only:\n"
            "   Deletes only the current tool record (current install date and endurance).\n"
            "   Previous history will remain and reappear if the tool is added again.\n\n"
            "Delete Tool & History:\n"
            "   Deletes the current tool record AND all archived history for this tool.\n"
            "   Everything for tool %s will be deleted."
        ) % (tool, tool)
        tk.Label(
            dialog,
            text=msg,
            justify="left",
            anchor="w",
            bg=self.BG_APP,
            padx=16,
            pady=16
        ).pack(fill="both")
        btn_frame = tk.Frame(dialog, bg=self.BG_APP)
        btn_frame.pack(pady=(0, 16))

        def choose(value):
            result["choice"] = value
            dialog.destroy()
        tk.Button(
            btn_frame,
            text="Delete Current Run Only",
            command=lambda: choose("tool_only"),
            width=28
        ).pack(side="left", padx=4)
        tk.Button(
            btn_frame,
            text="Delete Tool & History",
            command=lambda: choose("tool_and_history"),
            width=22,
            bg="#d9534f",
            activebackground="#c9302c",
            fg="white"
        ).pack(side="left", padx=4)
        tk.Button(
            btn_frame,
            text="Cancel",
            command=lambda: choose(None),
            width=10
        ).pack(side="left", padx=4)
        dialog.protocol("WM_DELETE_WINDOW", lambda: choose(None))
        dialog.update_idletasks()
        x = self.root.winfo_rootx() + (self.root.winfo_width() / 2) - (dialog.winfo_width() / 2)
        y = self.root.winfo_rooty() + (self.root.winfo_height() / 2) - (dialog.winfo_height() / 2)
        dialog.geometry("+%d+%d" % (x, y))
        self.root.wait_window(dialog)
        return result["choice"]

    def on_delete_tool(self, tool):
        choice = self.ask_delete_tool_choice(tool)
        if choice is None:
            return
        if choice == "tool_only":
            self.repo.delete_tool(tool)
        elif choice == "tool_and_history":
            self.repo.delete_tool_and_history(tool)
        self.refresh_table()

    def on_reset_tool(self, tool):
        if not self.ask_yes_no(
            "Start New Cutter Run",
            "Install new cutter for Tool %s?\n\n"
            "This will save the current install date and usage to history, "
            "then start Tool (Cutter) %s over from right now.\n\n"
            "Use this if you want to manually log and update a cutter change or if the script missed a change inside PathPilot, etc."
            % (tool, tool)):
            return
        row = self.repo.get_tool(tool)
        if not row:
            return
        installed_date = row[1]
        reset_dt = datetime.datetime.now()
        reset_text = reset_dt.strftime("%Y-%m-%d %H:%M:%S")
        elapsed_seconds = self.service.get_elapsed_since_install(installed_date)
        if elapsed_seconds is None or elapsed_seconds < 0:
            elapsed_seconds = 0
        self.repo.archive_and_reset_tool(tool, reset_text, elapsed_seconds)
        self.refresh_table()

    def on_view_history(self, tool):
        HistoryWindow(self.root, self.repo, self.service, tool, self.refresh_table)

def launch_tool_reminders(start_tool=None):
    if not os.path.exists(DB_FILE):
        root = tk.Tk()
        root.withdraw()
        tkMessageBox.showerror(
            "Database Not Found",
            "tool_lifetime.db not found.\nRun the spindle timer plugin first."
        )
        root.destroy()
        return
    repo = ToolLifeRepository(DB_FILE)
    service = ToolLifeService()
    app = MainWindow(repo, service)

    def open_reminder_dialog():
        tool = start_tool
        if tool is None:
            tool = app.ask_tool_number_centered(
                app.root,
                "Tool Reminder",
                "Enter tool number:"
            )
        if tool is not None:
            app.on_manage_reminders(tool)

    def on_close():
        try:
            repo.close()
        except:
            pass
        app.root.destroy()
    app.root.protocol("WM_DELETE_WINDOW", on_close)
    app.root.after(300, open_reminder_dialog)
    app.root.mainloop()

def main():
    try:
        import update_checker
        update_checker.tormachtips(__file__, CURRENT_VER)
    except Exception:
        pass    
    if not os.path.exists(DB_FILE):
        root = tk.Tk()
        root.withdraw()
        tkMessageBox.showerror("Database Not Found","tool_lifetime.db not found.\nRun the spindle timer plugin first.")
        root.destroy()
        return
    repo    = ToolLifeRepository(DB_FILE)
    service = ToolLifeService()
    app     = MainWindow(repo, service)
    app.run()

if __name__ == "__main__":
    main()