#!/usr/bin/env python3
import os, sys, time, hashlib, random, string, requests, re, shutil, tkinter as tk
from tkinter import messagebox, simpledialog

#WebLinks Utility to pull URLs from Cloudshare Metadata andAPI and insert onto desktop

# --- Logger ---
class Logger:
    def __init__(self, logfile):
        self.logfile = logfile
        with open(self.logfile, "w") as f:
            f.write(f"Starting WebLinks V2.0 -- {time.ctime()}\n")

    def log(self, msg=""):
        print(msg)
        with open(self.logfile, "a") as f:
            f.write(msg + "\n")


# --- Cloudshare API Client ---
class CloudshareClient:
    def __init__(self, client_id, client_secret, logger):
        self.client_id = client_id
        self.client_secret = client_secret
        self.logger = logger
        self.oauth_url = "https://api.accelerate.cloudshare.com/v4/oauth/token"
        self._metadata_cache = None

    def get_metadata(self, timeout: int = 3):
        """
        Retrieve and cache all metadata from CloudShare.
        Data is cached after first call to avoid redundant requests.
        """
        if self._metadata_cache is not None:
            return self._metadata_cache
        
        url = "https://metadata.cloudshare.com/api/v3/unauthenticated/metadata"
        try:
            resp = requests.get(url, timeout=timeout)
            resp.raise_for_status()
            self._metadata_cache = resp.json()
            self.logger.log(f": CS metadata is: {resp.text}")
            return self._metadata_cache
        except requests.exceptions.Timeout:
            self.logger.log(f"Error: Metadata request timed out after {timeout} seconds.")
            return {}
        except requests.exceptions.RequestException as e:
            self.logger.log(f"Error retrieving metadata: {e}")
            return {}

    def get_class_id(self, timeout: int = 3):
        """
        Retrieve the class-id from CloudShare metadata.
        Uses cached metadata to avoid redundant requests.
        """
        metadata = self.get_metadata(timeout=timeout)
        class_id = metadata.get("class-id")
        if not class_id or class_id == "null":
            self.logger.log("Error: No valid class-id found in metadata response.")
            return None
        return class_id

    def get_custom_property(self, property_name, timeout: int = 3):
        """
        Retrieve a custom property from CloudShare metadata.
        Custom properties are nested under 'custom-properties' key.
        """
        metadata = self.get_metadata(timeout=timeout)
        custom_props = metadata.get("custom-properties", {})
        value = custom_props.get(property_name)
        return value

    def get_stud_reg_num(self, timeout: int = 3):
        """
        Retrieve the student registration number from custom properties.
        """
        return self.get_custom_property("studRegNum", timeout=timeout)

    def has_custom_property(self, property_name, timeout: int = 3):
        """
        Check if a custom property is defined and not null.
        Returns True if property exists and has a non-null, non-empty value.
        """
        metadata = self.get_metadata(timeout=timeout)
        custom_props = metadata.get("custom-properties", {})
        # Check if property exists in custom_props
        if property_name not in custom_props:
            return False
        value = custom_props.get(property_name)
        return value is not None and value != "" and value != "null"

    def custom_property_exists_but_null(self, property_name, timeout: int = 3):
        """
        Check if a custom property exists but is null/empty.
        Returns True if property is defined but has no value set.
        """
        metadata = self.get_metadata(timeout=timeout)
        custom_props = metadata.get("custom-properties", {})
        # Check if property exists in custom_props
        if property_name not in custom_props:
            return False
        value = custom_props.get(property_name)
        return value is None or value == "" or value == "null"

    def get_all_group_links(self, timeout: int = 3):
        """
        Retrieve all group links from custom properties.
        Collects links numbered 1 through 8, stopping when a link is not found or is null.
        Returns a list of non-null links found.
        """
        links = []
        for i in range(1, 9):  # Check groups 1 through 8
            property_name = f"group {i} Link"
            value = self.get_custom_property(property_name, timeout=timeout)
            
            # Stop if link is not found or is null
            if value is None or value == "" or value == "null":
                break
            
            links.append({
                "group": i,
                "property": property_name,
                "link": value
            })
        
        return links

    def parse_group_links_from_description(self, description):
        """
        Parse group links from description format: G1:domain1,G2:domain2,G3:domain3
        Converts to full URLs by prepending 'http://www.' and appending '.com'
        Returns an array in the same format as get_all_group_links()
        """
        links = []
        if not description:
            return links
        
        try:
            # Split by comma to get individual group entries
            entries = description.split(",")
            
            for entry in entries:
                entry = entry.strip()
                if not entry or ":" not in entry:
                    continue
                
                # Split by colon to get group number and domain
                parts = entry.split(":")
                if len(parts) != 2:
                    continue
                
                group_str = parts[0].strip()  # e.g., "G1"
                domain = parts[1].strip()      # e.g., "gfajksdfgj"
                
                # Extract group number from "G1", "G2", etc.
                if group_str.upper().startswith("G"):
                    try:
                        group_num = int(group_str[1:])
                        # Build full URL
                        full_url = f"http://www.{domain}.com"
                        
                        links.append({
                            "group": group_num,
                            "property": f"group {group_num} Link",
                            "link": full_url
                        })
                    except ValueError:
                        continue
        except Exception as e:
            self.logger.log(f"Error parsing group links from description: {e}")
        
        return links

    def get_class_name(self, classid):
        api_url = f"https://use.cloudshare.com/api/v3/class/{classid}"
        timestamp = str(int(time.time()))
        token = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
        hmac_input = f"{self.client_secret}{api_url}{timestamp}{token}"
        hmac_val = hashlib.sha1(hmac_input.encode()).hexdigest()
        auth_param = f"userapiid:{self.client_id};timestamp:{timestamp};token:{token};hmac:{hmac_val}"

        resp = requests.get(api_url, headers={
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": f"cs_sha1 {auth_param}"
        })
        env = resp.json().get("name")
        self.logger.log(f": Class name pulled from API is: {env}")
        return env

    def get_access_token(self):
        post_data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret
        }
        resp = requests.post(self.oauth_url, data=post_data)
        tokdata = resp.json()
        access_token = tokdata.get("access_token")
        token_type = tokdata.get("token_type")
        expires_in = tokdata.get("expires_in")

        if not access_token:
            self.logger.log("Error: Failed to obtain OAuth2 Access Token.")
            sys.exit(1)

        self.logger.log(": Cloudshare API access token obtained successfully!")
        self.logger.log(f": Token Type: {token_type}")
        self.logger.log(f": Expires In: {expires_in} seconds")
        return token_type, access_token

    def get_description(self, env, token_type, access_token):
        api_url = f"https://api.accelerate.cloudshare.com/v4/trainings?name=contains({env})"
        resp = requests.get(api_url, headers={
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": f"{token_type} {access_token}"
        })
        desc = resp.json()["items"][0]["description"]
        #openai = desc.split("=")[1] if "=" in desc else None
        self.logger.log(f": Description field pulled is: {desc}")
        #self.logger.log(f": Pulled value to use is: {openai}")
        return desc


