"""
Petnetizen Feeder BLE Device Controller

Main library class for controlling feeder devices.
"""

import asyncio
import logging
from datetime import datetime
from typing import Any, List, Dict, Optional
from .protocol import (
    FeederBLEProtocol,
    discover_feeders,
    CMD_FEEDING,
    CMD_SET_FEEDER_PLAN,
    CMD_CHILD_LOCK,
    CMD_REMINDER_TONE,
    CMD_QUERY_FEEDER_PLAN,
    CMD_QUERY_NAME_VERSION,
    DEFAULT_VERIFICATION_CODE,
)

_LOGGER = logging.getLogger(__name__)

# Weekday bitmask values (from FeedInfo.Companion.getWeekValue)
WEEKDAY_BITMASK = {
    "sun": 1,
    "mon": 2,
    "tue": 4,
    "wed": 8,
    "thu": 16,
    "fri": 32,
    "sat": 64,
}


class Weekday:
    """Weekday constants for schedule"""
    SUNDAY = "sun"
    MONDAY = "mon"
    TUESDAY = "tue"
    WEDNESDAY = "wed"
    THURSDAY = "thu"
    FRIDAY = "fri"
    SATURDAY = "sat"

    ALL_DAYS = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]
    WEEKDAYS = ["mon", "tue", "wed", "thu", "fri"]
    WEEKEND = ["sat", "sun"]


class FeedSchedule:
    """Represents a single feed schedule entry"""

    def __init__(self, weekdays: List[str], time: str, portions: int, enabled: bool = True):
        """
        Args:
            weekdays: List of weekday names (e.g., ["mon", "wed", "fri"])
            time: Time in HH:MM format (e.g., "08:00")
            portions: Number of portions to feed (1-15)
            enabled: Whether this schedule is enabled
        """
        self.weekdays = weekdays
        self.time = time
        self.portions = portions
        self.enabled = enabled

    def to_bytes(self) -> bytes:
        """Convert schedule to protocol format"""
        # Calculate week bitmask
        week_value = 0
        for day in self.weekdays:
            day_lower = day.lower()
            if day_lower in WEEKDAY_BITMASK:
                week_value |= WEEKDAY_BITMASK[day_lower]

        # Parse time
        hour, minute = map(int, self.time.split(":"))

        # Format: week(1 hex) + hour(1 hex) + minute(1 hex) + count(1 hex) + enabled(1 hex)
        return bytes([
            week_value,
            hour,
            minute,
            self.portions,
            1 if self.enabled else 0
        ])


