"""CalendarTool — Agent tool for calendar event CRUD via ReAct integration.

Wraps CalendarService so the LLM can create, query, update, and delete
calendar events via function calling. The tool delegates all business logic
to CalendarService — it only handles input validation, name→id resolution
for event types and tags, and result formatting.

The tool trusts the caller (the agent framework) to provide the correct
user_id; it does not perform auth (same pattern as DocumentTool).
"""

from __future__ import annotations

import re
from datetime import datetime, timedelta
from typing import Any

from agentkit.calendar.models import ReminderRule
from agentkit.calendar.service import CalendarService
from agentkit.tools.base import Tool


# ---------------------------------------------------------------------------
# Chinese relative date/time parsing
# ---------------------------------------------------------------------------

_CN_WEEKDAY: dict[str, int] = {
    "一": 0, "二": 1, "三": 2, "四": 3, "五": 4, "六": 5, "日": 6, "天": 6,
    "1": 0, "2": 1, "3": 2, "4": 3, "5": 4, "6": 5, "0": 6,
}

_CN_NUM_MAP: dict[str, int] = {
    "零": 0, "一": 1, "二": 2, "三": 3, "四": 4, "五": 5,
    "六": 6, "七": 7, "八": 8, "九": 9, "十": 10,
}

# 下周三 / 下周星期三 / 这周三 / 下下周三 / 明天 / 后天 / 大后天 / 今天
_CN_DATE_RE = re.compile(
    r"(下下周|下周|这周|本周|大后天|后天|明天|今天)"
    r"(?:周|星期)?"
    r"([一二三四五六日天0-9])?"
)

# N天后 / N小时后 (N can be Arabic or Chinese number)
_CN_NDAYS_RE = re.compile(r"(\d+|[零一二三四五六七八九十]+)\s*天[后以]?")
_CN_NHOURS_RE = re.compile(r"(\d+|[零一二三四五六七八九十]+)\s*小时[后以]?")

# Time: 下午3点 / 晚上3:30 / 上午9点 / 15:00 / 3pm
_CN_TIME_RE = re.compile(
    r"(下午|晚上|傍晚|上午|早上|凌晨)?"
    r"\s*"
    r"(\d{1,2})"
    r"\s*[:：点]?"
    r"\s*"
    r"(\d{1,2})?"
    r"\s*(?:分|点半|pm|PM|am|AM)?"
)

# Detect any CJK character — used to guard dateutil fuzzy mode
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")


def _cn_num(s: str) -> int:
    """Parse a small Chinese or Arabic number (0–99)."""
    if s.isdigit():
        return int(s)
    if s in _CN_NUM_MAP:
        return _CN_NUM_MAP[s]
    # Handle "十一".."九十九", "二十".."三十"
    if "十" in s:
        parts = s.split("十")
        tens = _CN_NUM_MAP.get(parts[0], 1) if parts[0] else 1
        ones = _CN_NUM_MAP.get(parts[1], 0) if len(parts) > 1 and parts[1] else 0
        return tens * 10 + ones
    return 0


def _extract_cn_time(value: str, date_base: datetime) -> datetime:
    """Extract time from Chinese/Arabic time patterns and apply to date_base."""
    m = _CN_TIME_RE.search(value)
    if not m:
        return date_base.replace(hour=9, minute=0, second=0, microsecond=0)
    pm_kw, hour_str, minute_str = m.groups()
    hour = int(hour_str)
    minute = int(minute_str) if minute_str else 0
    if pm_kw in ("下午", "晚上", "傍晚") and hour < 12:
        hour += 12
    return date_base.replace(hour=hour, minute=minute, second=0, microsecond=0)


