from requests.auth import HTTPBasicAuth
import requests
import json
from pypdf import PdfReader, PdfWriter
import pandas as pd
import mimetypes
import time
from selenium.webdriver.common.by import By
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
import numpy as np
import platform
import subprocess
import os
import getpass

def is_dark_mode() -> bool:
    """
    Detects whether the system appearance is set to dark mode on Windows or macOS.

    Returns:
        bool: True if the OS is in dark mode, False if in light mode, or False by default if undetectable.
    """
    system = platform.system()
    
    # ---- Windows ----
    if system == "Windows":
        try:
            import winreg
            # Registry path for app theme preference
            registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
            key_path = r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
            key = winreg.OpenKey(registry, key_path)
            # 0 = Dark, 1 = Light
            value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
            winreg.CloseKey(key)
            return value == 0
        except Exception:
            return False

    # ---- macOS ----
    else:
        try:
            # The macOS 'defaults' command returns 1 for dark mode
            result = subprocess.run(
                ["defaults", "read", "-g", "AppleInterfaceStyle"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
            )
            return "Dark" in result.stdout
        except Exception:
            return False  # Defaults to light if key not found

def click_button(value, driver, type='XPATH', index=0, timeout=None):
    """
    Clicks a button on a webpage using Selenium WebDriver.

    Args:
        value (str): The value of the locator to find the button.
        driver (selenium.webdriver): The Selenium WebDriver instance.
        type (str, optional): The type of locator to use (e.g., 'XPATH', 'CSS_SELECTOR', 'ID', etc.). Defaults to 'XPATH'.
        timeout (float, optional): The time (in seconds) to attempt clicking before timing out. Defaults to None.
        index (int, optional): The index of the element to click (used for locators that return multiple elements). Defaults to 0.

    Returns:
        True if the button was clicked
        False if the button was not clicked
    """
    
    start_time = time.time()
    locator_types = {
        'XPATH': By.XPATH,
        'CLASS_NAME': By.CLASS_NAME,
        'CSS_SELECTOR': By.CSS_SELECTOR,
        'ID': By.ID,
        'NAME': By.NAME,
        'LINK_TEXT': By.LINK_TEXT,
        'PARTIAL_LINK_TEXT': By.PARTIAL_LINK_TEXT,
        'TAG_NAME': By.TAG_NAME,
    }

    if type not in locator_types:
        raise ValueError(f"Invalid locator type '{type}'. Valid options are: {list(locator_types.keys())}")

    while True:
        try:
            if index != 0:  # Handle multiple elements for class_name
                elements = driver.find_elements(locator_types[type], value)
                if len(elements) <= index:
                    raise IndexError(f"No element found at index {index} for {type} '{value}'.")
                button = elements[index]
            else:
                button = driver.find_element(locator_types[type], value)
            
            button.click()
            return True
        except Exception as e:
            if timeout is not None and (time.time() - start_time) > timeout:
                print(f'Error clicking button: {e}')
                return False

def send_text(text, value, driver, type='XPATH', index=0, timeout=None):
    """
    Sends text to a specified input field on a webpage using Selenium WebDriver.

    Args:
        text (str): The text to send to the input field.
        value (str): The value of the locator to find the input field.
        driver (selenium.webdriver): The Selenium WebDriver instance.
        type (str, optional): The type of locator to use (e.g., 'XPATH', 'CSS_SELECTOR', 'ID', etc.). Defaults to 'XPATH'.
        index (int, optional): The index of the element to send text to (used for locators that return multiple elements). Defaults to 0.
        timeout (float, optional): The time (in seconds) to attempt sending text before timing out. Defaults to None.

    Returns:
        True if the text was entered properly
        False if the text was not entered properly
    """
    
    start_time = time.time()
    locator_types = {
        'XPATH': By.XPATH,
        'CLASS_NAME': By.CLASS_NAME,
        'CSS_SELECTOR': By.CSS_SELECTOR,
        'ID': By.ID,
        'NAME': By.NAME,
        'LINK_TEXT': By.LINK_TEXT,
        'PARTIAL_LINK_TEXT': By.PARTIAL_LINK_TEXT,
        'TAG_NAME': By.TAG_NAME,
    }

    if type not in locator_types:
        raise ValueError(f"Invalid locator type '{type}'. Valid options are: {list(locator_types.keys())}")

    while True:
        try:
            if index != 0:
                elements = driver.find_elements(locator_types[type], value)
                if len(elements) <= index:
                    raise IndexError(f"No element found at index {index} for {type} '{value}'.")
                input_field = elements[index]
            else:
                input_field = driver.find_element(locator_types[type], value)
            input_field.send_keys(text)
            return True
        except Exception as e:
            if timeout is not None and (time.time() - start_time) > timeout:
                print(f'Error sending text: {e}')
                return False

def switch_to_iframe(value, driver, type='XPATH', index=0, timeout=None):
    """
    Switches the WebDriver context to a specified iframe.

    Args:
        value (str): The value of the locator to find the iframe.
        driver (selenium.webdriver): The Selenium WebDriver instance.
        type (str, optional): The type of locator to use (e.g., 'XPATH', 'CSS_SELECTOR', 'ID', etc.). Defaults to 'XPATH'.
        index (int, optional): The index of the iframe to switch to (used for locators that return multiple elements). Defaults to 0.
        timeout (float, optional): The time (in seconds) to attempt switching before timing out. Defaults to None.

    Returns:
        True if the iframe was located and switched to
        False if the iframe was not located and not switched to
    """
    start_time = time.time()
    locator_types = {
        'XPATH': By.XPATH,
        'CLASS_NAME': By.CLASS_NAME,
        'CSS_SELECTOR': By.CSS_SELECTOR,
        'ID': By.ID,
        'NAME': By.NAME,
        'LINK_TEXT': By.LINK_TEXT,
        'PARTIAL_LINK_TEXT': By.PARTIAL_LINK_TEXT,
        'TAG_NAME': By.TAG_NAME,
    }

    if type not in locator_types:
        raise ValueError(f"Invalid locator type '{type}'. Valid options are: {list(locator_types.keys())}")

    while True:
        try:
            if index != 0:
                elements = driver.find_elements(locator_types[type], value)
                if len(elements) <= index:
                    raise IndexError(f"No iframe found at index {index} for {type} '{value}'.")
                iframe = elements[index]
            else:
                iframe = driver.find_element(locator_types[type], value)
            
            driver.switch_to.frame(iframe)
            return True
        except Exception as e:
            if timeout is not None and (time.time() - start_time) > timeout:
                print(f'Error switching to iframe: {e}')
                return False


def switch_to_default_frame(driver):
    """
    Switches the WebDriver context back to the default content frame.

    Args:
        driver (selenium.webdriver): The Selenium WebDriver instance.

    Returns:
        True
    """
    driver.switch_to.default_content()
    return True

def refresh_quickbooks_access_token(QUICKBOOKS_TOKENS, QUICKBOOKS_CLIENT_ID, QUICKBOOKS_CLIENT_SECRET):

    """
    Refreshes the QuickBooks Online OAuth2 access token.

    Args:
        QUICKBOOKS_TOKENS (dict): A dictionary containing the 'accessToken' and 'refreshToken'.
        QUICKBOOKS_CLIENT_ID (str): The QuickBooks client ID.
        QUICKBOOKS_CLIENT_SECRET (str): The QuickBooks client secret.

    Returns:
        dict: The updated QuickBooks tokens.
    """

    # QuickBooks Online OAuth2 token endpoint
    token_url = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'

    # Prepare the request payload
    payload = {
        'grant_type': 'refresh_token',
        'refresh_token': QUICKBOOKS_TOKENS['refreshToken'],
    }

    # Make the request to get a new access token
    response = requests.post(
        token_url,
        data=payload,
        auth=HTTPBasicAuth(QUICKBOOKS_CLIENT_ID, QUICKBOOKS_CLIENT_SECRET),
        headers={'Accept': 'application/json'},
    )

    # Check if the request was successful
    if response.status_code == 200:
        # Parse the JSON response
        new_tokens = response.json()
        QUICKBOOKS_TOKENS['accessToken'] = new_tokens.get('access_token')
        QUICKBOOKS_TOKENS['refreshToken'] = new_tokens.get('refresh_token')
        print('TOKENS REFRESHED SUCCESSFULLY')
        # print(json.dumps(new_tokens,indent=2))
    else:
        # Handle errors
        print(f"FAILED TO REFRESH TOKENS: {response.status_code}")
        #print(response.json())
    return QUICKBOOKS_TOKENS


def get_pdf(filepath):
    """
    Extracts form fields from a PDF and returns them as a Pandas DataFrame.

    Args:
        filepath (str): The path to the PDF file.

    Returns:
        pandas.DataFrame: A DataFrame containing field names, types, and values.
    """
    reader = PdfReader(filepath)

    fields = reader.get_fields()

    field_names = []
    field_types = []
    field_values = []

    for field_name, field in fields.items():
        field_type = field.get('/FT', '')
        field_value = field.get('/V', '')

        field_names.append(field_name)
        field_types.append(field_type)
        field_values.append(field_value)

    df = pd.DataFrame(columns=['name', 'type', 'value'])
    df['name'] = field_names
    df['type'] = field_types
    df['value'] = field_values
    return df

def create_pdf(templatepath, topath, update_dict):
    """
    Creates a new PDF by updating fields in a template PDF.

    Args:
        templatepath (str): The path to the template PDF.
        topath (str): The path to save the updated PDF.
        update_dict (dict): A dictionary of field names and values to update.

    Returns:
        None
    """
    # Open the PDF
    reader = PdfReader(templatepath)
    writer = PdfWriter()
    writer.append(reader)

    # Write data to each page in the PDF file
    pagenum = 0
    while True:
        try:
            writer.update_page_form_field_values(writer.pages[pagenum], fields={k: str(v) for k, v in update_dict.items()}, auto_regenerate=False)
            pagenum = pagenum + 1
        except Exception: # An exception is raised when the pagenum exceeds the number of pages in the PDF document. When that happens, we are done adding data to the PDF file so we break out of this while loop.
            break

    # Save the updated PDF
    with open(topath, "wb") as pdf_output:
        writer.write(pdf_output)

# Add attachment to a GMAIL email

def add_attachment(path_to_file, message):
    """
    Adds an attachment to an email message.

    Args:
        path_to_file (str): The path to the file to attach.
        message (email.message.EmailMessage): The email message object.

    Returns:
        email.message.EmailMessage: The updated email message object with the attachment.
    """

    # Extract filename cross-platform
    filename = os.path.basename(path_to_file)

    # if '/' in path_to_file:
    #     filename = path_to_file.split('/')[-1]
    # if '\\' in path_to_file:
    #     filename = path_to_file.split('\\')[-1]

    mime_type, _ = mimetypes.guess_type(path_to_file)
    # Fallback for unknown types
    if mime_type is None:
        mime_type, mime_subtype = 'application', 'octet-stream'
    else:
        mime_type, mime_subtype = mime_type.split('/')

    with open(path_to_file, 'rb') as file:
        message.add_attachment(file.read(), maintype=mime_type, subtype=mime_subtype, filename=filename)
    return message

def single_input(prompt: str, mask: bool = False, width: int = 300, height: int = 150):
    """
    Displays a Tkinter input dialog box and retrieves a user input.

    Args:
        prompt (str): The prompt text displayed in the dialog.
        mask (bool, optional): Whether to mask the input (e.g., for passwords). Defaults to False.
        width (int, optional): Minimum width of the dialog. Defaults to 300.
        height (int, optional): Minimum height of the dialog. Defaults to 150.

    Returns:
        str: The user input as a string.
    """
    root = tk.Tk()
    root.attributes('-topmost', True)
    root.title("Input")

    var = tk.StringVar()

    # Check if system is in dark_mode
    dark_mode = is_dark_mode()

    # ----- Color Schemes -----
    if not dark_mode:
        footer_bg = root.cget("bg")
        body_bg = "white"
        input_border_color = "#d9d9d9"
        entry_bg = "white"
        text_color = "black"
        button_bg = "#f0f0f0"
        button_fg = "black"
    else:
        footer_bg = "#252526"
        body_bg = "#1E1E1E"
        input_border_color = "#3E3E3E"
        entry_bg = "#2D2D2D"
        text_color = "#FFFFFF"
        button_bg = "#3A3A3A"
        button_fg = "#FFFFFF"
        root.configure(bg=body_bg)  # ✅ Make entire window background dark

        # ✅ Custom ttk dark theme
        style = ttk.Style()
        style.theme_use('clam')  # Base theme that supports custom colors
        style.configure(
            "Dark.TButton",
            background=button_bg,
            foreground=button_fg,
            borderwidth=1,
            focusthickness=3,
            focuscolor='none'
        )
        style.map(
            "Dark.TButton",
            background=[("active", "#505050")],  # hover color
            foreground=[("active", "#FFFFFF")]
        )

    # ----- Body -----
    body_frame = tk.Frame(root, bg=body_bg)
    body_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

    prompt_label = tk.Label(body_frame, text=prompt, bg=body_bg, fg=text_color, wraplength=width - 20, font=("Helvetica", 12))
    prompt_label.pack(pady=10)

    input_row_frame = tk.Frame(body_frame, bg=body_bg)
    input_row_frame.pack(fill=tk.X, pady=10)

    entry_frame = tk.Frame(input_row_frame, bg=input_border_color, padx=1, pady=1)
    entry_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)

    entry = tk.Entry(entry_frame, textvariable=var, font=("Helvetica", 12),
                     bg=entry_bg, fg=text_color, insertbackground=text_color,
                     show='*' if mask else '')
    entry.pack(fill=tk.X)

    # ----- Footer -----
    footer_frame = tk.Frame(root, bg=footer_bg)
    footer_frame.pack(fill=tk.X, side=tk.BOTTOM)

    def save_close():
        root.after(1, root.destroy())

    entry.bind('<Return>', lambda event: save_close())

    # ✅ Use dark-styled button if dark_mode
    button_style = "Dark.TButton" if dark_mode else "TButton"
    confirm_button = ttk.Button(footer_frame, text='Confirm', command=save_close, style=button_style)
    confirm_button.pack(side=tk.RIGHT, padx=10, pady=10)

    def set_focus():
        entry.focus_force()
        root.attributes('-topmost', False)

    root.update_idletasks()
    content_width = max(root.winfo_reqwidth(), width)
    content_height = max(root.winfo_reqheight(), height)
    root.geometry(f"{content_width}x{content_height}")

    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    x = (screen_width - content_width) // 2
    y = (screen_height - content_height) // 2
    root.geometry(f"+{x}+{y}")

    root.after(50, set_focus)
    root.mainloop()

    return var.get()