# --- File Editor ---
class FileEditor:
    def __init__(self, logger):
        self.logger = logger

    def update_key_in_files(self, files, key):
        edited = False
        for f in files:
            if os.path.isfile(f):
                with open(f, "r+") as fh:
                    content = fh.read()
                    content = re.sub(r'OPENAI_API_KEY=.*', f'OPENAI_API_KEY={key}', content)
                    fh.seek(0); fh.write(content); fh.truncate()
                self.logger.log(f": Edited OpenAI key into file {f}")
                edited = True
            else:
                self.logger.log(f": Did not locate file {f}")
        return edited

    def replace_file_with_key(self, files, key):
        edited = False
        for f in files:
            if os.path.isfile(f):
                with open(f, "w") as fh:
                    fh.write(key)
                self.logger.log(f": Inserted OpenAI key into file {f}")
                edited = True
            else:
                self.logger.log(f": Did not locate file {f}")
        return edited


# --- Tkinter Popup Manager with Timeout ---
class TkPopupManager:
    def __init__(self, enabled, logger, timeout=120):
        self.enabled = enabled
        self.logger = logger
        self.timeout = timeout
        self.root = tk.Tk()
        self.root.withdraw()

    def ask_install(self):
        if not self.enabled:
            return True

        result = [None]

        def default_yes():
            if result[0] is None:
                result[0] = True
                self.root.quit()

        # Schedule timeout
        self.root.after(self.timeout * 1000, default_yes)

        result[0] = messagebox.askyesno("OpenAI Key",
                                        "Do you wish to install the course OpenAI key?\n\n(times out defaults to YES)")
        if result[0] is False:
            self.logger.log("Exited without installing the key")
            return False
        return True

    def notify_success(self):
        if self.enabled:
            messagebox.showinfo("OpenAI Key", "The OpenAI key has been updated")