class FeederDevice:
    """
    Main class for controlling Petnetizen feeder devices via BLE.

    Example:
        async def main():
            feeder = FeederDevice("E6:C0:07:09:A3:D3")
            await feeder.connect()
            await feeder.feed(portions=2)
            await feeder.disconnect()

        asyncio.run(main())
    """

    def __init__(
        self,
        address: str,
        verification_code: str = DEFAULT_VERIFICATION_CODE,
        device_type: Optional[str] = None,
    ):
        """
        Initialize feeder device controller.

        Args:
            address: BLE device address (e.g., "E6:C0:07:09:A3:D3")
            verification_code: Verification code (default: "00000000")
            device_type: Optional "standard", "jk", or "ali" (auto-detected from name if not set)
        """
        self.address = address
        self.verification_code = verification_code
        self._protocol = FeederBLEProtocol(address, device_type=device_type)
        self._connected = False

    async def connect(self, ble_client: Optional[Any] = None) -> bool:
        """
        Connect to the feeder device.

        Args:
            ble_client: Optional already-connected BleakClient (e.g. from bleak_retry_connector).
                        When provided, the library uses it instead of creating a new connection.

        Returns:
            True if connection successful, False otherwise
        """
        if await self._protocol.connect(ble_client=ble_client):
            # Send verification code
            await self._protocol.send_verification_code(self.verification_code)
            await asyncio.sleep(0.5)
            self._connected = True
            return True
        return False

    async def disconnect(self):
        """Disconnect from the device"""
        await self._protocol.disconnect()
        self._connected = False

    async def feed(self, portions: int = 1) -> bool:
        """
        Trigger manual feed with specified number of portions.

        Args:
            portions: Number of portions to feed (1-15, typically 1-3)

        Returns:
            True if feed command was acknowledged, False otherwise

        Raises:
            RuntimeError: If not connected
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")

        # Ensure connection is still active
        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")

        # Check device state
        await self._protocol.query_fault()
        await asyncio.sleep(0.5)
        await self._protocol.query_child_lock()
        await asyncio.sleep(0.5)
        await self._protocol.query_feeding_status()
        await asyncio.sleep(0.5)

        # Send feed command
        # Format: EA + 08 + 01 + portions(1 byte) + 00 + AE
        command = self._protocol.encode_command(CMD_FEEDING, length=1, action_hex=f"{portions:02X}")

        notification_count_before = len(self._protocol.received_data)

        try:
            await self._protocol.client.write_gatt_char(
                self._protocol.write_uuid, command, response=False
            )

            # Wait for response
            feed_triggered = False
            for _ in range(20):  # Wait up to 10 seconds
                await asyncio.sleep(0.5)
                if len(self._protocol.received_data) > notification_count_before:
                    new_notifications = self._protocol.received_data[notification_count_before:]
                    for data in new_notifications:
                        decoded = self._protocol.decode_notification(data)
                        cmd = decoded.get("command", "")

                        if cmd == "08":  # FEEDING response
                            feed_triggered = True
                        elif cmd == "0C":  # MANUAL_FEED_RESULT
                            return True  # Feed completed

            return feed_triggered
        except Exception as e:
            raise RuntimeError(f"Failed to send feed command: {e}") from e

    async def set_schedule(self, schedules: List[FeedSchedule]) -> bool:
        """
        Set feed schedule.

        Args:
            schedules: List of FeedSchedule objects

        Returns:
            True if command was sent successfully

        Raises:
            RuntimeError: If not connected
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")

        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")

        # Build schedule data: each entry is 5 bytes
        schedule_data = bytearray()
        for schedule in schedules:
            schedule_data.extend(schedule.to_bytes())

        # Send command
        # Format: EA + 07 + length + schedule_data + 00 + AE
        command = self._protocol.encode_command(
            CMD_SET_FEEDER_PLAN,
            length=len(schedule_data),
            action_hex=schedule_data.hex().upper()
        )

        try:
            await self._protocol.client.write_gatt_char(
                self._protocol.write_uuid, command, response=False
            )
            await asyncio.sleep(1)
            return True
        except Exception as e:
            raise RuntimeError(f"Failed to set schedule: {e}") from e

    async def set_child_lock(self, locked: bool) -> bool:
        """
        Set child lock state.

        Args:
            locked: True to lock, False to unlock

        Returns:
            True if command was sent successfully

        Raises:
            RuntimeError: If not connected
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")

        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")

        # Format: EA + 0D + 01 + value(00/01) + 00 + AE
        value = "01" if locked else "00"
        command = self._protocol.encode_command(CMD_CHILD_LOCK, length=1, action_hex=value)

        try:
            await self._protocol.client.write_gatt_char(
                self._protocol.write_uuid, command, response=False
            )
            await asyncio.sleep(1)
            return True
        except Exception as e:
            raise RuntimeError(f"Failed to set child lock: {e}") from e

    async def set_sound(self, enabled: bool) -> bool:
        """
        Set reminder tone/sound state.

        Args:
            enabled: True to enable sound, False to disable

        Returns:
            True if command was sent successfully

        Raises:
            RuntimeError: If not connected
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")

        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")

        # Format: EA + 12 + 01 + value(00/01) + 00 + AE
        value = "01" if enabled else "00"
        command = self._protocol.encode_command(CMD_REMINDER_TONE, length=1, action_hex=value)

        try:
            await self._protocol.client.write_gatt_char(
                self._protocol.write_uuid, command, response=False
            )
            await asyncio.sleep(1)
            return True
        except Exception as e:
            raise RuntimeError(f"Failed to set sound: {e}") from e

    async def query_schedule(self) -> List[Dict]:
        """
        Query current feed schedule.

        Returns:
            List of schedule dictionaries

        Raises:
            RuntimeError: If not connected
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")

        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")

        # Send query command
        command = self._protocol.encode_command(CMD_QUERY_FEEDER_PLAN, length=0)

        notification_count_before = len(self._protocol.received_data)

        try:
            await self._protocol.client.write_gatt_char(
                self._protocol.write_uuid, command, response=False
            )
            await asyncio.sleep(4.0)

            # Parse response
            new_count = len(self._protocol.received_data) - notification_count_before
            if new_count > 0:
                new_notifications = self._protocol.received_data[notification_count_before:]
                _LOGGER.debug(
                    "query_schedule: got %d new notification(s)", len(new_notifications)
                )
                for data in new_notifications:
                    decoded = self._protocol.decode_notification(data)
                    cmd = decoded.get("command")
                    if cmd == "11":  # QUERY_FEEDER_PLAN
                        slots = decoded.get("feed_plan_slots") or []
                        if slots:
                            return slots
                        # 0x11 but no parsed slots - data might be different format
                        if "data_hex" in decoded:
                            _LOGGER.debug(
                                "QUERY_FEEDER_PLAN response data_hex=%s len=%s",
                                decoded.get("data_hex"),
                                len(data) if hasattr(data, "__len__") else None,
                            )
                    elif cmd and "error" not in decoded:
                        _LOGGER.debug("query_schedule saw notification cmd=%s", cmd)
            return []
        except Exception as e:
            raise RuntimeError(f"Failed to query schedule: {e}") from e

    async def get_device_info(self) -> Dict:
        """
        Query device name and firmware version.

        Returns:
            Dict with "device_name", "device_version" (or empty strings if not available).
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")
        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")
        before = len(self._protocol.received_data)
        await self._protocol.query_name_version()
        await asyncio.sleep(2)
        result: Dict = {"device_name": "", "device_version": ""}
        for data in self._protocol.received_data[before:]:
            decoded = self._protocol.decode_notification(data)
            if decoded.get("command") == "00":
                result["device_name"] = decoded.get("device_name", "") or ""
                result["device_version"] = decoded.get("device_version", "") or ""
                break
        return result

    async def get_child_lock_status(self) -> Optional[bool]:
        """
        Query child lock status from the device.

        Returns:
            True if locked, False if unlocked, or None if query failed or no response.
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")
        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")
        before = len(self._protocol.received_data)
        await self._protocol.query_child_lock()
        await asyncio.sleep(1.5)
        for data in self._protocol.received_data[before:]:
            decoded = self._protocol.decode_notification(data)
            if decoded.get("command") == "0D" and "child_lock" in decoded:
                return decoded["child_lock"] == 1
        return None

    async def get_prompt_sound_status(self) -> Optional[bool]:
        """
        Query prompt sound / reminder tone status from the device.

        Returns:
            True if sound is on, False if off, or None if query failed or no response.
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")
        if not await self._protocol._ensure_connected():
            raise RuntimeError("Connection lost. Please reconnect.")
        before = len(self._protocol.received_data)
        await self._protocol.query_reminder_tone()
        await asyncio.sleep(1.5)
        for data in self._protocol.received_data[before:]:
            decoded = self._protocol.decode_notification(data)
            if decoded.get("command") == "12" and "prompt_sound" in decoded:
                return decoded["prompt_sound"] == 1
        return None

    async def sync_time(self, dt: Optional[datetime] = None) -> None:
        """
        Sync device clock to the given time (default: now).

        Args:
            dt: Time to set on the device; defaults to datetime.now().
        """
        if not self._connected:
            raise RuntimeError("Not connected to device. Call connect() first.")
        await self._protocol.send_sync_time(dt)

    @property
    def is_connected(self) -> bool:
        """Check if device is connected"""
        return self._connected and (
            self._protocol.client is not None and self._protocol.client.is_connected
        )
