"""
Component-based persistence for scheduling data using ElizaOS Components.

Mirrors the TypeScript storage layer with component-type keyed storage,
indexing for meetings by room and participant, and reminder registries.
"""

from __future__ import annotations

import time
import uuid
from datetime import datetime, timezone
from typing import Protocol, cast, runtime_checkable

from .types import (
    Availability,
    AvailabilityException,
    AvailabilityWindow,
    DayOfWeek,
    LocationType,
    Meeting,
    MeetingLocation,
    MeetingParticipant,
    MeetingStatus,
    Participant,
    ParticipantRole,
    Reminder,
    ReminderStatus,
    ReminderType,
    SchedulingConstraints,
    SchedulingRequest,
    SchedulingUrgency,
    TimeSlot,
)

# Component type prefixes
SCHEDULING_AVAILABILITY = "scheduling_availability"
SCHEDULING_MEETING = "scheduling_meeting"
SCHEDULING_REMINDER = "scheduling_reminder"
SCHEDULING_REQUEST = "scheduling_request"
SCHEDULING_MEETING_INDEX_ROOM = "scheduling_meeting_index:room"
SCHEDULING_MEETING_INDEX_PARTICIPANT = "scheduling_meeting_index:participant"
SCHEDULING_REMINDER_INDEX_MEETING = "scheduling_reminder_index:meeting"
REMINDER_REGISTRY = "scheduling_reminder_registry"

# Type alias for component storage data (heterogeneous JSON-like dicts)
_Data = dict[str, object]


# ============================================================================
# RUNTIME PROTOCOL
# ============================================================================


@runtime_checkable
class Component(Protocol):
    """Minimal component protocol matching ElizaOS Component."""

    id: str
    entity_id: str
    agent_id: str
    room_id: str
    world_id: str
    source_entity_id: str
    type: str
    created_at: int
    data: _Data


@runtime_checkable
class AgentRuntime(Protocol):
    """Protocol for the ElizaOS agent runtime that storage needs."""

    @property
    def agent_id(self) -> str: ...

    async def get_component(self, entity_id: str, component_type: str) -> Component | None: ...

    async def create_component(self, component: _Data) -> None: ...

    async def update_component(self, component: _Data) -> None: ...

    async def delete_component(self, component_id: str) -> None: ...

    async def get_components(self, entity_id: str) -> list[Component]: ...


# ============================================================================
# STORAGE DATA HELPERS
# ============================================================================


def _now_ms() -> int:
    """Get current time in milliseconds."""
    return int(time.time() * 1000)


def _as_list(d: _Data, key: str) -> list[_Data]:
    """Extract a list of dicts from storage data."""
    val = d.get(key, [])
    return cast(list[_Data], val) if isinstance(val, list) else []


def _as_dict(d: _Data, key: str) -> _Data:
    """Extract a sub-dict from storage data."""
    val = d.get(key, {})
    return cast(_Data, val) if isinstance(val, dict) else {}


# ============================================================================
# INDEX HELPERS
# ============================================================================


async def _get_meeting_index(
    runtime: AgentRuntime, index_type: str, index_key: str
) -> dict[str, list[str]]:
    component = await runtime.get_component(runtime.agent_id, f"{index_type}:{index_key}")
    if not component:
        return {"meeting_ids": []}
    data = component.data
    return cast(dict[str, list[str]], data) if isinstance(data, dict) else {"meeting_ids": []}


async def _save_meeting_index(
    runtime: AgentRuntime, index_type: str, index_key: str, index: dict[str, list[str]]
) -> None:
    component_type = f"{index_type}:{index_key}"
    existing = await runtime.get_component(runtime.agent_id, component_type)

    component_data: _Data = {
        "id": existing.id if existing else str(uuid.uuid4()),
        "entity_id": runtime.agent_id,
        "agent_id": runtime.agent_id,
        "room_id": str(uuid.uuid4()),
        "world_id": existing.world_id if existing else str(uuid.uuid4()),
        "source_entity_id": runtime.agent_id,
        "type": component_type,
        "created_at": existing.created_at if existing else _now_ms(),
        "data": index,
    }

    if existing:
        await runtime.update_component(component_data)
    else:
        await runtime.create_component(component_data)


