"""
Filename: blazes_updater.py
Description: BLAZES updater executable

Author: Tyler de Zeeuw
License: GPL-3.0
"""

# Built-in imports
import os
import sys
import time
import shlex
import psutil
import shutil
import platform
import subprocess
from datetime import datetime


PLATFORM_NAME = platform.system().lower()
APP_NAME = "blazes"

if PLATFORM_NAME == 'darwin':
    LOG_FILE = os.path.join(os.path.dirname(sys.executable), f"../../../{APP_NAME}_updater.log")
else:
    LOG_FILE = os.path.join(os.getcwd(), f"{APP_NAME}_updater.log")


def log(msg):
    with open(LOG_FILE, "a", encoding="utf-8") as f:
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        f.write(f"{timestamp} - {msg}\n")


def kill_all_processes_by_executable(exe_path):
    terminated_any = False
    exe_path = os.path.realpath(exe_path)

    if PLATFORM_NAME == 'windows':
        for proc in psutil.process_iter(['pid', 'exe']):
            try:
                proc_exe = proc.info.get('exe')
                if proc_exe and os.path.samefile(os.path.realpath(proc_exe), exe_path):
                    log(f"Terminating process: PID {proc.pid}")
                    _terminate_process(proc)
                    terminated_any = True
            except Exception as e:
                log(f"Error terminating process (Windows): {e}")
    elif PLATFORM_NAME == 'linux':
        for proc in psutil.process_iter(['pid', 'cmdline']):
            try:
                cmdline = proc.info.get('cmdline', [])
                if cmdline:
                    proc_cmd = os.path.realpath(cmdline[0])
                    if os.path.samefile(proc_cmd, exe_path):
                        log(f"Terminating process: PID {proc.pid}")
                        _terminate_process(proc)
                        terminated_any = True
            except Exception as e:
                log(f"Error terminating process (Linux): {e}")

    if not terminated_any:
        log(f"No running processes found for {exe_path}")
    return terminated_any


def _terminate_process(proc):
    try:
        proc.terminate()
        proc.wait(timeout=10)
        log(f"Process {proc.pid} terminated gracefully.")
    except psutil.TimeoutExpired:
        log(f"Process {proc.pid} did not terminate in time. Killing forcefully.")
        proc.kill()
        proc.wait(timeout=5)
        log(f"Process {proc.pid} killed.")


def wait_for_unlock(path, timeout=100):
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            if os.path.isdir(path):
                shutil.rmtree(path)
            else:
                os.remove(path)
            log(f"Deleted (after wait): {path}")
            return
        except Exception as e:
            log(f"Still locked: {path} - {e}")
            time.sleep(1)
    log(f"Failed to delete after wait: {path}")
    

def delete_path(path):
    if os.path.exists(path):
        try:
            if os.path.isdir(path):
                shutil.rmtree(path)
                log(f"Deleted directory: {path}")
            else:
                os.remove(path)
                log(f"Deleted file: {path}")
        except Exception as e:
            log(f"Error deleting {path}: {e}")


def copy_update_files(src_folder, dest_folder, updater_name):
    for item in os.listdir(src_folder):
        if item.lower() == updater_name.lower():
            log(f"Skipping updater executable: {item}")
            continue
        s = os.path.join(src_folder, item)
        d = os.path.join(dest_folder, item)
        delete_path(d)
        try:
            if os.path.isdir(s):
                shutil.copytree(s, d)
                log(f"Copied folder: {s} -> {d}")
            else:
                shutil.copy2(s, d)
                log(f"Copied file: {s} -> {d}")
        except Exception as e:
            log(f"Error copying {s} -> {d}: {e}")


def copy_update_files_darwin(src_folder, dest_folder, updater_name):

    updater_name = updater_name + ".app"

    for item in os.listdir(src_folder):
        if item.lower() == updater_name.lower():
            log(f"Skipping updater executable: {item}")
            continue
        s = os.path.join(src_folder, item)
        d = os.path.join(dest_folder, item)
        delete_path(d)
        try:
            if os.path.isdir(s):
                subprocess.check_call(["ditto", s, d])
                log(f"Copied folder with ditto: {s} -> {d}")
            else:
                shutil.copy2(s, d)
                log(f"Copied file: {s} -> {d}")
        except Exception as e:
            log(f"Error copying {s} -> {d}: {e}")


