from flask import Flask, jsonify, render_template, request, send_from_directory, Response
from flask_cors import CORS
from flask_sock import Sock
import json
import os
import base64
from datetime import datetime

try:
    import cv2
except Exception:  # noqa: BLE001
    cv2 = None

# In a real project this would be: from services.sensor_service import SENSOR
from services.sensor_service import SENSOR
from services.driver_service import DRIVER
from services.location_service import LOCATION
from services.auto_service import AUTO


app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}})

sock = Sock(app)


# ---------- Helpers ----------

def ok(data, status=200):
    return jsonify({"ok": True, "data": data}), status


def err(code: str, message: str, details=None, status=400):
    return (
        jsonify({"ok": False, "error": {"code": code, "message": message, "details": details or {}}}),
        status,
    )

def voltage_percent(volt: float, full: float = 12.6, empty: float = 10.5):
    try:
        v = float(volt)
    except Exception:
        return None
    v = max(empty, min(full, v))
    pct = (v - empty) / (full - empty) * 100.0
    return round(pct, 1)



# ---------- Page ----------

@app.route("/")
def index():
    return render_template("index.html")


# Optional static aliases to support /asset or /assets paths
@app.get("/assets/<path:filename>")
def static_assets(filename: str):
    return app.send_static_file(filename)


@app.get("/asset/<path:filename>")
def static_asset(filename: str):
    return app.send_static_file(filename)


@app.post("/snapshot")
def snapshot():
    try:
        body = request.get_json(silent=True) or {}
        data_url = body.get("image")
        if not data_url or not isinstance(data_url, str) or ";base64," not in data_url:
            return err("BAD_REQUEST", "missing or invalid image data URL")
        suffix = str(body.get("suffix") or "").strip()
        if suffix:
            suffix = "_" + "".join(ch for ch in suffix if ch.isalnum() or ch in "-_")[:6]

        header, b64 = data_url.split(",", 1)
        # default to png
        ext = "png"
        if header.startswith("data:image/"):
            ext = header.split("/")[1].split(";")[0] or "png"
        img_bytes = base64.b64decode(b64)

        os.makedirs("file/img", exist_ok=True)
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        fname = f"seubot_{ts}{suffix}." + ext
        fpath = os.path.join("file", "img", fname)
        with open(fpath, "wb") as f:
            f.write(img_bytes)
        return ok({"path": fpath, "filename": fname})
    except Exception as e:  # noqa: BLE001
        return err("SNAPSHOT_FAILED", str(e), status=500)


@app.get("/snapshot/list")
def snapshot_list():
    try:
        base = os.path.join("file", "img")
        if not os.path.isdir(base):
            return ok({"items": []})
        items = []
        for name in os.listdir(base):
            if not name.lower().endswith((".png", ".jpg", ".jpeg")):
                continue
            path = os.path.join(base, name)
            try:
                st = os.stat(path)
                items.append({
                    "filename": name,
                    "url": f"/snapshot/file/{name}",
                    "mtime": int(st.st_mtime),
                })
            except OSError:
                continue
        # newest first
        items.sort(key=lambda x: x["mtime"], reverse=True)
        return ok({"items": items})
    except Exception as e:  # noqa: BLE001
        return err("SNAPSHOT_LIST_FAILED", str(e), status=500)


@app.get("/snapshot/file/<path:filename>")
def snapshot_file(filename: str):
    # serve saved snapshots
    base = os.path.join("file", "img")
    return send_from_directory(base, filename)


@app.delete("/snapshot/file/<path:filename>")
def snapshot_delete(filename: str):
    try:
        base = os.path.join("file", "img")
        path = os.path.join(base, filename)
        if not os.path.isfile(path):
            return err("NOT_FOUND", "file not found", status=404)
        os.remove(path)
        return ok({"deleted": filename})
    except Exception as e:  # noqa: BLE001
        return err("SNAPSHOT_DELETE_FAILED", str(e), status=500)


# (license endpoints removed per request)


# ---------- Capture control ----------

@app.post("/start_capture")
def start_capture():
    try:
        body = request.get_json(silent=True) or {}
        rate_hz = body.get("rate_hz")
        csv_path = SENSOR.start(rate_hz=rate_hz)
        return ok({"csv": csv_path, "capturing": True, "rate_hz": SENSOR.state.rate_hz})
    except Exception as e:  # noqa: BLE001 - return structured error
        return err("START_FAILED", str(e), status=500)