async def _add_to_meeting_index(
    runtime: AgentRuntime, index_type: str, index_key: str, meeting_id: str
) -> None:
    index = await _get_meeting_index(runtime, index_type, index_key)
    if meeting_id not in index["meeting_ids"]:
        index["meeting_ids"].append(meeting_id)
        await _save_meeting_index(runtime, index_type, index_key, index)


async def _remove_from_meeting_index(
    runtime: AgentRuntime, index_type: str, index_key: str, meeting_id: str
) -> None:
    index = await _get_meeting_index(runtime, index_type, index_key)
    index["meeting_ids"] = [mid for mid in index["meeting_ids"] if mid != meeting_id]
    await _save_meeting_index(runtime, index_type, index_key, index)


async def _get_reminder_index(runtime: AgentRuntime, meeting_id: str) -> dict[str, list[str]]:
    component = await runtime.get_component(
        runtime.agent_id, f"{SCHEDULING_REMINDER_INDEX_MEETING}:{meeting_id}"
    )
    return cast(dict[str, list[str]], component.data) if component else {"reminder_ids": []}


async def _save_reminder_index(
    runtime: AgentRuntime, meeting_id: str, index: dict[str, list[str]]
) -> None:
    component_type = f"{SCHEDULING_REMINDER_INDEX_MEETING}:{meeting_id}"
    existing = await runtime.get_component(runtime.agent_id, component_type)

    component_data: _Data = {
        "id": existing.id if existing else str(uuid.uuid4()),
        "entity_id": runtime.agent_id,
        "agent_id": runtime.agent_id,
        "room_id": str(uuid.uuid4()),
        "world_id": existing.world_id if existing else str(uuid.uuid4()),
        "source_entity_id": runtime.agent_id,
        "type": component_type,
        "created_at": existing.created_at if existing else _now_ms(),
        "data": index,
    }

    if existing:
        await runtime.update_component(component_data)
    else:
        await runtime.create_component(component_data)


async def _get_reminder_registry(runtime: AgentRuntime) -> dict[str, list[str]]:
    component = await runtime.get_component(runtime.agent_id, REMINDER_REGISTRY)
    return cast(dict[str, list[str]], component.data) if component else {"reminder_ids": []}


async def _save_reminder_registry(runtime: AgentRuntime, registry: dict[str, list[str]]) -> None:
    existing = await runtime.get_component(runtime.agent_id, REMINDER_REGISTRY)

    component_data: _Data = {
        "id": existing.id if existing else str(uuid.uuid4()),
        "entity_id": runtime.agent_id,
        "agent_id": runtime.agent_id,
        "room_id": str(uuid.uuid4()),
        "world_id": existing.world_id if existing else str(uuid.uuid4()),
        "source_entity_id": runtime.agent_id,
        "type": REMINDER_REGISTRY,
        "created_at": existing.created_at if existing else _now_ms(),
        "data": registry,
    }

    if existing:
        await runtime.update_component(component_data)
    else:
        await runtime.create_component(component_data)


# ============================================================================
# SERIALIZATION / DESERIALIZATION
# ============================================================================


def _availability_to_dict(avail: Availability) -> _Data:
    return {
        "time_zone": avail.time_zone,
        "weekly": [
            {"day": w.day.value, "start_minutes": w.start_minutes, "end_minutes": w.end_minutes}
            for w in avail.weekly
        ],
        "exceptions": [
            {
                "date": e.date,
                "unavailable": e.unavailable,
                "start_minutes": e.start_minutes,
                "end_minutes": e.end_minutes,
                "reason": e.reason,
            }
            for e in avail.exceptions
        ],
    }


