#!/usr/bin/env python3
"""
Aurora Camera System - Pi Zero 2 W Controller
- Web server with camera streaming
- UART communication with Pico 2 W for servo control
- Motion detection integration
- Distance sensor display

This version is robust: if UART or camera are missing the web server still runs
and reports the device as disconnected rather than crashing the whole service.
"""

from flask import Flask, render_template, Response, jsonify, request
import logging
import io
import json
import threading
import time
import os

# Optional hardware libraries
try:
    from picamera2 import Picamera2
except Exception:
    Picamera2 = None

try:
    import serial
except Exception:
    serial = None

# -------------------------
# App / Logging
# -------------------------
app = Flask(__name__)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("aurora")

# -------------------------
# Configuration
# -------------------------
UART_PORT = "/dev/serial0"     # default primary UART alias on Raspberry Pi
UART_BAUDRATE = 115200
STREAM_WIDTH = 640
STREAM_HEIGHT = 480
STREAM_FPS = 15

# Current state (globals)
current_pan = 0
current_tilt = 0
current_distance = 0.0
motion_detected = False

STEP_SIZE = 10

# -------------------------
# UART Handler
# -------------------------
class UARTHandler:
    def __init__(self, port, baudrate):
        self.port = port
        self.baudrate = baudrate
        self.ser = None
        self.running = False
        self._lock = threading.Lock()
        self.connect()

    def connect(self):
        """Attempt to open serial port. Does not raise on failure."""
        if serial is None:
            log.warning("UART: pyserial not available; UART disabled.")
            return False

        try:
            self.ser = serial.Serial(port=self.port, baudrate=self.baudrate, timeout=0.1)
            log.info(f"UART: connected {self.port} @ {self.baudrate}")
            return True
        except Exception as e:
            log.error(f"UART: connection failed: {e}")
            self.ser = None
            return False

    def send_command(self, command, **kwargs):
        """Send a JSON command to the Pico and wait briefly for a response."""
        if not self.ser:
            return None

        try:
            msg = {"type": "cmd", "cmd": command}
            msg.update(kwargs)
            data = json.dumps(msg) + "\n"
            with self._lock:
                self.ser.write(data.encode("utf-8"))

                # wait for a response up to 1.0s
                start = time.time()
                while time.time() - start < 1.0:
                    if self.ser.in_waiting:
                        line = self.ser.readline().decode("utf-8", "ignore").strip()
                        if not line:
                            continue
                        try:
                            resp = json.loads(line)
                            if resp.get("type") == "resp":
                                return resp
                        except Exception:
                            # not JSON — ignore
                            pass
                    time.sleep(0.01)
            return None
        except Exception as e:
            log.error(f"UART: send_command error: {e}")
            return None

    def listen_loop(self):
        """Background loop that reads events from the Pico."""
        global current_distance, motion_detected

        if not self.ser:
            log.info("UART: listen_loop not started (no serial).")
            return

        log.info("UART: listener started")
        self.running = True
        while self.running:
            try:
                if self.ser and self.ser.in_waiting:
                    line = self.ser.readline().decode("utf-8", "ignore").strip()
                    if line:
                        try:
                            msg = json.loads(line)
                            if msg.get("type") == "event":
                                event = msg.get("event")
                                if event == "distance":
                                    current_distance = float(msg.get("distance", 0.0))
                                    log.info(f"EVENT: distance={current_distance}")
                                elif event == "motion":
                                    motion_detected = bool(msg.get("detected", False))
                                    log.info(f"EVENT: motion={'DETECTED' if motion_detected else 'clear'}")
                        except Exception:
                            # ignore malformed lines
                            pass
                time.sleep(0.05)
            except Exception as e:
                log.error(f"UART: listener error: {e}")
                time.sleep(1)

    def stop(self):
        self.running = False
        try:
            if self.ser:
                self.ser.close()
                log.info("UART: serial closed")
        except Exception as e:
            log.debug(f"UART: close error: {e}")


# -------------------------
# Camera streamer
# -------------------------
class CameraStreamer:
    def __init__(self):
        self.picam2 = None
        self.streaming = False
        self._configured = False
        self.setup_camera()

    def setup_camera(self):
        """Attempt to initialize Picamera2. Does not exit on failure."""
        if Picamera2 is None:
            log.warning("CAMERA: Picamera2 library not available; camera disabled.")
            return

        try:
            self.picam2 = Picamera2()
            config = self.picam2.create_video_configuration(
                main={"size": (STREAM_WIDTH, STREAM_HEIGHT), "format": "RGB888"},
                lores={"size": (320, 240), "format": "YUV420"}
            )
            self.picam2.configure(config)
            # Set frame rate if supported
            try:
                self.picam2.set_controls({"FrameRate": STREAM_FPS})
            except Exception:
                pass
            log.info(f"CAMERA: initialized {STREAM_WIDTH}x{STREAM_HEIGHT} @ {STREAM_FPS}fps")
            self._configured = True
        except Exception as e:
            log.error(f"CAMERA: initialization error: {e}")
            self.picam2 = None
            self._configured = False

    def start_streaming(self):
        if not self.picam2 or not self._configured:
            log.info("CAMERA: start_streaming skipped (not configured)")
            return
        if not self.streaming:
            try:
                self.picam2.start()
                self.streaming = True
                log.info("CAMERA: streaming started")
            except Exception as e:
                log.error(f"CAMERA: failed to start streaming: {e}")

    def stop_streaming(self):
        if self.picam2 and self.streaming:
            try:
                self.picam2.stop()
                self.streaming = False
                log.info("CAMERA: streaming stopped")
            except Exception as e:
                log.error(f"CAMERA: failed to stop streaming: {e}")

    def get_frame(self):
        if not self.picam2 or not self.streaming:
            return None
        try:
            buf = io.BytesIO()
            # capture_file can raise if Picamera2 not ready; handle that
            self.picam2.capture_file(buf, format="jpeg")
            buf.seek(0)
            return buf.getvalue()
        except Exception as e:
            log.debug(f"CAMERA: frame capture error: {e}")
            return None


