"""Shared helpers for PIM data modules (calendar, todos).

Extracted to avoid duplication across VEVENT and VTODO operations.
"""

from __future__ import annotations

import logging
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any

import icalendar

logger = logging.getLogger(__name__)


def validate_date_not_past(date_str: str, label: str = "date") -> str | None:
    """Validate an ISO date string is not in the past.

    Returns an error message string if invalid or in the past, None if OK.
    Used by tool layer to catch LLMs using training-data years.
    """
    try:
        dt = datetime.fromisoformat(date_str)
    except (ValueError, TypeError):
        return (
            f"Invalid date format for {label}: '{date_str}'. "
            f"Use ISO format like '2026-03-15T10:00'."
        )
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    now = datetime.now(tz=timezone.utc)
    if dt < now:
        return (
            f"Cannot use a {label} in the past. "
            f"'{date_str}' is before today ({now.strftime('%B %d, %Y %H:%M UTC')}). "
            f"Check the year — use the current date from your time context."
        )
    return None


def _to_datetime(val: Any) -> datetime | None:
    """Coerce a date/datetime/ical value to a timezone-aware datetime."""
    if val is None:
        return None
    if isinstance(val, datetime):
        if val.tzinfo is None:
            return val.replace(tzinfo=timezone.utc)
        return val
    if isinstance(val, date):
        return datetime(val.year, val.month, val.day, tzinfo=timezone.utc)
    # icalendar may wrap in vDate/vDatetime
    dt = getattr(val, "dt", None)
    if dt is not None:
        return _to_datetime(dt)
    return None


def _parse_attendees(
    raw_attendees: Any,
    *,
    detect_assignee: bool = False,
) -> tuple[list[dict[str, str]], str]:
    """Parse ATTENDEE properties from an iCalendar component.

    Args:
        raw_attendees: The raw attendee value(s) from a component
            (single vCalAddress or list).
        detect_assignee: If True, attendees with ROLE=ASSIGNEE are
            separated out and the best match is returned as the
            assigned_to string. Used by VTODO parsing.

    Returns:
        Tuple of (attendees_list, assigned_to_str). When detect_assignee
        is False, assigned_to_str is always "".
    """
    attendees: list[dict[str, str]] = []
    assigned_to = ""

    if raw_attendees is None:
        return attendees, assigned_to

    if not isinstance(raw_attendees, list):
        raw_attendees = [raw_attendees]

    for att in raw_attendees:
        addr = str(att).replace("mailto:", "").replace("MAILTO:", "")
        cn = att.params.get("CN", "") if hasattr(att, "params") else ""
        role = str(att.params.get("ROLE", "")) if hasattr(att, "params") else ""
        entry = {"name": str(cn), "email": addr}

        if detect_assignee and role == "ASSIGNEE":
            assigned_to = str(cn) if cn else addr
        else:
            attendees.append(entry)

    return attendees, assigned_to


def _serialize_attendee(
    component: icalendar.cal.Component,
    att_data: Any,
    role: str = "REQ-PARTICIPANT",
) -> None:
    """Serialize a single attendee onto an iCalendar component.

    Args:
        component: The VEVENT or VTODO component to add the attendee to.
        att_data: Either a dict with "name"/"email" keys, or a string in
            ``"Name <email>"`` or plain email format.
        role: The ROLE parameter value (e.g. "REQ-PARTICIPANT", "ASSIGNEE").
    """
    if isinstance(att_data, dict):
        email = att_data.get("email", "")
        cn = att_data.get("name", "")
    else:
        att_str = str(att_data)
        if "<" in att_str and ">" in att_str:
            cn = att_str[:att_str.index("<")].strip()
            email = att_str[att_str.index("<") + 1:att_str.index(">")]
        else:
            cn, email = "", att_str.strip()

    if email:
        attendee = icalendar.vCalAddress(f"mailto:{email}")
        if cn:
            attendee.params["CN"] = icalendar.vText(cn)
        attendee.params["ROLE"] = icalendar.vText(role)
        component.add("attendee", attendee, encode=0)


def _find_ics_file(
    directory: Path,
    uid: str,
    component_type: str,
) -> Path | None:
    """Find the .ics file containing a component with the given UID.

    Args:
        directory: Directory to search in.
        uid: The UID to look for.
        component_type: The component name to match (e.g. "VEVENT", "VTODO").

    Returns:
        Path to the matching .ics file, or None if not found.
    """
    if not directory.exists():
        return None

    # First try direct filename match
    safe_uid = uid.replace("/", "_").replace("\\", "_")
    direct = directory / f"{safe_uid}.ics"
    if direct.exists():
        return direct

    # Fallback: scan all files
    for ics_path in directory.glob("*.ics"):
        try:
            data = ics_path.read_bytes()
            cal = icalendar.Calendar.from_ical(data)
            for component in cal.walk():
                if component.name == component_type and str(component.get("uid", "")) == uid:
                    return ics_path
        except Exception as e:  # noqa: BLE001
            logger.warning("Failed to parse %s: %s", ics_path, e)
            continue
    return None