def input_form(prompt: str=None, inputs: list=None, masks: list=None, width: int = 0, height: int = 0):
    
    """
    Create a dynamic tkinter-based input form with customizable input fields and optional masking for secure input.

    Args:
        prompt (str, optional): Text to display at the top of the form. Defaults to None.
        inputs (list): A list of strings representing the labels for each input field. 
            This argument is required.
        masks (list, optional): A list of booleans indicating whether each input field 
            should be masked (e.g., for password input). Defaults to None.
            If provided, the length of `masks` must match the length of `inputs`.
        width (int, optional): The minimum width of the form in pixels. 
            Automatically calculated if not provided. Defaults to 0.
        height (int, optional): The minimum height of the form in pixels. 
            Automatically calculated if not provided. Defaults to 0.

    Raises:
        Exception: If the `inputs` argument is not provided.
        Exception: If the lengths of `inputs` and `masks` (if provided) do not match.

    Returns:
        dict: A dictionary where the keys are the input labels and the values 
            are the user-entered responses as strings.

    Example:
        To collect a username and password:
        
        ```python
        inputs = ["Username", "Password"]
        masks = [False, True]
        user_data = input_form(prompt="Please enter your login details:", inputs=inputs, masks=masks)
        print(user_data)  # {'Username': 'entered_username', 'Password': 'entered_password'}
        ```

    Notes:
        - The form dynamically adjusts its size based on the number of input fields.
        - A confirm button allows users to submit their input and close the form.
        - Pressing the Enter key in the last input field also submits the form.

    """

    if inputs is None:
        raise Exception("inputs must be declared.")
    if masks is not None and inputs is not None:
        if len(masks) != len(inputs):
            raise Exception("The length of masks does not match the length of inputs.")

    root = tk.Tk()
    root.attributes('-topmost', True)
    root.focus_force()
    root.title('Input Form')

    # Check if system is in dark_mode
    dark_mode = is_dark_mode()

    # ----- Color Schemes -----
    if not dark_mode:
        footer_bg = root.cget("bg")
        body_bg = "white"
        input_border_color = "#d9d9d9"
        entry_bg = "white"
        text_color = "black"
        button_bg = "#f0f0f0"
        button_fg = "black"
    else:
        footer_bg = "#252526"
        body_bg = "#1E1E1E"
        input_border_color = "#3E3E3E"
        entry_bg = "#2D2D2D"
        text_color = "#FFFFFF"
        button_bg = "#3A3A3A"
        button_fg = "#FFFFFF"

        # ✅ Make entire window dark
        root.configure(bg=body_bg)

        # ✅ Create dark ttk button style
        style = ttk.Style()
        style.theme_use('clam')
        style.configure(
            "Dark.TButton",
            background=button_bg,
            foreground=button_fg,
            borderwidth=1,
            focusthickness=3,
            focuscolor='none'
        )
        style.map(
            "Dark.TButton",
            background=[("active", "#505050")],
            foreground=[("active", "#FFFFFF")]
        )

    # ----- Layout Setup -----
    label_width = max(len(x) for x in inputs) * 8
    input_field_height = 30
    padding = 20
    footer_height = 50
    prompt_height = 60

    required_width = max(width, label_width + 220)
    required_height = max(height, prompt_height + footer_height + len(inputs) * input_field_height + padding + 40)

    # Center window
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    x_offset = (screen_width - required_width) // 2
    y_offset = (screen_height - required_height) // 2
    root.geometry(f'{required_width}x{required_height}+{x_offset}+{y_offset}')
    root.minsize(required_width, required_height)

    # ----- Main Body -----
    body_frame = tk.Frame(root, bg=body_bg)
    body_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

    if prompt:
        tk.Label(
            body_frame,
            text=prompt,
            bg=body_bg,
            fg=text_color,
            wraplength=required_width - 20,
            font=("Helvetica", 12)
        ).pack(pady=10)

    input_fields_frame = tk.Frame(body_frame, bg=body_bg)
    input_fields_frame.pack(fill=tk.BOTH, expand=True, pady=10)

    # ----- Input Fields -----
    values = {}
    last_entry = None
    first_entry = None

    for i, label_text in enumerate(inputs):
        try:
            is_masked = masks[i]
        except Exception:
            is_masked = False

        input_frame = tk.Frame(input_fields_frame, bg=body_bg)
        input_frame.pack(fill=tk.X, pady=5)

        tk.Label(
            input_frame,
            text=label_text + ":",
            bg=body_bg,
            fg=text_color,
            font=("Helvetica", 12),
            anchor='e',
            width=15
        ).pack(side=tk.LEFT, padx=5)

        entry_frame = tk.Frame(input_frame, bg=input_border_color, padx=0, pady=3)
        entry_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)

        entry = tk.Entry(
            entry_frame,
            show='*' if is_masked else '',
            font=("Helvetica", 12),
            bg=entry_bg,
            fg=text_color,
            insertbackground=text_color
        )
        entry.pack(fill=tk.X, padx=5)

        values[label_text] = entry
        last_entry = entry
        if first_entry is None:
            first_entry = entry

    # ----- Footer -----
    footer_frame = tk.Frame(root, bg=footer_bg, height=footer_height)
    footer_frame.pack(fill=tk.X, side=tk.BOTTOM)
    footer_frame.grid_propagate(False)

    user_input_values = {}

    def save_close():
        nonlocal user_input_values
        user_input_values = {key: entry.get() for key, entry in values.items()}
        root.after(1, root.destroy())

    # ✅ Apply dark button style if dark mode
    button_style = "Dark.TButton" if dark_mode else "TButton"
    confirm_button = ttk.Button(footer_frame, text='Confirm', command=save_close, style=button_style)
    confirm_button.pack(side=tk.RIGHT, padx=20, pady=10)

    if last_entry:
        last_entry.bind('<Return>', lambda event: save_close())

    if first_entry:
        root.after(100, lambda: first_entry.focus_set())

    root.mainloop()
    return user_input_values