def _dict_to_availability(data: _Data) -> Availability:
    return Availability(
        time_zone=cast(str, data["time_zone"]),
        weekly=[
            AvailabilityWindow(
                day=DayOfWeek(cast(str, w["day"])),
                start_minutes=cast(int, w["start_minutes"]),
                end_minutes=cast(int, w["end_minutes"]),
            )
            for w in _as_list(data, "weekly")
        ],
        exceptions=[
            AvailabilityException(
                date=cast(str, e["date"]),
                unavailable=bool(e.get("unavailable", False)),
                start_minutes=cast(int | None, e.get("start_minutes")),
                end_minutes=cast(int | None, e.get("end_minutes")),
                reason=cast(str | None, e.get("reason")),
            )
            for e in _as_list(data, "exceptions")
        ],
    )


def _meeting_to_dict(meeting: Meeting) -> _Data:
    return {
        "id": meeting.id,
        "request_id": meeting.request_id,
        "room_id": meeting.room_id,
        "title": meeting.title,
        "description": meeting.description,
        "slot": {
            "start": meeting.slot.start,
            "end": meeting.slot.end,
            "time_zone": meeting.slot.time_zone,
        },
        "location": {
            "type": meeting.location.type.value,
            "name": meeting.location.name,
            "address": meeting.location.address,
            "city": meeting.location.city,
            "place_id": meeting.location.place_id,
            "video_url": meeting.location.video_url,
            "phone_number": meeting.location.phone_number,
            "notes": meeting.location.notes,
        },
        "participants": [
            {
                "entity_id": p.entity_id,
                "name": p.name,
                "email": p.email,
                "phone": p.phone,
                "role": p.role.value,
                "confirmed": p.confirmed,
                "confirmed_at": p.confirmed_at,
                "decline_reason": p.decline_reason,
            }
            for p in meeting.participants
        ],
        "status": meeting.status.value,
        "reschedule_count": meeting.reschedule_count,
        "cancellation_reason": meeting.cancellation_reason,
        "created_at": meeting.created_at,
        "updated_at": meeting.updated_at,
        "notes": meeting.notes,
        "meta": meeting.meta,
    }


def _dict_to_meeting(data: _Data) -> Meeting:
    slot_data = _as_dict(data, "slot")
    loc_data = _as_dict(data, "location")

    return Meeting(
        id=cast(str, data["id"]),
        request_id=cast(str, data["request_id"]),
        room_id=cast(str, data["room_id"]),
        title=cast(str, data["title"]),
        description=cast(str | None, data.get("description")),
        slot=TimeSlot(
            start=cast(str, slot_data["start"]),
            end=cast(str, slot_data["end"]),
            time_zone=cast(str, slot_data["time_zone"]),
        ),
        location=MeetingLocation(
            type=LocationType(cast(str, loc_data["type"])),
            name=cast(str | None, loc_data.get("name")),
            address=cast(str | None, loc_data.get("address")),
            city=cast(str | None, loc_data.get("city")),
            place_id=cast(str | None, loc_data.get("place_id")),
            video_url=cast(str | None, loc_data.get("video_url")),
            phone_number=cast(str | None, loc_data.get("phone_number")),
            notes=cast(str | None, loc_data.get("notes")),
        ),
        participants=[
            MeetingParticipant(
                entity_id=cast(str, p["entity_id"]),
                name=cast(str, p["name"]),
                email=cast(str | None, p.get("email")),
                phone=cast(str | None, p.get("phone")),
                role=ParticipantRole(cast(str, p["role"])),
                confirmed=bool(p.get("confirmed", False)),
                confirmed_at=cast(int | None, p.get("confirmed_at")),
                decline_reason=cast(str | None, p.get("decline_reason")),
            )
            for p in _as_list(data, "participants")
        ],
        status=MeetingStatus(cast(str, data.get("status", "proposed"))),
        reschedule_count=cast(int, data.get("reschedule_count", 0)),
        cancellation_reason=cast(str | None, data.get("cancellation_reason")),
        created_at=cast(int, data.get("created_at", 0)),
        updated_at=cast(int, data.get("updated_at", 0)),
        notes=cast(str | None, data.get("notes")),
        meta=cast(dict[str, str | int | float | bool | None] | None, data.get("meta")),
    )