@app.post("/stop_capture")
def stop_capture():
    try:
        csv_path = SENSOR.stop()
        return ok({"csv": csv_path, "capturing": False})
    except Exception as e:  # noqa: BLE001 - return structured error
        return err("STOP_FAILED", str(e), status=500)


# ---------- Location / XY ----------

@app.post("/location/origin")
def set_origin():
    try:
        body = request.get_json(silent=True) or {}
        lat = float(body.get("lat"))
        lon = float(body.get("lon"))
        alt = body.get("alt")
        alt_f = float(alt) if alt is not None else None
        o = LOCATION.set_origin(lat, lon, alt_f)
        return ok({"lat": o.lat, "lon": o.lon, "alt": o.alt})
    except Exception as e:  # noqa: BLE001
        return err("ORIGIN_SET_FAILED", str(e), status=400)


@app.delete("/location/origin")
def clear_origin():
    LOCATION.clear_origin()
    return ok({"cleared": True})


@app.get("/location/xy")
def current_xy():
    try:
        lat = SENSOR.state.gps_lat
        lon = SENSOR.state.gps_lon
        x, y = LOCATION.xy_from_latlon(lat, lon)
        return ok({"x": x, "y": y, "z": SENSOR.state.gps_alt_m})
    except Exception as e:  # noqa: BLE001
        return err("XY_FAILED", str(e), status=500)


# ---------- SLAM / unit detection ----------

@app.post("/slam/start")
def slam_start():
    # placeholder - real impl would start backend job
    return ok({"running": True})


@app.post("/slam/done")
def slam_done():
    try:
        body = request.get_json(silent=True) or {}
        a = float(body.get("a", AUTO.unit.a))
        b = float(body.get("b", AUTO.unit.b))
        u = AUTO.set_unit(a, b)
        return ok({"a": u.a, "b": u.b})
    except Exception as e:  # noqa: BLE001
        return err("SLAM_DONE_FAILED", str(e), status=400)


# ---------- Auto planner ----------

@app.post("/auto/plan")
def auto_plan():
    try:
        body = request.get_json(silent=True) or {}
        nx = int(body.get("nx", 1))
        ny = int(body.get("ny", 1))
        mode = (body.get("mode") or "serpentine").lower()
        start = (body.get("start") or "tl").lower()
        pts = AUTO.plan_path(nx, ny, mode, start)
        return ok({"plan": pts, "count": len(pts), "unit": {"a": AUTO.unit.a, "b": AUTO.unit.b}})
    except Exception as e:  # noqa: BLE001
        return err("AUTO_PLAN_FAILED", str(e), status=400)


# ---------- Video feed (usb0 MJPEG) ----------


def _resolve_resolution(res: str):
    res_map = {
        "360p": (640, 360),
        "480p": (848, 480),
        "720p": (1280, 720),
        "1080p": (1920, 1080),
        "2k": (2560, 1440),
        "1440p": (2560, 1440),
    }
    return res_map.get((res or "").lower(), res_map["720p"])