def show_message(title: str, message: str, width: int = 0, height: int = 0):
    """
    Display a customizable message window using Tkinter.

    This function creates a pop-up window with a specified title and message. 
    The window is centered on the screen and automatically adjusts its size 
    based on the message content, ensuring readability. The user can optionally 
    specify minimum width and height dimensions for the window.

    Args:
        title (str): The title of the message window.
        message (str): The message to display in the window. 
                       Text will wrap to fit the width of the window.
        width (int, optional): The minimum width of the window in pixels. 
                               Defaults to 0, allowing the size to be 
                               determined by the content.
        height (int, optional): The minimum height of the window in pixels. 
                                Defaults to 0, allowing the size to be 
                                determined by the content.

    Returns:
        None
    """

    # Initialize Tkinter root window
    root = tk.Tk()
    root.attributes('-topmost', True)
    root.title(title)

    # Check if system is in dark_mode
    dark_mode = is_dark_mode()

    # ----- Color Schemes -----
    if not dark_mode:
        footer_bg = root.cget("bg")
        body_bg = "white"
        text_color = "black"
        button_bg = "#f0f0f0"
        button_fg = "black"
    else:
        footer_bg = "#252526"
        body_bg = "#1E1E1E"
        text_color = "#FFFFFF"
        button_bg = "#3A3A3A"
        button_fg = "#FFFFFF"

        # ✅ Apply dark background to entire window
        root.configure(bg=body_bg)

        # ✅ Create dark ttk button style
        style = ttk.Style()
        style.theme_use('clam')
        style.configure(
            "Dark.TButton",
            background=button_bg,
            foreground=button_fg,
            borderwidth=1,
            focusthickness=3,
            focuscolor='none'
        )
        style.map(
            "Dark.TButton",
            background=[("active", "#505050")],
            foreground=[("active", "#FFFFFF")]
        )

    # Create a temporary label to measure the required size
    temp_label = tk.Label(text=message, font=('Helvetica', 12), wraplength=width - 20)
    temp_label.update_idletasks()  # Update geometry calculations

    # Get the required width and height
    required_width = temp_label.winfo_reqwidth() + 20
    required_height = temp_label.winfo_reqheight() + 80  # Add space for padding and buttons
    temp_label.destroy()

    # Ensure the dimensions respect the minimum size constraints
    final_width = max(width, required_width)
    final_height = max(height, required_height)

    # Center the window on the screen
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    x = (screen_width - final_width) // 2
    y = (screen_height - final_height) // 2

    root.geometry(f'{final_width}x{final_height}+{x}+{y}')
    root.minsize(width, height)

    # ----- Message Frame -----
    message_frame = tk.Frame(root, bg=body_bg)
    message_frame.pack(fill='both', expand=True)

    # Message label
    message_label = tk.Label(
        message_frame,
        text=message,
        bg=body_bg,
        fg=text_color,
        wraplength=final_width - 20,
        font=('Helvetica', 12)
    )
    message_label.place(relx=0.5, rely=0.5, anchor='center')

    # ----- Footer -----
    footer_frame = tk.Frame(root, bg=footer_bg)
    footer_frame.pack(fill='x', side='bottom')

    # OK Button using ttk for native styling
    def close_window():
        root.after(1, root.destroy())

    button_style = "Dark.TButton" if dark_mode else "TButton"
    ok_button = ttk.Button(footer_frame, text="OK", command=close_window, style=button_style)
    ok_button.pack(pady=10, side='right', padx=10)

    # Bind the Enter key to close the window
    root.bind('<Return>', lambda event: close_window())

    # Ensure the window gains focus
    root.focus_force()
    
    root.mainloop()