def remove_quarantine(app_path):
    script = f'''
    do shell script "xattr -d -r com.apple.quarantine {shlex.quote(app_path)}" with administrator privileges with prompt "{APP_NAME} needs privileges to finish the update. (1/2)"
    '''
    try:
        subprocess.run(['osascript', '-e', script], check=True)
        print("✅ Quarantine attribute removed.")
    except subprocess.CalledProcessError as e:
        print("❌ Failed to remove quarantine attribute.")
        print(e)


def main():
    try:
        log(f"[Updater] sys.argv: {sys.argv}")

        if len(sys.argv) != 3:
            log(f"Invalid arguments. Usage: {APP_NAME}_updater <update_folder> <main_app_executable>")
            sys.exit(1)

        update_folder = sys.argv[1]
        main_exe = sys.argv[2]

        # Interesting naming convention
        parent_dir = os.path.dirname(os.path.abspath(main_exe))
        pparent_dir = os.path.dirname(parent_dir)
        ppparent_dir = os.path.dirname(pparent_dir)
        pppparent_dir = os.path.dirname(ppparent_dir)

        updater_name = os.path.basename(sys.argv[0])

        log("Updater started.")
        log(f"Update folder: {update_folder}")
        log(f"Main EXE: {main_exe}")
        log(f"Updater EXE: {updater_name}")
        if PLATFORM_NAME == 'darwin':
            log(f"Main App Folder: {ppparent_dir}")

        # Kill all instances of main app
        kill_all_processes_by_executable(main_exe)

        # Wait until main_exe process is fully gone (polling)
        for _ in range(20):  # wait max 10 seconds
            running = False
            for proc in psutil.process_iter(['exe', 'cmdline']):
                try:
                    if PLATFORM_NAME == 'windows':
                        proc_exe = proc.info.get('exe')
                        if proc_exe and os.path.samefile(os.path.realpath(proc_exe), os.path.realpath(main_exe)):
                            running = True
                            break
                    elif PLATFORM_NAME == 'linux':
                        cmdline = proc.info.get('cmdline', [])
                        if cmdline:
                            proc_cmd = os.path.realpath(cmdline[0])
                            if os.path.samefile(proc_cmd, os.path.realpath(main_exe)):
                                running = True
                                break
                except Exception as e:
                    log(f"Polling error: {e}")
            if not running:
                break
            time.sleep(0.5)
        else:
            log("Warning: main executable still running after wait timeout.")

        # Delete old version files
        if PLATFORM_NAME == 'darwin':
            log(f'Attempting to delete {ppparent_dir}')
            delete_path(ppparent_dir)
            update_folder = os.path.join(sys.argv[1], f"{APP_NAME}-darwin")
            copy_update_files_darwin(update_folder, pppparent_dir, updater_name)

        else:
            delete_path(main_exe)
            wait_for_unlock(os.path.join(parent_dir, "_internal"))

            # Copy new files excluding the updater itself
            copy_update_files(update_folder, parent_dir, updater_name)

    except Exception as e:
        log(f"Something went wrong: {e}")

    # Relaunch main app
    try:
        if PLATFORM_NAME == 'linux':
            os.chmod(main_exe, 0o755)
            log("Added executable bit")
    
        if PLATFORM_NAME == 'darwin':
            os.chmod(ppparent_dir, 0o755)
            log("Added executable bit")
            remove_quarantine(ppparent_dir)
            log(f"Removed the quarantine flag on {ppparent_dir}")
            subprocess.Popen(['open', ppparent_dir, "--args", "--finish-update"])
        else:
            subprocess.Popen([main_exe, "--finish-update"], cwd=parent_dir)

        log("Relaunched main app.")
    except Exception as e:
        log(f"Failed to relaunch main app: {e}")

    log("Updater completed. Exiting.")
    sys.exit(0)


if __name__ == "__main__":
    main()