Can you review and suggest improvements to this python script?

"""
Build Instructions (Windows):
-----------------------------
To create a standalone executable using PyInstaller, run the following command:

    python -m PyInstaller --onefile --windowed --add-data "light.png;." --add-data "medium.png;." --add-data "dark.png;." daily-update-gui.py

This will generate a .exe file in the `dist` directory. Ensure the images (light.png, medium.png, dark.png) are in the same folder as this script.
"""
import tableauserverclient as TSC
import keyring
import pandas as pd
import os
import io
import time
import shutil
import platform
import datetime
import argparse
import tkinter as tk
from tkinter import filedialog, messagebox
import re  # Ensure this is at the top of your file with other imports
import getpass
import uuid  # Ensure this is at the top of your file with other imports
from openpyxl import load_workbook
from openpyxl.worksheet.table import Table, TableStyleInfo
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font
import yaml
import ctypes
import threading
from PIL import Image, ImageTk

# === Global stop event for cancellation ===
stop_event = threading.Event()

def get_color_code(color_name):
    color_map = {
        "Black": 1, "Blue": 2, "Red": 3, "Green": 4,
        "Purple": 5, "Teal": 6, "Orange": 7
    }
    return color_map.get(color_name, 2)

# === Parse command-line arguments ===
default_onedrive = r"C:\Users\c14847\Microchip Technology Inc\SiC Power Solutions Group - Documents\Daily Reports"

def load_config():
    config_path = os.path.join(os.path.expanduser("~"), ".daily_update_config.yaml")
    config_defaults = {
        "workbook": "Daily Updates",
        "report": "Daily Download-HPS",
        "archive_days": 5,
        "username": getpass.getuser(),
        "token": "TabCmdToken",
        "onedrive": default_onedrive,
        "table_color": "Blue",
        "table_weight": "Medium"
    }
    if os.path.exists(config_path):
        with open(config_path, "r") as f:
            return {**config_defaults, **yaml.safe_load(f)}
    return config_defaults.copy()

def save_config(config_data):
    config_path = os.path.join(os.path.expanduser("~"), ".daily_update_config.yaml")
    with open(config_path, "w") as f:
        yaml.safe_dump(config_data, f)

