#!/usr/bin/env python3
import time
import sys
import os
import signal
import importlib
import subprocess
from datetime import datetime
import random
from babicli.colors import GREEN, RED, RESET

# ===============================================================
# CONFIG
# ===============================================================
ADMIN_CODE = "28021986"          # admin override
LAUNCH_CODE_LENGTH = 6          # one-time code digits
LOG_DIR = "launchlogs"
ARM_WINDOW = 0.50              # seconds to turn both keys together
LAUNCH_WINDOW_SECONDS = 30     # launch-button window
BAR_LENGTH = 29                # progress bar width

# GPIO pins (BCM)
KEY1 = 17
KEY2 = 27
LAUNCH = 22
LED_RED = 5
LED_GREEN = 6
LAUNCH_OUT = 13

# ensure log dir exists
os.makedirs(LOG_DIR, exist_ok=True)

# global log handle
log_file = None
current_log_path = None


# ===============================================================
# AUTO-INSTALL UTILITY
# ===============================================================
def ensure(pkg):
    try:
        importlib.import_module(pkg)
        return True
    except ImportError:
        ans = input(f"Package '{pkg}' is required. Install it? [Y/n] ").strip().lower()
        if ans in ["", "y", "yes"]:
            try:
                subprocess.check_call([sys.executable, "-m", "pip", "install", pkg])
                return True
            except Exception as e:
                print(f"Install failed: {e}")
                return False
        return False


# ===============================================================
# CTRL-C DISABLED
# ===============================================================
def block_ctrl_c(signum, frame):
    print("\nCTRL-C disabled. Close the terminal window (X) to exit safely.")
signal.signal(signal.SIGINT, block_ctrl_c)


# ===============================================================
# SIM / GPIO MODE SETUP
# ===============================================================
SIM_REQUESTED = (len(sys.argv) > 2 and sys.argv[2].lower() == "sim")
SIM = SIM_REQUESTED

if not SIM:
    if ensure("RPi.GPIO"):
        try:
            import RPi.GPIO as GPIO
        except Exception:
            SIM = True
    else:
        SIM = True

system_wants = {"k1": None, "k2": None, "launch": None}

if SIM:
    print("SIMULATION MODE (1=KEY1, 2=KEY2, 3=LAUNCH)")
    ensure("keyboard")
    import keyboard

    class FakeGPIO:
        BCM = IN = OUT = PUD_DOWN = None
        pins = {}
        def setmode(self, *a, **k): pass
        def setup(self, pin, mode, pull_up_down=None): self.pins[pin] = 0
        def input(self, pin): return self.pins.get(pin, 0)
        def output(self, pin, value): self.pins[pin] = value
        def cleanup(self): pass

    GPIO = FakeGPIO()

    def apply_state_change(key):
        if system_wants[key] is None:
            return
        if key == "k1":
            GPIO.pins[KEY1] = int(system_wants[key])
            print(f"KEY1 set to {GPIO.pins[KEY1]}")
        elif key == "k2":
            GPIO.pins[KEY2] = int(system_wants[key])
            print(f"KEY2 set to {GPIO.pins[KEY2]}")
        elif key == "launch":
            print("LAUNCH TRIGGER")
            GPIO.pins[LAUNCH] = 1
            time.sleep(0.15)
            GPIO.pins[LAUNCH] = 0
        system_wants[key] = None

    keyboard.on_press_key("1", lambda e: apply_state_change("k1"))
    keyboard.on_press_key("2", lambda e: apply_state_change("k2"))
    keyboard.on_press_key("3", lambda e: apply_state_change("launch"))


# ===============================================================
# LOGGING
# ===============================================================
def open_log_for_code(code_used, is_admin):
    global log_file, current_log_path
    # close any previous
    close_log()

    if is_admin:
        name = f"ADMIN_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
    else:
        name = f"{code_used}.txt"

    current_log_path = os.path.join(LOG_DIR, name)
    log_file = open(current_log_path, "w", encoding="utf-8")

    # header
    now = datetime.now()
    log_file.write("==== LAUNCH LOG ====\n")
    log_file.write(f"DATE: {now.strftime('%Y-%m-%d')}\n")
    log_file.write(f"TIME: {now.strftime('%H:%M:%S')}\n")
    if is_admin:
        log_file.write("MODE: ADMIN OVERRIDE\n")
    else:
        log_file.write(f"LAUNCH CODE: {code_used}\n")
    log_file.write("--------------------\n")
    log_file.flush()