def _reminder_to_dict(reminder: Reminder) -> _Data:
    return {
        "id": reminder.id,
        "meeting_id": reminder.meeting_id,
        "participant_id": reminder.participant_id,
        "scheduled_for": reminder.scheduled_for,
        "type": reminder.type.value,
        "message": reminder.message,
        "status": reminder.status.value,
        "sent_at": reminder.sent_at,
        "error": reminder.error,
        "created_at": reminder.created_at,
    }


def _dict_to_reminder(data: _Data) -> Reminder:
    return Reminder(
        id=cast(str, data["id"]),
        meeting_id=cast(str, data["meeting_id"]),
        participant_id=cast(str, data["participant_id"]),
        scheduled_for=cast(str, data["scheduled_for"]),
        type=ReminderType(cast(str, data["type"])),
        message=cast(str, data["message"]),
        status=ReminderStatus(cast(str, data.get("status", "pending"))),
        sent_at=cast(int | None, data.get("sent_at")),
        error=cast(str | None, data.get("error")),
        created_at=cast(int, data.get("created_at", 0)),
    )


def _request_to_dict(req: SchedulingRequest) -> _Data:
    return {
        "id": req.id,
        "room_id": req.room_id,
        "title": req.title,
        "description": req.description,
        "participants": [
            {
                "entity_id": p.entity_id,
                "name": p.name,
                "email": p.email,
                "phone": p.phone,
                "priority": p.priority,
                "availability": _availability_to_dict(p.availability),
            }
            for p in req.participants
        ],
        "constraints": {
            "min_duration_minutes": req.constraints.min_duration_minutes,
            "preferred_duration_minutes": req.constraints.preferred_duration_minutes,
            "max_days_out": req.constraints.max_days_out,
            "preferred_times": req.constraints.preferred_times,
            "preferred_days": (
                [d.value for d in req.constraints.preferred_days]
                if req.constraints.preferred_days
                else None
            ),
            "location_type": (
                req.constraints.location_type.value if req.constraints.location_type else None
            ),
            "location_constraint": req.constraints.location_constraint,
        },
        "urgency": req.urgency.value,
        "created_at": req.created_at,
        "max_proposals": req.max_proposals,
    }


def _dict_to_request(data: _Data) -> SchedulingRequest:
    constraints_data = _as_dict(data, "constraints")
    pref_days = cast(list[str] | None, constraints_data.get("preferred_days"))
    loc_type = cast(str | None, constraints_data.get("location_type"))

    return SchedulingRequest(
        id=cast(str, data["id"]),
        room_id=cast(str, data["room_id"]),
        title=cast(str, data["title"]),
        description=cast(str | None, data.get("description")),
        participants=[
            Participant(
                entity_id=cast(str, p["entity_id"]),
                name=cast(str, p["name"]),
                email=cast(str | None, p.get("email")),
                phone=cast(str | None, p.get("phone")),
                priority=cast(int | None, p.get("priority")),
                availability=_dict_to_availability(cast(_Data, p["availability"])),
            )
            for p in _as_list(data, "participants")
        ],
        constraints=SchedulingConstraints(
            min_duration_minutes=cast(int, constraints_data.get("min_duration_minutes", 30)),
            preferred_duration_minutes=cast(int, constraints_data.get("preferred_duration_minutes", 60)),
            max_days_out=cast(int, constraints_data.get("max_days_out", 7)),
            preferred_times=cast(list[str] | None, constraints_data.get("preferred_times")),
            preferred_days=[DayOfWeek(d) for d in pref_days] if pref_days else None,
            location_type=LocationType(loc_type) if loc_type else None,
            location_constraint=cast(str | None, constraints_data.get("location_constraint")),
        ),
        urgency=SchedulingUrgency(cast(str, data.get("urgency", "flexible"))),
        created_at=cast(int, data.get("created_at", 0)),
        max_proposals=cast(int | None, data.get("max_proposals")),
    )