def launch_gui():
    if os.name == 'nt':
        ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0)

    root = tk.Tk()
    root.title("Tableau Export Configuration")

    config_defaults = {
        "workbook": "Daily Updates",
        "report": "Daily Download-HPS",
        "archive_days": 5,
        "username": getpass.getuser(),
        "token": "TabCmdToken",
        "onedrive": default_onedrive,
        "table_color": "Blue",
        "table_weight": "Medium"
    }
    config_path = os.path.join(os.path.expanduser("~"), ".daily_update_config.yaml")
    config_defaults.update(load_config())

    fields = {}

    def add_field(label, key):
        tk.Label(root, text=label).pack()
        var = tk.StringVar(value=str(config_defaults.get(key, "")))
        entry = tk.Entry(root, textvariable=var, width=60)
        entry.pack()
        fields[key] = var

    add_field("Excel File Name", "workbook")
    add_field("Tableau Report Name (must match exactly)", "report")
    add_field("Archive after 'n' days", "archive_days")
    add_field("User Name (MCHP Badge)", "username")
    add_field("Tableau Token Name", "token")

    def set_token():
        def save_token():
            token_name = fields["token"].get()
            token_value = token_entry.get()
            if token_name and token_value:
                keyring.set_password("TableauToken", token_name, token_value)
                messagebox.showinfo("Success", f"Token '{token_name}' stored securely.")
                popup.destroy()
            else:
                messagebox.showerror("Error", "Both token name and value are required.")

        popup = tk.Toplevel(root)
        popup.title("Store Tableau Token")

        tk.Label(popup, text="Enter Personal Access Token (PAT):").pack(padx=10, pady=(10, 5))
        token_entry = tk.Entry(popup, show="*", width=50)
        token_entry.pack(padx=10, pady=(0, 10))

        tk.Button(popup, text="Save Token", command=save_token).pack(pady=(0, 10))

    # tk.Button(root, text="Save Tableau Token", command=set_token).pack(pady=(0, 10))
    # Place Save and Check buttons side by side
    button_frame = tk.Frame(root)
    button_frame.pack(pady=(0, 10))

    tk.Button(button_frame, text="Save Tableau Token", command=set_token).pack(side="left", padx=(0, 10))

    def check_token():
        token_name = fields["token"].get()
        if not token_name:
            messagebox.showerror("Error", "Please enter a token name first.")
            return
        token_value = keyring.get_password("TableauToken", token_name)
        if token_value:
            messagebox.showinfo("Token Check", f"Token '{token_name}' exists in keyring.")
        else:
            messagebox.showwarning("Token Check", f"Token '{token_name}' not found in keyring.")

    tk.Button(button_frame, text="Check Tableau Token", command=check_token).pack(side="left")

    add_field("Save To", "onedrive")

    # Move the Browse button here, right after OneDrive field, with tight padding
    def browse_onedrive():
        path = filedialog.askdirectory()
        if path:
            fields["onedrive"].set(path)

    browse_btn = tk.Button(root, text="Select Folder to Save File To", command=browse_onedrive)
    browse_btn.pack(pady=(0, 5))

    tk.Label(root, text="Table Color").pack()
    table_color_var = tk.StringVar(value=config_defaults.get("table_color", "Blue"))
    tk.OptionMenu(root, table_color_var, "Black", "Blue", "Red", "Green", "Purple", "Teal", "Orange").pack()

    tk.Label(root, text="Table Weight").pack()

    table_weight_var = tk.StringVar(value=config_defaults.get("table_weight", "Medium"))

    frame = tk.Frame(root)
    frame.pack(pady=5)

    images = {}
    for weight in ["Light", "Medium", "Dark"]:
        img_path = os.path.join(os.path.dirname(__file__), f"{weight.lower()}.png")
        pil_img = Image.open(img_path).resize((100, 50))
        images[weight] = ImageTk.PhotoImage(pil_img)

        subframe = tk.Frame(frame)
        subframe.pack(side="left", padx=10)

        tk.Label(subframe, image=images[weight]).pack()
        tk.Radiobutton(subframe, text=weight, variable=table_weight_var, value=weight).pack()

    # (Browse button moved above)

    def run_export_in_thread():
        stop_event.clear()
        try:
            download_all_views_to_excel(args)
        except Exception as e:
            print(f"❌ Thread crashed: {e}")
            messagebox.showerror("Export Error", str(e))
        finally:
            run_button.config(state='normal')
            browse_btn.config(state='normal')
            cancel_button.config(state='disabled')

    def save_and_run():
        try:
            new_config = {
                k: (int(v.get()) if k == "archive_days" else v.get())
                for k, v in fields.items()
            }
            new_config["table_color"] = table_color_var.get()
            new_config["table_weight"] = table_weight_var.get()
            save_config(new_config)
            global args
            class Args: pass
            args = Args()
            for k, v in new_config.items():
                setattr(args, k, v)
            args.now = True  # Assume GUI always runs immediately
            # Disable UI controls before running export
            for entry in fields.values():
                entry.set(entry.get())  # Lock in values
            run_button.config(state='disabled')
            browse_btn.config(state='disabled')
            cancel_button.config(state='normal')
            # Leave the window open while exporting
            threading.Thread(target=run_export_in_thread).start()
        except Exception as e:
            messagebox.showerror("Error", f"Failed to save/run: {e}")
            print(f"Exception occurred: {e}")

    # Button frame for Save and Run, Reset, and Cancel
    action_frame = tk.Frame(root)
    action_frame.pack(pady=10)

    run_button = tk.Button(action_frame, text="Save and Run", command=save_and_run)
    run_button.pack(side='left', padx=5)

    def cancel_export():
        stop_event.set()

    def reset_to_defaults():
        for key, var in fields.items():
            var.set(str(config_defaults.get(key, "")))
        table_color_var.set(config_defaults.get("table_color", "Blue"))
        table_weight_var.set(config_defaults.get("table_weight", "Medium"))

    reset_button = tk.Button(action_frame, text="Reset to Defaults", command=reset_to_defaults)
    reset_button.pack(side='left', padx=5)

    cancel_button = tk.Button(action_frame, text="Cancel", command=cancel_export, bg="red", fg="white", state='disabled')
    cancel_button.pack(side='left', padx=5)

    log_box = tk.Text(root, height=15, width=100, state='disabled', wrap='word')
    log_box.pack(padx=10, pady=5)

    class RedirectText:
        def __init__(self, widget):
            self.widget = widget

        def write(self, string):
            self.widget.configure(state='normal')
            self.widget.insert(tk.END, string)
            self.widget.see(tk.END)
            self.widget.configure(state='disabled')

        def flush(self):
            pass

    sys.stdout = sys.stderr = RedirectText(log_box)
    root.mainloop()