def select_file(title: str, filetypes: list=None):
    """
    Opens a file dialog for the user to select a file.

    Args:
        title (str): The title for the file selection dialog.
        filetypes (list): A list of file type filters (e.g., [("Text files", "*.txt")]).

    Returns:
        str: The path of the selected file or an empty string if no file is selected.
    """

    show_message("Select File", title, width=4, height=5)

    root = tk.Tk()
    root.withdraw()  # Hide the root window
    root.attributes('-topmost', True)  # Ensure the dialog appears on top

    # Open file dialog with the specified title and file types
    if filetypes is not None:
        selected_file = filedialog.askopenfilename(title=title, filetypes=filetypes)
    else:
        selected_file = filedialog.askopenfilename(title=title)
    # Destroy the root window after use
    root.after(1, root.destroy())

    return selected_file

def select_file_alt(title: str, filetypes: list=None):
    """
    Console-based alternative to select_file using plain input.

    Args:
        title (str): The title or description for the file selection.
        filetypes (list, optional): Ignored in this console version.

    Returns:
        str: Path to the selected file (validated to exist).
    """
    print(f"\n--- {title} ---")
    while True:
        path = input("Enter full path to the file: ").strip('"').strip("'")
        if os.path.exists(path):
            return path
        else:
            print("❌ File not found. Please try again.")