# ============================================================================
# STORAGE CLASSES
# ============================================================================


class AvailabilityStorage:
    """Storage for participant availability."""

    def __init__(self, runtime: AgentRuntime) -> None:
        self._runtime = runtime

    async def get(self, entity_id: str) -> Availability | None:
        component_type = f"{SCHEDULING_AVAILABILITY}:{entity_id}"
        component = await self._runtime.get_component(entity_id, component_type)
        return _dict_to_availability(component.data) if component else None

    async def save(self, entity_id: str, availability: Availability) -> None:
        component_type = f"{SCHEDULING_AVAILABILITY}:{entity_id}"
        existing = await self._runtime.get_component(entity_id, component_type)

        component_data: _Data = {
            "id": existing.id if existing else str(uuid.uuid4()),
            "entity_id": entity_id,
            "agent_id": self._runtime.agent_id,
            "room_id": str(uuid.uuid4()),
            "world_id": existing.world_id if existing else str(uuid.uuid4()),
            "source_entity_id": self._runtime.agent_id,
            "type": component_type,
            "created_at": existing.created_at if existing else _now_ms(),
            "data": _availability_to_dict(availability),
        }

        if existing:
            await self._runtime.update_component(component_data)
        else:
            await self._runtime.create_component(component_data)

    async def delete(self, entity_id: str) -> None:
        component_type = f"{SCHEDULING_AVAILABILITY}:{entity_id}"
        if existing := await self._runtime.get_component(entity_id, component_type):
            await self._runtime.delete_component(existing.id)


class SchedulingRequestStorage:
    """Storage for scheduling requests."""

    def __init__(self, runtime: AgentRuntime) -> None:
        self._runtime = runtime

    async def get(self, request_id: str) -> SchedulingRequest | None:
        component_type = f"{SCHEDULING_REQUEST}:{request_id}"
        component = await self._runtime.get_component(self._runtime.agent_id, component_type)
        return _dict_to_request(component.data) if component else None

    async def save(self, request: SchedulingRequest) -> None:
        component_type = f"{SCHEDULING_REQUEST}:{request.id}"
        existing = await self._runtime.get_component(self._runtime.agent_id, component_type)

        component_data: _Data = {
            "id": existing.id if existing else str(uuid.uuid4()),
            "entity_id": self._runtime.agent_id,
            "agent_id": self._runtime.agent_id,
            "room_id": request.room_id,
            "world_id": existing.world_id if existing else str(uuid.uuid4()),
            "source_entity_id": self._runtime.agent_id,
            "type": component_type,
            "created_at": existing.created_at if existing else _now_ms(),
            "data": _request_to_dict(request),
        }

        if existing:
            await self._runtime.update_component(component_data)
        else:
            await self._runtime.create_component(component_data)

    async def delete(self, request_id: str) -> None:
        component_type = f"{SCHEDULING_REQUEST}:{request_id}"
        if existing := await self._runtime.get_component(self._runtime.agent_id, component_type):
            await self._runtime.delete_component(existing.id)

    async def get_by_room(self, room_id: str) -> list[SchedulingRequest]:
        return [
            _dict_to_request(c.data)
            for c in await self._runtime.get_components(self._runtime.agent_id)
            if hasattr(c, "type")
            and c.type.startswith(f"{SCHEDULING_REQUEST}:")
            and cast(str, c.data.get("room_id")) == room_id
        ]


