"""Movement tools — motor positioning, homing, and engagement."""

from fastmcp import Context

from mcbirdcage.state import BirdcageState


def _require_device(state: BirdcageState):
    """Raise if no device is connected."""
    if state.device is None:
        raise RuntimeError(
            "No device connected. Use the 'connect' tool first."
        )


def register(mcp):
    """Register movement tools on the FastMCP instance."""

    @mcp.tool()
    async def get_position(ctx: Context) -> dict:
        """Query the current azimuth and elevation of the dish.

        Returns {"azimuth": float, "elevation": float} in degrees.
        Azimuth range is 0-455 (before cable wrap). Elevation range
        depends on firmware variant (typically 18-65 deg for Carryout G2).
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        return state.device.get_position()

    @mcp.tool()
    async def move_to(
        ctx: Context,
        azimuth: float,
        elevation: float,
    ) -> dict:
        """Move the dish to an absolute azimuth and elevation.

        Both motors are commanded sequentially (AZ then EL). The firmware
        queues commands, so this returns immediately — the dish moves in
        the background. Poll get_position to track progress.

        Args:
            azimuth: Target azimuth in degrees (0-455).
            elevation: Target elevation in degrees (18-65 for Carryout G2).
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        state.device.move_to(azimuth, elevation)
        return {
            "status": "moving",
            "target_azimuth": azimuth,
            "target_elevation": elevation,
        }

    @mcp.tool()
    async def move_motor(
        ctx: Context,
        motor_id: int,
        degrees: float,
    ) -> dict:
        """Move a single motor to an absolute position.

        Args:
            motor_id: 0 for azimuth, 1 for elevation.
            degrees: Target angle in degrees.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        if motor_id not in (0, 1):
            raise ValueError("motor_id must be 0 (azimuth) or 1 (elevation)")
        state.device.move_motor(motor_id, degrees)
        axis = "azimuth" if motor_id == 0 else "elevation"
        return {"status": "moving", "axis": axis, "target": degrees}

    @mcp.tool()
    async def home_motor(ctx: Context, motor_id: int) -> dict:
        """Home a motor to its reference position using stall detection.

        The dish uses motor stalling (not limit switches) to find the
        mechanical home position. Expect audible grinding during homing.

        Args:
            motor_id: 0 for azimuth, 1 for elevation.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        if motor_id not in (0, 1):
            raise ValueError("motor_id must be 0 (azimuth) or 1 (elevation)")
        state.device.home_motor(motor_id)
        axis = "azimuth" if motor_id == 0 else "elevation"
        return {"status": "homing", "axis": axis}

    @mcp.tool()
    async def engage_motors(ctx: Context) -> dict:
        """Energize the stepper motors (apply holding torque).

        Motors must be engaged before movement commands will work.
        They draw power while engaged even when stationary.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        state.device.engage()
        return {"status": "engaged"}

    @mcp.tool()
    async def release_motors(ctx: Context) -> dict:
        """De-energize the stepper motors (remove holding torque).

        The dish will be free to move under wind/gravity load.
        Use before manual repositioning or to save power.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        state.device.release()
        return {"status": "released"}

    @mcp.tool()
    async def stow(ctx: Context) -> dict:
        """Move the dish to stow position (AZ=0, EL=65).

        Stow position is safe for transport with the standard feed horn.
        WARNING: Modified feeds may not survive the stow movement.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        state.device.move_to(0.0, 65.0)
        return {
            "status": "stowing",
            "target_azimuth": 0.0,
            "target_elevation": 65.0,
        }

    @mcp.tool()
    async def get_step_positions(ctx: Context) -> dict:
        """Read raw stepper motor positions in step counts.

        Returns {"az_steps": int, "el_steps": int}. The Carryout G2 has
        40000 steps/rev for AZ and 24960 steps/rev for EL.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        return state.device.get_step_positions()

    @mcp.tool()
    async def get_el_limits(ctx: Context) -> dict:
        """Read the firmware elevation angle limits.

        Returns {"min": float, "max": float, "home": float} in degrees.
        The Carryout G2 defaults are min=18, max=65, home=65.
        """
        state: BirdcageState = ctx.request_context.lifespan_context
        _require_device(state)
        return state.device.get_el_limits()
