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

# Use PulseAudio/PipeWire audio driver to avoid ALSA device-busy errors.
os.environ.setdefault("SDL_AUDIODRIVER", "pulse")
from logging.handlers import RotatingFileHandler

from common import PID_FILE
from styles import STYLES, LEGACY_STYLE, get_style

# Configure logging with rotation to avoid unbounded growth on the SD card.
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(message)s",
    handlers=[
        RotatingFileHandler("wecker.log", maxBytes=1_000_000, backupCount=3),
        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


BUTTON_PIN = 17
LED_PIN = 27


DEFAULT_MUSIC_FILE = "Laid Back - Sunshine Reggae.mp3"


def setup(music_file: str | None = None):
    """Initialize GPIO and audio hardware. Call once before running."""
    if music_file is None:
        music_file = os.environ.get("MUSIC_FILE", DEFAULT_MUSIC_FILE)

    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(music_file)
        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)


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 run_alarm(
    style_name: str = LEGACY_STYLE,
    test_mode: bool = False,
    music_file: str | None = None,
):
    """Run the alarm clock with the given style until the style signals stop.

    The runner owns everything shared (music loop, button sampling, cleanup);
    the style only decides when to stop and what the LED does.
    """
    style = get_style(style_name)(set_led)
    setup(music_file=music_file)
    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; the style drives it from here.

    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 = style.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__":
    import argparse

    parser = argparse.ArgumentParser(description="Raspberry Pi alarm clock.")
    parser.add_argument(
        "--music-file",
        default=os.environ.get("MUSIC_FILE", DEFAULT_MUSIC_FILE),
        help="Path to the MP3 file to play (default: $MUSIC_FILE or default track).",
    )
    parser.add_argument(
        "--style",
        default=LEGACY_STYLE,
        choices=sorted(STYLES),
        help="Alarm style to run (default: %(default)s).",
    )
    args = parser.parse_args()

    ensure_single_instance()
    try:
        run_alarm(style_name=args.style, music_file=args.music_file)
    finally:
        pygame.mixer.quit()
        GPIO.cleanup()
        remove_pid_file()
        logging.info("Program finished.")