class MeetingStorage:
    """Storage for meetings with room and participant indexing."""

    def __init__(self, runtime: AgentRuntime) -> None:
        self._runtime = runtime

    async def get(self, meeting_id: str) -> Meeting | None:
        component_type = f"{SCHEDULING_MEETING}:{meeting_id}"
        component = await self._runtime.get_component(self._runtime.agent_id, component_type)
        return _dict_to_meeting(component.data) if component else None

    async def save(self, meeting: Meeting) -> None:
        component_type = f"{SCHEDULING_MEETING}:{meeting.id}"
        existing = await self._runtime.get_component(self._runtime.agent_id, component_type)

        component_data: _Data = {
            "id": existing.id if existing else str(uuid.uuid4()),
            "entity_id": self._runtime.agent_id,
            "agent_id": self._runtime.agent_id,
            "room_id": meeting.room_id,
            "world_id": existing.world_id if existing else str(uuid.uuid4()),
            "source_entity_id": self._runtime.agent_id,
            "type": component_type,
            "created_at": existing.created_at if existing else meeting.created_at,
            "data": _meeting_to_dict(meeting),
        }

        if existing:
            await self._runtime.update_component(component_data)
        else:
            await self._runtime.create_component(component_data)
            await _add_to_meeting_index(
                self._runtime, SCHEDULING_MEETING_INDEX_ROOM, meeting.room_id, meeting.id
            )
            for participant in meeting.participants:
                await _add_to_meeting_index(
                    self._runtime, SCHEDULING_MEETING_INDEX_PARTICIPANT,
                    participant.entity_id, meeting.id,
                )

    async def delete(self, meeting_id: str) -> None:
        component_type = f"{SCHEDULING_MEETING}:{meeting_id}"
        existing = await self._runtime.get_component(self._runtime.agent_id, component_type)
        if not existing:
            return

        meeting = _dict_to_meeting(existing.data)
        await _remove_from_meeting_index(
            self._runtime, SCHEDULING_MEETING_INDEX_ROOM, meeting.room_id, meeting_id
        )
        for participant in meeting.participants:
            await _remove_from_meeting_index(
                self._runtime, SCHEDULING_MEETING_INDEX_PARTICIPANT,
                participant.entity_id, meeting_id,
            )
        await self._runtime.delete_component(existing.id)

    async def get_by_room(self, room_id: str) -> list[Meeting]:
        index = await _get_meeting_index(self._runtime, SCHEDULING_MEETING_INDEX_ROOM, room_id)
        meetings: list[Meeting] = []
        for meeting_id in index["meeting_ids"]:
            component = await self._runtime.get_component(
                self._runtime.agent_id, f"{SCHEDULING_MEETING}:{meeting_id}"
            )
            if component:
                meetings.append(_dict_to_meeting(component.data))
        return meetings

    async def get_upcoming_for_participant(self, entity_id: str) -> list[Meeting]:
        index = await _get_meeting_index(
            self._runtime, SCHEDULING_MEETING_INDEX_PARTICIPANT, entity_id
        )
        now = _now_ms()
        meetings: list[Meeting] = []

        for meeting_id in index["meeting_ids"]:
            component = await self._runtime.get_component(
                self._runtime.agent_id, f"{SCHEDULING_MEETING}:{meeting_id}"
            )
            if not component:
                continue
            meeting = _dict_to_meeting(component.data)
            start_ms = int(
                datetime.fromisoformat(meeting.slot.start.replace("Z", "+00:00")).timestamp() * 1000
            )
            if start_ms > now and meeting.status != MeetingStatus.CANCELLED:
                meetings.append(meeting)

        meetings.sort(key=lambda m: m.slot.start)
        return meetings