def _resolve_chinese_datetime(value: str) -> datetime | None:
    """Parse common Chinese relative date/time phrases.

    Handles: 下周三, 下周三下午3点, 明天, 后天15:00, 3天后, 2小时后, etc.
    Returns None if no Chinese pattern matches.
    """
    base = datetime.now().astimezone()

    # N天后
    m = _CN_NDAYS_RE.search(value)
    if m:
        n = _cn_num(m.group(1))
        target = base + timedelta(days=n)
        return _extract_cn_time(value, target)

    # N小时后
    m = _CN_NHOURS_RE.search(value)
    if m:
        n = _cn_num(m.group(1))
        return base + timedelta(hours=n)

    # Relative date phrases (下周X / 明天 / 后天 / ...)
    m = _CN_DATE_RE.search(value)
    if not m:
        return None

    week_kw, weekday_cn = m.groups()
    today_wd = base.weekday()

    if week_kw in ("这周", "本周"):
        if weekday_cn and weekday_cn in _CN_WEEKDAY:
            target_wd = _CN_WEEKDAY[weekday_cn]
            delta = target_wd - today_wd
            if delta < 0:
                delta += 7
            target = base + timedelta(days=delta)
        else:
            return None
    elif week_kw == "下周":
        if weekday_cn and weekday_cn in _CN_WEEKDAY:
            target_wd = _CN_WEEKDAY[weekday_cn]
            delta = (7 - today_wd) + target_wd
            target = base + timedelta(days=delta)
        else:
            delta = 7 - today_wd if today_wd != 0 else 7
            target = base + timedelta(days=delta)
    elif week_kw == "下下周":
        if weekday_cn and weekday_cn in _CN_WEEKDAY:
            target_wd = _CN_WEEKDAY[weekday_cn]
            delta = (14 - today_wd) + target_wd
            target = base + timedelta(days=delta)
        else:
            delta = 14 - today_wd
            target = base + timedelta(days=delta)
    elif week_kw == "今天":
        target = base
    elif week_kw == "明天":
        target = base + timedelta(days=1)
    elif week_kw == "后天":
        target = base + timedelta(days=2)
    elif week_kw == "大后天":
        target = base + timedelta(days=3)
    else:
        return None

    return _extract_cn_time(value, target)


# ---------------------------------------------------------------------------


def _resolve_datetime(value: str | None) -> str | None:
    """Resolve a date string to ISO 8601, or return None if unparseable.

    Accepts:
      - absolute ISO 8601 strings (returned unchanged)
      - Chinese relative phrases: "下周三", "下周三下午3点", "明天", "后天", "3天后"
      - English relative phrases: "next monday", "tomorrow 3pm", "+3 days"
    """
    if not value:
        return None
    value = value.strip()
    # Fast path: already ISO 8601 (has a date separator and a length typical of ISO).
    if "T" in value or value.endswith("Z") or len(value) >= 10 and value[4:5] == "-":
        try:
            datetime.fromisoformat(value.replace("Z", "+00:00"))
            return value
        except ValueError:
            pass

    # Chinese relative date/time parsing — must run BEFORE dateutil fuzzy
    # because dateutil cannot parse Chinese and its fuzzy mode silently
    # drops Chinese characters, producing wrong dates (e.g. "下周三 3pm"
    # → today's date at 15:00, completely wrong).
    if _CJK_RE.search(value):
        cn_dt = _resolve_chinese_datetime(value)
        if cn_dt is not None:
            return cn_dt.isoformat()
        # Chinese characters present but no pattern matched → don't fall
        # through to dateutil fuzzy (it will produce garbage). Return None
        # so the caller reports "could not parse" instead of a wrong date.
        return None

    try:
        from dateutil import parser as _du  # type: ignore[import-untyped]
        from datetime import datetime as _dt

        dt = _du.parse(value, fuzzy=True)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=_dt.now().astimezone().tzinfo)
        return dt.isoformat()
    except (ValueError, TypeError, ImportError):
        return None


