import threading
import time
from typing import Optional


class HardwareBackend:
    """Abstract backend that executes DJI actions. Implement this for real hardware."""
    def takeoff(self):
        raise NotImplementedError

    def land(self):
        raise NotImplementedError

    def return_to_home(self):
        raise NotImplementedError

    def upload_mission(self, mission_items):
        """Receive a list of mission item dicts"""
        raise NotImplementedError

    def pause_mission(self):
        raise NotImplementedError
    def gimbal_control(self, pitch=0, roll=0, yaw=0):
        raise NotImplementedError
    def start_video_recording(self):
        """Begin video recording if supported by the backend / SDK."""
        raise NotImplementedError

    def stop_video_recording(self):
        """Stop video recording if supported by the backend / SDK."""
        raise NotImplementedError

    def take_photo(self):
        """Take a single photo / trigger camera capture on the backend / SDK."""
        raise NotImplementedError

    def set_rc_override(self, channels):
        raise NotImplementedError
    def set_attitude(self, roll, pitch, yaw, thrust, ctrl_flag=None):
        raise NotImplementedError

    def set_velocity(self, vx, vy, vz, frame="local"):
        raise NotImplementedError

    def control_management(self, obtain=True):
        """Request or release control authority."""
        raise NotImplementedError
    # Optional lifecycle hooks for real backends
    def activate(self, app_id=None, app_key=None, api_level=None):
        """Activate the DJI SDK / app. Optional for mock backends."""
        raise NotImplementedError

    def start_polling(self, state_update_cb=None, interval=0.02):
        """Start polling / receiving broadcast telemetry and call state_update_cb(snapshot)."""
        raise NotImplementedError

    def stop(self):
        """Stop any background threads / resources used by the backend."""
        raise NotImplementedError
    def start_mission(self, mission_id=0, start_seq=0):
        """Begin executing a previously uploaded mission (if supported)."""
        raise NotImplementedError

    # --- Command ACK callback registration --------------------------------
    def set_cmd_send_cb(self, cb):
        """Register a callback to receive SDK-level command ack events.

        The callback signature is expected to be: cb(cmd_id, status_str)
        where `cmd_id` is an integer command identifier and `status_str` is one
        of the status tokens documented in the project TODO (e.g. 'CMD_RECIEVE',
        'STATUS_CMD_EXE_SUCCESS', etc).
        """
        # Default implementation stores callback for backends that want to
        # invoke it later via self._invoke_cmd_send_cb.
        setattr(self, "_cmd_send_cb", cb)
        return True

    def _invoke_cmd_send_cb(self, cmd_id, status_str):
        """Internal helper for backends to call the registered callback."""
        cb = getattr(self, "_cmd_send_cb", None)
        try:
            if cb:
                cb(cmd_id, status_str)
                return True
        except Exception:
            pass
        return False