class ReminderStorage:
    """Storage for reminders with meeting indexing and due-query support."""

    def __init__(self, runtime: AgentRuntime) -> None:
        self._runtime = runtime

    async def get(self, reminder_id: str) -> Reminder | None:
        component = await self._runtime.get_component(
            self._runtime.agent_id, f"{SCHEDULING_REMINDER}:{reminder_id}"
        )
        return _dict_to_reminder(component.data) if component else None

    async def save(self, reminder: Reminder) -> None:
        component_type = f"{SCHEDULING_REMINDER}:{reminder.id}"
        existing = await self._runtime.get_component(self._runtime.agent_id, component_type)

        component_data: _Data = {
            "id": existing.id if existing else str(uuid.uuid4()),
            "entity_id": self._runtime.agent_id,
            "agent_id": self._runtime.agent_id,
            "room_id": str(uuid.uuid4()),
            "world_id": existing.world_id if existing else str(uuid.uuid4()),
            "source_entity_id": self._runtime.agent_id,
            "type": component_type,
            "created_at": existing.created_at if existing else reminder.created_at,
            "data": _reminder_to_dict(reminder),
        }

        if existing:
            await self._runtime.update_component(component_data)
        else:
            await self._runtime.create_component(component_data)
            # Add to meeting index
            index = await _get_reminder_index(self._runtime, reminder.meeting_id)
            if reminder.id not in index["reminder_ids"]:
                index["reminder_ids"].append(reminder.id)
                await _save_reminder_index(self._runtime, reminder.meeting_id, index)
            # Add to global registry
            registry = await _get_reminder_registry(self._runtime)
            if reminder.id not in registry["reminder_ids"]:
                registry["reminder_ids"].append(reminder.id)
                await _save_reminder_registry(self._runtime, registry)

    async def delete(self, reminder_id: str) -> None:
        existing = await self._runtime.get_component(
            self._runtime.agent_id, f"{SCHEDULING_REMINDER}:{reminder_id}"
        )
        if not existing:
            return

        reminder = _dict_to_reminder(existing.data)
        index = await _get_reminder_index(self._runtime, reminder.meeting_id)
        index["reminder_ids"] = [rid for rid in index["reminder_ids"] if rid != reminder_id]
        await _save_reminder_index(self._runtime, reminder.meeting_id, index)

        registry = await _get_reminder_registry(self._runtime)
        registry["reminder_ids"] = [rid for rid in registry["reminder_ids"] if rid != reminder_id]
        await _save_reminder_registry(self._runtime, registry)

        await self._runtime.delete_component(existing.id)

    async def get_by_meeting(self, meeting_id: str) -> list[Reminder]:
        index = await _get_reminder_index(self._runtime, meeting_id)
        reminders: list[Reminder] = []
        for reminder_id in index["reminder_ids"]:
            component = await self._runtime.get_component(
                self._runtime.agent_id, f"{SCHEDULING_REMINDER}:{reminder_id}"
            )
            if component:
                reminders.append(_dict_to_reminder(component.data))
        return reminders

    async def get_due(self) -> list[Reminder]:
        registry = await _get_reminder_registry(self._runtime)
        now = _now_ms()
        due: list[Reminder] = []

        for reminder_id in registry["reminder_ids"]:
            component = await self._runtime.get_component(
                self._runtime.agent_id, f"{SCHEDULING_REMINDER}:{reminder_id}"
            )
            if not component:
                continue
            reminder = _dict_to_reminder(component.data)
            scheduled_ms = int(
                datetime.fromisoformat(
                    reminder.scheduled_for.replace("Z", "+00:00")
                ).timestamp() * 1000
            )
            if reminder.status == ReminderStatus.PENDING and scheduled_ms <= now:
                due.append(reminder)

        return due


# ============================================================================
# FACTORY FUNCTIONS
# ============================================================================


def get_availability_storage(runtime: AgentRuntime) -> AvailabilityStorage:
    return AvailabilityStorage(runtime)


def get_scheduling_request_storage(runtime: AgentRuntime) -> SchedulingRequestStorage:
    return SchedulingRequestStorage(runtime)


def get_meeting_storage(runtime: AgentRuntime) -> MeetingStorage:
    return MeetingStorage(runtime)


def get_reminder_storage(runtime: AgentRuntime) -> ReminderStorage:
    return ReminderStorage(runtime)