def single_input_alt(prompt: str, mask: bool = False, width: int = 300, height: int = 150):
    """
    Console-based alternative to single_input using input() or getpass().
    """
    print(f"\n{prompt}")
    if mask:
        return getpass.getpass("Input (hidden): ")
    else:
        return input("Input: ")


def input_form_alt(prompt: str=None, inputs: list=None, masks: list=None, width: int = 0, height: int = 0):
    """
    Console-based alternative to input_form.
    """
    if inputs is None:
        raise Exception("inputs must be declared.")
    if masks is not None and len(masks) != len(inputs):
        raise Exception("Length of masks must match inputs.")

    print()
    if prompt:
        print(f"--- {prompt} ---")

    responses = {}
    for i, label in enumerate(inputs):
        masked = masks[i] if masks else False
        if masked:
            responses[label] = getpass.getpass(f"{label}: ")
        else:
            responses[label] = input(f"{label}: ")
    return responses

def show_message_alt(title: str, message: str, width: int = 0, height: int = 0):
    """
    Console-based alternative to show_message.
    """
    print(f"\n=== {title} ===")
    print(message)
    #input("\nPress Enter to continue...")


def winsorize(data, lower_percentile, upper_percentile):
    """
    Winsorize a numeric array or list by handling extreme values.
    Missing values (NaN) are ignored in percentile calculations and preserved in output.
    """
    data = np.array(data, dtype=float)

    # Handle case where all values are NaN
    if np.all(np.isnan(data)):
        return data

    # Determine thresholds ignoring NaNs
    lower_threshold = np.nanpercentile(data, lower_percentile * 100)
    upper_threshold = np.nanpercentile(data, upper_percentile * 100)

    # Clip non-NaN values to the thresholds
    data_processed = np.where(np.isnan(data), np.nan, np.clip(data, lower_threshold, upper_threshold))

    return data_processed


def truncate(data, lower_percentile, upper_percentile):
    """
    Truncate a numeric array or list by handling extreme values.
    Missing values (NaN) are ignored in percentile calculations and preserved in output.
    """
    data = np.array(data, dtype=float)

    # Handle case where all values are NaN
    if np.all(np.isnan(data)):
        return data

    # Determine thresholds ignoring NaNs
    lower_threshold = np.nanpercentile(data, lower_percentile * 100)
    upper_threshold = np.nanpercentile(data, upper_percentile * 100)

    # Replace outliers with NaN, keep existing NaNs as is
    data_processed = np.where(
        np.isnan(data) | (data < lower_threshold) | (data > upper_threshold),
        np.nan,
        data
    )

    return data_processed