class MockBackend(HardwareBackend):
    """Simple mock backend used for development and tests."""
    def __init__(self):
        self._has_control = False

    def takeoff(self):
        print("[MockBackend] takeoff() called")
        # simulate immediate SDK ack if requested
        try:
            # prefer MAV_CMD_NAV_TAKEOFF constant if available
            cmd = getattr(self, "_last_cmd_id", None)
            if cmd is None:
                # common default
                cmd = 1
            self._invoke_cmd_send_cb(cmd, "CMD_RECIEVE")
            self._invoke_cmd_send_cb(cmd, "STATUS_CMD_EXECUTING")
            self._invoke_cmd_send_cb(cmd, "STATUS_CMD_EXE_SUCCESS")
        except Exception:
            pass

    def land(self):
        print("[MockBackend] land() called")
        try:
            cmd = getattr(self, "_last_cmd_id", None) or 2
            self._invoke_cmd_send_cb(cmd, "CMD_RECIEVE")
            self._invoke_cmd_send_cb(cmd, "STATUS_CMD_EXECUTING")
            self._invoke_cmd_send_cb(cmd, "STATUS_CMD_EXE_SUCCESS")
        except Exception:
            pass

    def return_to_home(self):
        print("[MockBackend] return_to_home() called")
        try:
            cmd = getattr(self, "_last_cmd_id", None) or 3
            self._invoke_cmd_send_cb(cmd, "CMD_RECIEVE")
            self._invoke_cmd_send_cb(cmd, "STATUS_CMD_EXECUTING")
            self._invoke_cmd_send_cb(cmd, "STATUS_CMD_EXE_SUCCESS")
        except Exception:
            pass

    def upload_mission(self, mission_items):
        print(f"[MockBackend] upload_mission() {len(mission_items)} items")
        for i, m in enumerate(mission_items):
            print(f"  {i}: {m}")

    def pause_mission(self):
        print("[MockBackend] pause_mission() called")

    def start_video_recording(self):
        print("[MockBackend] start_video_recording() called")
        return True

    def stop_video_recording(self):
        print("[MockBackend] stop_video_recording() called")
        return True

    def take_photo(self):
        print("[MockBackend] take_photo() called")
        return True

    def activate(self, app_id=None, app_key=None, api_level=None):
        print("[MockBackend] activate() called")

    def start_polling(self, state_update_cb=None, interval=0.02):
        # MockBackend doesn't produce live telemetry; no-op
        print("[MockBackend] start_polling() called (no-op)")

    def stop(self):
        print("[MockBackend] stop() called (no-op)")
    def start_mission(self, mission_id=0, start_seq=0):
        print(f"[MockBackend] start_mission(mission_id={mission_id}, start_seq={start_seq})")
        return True
    def set_attitude(self, roll, pitch, yaw, thrust, ctrl_flag=None):
        print(
            f"[MockBackend] set_attitude roll={roll} pitch={pitch} yaw={yaw} thrust={thrust} ctrl_flag={ctrl_flag}"
        )
        return True

    def set_velocity(self, vx, vy, vz, frame="local"):
        print(f"[MockBackend] set_velocity vx={vx} vy={vy} vz={vz} frame={frame}")
        return True

    def control_management(self, obtain=True):
        self._has_control = bool(obtain)
        state = "obtained" if self._has_control else "released"
        print(f"[MockBackend] control_management -> {state}")
        return True

