import RPi.GPIO as GPIO
import time
import logging
import pygame
import random
import sys
import os
import fcntl

from common import PID_FILE

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(message)s",
    handlers=[logging.FileHandler("wecker.log"), logging.StreamHandler()],
)


_pid_lock_fd = None


def ensure_single_instance():
    """Ensures that only one instance of the script is running.

    Uses an advisory file lock on the PID file so the check-and-write
    sequence is atomic and race-free.
    """
    global _pid_lock_fd
    _pid_lock_fd = open(PID_FILE, "a+")
    try:
        fcntl.flock(_pid_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except (BlockingIOError, OSError):
        logging.error("Another instance of wecker is already running. Exiting.")
        sys.exit(1)

    _pid_lock_fd.seek(0)
    _pid_lock_fd.truncate()
    _pid_lock_fd.write(str(os.getpid()))
    _pid_lock_fd.flush()


def remove_pid_file():
    """Releases the PID file lock and removes the file."""
    global _pid_lock_fd
    if _pid_lock_fd is not None:
        try:
            _pid_lock_fd.close()
        except OSError:
            pass
        _pid_lock_fd = None
    try:
        os.remove(PID_FILE)
    except FileNotFoundError:
        pass


# GPIO Setup
BUTTON_PIN = 17
LED_PIN = 27

GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

# Audio Setup (with optimized buffer)
pygame.mixer.pre_init(frequency=44100, size=-16, channels=2, buffer=4096)
pygame.mixer.init()

try:
    pygame.mixer.music.load("Laid Back - Sunshine Reggae.mp3")
    pygame.mixer.music.set_volume(1.0)
except Exception as e:
    logging.error(f"Error loading audio file: {e}")
    # Don't exit in test mode
    if "pytest" not in sys.modules:
        sys.exit(1)

# State Machine states for the flow
STATE_RINGING = 0
STATE_WAIT_BEFORE_BLINK = 1
STATE_BLINKING = 2
STATE_WAIT_FOR_INPUT = 3
STATE_EVALUATING = 4
STATE_WAIT_BEFORE_RETRY = 5

state = STATE_RINGING
target_blinks = 0
user_presses = 0
last_interaction_time = 0
button_was_pressed = False


def set_led(on):
    """Turns the LED on or off (LOW = ON, HIGH = OFF)"""
    if "GPIO" in globals() and hasattr(GPIO, "output"):
        if on:
            GPIO.output(LED_PIN, GPIO.LOW)
        else:
            GPIO.output(LED_PIN, GPIO.HIGH)


def blink_led(times):
    """Blinks the LED a specific number of times (blocking)"""
    for _ in range(times):
        set_led(True)
        time.sleep(0.3)  # LED on for 300ms
        set_led(False)
        time.sleep(0.3)  # LED off for 300ms


class AlarmClock:
    def __init__(self):
        self.state = STATE_RINGING
        self.target_blinks = 0
        self.user_presses = 0
        self.last_interaction_time = 0
        self.button_was_pressed = False

    def update(self, now, is_pressed):
        button_just_pressed = False
        if is_pressed and not self.button_was_pressed:
            self.button_was_pressed = True
            button_just_pressed = True
        elif not is_pressed and self.button_was_pressed:
            self.button_was_pressed = False

        if self.state == STATE_RINGING:
            if button_just_pressed:
                logging.info(
                    "Alarm button pressed! Puzzle started. Waiting 3 seconds..."
                )
                self.state = STATE_WAIT_BEFORE_BLINK
                self.last_interaction_time = now

        elif self.state == STATE_WAIT_BEFORE_BLINK:
            if now - self.last_interaction_time >= 3.0:
                self.target_blinks = random.randint(1, 7)
                logging.info(f"Blinking {self.target_blinks} times...")
                self.state = STATE_BLINKING

        elif self.state == STATE_BLINKING:
            # Move the blink_led out of the update loop for testability,
            # or just call it directly. Here we call it.
            blink_led(self.target_blinks)
            logging.info("Blinking finished. Waiting for input...")
            self.state = STATE_WAIT_FOR_INPUT
            self.user_presses = 0
            self.last_interaction_time = (
                time.time()
            )  # Use time.time() to account for blocking blink_led
            if "GPIO" in globals() and hasattr(GPIO, "input"):
                self.button_was_pressed = GPIO.input(BUTTON_PIN) == GPIO.LOW

        elif self.state == STATE_WAIT_FOR_INPUT:
            set_led(is_pressed)

            if button_just_pressed:
                self.user_presses += 1
                logging.info(f"Button pressed: {self.user_presses} times")

            if is_pressed:
                self.last_interaction_time = now

            if not is_pressed and (now - self.last_interaction_time >= 3.0):
                self.state = STATE_EVALUATING

        elif self.state == STATE_EVALUATING:
            logging.info(
                f"Evaluation: Target={self.target_blinks}, Entered={self.user_presses}"
            )
            if self.user_presses == self.target_blinks:
                logging.info("Puzzle solved correctly! Alarm clock is stopping.")
                if (
                    "pygame" in globals()
                    and hasattr(pygame, "mixer")
                    and pygame.mixer.get_init()
                ):
                    pygame.mixer.music.stop()
                set_led(False)
                return False  # Indicate we should stop running
            else:
                logging.info("Incorrect input! Waiting 3 seconds before retrying...")
                self.state = STATE_WAIT_BEFORE_RETRY
                self.last_interaction_time = time.time()
                set_led(False)

        elif self.state == STATE_WAIT_BEFORE_RETRY:
            if now - self.last_interaction_time >= 5.0:
                self.target_blinks = random.randint(1, 7)
                logging.info(f"New attempt! Blinking {self.target_blinks} times...")
                self.state = STATE_BLINKING

        return True  # Keep running


def run_alarm(test_mode=False):
    logging.info("Alarm clock started. Music is playing in an endless loop.")
    if "pygame" in globals() and hasattr(pygame, "mixer") and pygame.mixer.get_init():
        pygame.mixer.music.play(-1)

    set_led(False)  # LED off at start

    clock = AlarmClock()

    try:
        while True:
            now = time.time()

            if "GPIO" in globals() and hasattr(GPIO, "input"):
                is_pressed = GPIO.input(BUTTON_PIN) == GPIO.LOW
            else:
                is_pressed = False

            keep_running = clock.update(now, is_pressed)

            if not keep_running or test_mode:
                break

            time.sleep(0.02)

    except KeyboardInterrupt:
        logging.info("Manually aborted (CTRL+C).")


if __name__ == "__main__":
    ensure_single_instance()
    try:
        run_alarm()
    finally:
        pygame.mixer.quit()
        GPIO.cleanup()
        remove_pid_file()
        logging.info("Program finished.")
