"""Legacy LiquidHandler that delegates to the new slim LiquidHandler and Arm.

This module provides backward compatibility for code that uses
``LiquidHandler(Resource, Machine)`` directly.  Internally it composes the
new slim ``LiquidHandler`` (pipetting / tip handling) and ``Arm`` (resource
movement) and delegates method calls to them explicitly.
"""

from __future__ import annotations

import json
import logging
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Union

from pylabrobot.arms import Arm
from pylabrobot.liquid_handling.backends import LiquidHandlerBackend
from pylabrobot.liquid_handling.liquid_handler import BlowOutVolumeError  # noqa: F401
from pylabrobot.liquid_handling.liquid_handler import LiquidHandler as NewLH
from pylabrobot.liquid_handling.strictness import Strictness
from pylabrobot.machines.machine import Machine
from pylabrobot.resources import Coordinate, Deck, Resource
from pylabrobot.serializer import deserialize as _deserialize
from pylabrobot.serializer import serialize

logger = logging.getLogger("pylabrobot")


class LiquidHandler(Resource, Machine):
  """Legacy liquid handler that delegates to :class:`~pylabrobot.liquid_handling.LiquidHandler`
  (pipetting) and :class:`~pylabrobot.arms.Arm` (resource movement).

  This class maintains the old API surface (Resource + Machine inheritance, arm
  methods on ``self``, ``serialize``/``deserialize``/``load``) while forwarding
  the actual work to the new slim components.
  """

  def __init__(
    self,
    backend: LiquidHandlerBackend,
    deck: Deck,
    default_offset_head96: Optional[Coordinate] = None,
    name: Optional[str] = None,
  ):
    Resource.__init__(
      self,
      name=name if name is not None else f"lh_{deck.name}",
      size_x=deck._size_x,
      size_y=deck._size_y,
      size_z=deck._size_z,
      category="liquid_handler",
    )
    Machine.__init__(self, backend=backend)

    self.backend: LiquidHandlerBackend = backend
    self.deck = deck

    self.location = Coordinate.zero()
    super().assign_child_resource(deck, location=deck.location or Coordinate.zero())

    # Internal capability delegates
    self._lh = NewLH(backend=backend, deck=deck, default_offset_head96=default_offset_head96)
    self._arm = Arm(backend=backend, deck=deck)

  # ---------------------------------------------------------------------------
  # Properties delegated to _lh
  # ---------------------------------------------------------------------------

  @property
  def head(self):
    return self._lh.head

  @head.setter
  def head(self, value):
    self._lh.head = value

  @property
  def head96(self):
    return self._lh.head96

  @head96.setter
  def head96(self, value):
    self._lh.head96 = value

  @property
  def default_offset_head96(self):
    return self._lh.default_offset_head96

  @default_offset_head96.setter
  def default_offset_head96(self, value):
    self._lh.default_offset_head96 = value

  # ---------------------------------------------------------------------------
  # Properties delegated to _arm
  # ---------------------------------------------------------------------------

  @property
  def _resource_pickups(self):
    return self._arm._resource_pickups

  @property
  def _resource_pickup(self):
    return self._arm._resource_pickup

  @_resource_pickup.setter
  def _resource_pickup(self, value):
    self._arm._resource_pickup = value

  # ---------------------------------------------------------------------------
  # Lifecycle
  # ---------------------------------------------------------------------------

  async def setup(self, **backend_kwargs):
    """Prepare the robot for use."""
    if self.setup_finished:
      raise RuntimeError("The setup has already finished. See `LiquidHandler.stop`.")

    self.backend.set_deck(self.deck)
    await super().setup(**backend_kwargs)  # Machine.setup -> backend.setup()
    await self._lh.setup()
    await self._arm.setup()

    # Bridge internal state updates to the Resource tree.
    self._lh.register_state_callback(lambda _state: Resource._state_updated(self))
    self._arm.register_state_callback(lambda _state: Resource._state_updated(self))

  async def stop(self):
    await self._lh.stop()
    await self._arm.stop()
    await super().stop()

  # ---------------------------------------------------------------------------
  # State serialization (combines LH + Arm state)
  # ---------------------------------------------------------------------------

  def serialize_state(self) -> Dict[str, Any]:
    lh_state = self._lh.serialize_state()
    arm_state = self._arm.serialize_state()
    return {**lh_state, **arm_state}

  def load_state(self, state: Dict[str, Any]):
    self._lh.load_state(state)

  # ---------------------------------------------------------------------------
  # Persistence (serialize / deserialize / load)
  # ---------------------------------------------------------------------------

  def serialize(self) -> dict:
    return {
      **Resource.serialize(self),
      **Machine.serialize(self),
      "default_offset_head96": serialize(self._lh.default_offset_head96),
    }

  @classmethod
  def deserialize(cls, data: dict, allow_marshal: bool = False) -> LiquidHandler:
    deck_data = data["children"][0]
    deck = Deck.deserialize(data=deck_data, allow_marshal=allow_marshal)
    backend = LiquidHandlerBackend.deserialize(data=data["backend"])

    if "default_offset_head96" in data:
      default_offset = _deserialize(data["default_offset_head96"], allow_marshal=allow_marshal)
      assert isinstance(default_offset, Coordinate)
    else:
      default_offset = Coordinate.zero()

    return cls(
      deck=deck,
      backend=backend,
      default_offset_head96=default_offset,
    )

  @classmethod
  def load(cls, path: str) -> LiquidHandler:
    with open(path, "r", encoding="utf-8") as f:
      return cls.deserialize(json.load(f))

  # ---------------------------------------------------------------------------
  # Resource overrides
  # ---------------------------------------------------------------------------

  def assign_child_resource(
    self,
    resource: Resource,
    location: Optional[Coordinate] = None,
    reassign: bool = True,
  ):
    """Not implemented on LiquidHandler. Use ``lh.deck.assign_child_resource()``."""
    raise NotImplementedError(
      "Cannot assign child resource to liquid handler. Use lh.deck.assign_child_resource() instead."
    )

  # ---------------------------------------------------------------------------
  # Delegated to _lh: pipetting and tip handling
  # ---------------------------------------------------------------------------

  def update_head_state(self, *args, **kwargs):
    return self._lh.update_head_state(*args, **kwargs)

  def clear_head_state(self):
    return self._lh.clear_head_state()

  def summary(self):
    return self._lh.summary()

  def get_mounted_tips(self):
    return self._lh.get_mounted_tips()

  async def pick_up_tips(self, *args, **kwargs):
    return await self._lh.pick_up_tips(*args, **kwargs)

  async def drop_tips(self, *args, **kwargs):
    return await self._lh.drop_tips(*args, **kwargs)

  async def return_tips(self, *args, **kwargs):
    return await self._lh.return_tips(*args, **kwargs)

  async def discard_tips(self, *args, **kwargs):
    return await self._lh.discard_tips(*args, **kwargs)

  async def move_tips(self, *args, **kwargs):
    return await self._lh.move_tips(*args, **kwargs)

  async def aspirate(self, *args, **kwargs):
    return await self._lh.aspirate(*args, **kwargs)

  async def dispense(self, *args, **kwargs):
    return await self._lh.dispense(*args, **kwargs)

  async def transfer(self, *args, **kwargs):
    return await self._lh.transfer(*args, **kwargs)

  def use_channels(self, *args, **kwargs):
    return self._lh.use_channels(*args, **kwargs)

  async def use_tips(self, *args, **kwargs):
    return await self._lh.use_tips(*args, **kwargs)

  async def pick_up_tips96(self, *args, **kwargs):
    return await self._lh.pick_up_tips96(*args, **kwargs)

  async def drop_tips96(self, *args, **kwargs):
    return await self._lh.drop_tips96(*args, **kwargs)

  async def return_tips96(self, *args, **kwargs):
    return await self._lh.return_tips96(*args, **kwargs)

  async def discard_tips96(self, *args, **kwargs):
    return await self._lh.discard_tips96(*args, **kwargs)

  async def aspirate96(self, *args, **kwargs):
    return await self._lh.aspirate96(*args, **kwargs)

  async def dispense96(self, *args, **kwargs):
    return await self._lh.dispense96(*args, **kwargs)

  async def stamp(self, *args, **kwargs):
    return await self._lh.stamp(*args, **kwargs)

  async def prepare_for_manual_channel_operation(self, *args, **kwargs):
    return await self._lh.prepare_for_manual_channel_operation(*args, **kwargs)

  async def move_channel_x(self, *args, **kwargs):
    return await self._lh.move_channel_x(*args, **kwargs)

  async def move_channel_y(self, *args, **kwargs):
    return await self._lh.move_channel_y(*args, **kwargs)

  async def move_channel_z(self, *args, **kwargs):
    return await self._lh.move_channel_z(*args, **kwargs)

  async def probe_tip_presence_via_pickup(self, *args, **kwargs):
    return await self._lh.probe_tip_presence_via_pickup(*args, **kwargs)

  async def probe_tip_inventory(self, *args, **kwargs):
    return await self._lh.probe_tip_inventory(*args, **kwargs)

  async def consolidate_tip_inventory(self, *args, **kwargs):
    return await self._lh.consolidate_tip_inventory(*args, **kwargs)

  # ---------------------------------------------------------------------------
  # Delegated to _arm: resource movement
  # ---------------------------------------------------------------------------

  def get_picked_up_resource(self):
    return self._arm.get_picked_up_resource()

  async def pick_up_resource(self, *args, **kwargs):
    return await self._arm.pick_up_resource(*args, **kwargs)

  async def move_picked_up_resource(self, *args, **kwargs):
    return await self._arm.move_picked_up_resource(*args, **kwargs)

  async def drop_resource(self, *args, **kwargs):
    return await self._arm.drop_resource(*args, **kwargs)

  async def move_resource(self, *args, **kwargs):
    return await self._arm.move_resource(*args, **kwargs)

  async def move_lid(self, *args, **kwargs):
    return await self._arm.move_lid(*args, **kwargs)

  async def move_plate(self, *args, **kwargs):
    return await self._arm.move_plate(*args, **kwargs)