class DJIOnboardBackend(HardwareBackend):
    """
    Wrapper for a real DJI Onboard SDK binding. This class expects a Python
    binding named `djisdk` (or similar) to be available. If not present,
    the constructor will raise so callers can fallback to the prototype backend.
    """

    def __init__(
        self,
        binding_module: str = "py_mavlink_dji.djibindings",
        *,
        device: Optional[str] = None,
        baudrate: int = 230400,
        enable_advanced_sensing: bool = False,
        enable_broadcast: bool = True,
    ):
        try:
            self._sdk = __import__(binding_module)
        except Exception as e:
            raise RuntimeError(f"DJI SDK bindings '{binding_module}' not available: {e}")
        if getattr(self._sdk, "USING_FALLBACK", False):
            raise RuntimeError(
                "DJI SDK bindings fallback in use; install native module 'py_mavlink_dji.djibindings._djibindings'"
            )
        self._poll_thread = None
        self._running = False
        self._state_cb = None
        self._last_broadcast = time.time()
        self._handle = None
        self._connect_kwargs = {
            "device": device,
            "baudrate": baudrate,
            "enable_advanced_sensing": enable_advanced_sensing,
            "enable_broadcast": enable_broadcast,
        }
        self._connected = False
        if device:
            self._ensure_connection()

    def connect(
        self,
        *,
        device: Optional[str] = None,
        baudrate: Optional[int] = None,
        enable_advanced_sensing: Optional[bool] = None,
        enable_broadcast: Optional[bool] = None,
    ) -> bool:
        overrides = {
            "device": device if device is not None else self._connect_kwargs.get("device"),
            "baudrate": baudrate if baudrate is not None else self._connect_kwargs.get("baudrate"),
            "enable_advanced_sensing": (
                enable_advanced_sensing
                if enable_advanced_sensing is not None
                else self._connect_kwargs.get("enable_advanced_sensing")
            ),
            "enable_broadcast": (
                enable_broadcast if enable_broadcast is not None else self._connect_kwargs.get("enable_broadcast")
            ),
        }
        self._connect_kwargs.update(overrides)
        return self._ensure_connection(force=True)

    def disconnect(self) -> bool:
        if hasattr(self._sdk, "disconnect") and self._connected:
            try:
                self._sdk.disconnect()
            except Exception:
                pass
        self._connected = False
        return True

    def _ensure_connection(self, force: bool = False) -> bool:
        if self._connected and not force:
            return True
        if not hasattr(self._sdk, "connect"):
            self._connected = True
            return True
        device = self._connect_kwargs.get("device")
        if not device:
            raise RuntimeError("DJIOnboardBackend requires a serial `device`; provide via config or connect()")
        try:
            ok = bool(self._sdk.connect(**self._connect_kwargs))
        except Exception as exc:
            raise RuntimeError(f"Failed to connect to DJI vehicle via SDK: {exc}") from exc
        self._connected = ok
        return ok

    def _invoke(self, name: str, *args, **kwargs):
        try:
            self._ensure_connection()
        except Exception:
            return False
        fn = getattr(self._sdk, name, None)
        if not callable(fn):
            return False
        try:
            return fn(*args, **kwargs)
        except Exception:
            return False

    def activate(self, app_id=None, app_key=None, api_level=None):
        return bool(self._invoke("activate", app_id=app_id, app_key=app_key, api_level=api_level))

    def start_polling(self, state_update_cb=None, interval=0.02):
        """Start a thread that polls SDK broadcast data and calls state_update_cb(snapshot)."""
        if state_update_cb is None:
            raise ValueError("state_update_cb is required for DJIOnboardBackend.start_polling")
        self._ensure_connection()
        self._state_cb = state_update_cb
        self._running = True

        def _loop():
            while self._running:
                try:
                    if hasattr(self._sdk, "get_broadcast"):
                        data = self._sdk.get_broadcast()
                        try:
                            self._last_broadcast = time.time()
                        except Exception:
                            pass
                        try:
                            self._state_cb(data)
                        except Exception:
                            pass
                except Exception:
                    pass
                time.sleep(interval)

        self._poll_thread = threading.Thread(target=_loop, daemon=True)
        self._poll_thread.start()
        return True

    def stop(self):
        self._running = False
        try:
            if self._poll_thread:
                self._poll_thread.join(timeout=1.0)
        except Exception:
            pass
        self.disconnect()

    def health_check(self, timeout: float = 2.0) -> bool:
        try:
            return (time.time() - float(getattr(self, "_last_broadcast", 0.0))) < float(timeout)
        except Exception:
            return False

    def takeoff(self):
        return bool(self._invoke("takeoff"))

    def land(self):
        return bool(self._invoke("land"))

    def return_to_home(self):
        return bool(self._invoke("return_to_home"))

    def upload_mission(self, mission_items):
        return bool(self._invoke("upload_mission", mission_items))

    def pause_mission(self):
        return bool(self._invoke("pause_mission"))

    def gimbal_control(self, pitch=0, roll=0, yaw=0):
        return bool(self._invoke("gimbal_control", pitch=pitch, roll=roll, yaw=yaw))

    def set_rc_override(self, channels):
        return bool(self._invoke("set_rc_override", channels))

    def start_mission(self, mission_id=0, start_seq=0):
        return bool(self._invoke("start_mission", mission_id=mission_id, start_seq=start_seq))

    def set_attitude(self, roll, pitch, yaw, thrust, ctrl_flag=None):
        kwargs = {"roll": roll, "pitch": pitch, "yaw": yaw, "thrust": thrust}
        if ctrl_flag is not None:
            kwargs["ctrl_flag"] = ctrl_flag
        return bool(self._invoke("set_attitude", **kwargs))

    def set_velocity(self, vx, vy, vz, frame="local"):
        return bool(self._invoke("set_velocity", vx=vx, vy=vy, vz=vz, frame=frame))

    def control_management(self, obtain=True):
        return bool(self._invoke("control_management", obtain=obtain))

    def start_video_recording(self):
        return bool(self._invoke("start_video_recording"))

    def stop_video_recording(self):
        return bool(self._invoke("stop_video_recording"))

    def take_photo(self):
        return bool(self._invoke("take_photo"))