# --- Main ---
def main():
    # OPTIONAL ITEMS
    # Option to set hostname, option to disable info popups for silent operation, option for no group icon placement 
    # Comment out to disable and set browser as required (FIREFOX/CHROME/EDGE)
    # SETHOSTNAME="YES"
    setpopups = "YES" if not any(arg in sys.argv for arg in ["-s", "/s"]) else ""
    seticons = "YES"
    setbrowser = "EDGE"
    
    # Use current working directory for log file to ensure it's writable in PyInstaller
    log_path = os.path.join(os.getcwd(), "WebLinks.log")
    logger = Logger(log_path)
    client = CloudshareClient(os.getenv("CLIENT_ID", "538GACRZ9FABA07E"),
                              os.getenv("CLIENT_SECRET", "bkAyYSB3dL8GREF87zfqhjkUcW5xNSVyj0HDpQBqheX5vcHpbLE8WXqYjGmGMORt"),
                              logger)
    editor = FileEditor(logger)
    popups = TkPopupManager(setpopups, logger, timeout=120)

    # Check if Google Chrome is required and installed
    if setbrowser == "CHROME" and shutil.which("google-chrome") is None:
        print("Google Chrome browser is not installed. Please install it first.")
        sys.exit(1)

    # Check if Firefox is required and installed
    if setbrowser == "FIREFOX" and shutil.which("firefox") is None:
        print("Firefox browseris not installed. Please install it first.")
        sys.exit(1)

    # Check if Edge is required and installed
    if setbrowser == "EDGE" and shutil.which("microsoft-edge") is None:
        print("Edge browser is not installed. Please install it first.")
        sys.exit(1)

    logger.log(f": The {setbrowser} browser is installed")

    classid = client.get_class_id()
    if not classid or classid == "null":
        sys.exit(1)
    logger.log(f": The class-id pulled from metadatais {classid}")

    # Check for custom property (e.g., 'group 1 Link')
    if client.has_custom_property("group 1 Link"):
        #student case here
        group_links = client.get_all_group_links()
        logger.log(f": Found {len(group_links)} group links:")
        for link_info in group_links:
            logger.log(f":   Group {link_info['group']}: {link_info['link']}")
    elif client.custom_property_exists_but_null("group 1 Link"):
        logger.log(": Custom property 'group 1 Link' exists but has not been set (is null)")
        # Instructor case here - extract from description field
        env = client.get_class_name(classid)
        token_type, access_token = client.get_access_token()
        description = client.get_description(env, token_type, access_token)
        group_links = client.parse_group_links_from_description(description)
        logger.log(f": Found {len(group_links)} group links from description:")
        for link_info in group_links:
            logger.log(f":   Group {link_info['group']}: {link_info['link']}")
    else:
        logger.log(": Custom property 'group 1 Link' is not defined")
        group_links = []

    

    if not group_links:
        logger.log("No key provided in VM metadata")
        sys.exit(1)

    if not popups.ask_install():
        sys.exit(0)

    edited = False
    edited |= editor.update_key_in_files([
        "/home/student/.config/shell_gpt/.sgptrc",
        "/home/.bashsrc",
        "/home/student/.bashrc"
    ], openai_key)

    edited |= editor.replace_file_with_key([
        "/home/student/keys/key.txt",
        "/home/student/Keys/Key.txt",
        "/home/keys/key.txt"
    ], openai_key)

    if edited:
        popups.notify_success()

    logger.log("\nAll listed files have been edited if found, Exiting")


if __name__ == "__main__":
    main()