class CalendarTool(Tool):
    """Agent tool for calendar event management.

    Actions: create_event, query_events, update_event, delete_event.
    """

    def __init__(self, calendar_service: CalendarService, default_user_id: str | None = None):
        super().__init__(
            name="calendar",
            description=(
                "Create, query, update, and delete calendar events and reminders. "
                "Use create_event to schedule events and set reminders (e.g. 'remind me Monday morning'). "
                "Relative dates like 'next monday', 'tomorrow 3pm', '+3 days' are accepted "
                "and resolved server-side against the current local time. "
                "Actions: create_event, query_events, update_event, delete_event."
            ),
            input_schema={
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": [
                            "create_event",
                            "query_events",
                            "update_event",
                            "delete_event",
                        ],
                        "description": "Calendar operation to perform.",
                    },
                    "event_id": {
                        "type": "string",
                        "description": "Event ID (for update_event and delete_event).",
                    },
                    "title": {
                        "type": "string",
                        "description": "Event title (create_event, update_event).",
                    },
                    "start_time": {
                        "type": "string",
                        "description": (
                            "Event start time. Accepts ISO 8601 UTC, English relative "
                            "('tomorrow 3pm', 'next monday'), or Chinese relative "
                            "('下周三', '下周三下午3点', '明天', '后天', '3天后'). "
                            "(create_event, update_event)."
                        ),
                    },
                    "end_time": {
                        "type": "string",
                        "description": "Event end time, ISO 8601 UTC (create_event, update_event).",
                    },
                    "description": {
                        "type": "string",
                        "description": "Event description (create_event, update_event).",
                    },
                    "location": {
                        "type": "string",
                        "description": "Event location (create_event, update_event).",
                    },
                    "is_all_day": {
                        "type": "boolean",
                        "description": "Whether the event is all-day (create_event, update_event).",
                    },
                    "event_type_name": {
                        "type": "string",
                        "description": "Event type name; looked up or created if missing (create_event).",
                    },
                    "tag_names": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Tag names; each looked up or created if missing (create_event).",
                    },
                    "rrule": {
                        "type": "string",
                        "description": "RFC 5545 RRULE recurrence string, e.g. FREQ=WEEKLY;BYDAY=MO;COUNT=10 (create_event).",
                    },
                    "conversation_id": {
                        "type": "string",
                        "description": "Conversation ID to associate with the event (create_event).",
                    },
                    "reminder_offset_minutes": {
                        "type": "integer",
                        "description": "Minutes before event start to fire a reminder. Negative means before, e.g. -15 = 15 min before (create_event).",
                    },
                    "reminder_channels": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "Channels for the reminder, e.g. [\"client\"] (create_event).",
                    },
                    "start_date": {
                        "type": "string",
                        "description": "Range start, ISO 8601 UTC (query_events).",
                    },
                    "end_date": {
                        "type": "string",
                        "description": "Range end, ISO 8601 UTC (query_events).",
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Maximum number of events to return (query_events).",
                    },
                },
                "required": ["action"],
            },
        )
        self._service = calendar_service
        self._default_user_id = default_user_id

    def set_default_user_id(self, user_id: str | None) -> None:
        """Set the default user_id used when the agent does not provide one.

        Called by the server lifespan after user store is available.
        ponytail: single-user dev-mode simplification — multi-user needs
        per-request context passing through the agent framework. Upgrade
        path: add a ``user_id_resolver`` callback that reads from a
        contextvar set by the chat handler.
        """
        self._default_user_id = user_id

    def _resolve_user_id(self, kwargs: dict[str, Any]) -> str | None:
        """Resolve user_id: prefer caller-provided, fall back to default."""
        provided = kwargs.get("user_id")
        if provided and isinstance(provided, str) and provided.strip():
            return provided
        return self._default_user_id

    async def execute(self, **kwargs) -> dict[str, Any]:
        action = kwargs.get("action")

        if action == "create_event":
            return await self._create_event(**kwargs)
        if action == "query_events":
            return await self._query_events(**kwargs)
        if action == "update_event":
            return await self._update_event(**kwargs)
        if action == "delete_event":
            return await self._delete_event(**kwargs)
        return {"success": False, "error": f"Unknown action: {action!r}"}

    # ------------------------------------------------------------------
    # create_event
    # ------------------------------------------------------------------

    async def _create_event(self, **kwargs) -> dict[str, Any]:
        user_id = self._resolve_user_id(kwargs)
        title = kwargs.get("title")
        start_time = kwargs.get("start_time")
        end_time = kwargs.get("end_time")

        if not user_id:
            return {"success": False, "error": "Missing required field: user_id"}
        if not title:
            return {"success": False, "error": "Missing required field: title"}
        if not start_time:
            return {"success": False, "error": "Missing required field: start_time"}
        if not end_time:
            return {"success": False, "error": "Missing required field: end_time"}

        # Resolve relative date strings (e.g. "next monday", "tomorrow 3pm")
        # to absolute ISO 8601 datetimes. Accepts anything dateutil can parse,
        # anchored at the *server's* current local time.
        start_time = _resolve_datetime(start_time)
        end_time = _resolve_datetime(end_time)
        if start_time is None:
            return {
                "success": False,
                "error": f"Could not parse start_time: {kwargs.get('start_time')!r}",
            }
        if end_time is None:
            return {
                "success": False,
                "error": f"Could not parse end_time: {kwargs.get('end_time')!r}",
            }

        description = kwargs.get("description", "")
        location = kwargs.get("location", "")
        is_all_day = kwargs.get("is_all_day", False)
        rrule = kwargs.get("rrule")
        conversation_id = kwargs.get("conversation_id")
        reminder_offset = kwargs.get("reminder_offset_minutes")
        reminder_channels = kwargs.get("reminder_channels") or ["client"]

        # ponytail: validate reminder_channels type — LLM agents may pass a
        # bare string ("client") which list() would silently split into
        # ["c","l","i","e","n","t"]. Reject non-list input explicitly.
        if not isinstance(reminder_channels, list):
            return {
                "success": False,
                "error": (
                    f"reminder_channels must be a list, got {type(reminder_channels).__name__}: "
                    f"{reminder_channels!r}"
                ),
            }
        # ponytail: channel whitelist — prevents arbitrary strings (e.g.
        # "drop_table") from being persisted and dispatched by the scheduler.
        _ALLOWED_CHANNELS = {"client", "email", "webhook"}
        bad = [c for c in reminder_channels if c not in _ALLOWED_CHANNELS]
        if bad:
            return {
                "success": False,
                "error": (
                    f"reminder_channels contains unknown channel(s): {bad}. "
                    f"Allowed: {sorted(_ALLOWED_CHANNELS)}"
                ),
            }

        # Build explicit reminder rules if requested
        reminder_rules: list[ReminderRule] | None = None
        if reminder_offset is not None:
            try:
                offset_int = int(reminder_offset)
            except (TypeError, ValueError):
                return {
                    "success": False,
                    "error": f"reminder_offset_minutes must be an integer, got {reminder_offset!r}",
                }
            # ponytail: schema documents negative offsets as "before event"
            # (e.g. -15 = 15 min before). Previous code rejected all negatives,
            # breaking the entire reminder feature. ±43200 = ±30 days ceiling.
            if not -43200 <= offset_int <= 43200:
                return {
                    "success": False,
                    "error": f"reminder_offset_minutes must be in [-43200, 43200], got {offset_int}",
                }
            reminder_rules = [
                ReminderRule(
                    id="",
                    event_id=None,
                    event_type_id=None,
                    offset_minutes=offset_int,
                    channels=list(reminder_channels),
                )
            ]

        # Resolve event_type_name → event_type_id (look up or create)
        event_type_id: str | None = None
        event_type_name = kwargs.get("event_type_name")
        if event_type_name:
            event_type_id = await self._resolve_event_type_id(user_id, event_type_name)

        # Resolve tag_names → tag_ids (look up or create each)
        tag_ids: list[str] | None = None
        tag_names = kwargs.get("tag_names")
        if tag_names:
            tag_ids = await self._resolve_tag_ids(user_id, tag_names)

        try:
            event = await self._service.create_event(
                user_id=user_id,
                title=title,
                start_time=start_time,
                end_time=end_time,
                description=description,
                location=location,
                is_all_day=is_all_day,
                event_type_id=event_type_id,
                rrule=rrule,
                source="agent",
                conversation_id=conversation_id,
                tag_ids=tag_ids,
                reminder_rules=reminder_rules,
            )
            return {"success": True, "event": event.to_dict()}
        except Exception as e:
            return {"success": False, "error": f"create_event failed: {e}"}

    async def _resolve_event_type_id(self, user_id: str, name: str) -> str | None:
        """Look up an event type by name for the user; create if not found."""
        existing = await self._service.list_event_types(user_id)
        for et in existing:
            if et.name == name:
                return et.id
        et = await self._service.create_event_type(user_id, name)
        return et.id

    async def _resolve_tag_ids(self, user_id: str, names: list[str]) -> list[str]:
        """Look up tags by name for the user; create each if not found."""
        existing = await self._service.list_tags(user_id)
        existing_by_name = {t.name: t.id for t in existing}
        tag_ids: list[str] = []
        for name in names:
            if name in existing_by_name:
                tag_ids.append(existing_by_name[name])
            else:
                tag = await self._service.create_tag(user_id, name)
                tag_ids.append(tag.id)
        return tag_ids

    # ------------------------------------------------------------------
    # query_events
    # ------------------------------------------------------------------

    async def _query_events(self, **kwargs) -> dict[str, Any]:
        user_id = self._resolve_user_id(kwargs)
        if not user_id:
            return {"success": False, "error": "Missing required field: user_id"}

        start = kwargs.get("start_date")
        end = kwargs.get("end_date")
        limit = kwargs.get("limit")

        try:
            events = await self._service.list_events(
                user_id=user_id,
                start=start,
                end=end,
            )
            if limit is not None:
                events = events[:limit]
            return {"success": True, "events": [e.to_dict() for e in events]}
        except Exception as e:
            return {"success": False, "error": f"query_events failed: {e}"}

    # ------------------------------------------------------------------
    # update_event
    # ------------------------------------------------------------------

    async def _update_event(self, **kwargs) -> dict[str, Any]:
        event_id = kwargs.get("event_id")
        user_id = self._resolve_user_id(kwargs)
        if not event_id:
            return {"success": False, "error": "Missing required field: event_id"}
        if not user_id:
            return {"success": False, "error": "Missing required field: user_id"}

        # Ownership check
        event = await self._service.get_event(event_id)
        if event is None:
            return {"success": False, "error": "Event not found"}
        if event.user_id != user_id:
            return {"success": False, "error": "Permission denied"}

        # Build fields dict from updatable params (only those explicitly provided)
        updatable = ["title", "description", "location", "is_all_day"]
        fields: dict[str, Any] = {}
        for key in updatable:
            if key in kwargs and kwargs[key] is not None:
                fields[key] = kwargs[key]
        for time_key in ("start_time", "end_time"):
            if time_key in kwargs and kwargs[time_key] is not None:
                resolved = _resolve_datetime(kwargs[time_key])
                if resolved is None:
                    return {
                        "success": False,
                        "error": f"Could not parse {time_key}: {kwargs[time_key]!r}",
                    }
                fields[time_key] = resolved

        if not fields:
            return {"success": False, "error": "No fields to update"}

        try:
            updated = await self._service.update_event(event_id, fields)
            if not updated:
                return {"success": False, "error": f"Event not found: {event_id}"}
            return {"success": True}
        except Exception as e:
            return {"success": False, "error": f"update_event failed: {e}"}

    # ------------------------------------------------------------------
    # delete_event
    # ------------------------------------------------------------------

    async def _delete_event(self, **kwargs) -> dict[str, Any]:
        event_id = kwargs.get("event_id")
        user_id = self._resolve_user_id(kwargs)
        if not event_id:
            return {"success": False, "error": "Missing required field: event_id"}
        if not user_id:
            return {"success": False, "error": "Missing required field: user_id"}

        # Ownership check
        event = await self._service.get_event(event_id)
        if event is None:
            return {"success": False, "error": "Event not found"}
        if event.user_id != user_id:
            return {"success": False, "error": "Permission denied"}

        try:
            deleted = await self._service.delete_event(event_id)
            if not deleted:
                return {"success": False, "error": f"Event not found: {event_id}"}
            return {"success": True}
        except Exception as e:
            return {"success": False, "error": f"delete_event failed: {e}"}