def close_log():
    global log_file
    if log_file:
        try:
            log_file.write("\n--- END OF LOG ---\n")
            log_file.flush()
        except Exception:
            pass
        try:
            log_file.close()
        except Exception:
            pass
        log_file = None


def log(msg, also_print=True):
    ts = datetime.now().strftime("%H:%M:%S")
    line = f"[{ts}] {msg}\n"
    if also_print:
        print(line, end="")
    if log_file:
        log_file.write(line)
        log_file.flush()


# ===============================================================
# AUTHORIZATION + OPERATOR INFO
# ===============================================================
def require_authorization_and_operator():
    one_time_code = "".join(random.choice("0123456789") for _ in range(LAUNCH_CODE_LENGTH))

    print(f"ENTER LAUNCH CODE TO BEGIN: {one_time_code}")
    entered = input("Code: ").strip()

    if entered == ADMIN_CODE:
        open_log_for_code(entered, is_admin=True)
        log("ADMIN OVERRIDE USED")
        print(f"{GREEN}Authorization accepted.{RESET}\n")
    elif entered == one_time_code:
        open_log_for_code(one_time_code, is_admin=False)
        log(f"Launch code {one_time_code} accepted.")
    else:
        print(f"{RED}INVALID CODE. EXITING.{RESET}")
        # if GPIO exists, clean up before exit
        if "GPIO" in globals():
            try:
                GPIO.cleanup()
            except Exception:
                pass
        close_log()
        sys.exit(1)

    name = input("Enter operator name: ").strip()
    op_id = input("Enter operator ID number: ").strip()
    log(f"Operator: {name}")
    log(f"Operator ID: {op_id}")

    input("\nPress ENTER to continue...\n")


# ===============================================================
# COUNTDOWN BAR
# ===============================================================
def countdown_bar(total):
    for remaining in range(total, 0, -1):
        filled = int((remaining / total) * BAR_LENGTH)
        bar = RED + ("█" * filled + "░" * (BAR_LENGTH - filled)) + RESET
        sys.stdout.write(f"\r[{bar}] {remaining}s")
        sys.stdout.flush()
        yield remaining
        time.sleep(1)
    print()  # newline after bar


# ===============================================================
# RESET / RETRY HANDLING (best mode)
# ===============================================================
def wait_for_reset(reason):
    log(f"RESET REQUIRED: {reason}")

    print("\nReset required: turn KEY1 off, KEY2 off, and release LAUNCH.")
    # tell sim what we want
    system_wants["k1"] = False
    system_wants["k2"] = False
    system_wants["launch"] = False

    warned = False
    while GPIO.input(KEY1) or GPIO.input(KEY2) or GPIO.input(LAUNCH):
        if not warned:
            if GPIO.input(KEY1): print("Turn KEY1 off to continue.")
            if GPIO.input(KEY2): print("Turn KEY2 off to continue.")
            if GPIO.input(LAUNCH): print("Release LAUNCH to continue.")
            warned = True
        time.sleep(0.1)

    # reset outputs/LEDs
    try:
        GPIO.output(LED_RED, 0)
        GPIO.output(LED_GREEN, 0)
        GPIO.output(LAUNCH_OUT, 0)
    except Exception:
        pass

    print("System reset complete.\n")
    log("System reset complete.")