def _usb_stream(device: str = "/dev/video0", res: str = "720p"):
    if cv2 is None:
        raise RuntimeError("OpenCV not available")
    width, height = _resolve_resolution(res)
    cap = cv2.VideoCapture(device)
    if not cap.isOpened():
        raise RuntimeError(f"failed to open camera {device}")
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
    try:
        while True:
            ok, frame = cap.read()
            if not ok:
                break
            ok, buf = cv2.imencode(".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
            if not ok:
                continue
            chunk = buf.tobytes()
            yield (b"--frame\r\n"
                   b"Content-Type: image/jpeg\r\n\r\n" + chunk + b"\r\n")
    finally:
        cap.release()


@app.get("/usb<int:cam>")
def usb_cam_feed(cam: int):
    res = request.args.get("res", "720p")
    device = f"/dev/video{cam}"
    try:
        return Response(_usb_stream(device, res), mimetype="multipart/x-mixed-replace; boundary=frame")
    except Exception as e:  # noqa: BLE001
        return err("VIDEO_FEED_FAILED", str(e), status=503)


@app.get("/usb0")
def usb0_feed():
    res = request.args.get("res", "720p")
    try:
        return Response(_usb_stream("/dev/video0", res), mimetype="multipart/x-mixed-replace; boundary=frame")
    except Exception as e:  # noqa: BLE001
        return err("VIDEO_FEED_FAILED", str(e), status=503)


@app.get("/video_feed")
def video_feed():
    # Legacy alias; points to /usb0
    return usb0_feed()


# ---------- WebSockets ----------

@sock.route("/ws")
def ws_samples(ws):
    import time as _t

    # Emit samples only when capturing. Honor configured rate when active.
    while True:
        if SENSOR.is_capturing():
            sample = SENSOR.next_sample()
            if sample is not None:
                ws.send(json.dumps(sample))
            # sleep based on configured rate_hz
            try:
                hz = max(1, int(SENSOR.state.rate_hz))
            except Exception:  # defensive fallback
                hz = 50
            _t.sleep(1.0 / float(hz))
        else:
            # idle: push status (GPS/temperature/location) at low rate
            try:
                status = {
                    "t": _t.time(),
                    "temp_c": SENSOR.state.temperature_c,
                    "gps": {
                        "lat": SENSOR.state.gps_lat,
                        "lon": SENSOR.state.gps_lon,
                        "alt_m": SENSOR.state.gps_alt_m,
                        "sats": SENSOR.state.gps_sats,
                    },
                    "location": SENSOR.state.location_str,
                }
                ws.send(json.dumps(status))
            except Exception:
                pass
            _t.sleep(1.0)


@sock.route("/log_ws")
def ws_logs(ws):
    import time as _t

    while True:
        now = _t.strftime("%H:%M:%S", _t.localtime())
        if SENSOR.is_capturing() and SENSOR.get_csv_path():
            ws.send(f"[{now}] writing 鈫?{SENSOR.get_csv_path()}")
        else:
            ws.send(f"[{now}] idle")
        _t.sleep(1)


# ---------- Driver control ----------

@app.post("/driver/move")
def driver_move():
    try:
        body = request.get_json(silent=True) or {}
        direction = (body.get("dir") or "").lower()
        speed = int(body.get("speed", 70))
        d, s = DRIVER.move(direction, speed)
        return ok({"dir": d, "speed": s})
    except Exception as e:  # noqa: BLE001
        return err("DRIVE_FAILED", str(e), status=400)

@app.post("/move")
def driver_move_root():
    # Compatibility alias for mecanum drive control
    return driver_move()




@app.get("/driver/health")
def driver_health():
    try:
        h = DRIVER.health()
        return ok(h)
    except Exception as e:  # noqa: BLE001
        return err("HEALTH_FAILED", str(e), status=500)


@app.post("/drive/start")
def drive_start():
    try:
        body = request.get_json(silent=True) or {}
        target = body.get("target") or {}
        # Placeholder: in real impl, navigate to absolute XY target
        # Start capture once reached target (simulated immediately)
        SENSOR.start()
        return ok({"navigating": True, "target": target, "csv": SENSOR.state.csv_path})
    except Exception as e:  # noqa: BLE001
        return err("DRIVE_START_FAILED", str(e), status=400)


@app.post("/drive/work")
def drive_work():
    try:
        body = request.get_json(silent=True) or {}
        point = body.get("point") or {}
        # Placeholder: real impl should navigate and perform work/knock
        # Here we just acknowledge the command
        return ok({"arrived": True, "point": point, "action": "knock"})
    except Exception as e:  # noqa: BLE001
        return err("DRIVE_WORK_FAILED", str(e), status=400)


@app.post("/driver/servo")
def driver_servo():
    try:
        body = request.get_json(silent=True) or {}
        servo_id = int(body.get("id"))
        angle = float(body.get("angle"))
        sid, ang = DRIVER.set_servo(servo_id, angle)
        return ok({"id": sid, "angle": ang})
    except Exception as e:  # noqa: BLE001
        return err("SERVO_FAILED", str(e), status=400)


# ---------- Device / Battery ----------

@app.get("/device/battery")
def device_battery():
    try:
        volt = DRIVER.battery_voltage()
    except Exception as e:  # noqa: BLE001
        return err("BATTERY_READ_FAILED", str(e), status=503)
    pct = voltage_percent(volt)
    warn = pct is not None and pct <= 20
    data = {"volt": volt, "percent": pct, "warn": warn, "warn_threshold_percent": 20,
            "full_volt": 12.6, "protect_volt": 10.5}
    if warn:
        data["note"] = "浣庣數閲忥紝寤鸿灏藉揩鍏呯數"
    return ok(data)


# ---------- Errors ----------

@app.errorhandler(404)
def not_found(_):
    return err("NOT_FOUND", "route not found", status=404)


if __name__ == "__main__":
    # For production: hypercorn app:app --bind 0.0.0.0:5000
    app.run(host="0.0.0.0", port=50000, debug=True)