# === FILE PATHS ===
DOWNLOAD_FOLDER = os.path.join(os.path.expanduser("~"), "Downloads")

# === Authenticate using Personal Access Token ===
def get_tableau_connection(args):
    token_value = keyring.get_password("TableauToken", args.token)
    if not token_value:
        raise Exception(f"Token '{args.token}' not found in keyring. Please use the 'Save Tableau Token' button to store it.")

    tableau_auth = TSC.PersonalAccessTokenAuth(args.token, token_value, site_id="")
    server = TSC.Server("https://tableau.microchip.com")
    server.use_server_version()
    return server, tableau_auth

# === Main logic ===
def clean_dataframe(df):
    return df

def sanitize_sheet_name(name):
    name = re.sub(r'[\\/*?:\[\]]', '', name)  # Remove invalid Excel chars
    return name[:31]  # Truncate to Excel's 31-char limit

def download_all_views_to_excel(args):
    USERNAME = args.username
    TOKEN_NAME = args.token
    SERVER_URL = "https://tableau.microchip.com"
    WORKBOOK_NAME = args.workbook
    REPORT_NAME = args.report
    SITE_ID = ""  # Use "" for default site
    DATE_STAMP = datetime.datetime.now().strftime("%Y%m%d")
    ONEDRIVE_DESTINATION = args.onedrive
    FILENAME_SAFE_WB = WORKBOOK_NAME.replace(" ", "_")
    EXCEL_FILENAME = f"{FILENAME_SAFE_WB}-{DATE_STAMP}.xlsx"
    EXCEL_FILE = os.path.join(DOWNLOAD_FOLDER, EXCEL_FILENAME)
    EXCEL_FILE_DEST = os.path.join(ONEDRIVE_DESTINATION, EXCEL_FILENAME)

    if os.path.exists(EXCEL_FILE_DEST):
        print(f"⚠️ Excel file already exists: {EXCEL_FILE_DEST}")
        return

    server, auth = get_tableau_connection(args)
    with server.auth.sign_in(auth):
        print(f"\n🔐 Logged into Tableau as {USERNAME}")

        # Find the workbook
        workbooks = list(TSC.Pager(server.workbooks))
        print(f"Looking for workbook named exactly: '{REPORT_NAME}'")

        target_wb = next((wb for wb in workbooks if wb.name.lower() == REPORT_NAME.lower()), None)
        if target_wb:
            print(f"✅ Matched workbook: '{target_wb.name}' in project '{target_wb.project_name}'")
        if not target_wb:
            print(f"❌ Workbook '{REPORT_NAME}' not found.")
            return

        server.workbooks.populate_views(target_wb)
        views = target_wb.views

        if not views:
            print("⚠️ No views found in workbook.")
            return

        if not os.path.exists(DOWNLOAD_FOLDER):
            os.makedirs(DOWNLOAD_FOLDER)

        excel_writer = pd.ExcelWriter(EXCEL_FILE, engine='openpyxl')

        for view in views:
            if stop_event.is_set():
                print("🛑 Export cancelled by user.")
                break
            if view.name.strip().lower() in ("readme", "read me"):
                print(f"ℹ️ Skipping view '{view.name}' (not intended for export)")
                continue
            server.views.populate_csv(view)
            csv_data = b''.join(view.csv).decode('utf-8', errors='replace')

            try:
                df = pd.read_csv(
                    io.StringIO(csv_data),
                    engine="python",
                    encoding="utf-8",
                    sep=",",
                    quotechar='"',
                    escapechar="\\",
                    on_bad_lines="warn"
                )

                # Pivot Measure Names/Values into columns
                if "Measure Names" in df.columns and "Measure Values" in df.columns:
                    # Drop rows where Measure Values are all NaN
                    df = df.dropna(subset=["Measure Values"])

                    # Convert to numeric if possible
                    df["Measure Values"] = pd.to_numeric(df["Measure Values"], errors="coerce")

                    # Try to heuristically choose index columns
                    possible_id_vars = [
                        col for col in df.columns
                        if col not in ["Measure Names", "Measure Values"]
                        and df[col].nunique() < len(df) * 0.5
                        and df[col].dtype == object
                    ]

                    if not possible_id_vars:
                        possible_id_vars = [col for col in df.columns if col not in ["Measure Names", "Measure Values"]]

                    df = df.pivot_table(
                        index=possible_id_vars,
                        columns="Measure Names",
                        values="Measure Values",
                        aggfunc="first"
                    ).reset_index()

                    df.columns.name = None  # Flatten column headers

                # Remove numeric prefixes like '01-', '02-' from headers
                df.columns = df.columns.str.replace(r"^\d{2}-", "", regex=True)

                # Remove non-printable characters to avoid Excel export errors (e.g., GD SZ HANâ␦␦S LASER TECH issue)
                # Clean up any non-printable or problematic characters from all string cells
                df = df.map(lambda x: ''.join(c for c in str(x) if c.isprintable()) if isinstance(x, str) else x)

                sheet_name = sanitize_sheet_name(view.name)
                try:
                    df.to_excel(excel_writer, sheet_name=sheet_name, index=False)

                    if "Amt - Net USD" in df.columns or "Qty - Net" in df.columns or "Revenue" in df.columns:
                        wb = excel_writer.book
                        ws = wb[sheet_name]
                        totals = []
                        for col in df.columns:
                            if col in ["Amt - Net USD", "Qty - Net", "Revenue"]:
                                col_sum = df[col].sum()
                                totals.append(col_sum)
                            else:
                                totals.append("")
                        ws.append(totals)

                    print(f"✅ View '{view.name}' added to Excel")
                except Exception as e:
                    print(f"❌ Could not write view '{view.name}' to Excel sheet '{sheet_name}': {e}")
            except Exception as e:
                print(f"❌ Error processing view '{view.name}': {e}")

        excel_writer.close()
        time.sleep(0.5)
        # === Add tables, freeze headers, style, and auto-fit ===
        wb = load_workbook(EXCEL_FILE)

        for sheet in wb.sheetnames:
            ws = wb[sheet]
            max_row = ws.max_row
            max_col = ws.max_column
            last_col_letter = get_column_letter(max_col)

            # Add table with styling only if data exists
            if max_row > 1 and max_col > 0:
                table_range = f"A1:{last_col_letter}{max_row}"
                safe_sheet = re.sub(r"[^\w]", "_", sheet)[:20]
                table_name = f"Table_{safe_sheet}_{uuid.uuid4().hex[:6]}"
                try:
                    tab = Table(displayName=table_name, ref=table_range)
                    style_name = f"TableStyle{args.table_weight}{get_color_code(args.table_color)}"
                    style = TableStyleInfo(name=style_name, showFirstColumn=False,
                                           showLastColumn=False, showRowStripes=True, showColumnStripes=False)
                    tab.tableStyleInfo = style

                    ws.add_table(tab)
                    print(f"🧾 Table added to sheet '{sheet}' ({table_range})")

                except Exception as e:
                    print(f"⚠️ Could not add table to sheet '{sheet}': {e}")
            else:
                print(f"ℹ️ Skipped adding table to sheet '{sheet}' (empty or invalid range)")

            # Freeze header row
            ws.freeze_panes = "A2"

            # Set header row font to white and bold
            for col in range(1, max_col + 1):
                cell = ws.cell(row=1, column=col)
                cell.font = Font(color="FFFFFF", bold=True)

            # Auto-fit columns
            for col in ws.columns:
                max_length = 0
                col_letter = get_column_letter(col[0].column)
                for cell in col:
                    try:
                        cell_value = str(cell.value) if cell.value is not None else ""
                        max_length = max(max_length, len(cell_value))
                    except:
                        pass
                adjusted_width = max_length + 2
                ws.column_dimensions[col_letter].width = adjusted_width

        wb.save(EXCEL_FILE)
        print(f"📘 Excel workbook saved: {EXCEL_FILE}")

        # === Archive old files (older than 5 days) ===
        archive_folder = os.path.join(ONEDRIVE_DESTINATION, "Archive")
        os.makedirs(archive_folder, exist_ok=True)

        now = time.time()
        cutoff = now - (args.archive_days * 86400)  # archive_days in seconds

        for filename in os.listdir(ONEDRIVE_DESTINATION):
            if filename.startswith(FILENAME_SAFE_WB) and filename.endswith(".xlsx"):
                file_path = os.path.join(ONEDRIVE_DESTINATION, filename)
                if os.path.isfile(file_path) and os.path.getmtime(file_path) < cutoff:
                    try:
                        shutil.move(file_path, os.path.join(archive_folder, filename))
                        print(f"📦 Archived old file: {filename}")
                    except Exception as e:
                        print(f"⚠️ Failed to archive {filename}: {e}")

        # === Move Excel to OneDrive ===
        if os.path.exists(EXCEL_FILE):
            os.makedirs(ONEDRIVE_DESTINATION, exist_ok=True)
            shutil.move(EXCEL_FILE, EXCEL_FILE_DEST)
            print(f"📁 Excel file moved to: {EXCEL_FILE_DEST}")
        else:
            print("❌ Excel file not found to move.")

        success_message = "✅ All views processed, cleaned, formatted, exported, and archived successfully."
        print(success_message)
        try:
            messagebox.showinfo("Success", success_message)
        except:
            pass

# === Daily loop at 10:45 AM ===
def wait_until_1045_and_run_daily():
    if args.now:
        print("⏱ Running immediately due to --now argument")
        download_all_views_to_excel(args)
        return

    print("⏳ Script is running. Will execute daily at 10:45 AM...")

    while True:
        now = datetime.datetime.now()
        target_time = now.replace(hour=10, minute=45, second=0, microsecond=0)
        if now >= target_time:
            # If it's already past 10:45 AM today, schedule for the next day
            target_time = target_time + datetime.timedelta(days=1)

        wait_seconds = (target_time - now).total_seconds()
        print(f"🕒 Waiting {int(wait_seconds)} seconds until next execution at {target_time.strftime('%Y-%m-%d %H:%M:%S')}...")
        time.sleep(wait_seconds)

        print(f"\n🕙 Running report for {datetime.datetime.now().strftime('%Y-%m-%d')}...")
        try:
            download_all_views_to_excel(args)
        except Exception as e:
            print(f"❌ Error during execution: {e}")

if __name__ == "__main__":
    launch_gui()