# Global instances (initialized later in main)
camera = None
uart = None

# -------------------------
# Flask routes
# -------------------------
def generate_frames():
    while True:
        frame = camera.get_frame()
        if frame:
            yield (b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
        time.sleep(1.0 / max(1, STREAM_FPS))


@app.route("/")
def index():
    # make sure templates/index.html exists, otherwise return a small fallback
    template_path = os.path.join(app.root_path, "templates", "index.html")
    if os.path.exists(template_path):
        return render_template("index.html")
    return "<h1>Aurora Camera System</h1><p>No template found. Create templates/index.html</p>"


@app.route("/video_feed")
def video_feed():
    return Response(generate_frames(), mimetype="multipart/x-mixed-replace; boundary=frame")


@app.route("/control", methods=["POST"])
def control():
    global current_pan, current_tilt
    data = request.get_json(force=True, silent=True) or {}
    command = data.get("command")
    value = data.get("value")

    if command in ["up", "down", "left", "right"]:
        new_pan, new_tilt = current_pan, current_tilt
        if command == "up":
            new_tilt = max(-45, current_tilt - STEP_SIZE)
        elif command == "down":
            new_tilt = min(45, current_tilt + STEP_SIZE)
        elif command == "left":
            new_pan = max(-90, current_pan + STEP_SIZE)
        elif command == "right":
            new_pan = min(90, current_pan - STEP_SIZE)

        response = uart.send_command("move", pan=new_pan, tilt=new_tilt) if uart else None
        if response and response.get("status") == "ok":
            current_pan = response.get("pan", new_pan)
            current_tilt = response.get("tilt", new_tilt)
            return jsonify({"status": "success", "pan": current_pan, "tilt": current_tilt})

    elif command == "pan" and value is not None:
        new_pan = max(-90, min(90, value - 90))
        response = uart.send_command("move", pan=new_pan, tilt=current_tilt) if uart else None
        if response and response.get("status") == "ok":
            current_pan = response.get("pan", new_pan)
            return jsonify({"status": "success", "pan": current_pan, "tilt": current_tilt})

    elif command == "tilt" and value is not None:
        new_tilt = max(-45, min(45, value - 90))
        response = uart.send_command("move", pan=current_pan, tilt=new_tilt) if uart else None
        if response and response.get("status") == "ok":
            current_tilt = response.get("tilt", new_tilt)
            return jsonify({"status": "success", "pan": current_pan, "tilt": current_tilt})

    return jsonify({"status": "error", "message": "Invalid command or device not connected"}), 400


@app.route("/center", methods=["POST"])
def center():
    global current_pan, current_tilt
    response = uart.send_command("center") if uart else None
    if response and response.get("status") == "ok":
        current_pan, current_tilt = 0, 0
        return jsonify({"status": "success", "pan": 0, "tilt": 0})
    return jsonify({"status": "error", "message": "UART not connected"}), 500


@app.route("/status")
def status():
    return jsonify({
        "streaming": camera.streaming,
        "uart_connected": bool(uart and uart.ser),
        "resolution": f"{STREAM_WIDTH}x{STREAM_HEIGHT}",
        "fps": STREAM_FPS,
        "current_pan": current_pan,
        "current_tilt": current_tilt,
        "distance": current_distance,
        "motion_detected": motion_detected
    })


# -------------------------
# Main entry
# -------------------------
def main():
    global uart, camera

    # Optional: ensure working dir is user's home if running under systemd
    try:
        os.chdir(os.path.expanduser("~"))
    except Exception:
        pass

    log.info("Starting Aurora Camera System (web server)")

    # Initialize UART but do not abort server if it fails
    if serial is not None:
        uart = UARTHandler(UART_PORT, UART_BAUDRATE)
        if uart and uart.ser:
            uart_thread = threading.Thread(target=uart.listen_loop, daemon=True)
            uart_thread.start()
        else:
            log.warning("UART not available at startup; continuing without UART.")
            uart = None
    else:
        uart = None
        log.warning("pyserial not installed; UART disabled.")

    # Start camera streaming if available
    camera = CameraStreamer()
    if camera.picam2 and camera._configured:
        # small delay can help if camera not ready immediately
        try:
            camera.start_streaming()
        except Exception as e:
            log.warning(f"Failed to start camera stream: {e}")

    # Try to get initial position from UART (if connected)
    if uart:
        resp = uart.send_command("get_position")
        if resp:
            try:
                global current_pan, current_tilt
                current_pan = resp.get("pan", current_pan)
                current_tilt = resp.get("tilt", current_tilt)
                log.info(f"Initial position: pan={current_pan}, tilt={current_tilt}")
            except Exception:
                pass

    # Run Flask app
    try:
        log.info("Web server listening on 0.0.0.0:5000")
        app.run(host="0.0.0.0", port=5000, threaded=True, debug=False)
    except Exception as e:
        log.error(f"Web server stopped unexpectedly: {e}")
    finally:
        log.info("Shutting down: stopping camera and UART")
        try:
            camera.stop_streaming()
        except Exception:
            pass
        if uart:
            uart.stop()
        log.info("Goodbye")


if __name__ == "__main__":
    main()