# ===============================================================
# MAIN LAUNCH SEQUENCE (single attempt)
# Returns: "success" | "reset"
# ===============================================================
def launch_sequence():
    # PRE-LAUNCH CONFIRM
    print("Press the LAUNCH button once to confirm starting the launch sequence.")
    log("Waiting for pre-launch button confirmation...")

    system_wants["launch"] = True
    while not GPIO.input(LAUNCH):
        time.sleep(0.05)
    while GPIO.input(LAUNCH):
        time.sleep(0.05)

    print(f"{GREEN}Launch-button confirmation received.{RESET}")
    log("Launch-button confirmation received.")

    # ARM STEP
    print("Turn both keys to ARM...")
    log("Waiting for keys to ARM...")
    system_wants["k1"] = True
    system_wants["k2"] = True

    # anti-spam: don't start window until at least one key moves
    while not GPIO.input(KEY1) and not GPIO.input(KEY2):
        time.sleep(0.05)

    armed = False
    t0 = time.time()
    while time.time() - t0 < ARM_WINDOW:
        if GPIO.input(KEY1) and GPIO.input(KEY2):
            armed = True
            break
        time.sleep(0.01)

    if not armed:
        print("\nKeys not turned together in time.")
        wait_for_reset("Keys not turned together in ARM window")
        return "reset"

    print(f"{RED}ARMED.{RESET}")
    log("System ARMED.")
    GPIO.output(LED_RED, 1)

    if GPIO.input(LAUNCH):
        print("\nLaunch pressed early. Resetting.")
        wait_for_reset("Launch pressed early")
        return "reset"

    print("Press launch button within 30 seconds.")
    log("Launch window started.")
    system_wants["launch"] = True

    for _ in countdown_bar(LAUNCH_WINDOW_SECONDS):
        # abort if any key turned off
        if not GPIO.input(KEY1) or not GPIO.input(KEY2):
            print("\nABORT: A key was turned OFF during launch window.")
            wait_for_reset("Key turned OFF during launch window")
            return "reset"

        if GPIO.input(LAUNCH):
            print(f"\n{GREEN}LAUNCH CONFIRMED{RESET}")
            log("Launch button pressed within window.")
            GPIO.output(LED_RED, 0)
            GPIO.output(LED_GREEN, 1)
            time.sleep(1)

            GPIO.output(LAUNCH_OUT, 1)
            print("FIRING COMPLETE")
            log("Launch COMPLETED successfully.")
            return "success"

    print("\nLaunch window expired.")
    wait_for_reset("Launch window expired")
    return "reset"


# ===============================================================
# RUN
# ===============================================================
def run():
    # authorization + operator
    require_authorization_and_operator()

    # SAFE GPIO INIT
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(KEY1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(KEY2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(LAUNCH, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(LED_RED, GPIO.OUT)
    GPIO.setup(LED_GREEN, GPIO.OUT)
    GPIO.setup(LAUNCH_OUT, GPIO.OUT)

    GPIO.output(LED_RED, 0)
    GPIO.output(LED_GREEN, 0)
    GPIO.output(LAUNCH_OUT, 0)

    # main loop = "best mode": reset & retry from scratch
    while True:
        outcome = launch_sequence()
        if outcome == "success":
            # after success: require all OFF before exiting
            print("Waiting for all keys OFF to exit program.")
            log("Waiting for all keys OFF to exit program.")
            system_wants["k1"] = False
            system_wants["k2"] = False
            system_wants["launch"] = False

            warned = False
            while GPIO.input(KEY1) or GPIO.input(KEY2) or GPIO.input(LAUNCH):
                if not warned:
                    if GPIO.input(KEY1): print("Turn KEY1 off to exit.")
                    if GPIO.input(KEY2): print("Turn KEY2 off to exit.")
                    if GPIO.input(LAUNCH): print("Release LAUNCH to exit.")
                    warned = True
                time.sleep(0.1)

            log("All inputs OFF. Exiting.")
            return

        # outcome == "reset" -> start over with NEW code + NEW log
        close_log()
        require_authorization_and_operator()
        # (GPIO already setup; keep setmode/setup as-is)
        GPIO.output(LED_RED, 0)
        GPIO.output(LED_GREEN, 0)
        GPIO.output(LAUNCH_OUT, 0)


# ===============================================================
# SAFE EXIT
# ===============================================================
try:
    run()
finally:
    try:
        if "GPIO" in globals():
            GPIO.cleanup()
    except Exception:
        pass
    close_log()
