from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime
from typing import Any

from backend.appointment_intents import appointment_intent_to_create_payload
from backend.local_time import localize_calendar_match_datetime
from backend.config import _normalize_tool_name
from backend.direct_accelerators import resolve_direct_acceleration
from backend.email_intents import email_intent_to_send_payload
from backend.protocol_router import refine_protocol_route, scope_task_state_to_domain
from backend.task_graph import TaskGraph, task_graph_from_direct_calls
from backend.tool_catalog import ToolCapability, build_runtime_tool_catalog


def _settings_provider_enabled(settings: dict | None, provider: str) -> bool:
    source = dict(settings or {})
    normalized = str(provider or "").strip().lower()
    if normalized == "outlook":
        if "use_outlook" in source:
            return bool(source.get("use_outlook", False))
        return bool(source.get("outlook_enabled", False))
    if normalized == "gmail":
        return bool(source.get("google_enabled", False))
    return False


def _available_email_send_providers(catalog: dict[str, ToolCapability]) -> list[str]:
    providers: list[str] = []
    for provider, tool_name in (("outlook", "outlook_send_email"), ("gmail", "gmail_send_email")):
        capability = catalog.get(tool_name)
        if capability and capability.enabled and capability.available:
            providers.append(provider)
    return providers


def _email_access_disabled_clarification(settings: dict | None, *, provider: str = "", dutch: bool = False) -> str:
    normalized = str(provider or "").strip().lower()
    if normalized == "outlook":
        return (
            "Outlook email access staat uit. Zet Outlook aan in Settings > Integrations en probeer het daarna opnieuw."
            if dutch else
            "Outlook email access is turned off. Enable Outlook in Settings > Integrations and try again."
        )
    if normalized == "gmail":
        return (
            "Gmail email access staat uit. Zet Gmail aan in Settings > Integrations en probeer het daarna opnieuw."
            if dutch else
            "Gmail email access is turned off. Enable Gmail in Settings > Integrations and try again."
        )

    outlook_enabled = _settings_provider_enabled(settings, "outlook")
    gmail_enabled = _settings_provider_enabled(settings, "gmail")
    if not outlook_enabled and not gmail_enabled:
        return (
            "Email access staat uit. Zet Outlook of Gmail aan in Settings > Integrations en probeer het daarna opnieuw."
            if dutch else
            "Email access is turned off. Enable Outlook or Gmail in Settings > Integrations and try again."
        )
    if not outlook_enabled:
        return (
            "Outlook email access staat uit. Zet Outlook aan in Settings > Integrations, of kies expliciet Gmail als verzendaccount."
            if dutch else
            "Outlook email access is turned off. Enable Outlook in Settings > Integrations, or explicitly choose Gmail as the sending account."
        )
    if not gmail_enabled:
        return (
            "Gmail email access staat uit. Zet Gmail aan in Settings > Integrations, of kies expliciet Outlook als verzendaccount."
            if dutch else
            "Gmail email access is turned off. Enable Gmail in Settings > Integrations, or explicitly choose Outlook as the sending account."
        )
    return (
        "Ik kan de e-mail via Outlook of Gmail versturen. Zeg expliciet welk account ik moet gebruiken."
        if dutch else
        "I can send the email via Outlook or Gmail. Please say explicitly which account I should use."
    )


@dataclass(slots=True)
class PlannedExecution:
    goal: str
    display_goal: str
    intent: dict | None
    calls: list[dict[str, Any]] = field(default_factory=list)
    task_graph: TaskGraph | None = None
    graph_route: Any = None
    catalog: dict[str, ToolCapability] = field(default_factory=dict)
    unavailable_tools: list[str] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)


@dataclass(slots=True)
class PlannedCapabilityStep:
    step: str
    capabilities: list[str] = field(default_factory=list)
    preferred_tools: list[str] = field(default_factory=list)
    selected_tools: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "step": self.step,
            "capabilities": list(self.capabilities),
            "preferred_tools": list(self.preferred_tools),
            "selected_tools": list(self.selected_tools),
        }


@dataclass(slots=True)
class IntentCapabilityPlan:
    domain: str
    subdomain: str
    goal_type: str
    action: str
    candidate_steps: list[str] = field(default_factory=list)
    preferred_tools: list[str] = field(default_factory=list)
    selected_tools: list[str] = field(default_factory=list)
    unavailable_tools: list[str] = field(default_factory=list)
    steps: list[PlannedCapabilityStep] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "domain": self.domain,
            "subdomain": self.subdomain,
            "goal_type": self.goal_type,
            "action": self.action,
            "candidate_steps": list(self.candidate_steps),
            "preferred_tools": list(self.preferred_tools),
            "selected_tools": list(self.selected_tools),
            "unavailable_tools": list(self.unavailable_tools),
            "steps": [step.to_dict() for step in self.steps],
        }


@dataclass(slots=True)
class PlannedEmailAction:
    action: str
    clarification: str = ""
    read_graphs: list[TaskGraph] = field(default_factory=list)
    write_graphs: list[TaskGraph] = field(default_factory=list)
    tool_details: list[dict[str, Any]] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)
    unavailable_tools: list[str] = field(default_factory=list)


@dataclass(slots=True)
class PlannedCalendarAction:
    action: str
    clarification: str = ""
    read_graphs: list[TaskGraph] = field(default_factory=list)
    write_graphs: list[TaskGraph] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)
    unavailable_tools: list[str] = field(default_factory=list)


def _normalize_plan_domain(value: str, *, route_domain: str = "") -> str:
    candidate = str(value or "").strip().lower()
    if candidate in {"email", "calendar", "search", "code", "general"}:
        return candidate
    normalized_route = str(route_domain or "").strip().lower()
    return normalized_route or "general"


def _infer_plan_signal(
    goal: str,
    *,
    route,
    classified_intent: dict | None = None,
) -> dict[str, Any]:
    payload = dict(classified_intent or {})
    route_domain = str(getattr(route, "domain", "") or "").strip().lower()
    route_protocol = str(getattr(route, "protocol", "") or "").strip().lower()
    domain = _normalize_plan_domain(payload.get("domain") or route_domain, route_domain=route_domain)

    action = str(payload.get("action") or "").strip().lower()
    if action not in {"read", "create", "update", "delete", "send", "none"}:
        if route_protocol.endswith("_read"):
            action = "read"
        elif route_protocol.endswith("_write"):
            action = "update"
        else:
            action = "none"

    if payload.get("goal_type") or payload.get("candidate_steps"):
        return {
            "domain": domain,
            "subdomain": str(payload.get("subdomain") or "").strip().lower(),
            "goal_type": str(payload.get("goal_type") or "").strip().lower(),
            "action": action,
            "candidate_steps": [str(step or "").strip().lower() for step in (payload.get("candidate_steps") or []) if str(step or "").strip()],
        }

    intent_name = str(payload.get("intent") or "").strip().lower()
    source_scope = str(payload.get("source_scope") or "").strip().lower()
    if intent_name in {"inbox_read", "email_search"}:
        return {
            "domain": "email",
            "subdomain": "mailbox",
            "goal_type": "lookup_email",
            "action": "read",
            "candidate_steps": ["select_mail_source", "retrieve_messages", "summarize_results"],
        }
    if intent_name == "calendar_read":
        return {
            "domain": "calendar",
            "subdomain": "schedule",
            "goal_type": "lookup_calendar",
            "action": "read",
            "candidate_steps": ["select_calendar_source", "retrieve_events", "summarize_results"],
        }
    if intent_name == "search_lookup":
        subdomain = "workspace" if source_scope == "workspace" else "web" if source_scope == "web" else "search"
        goal_type = "search_workspace" if subdomain == "workspace" else "search_web"
        steps = ["resolve_search_scope", "run_workspace_search", "summarize_results"] if subdomain == "workspace" else ["formulate_query", "search_web", "summarize_results"]
        return {
            "domain": "search",
            "subdomain": subdomain,
            "goal_type": goal_type,
            "action": "read",
            "candidate_steps": steps,
        }

    return {
        "domain": domain,
        "subdomain": "",
        "goal_type": "",
        "action": action,
        "candidate_steps": [],
    }


def _capabilities_for_step(domain: str, subdomain: str, goal_type: str, action: str, step: str) -> list[str]:
    if domain == "search" and subdomain == "filesystem":
        mapping = {
            "resolve_target_path": ["filesystem.resolve_path"],
            "list_files": ["filesystem.list"],
            "filter_results": ["filesystem.filter"],
            "summarize_results": ["report.summarize"],
        }
        return mapping.get(step, ["filesystem.inspect"])
    if domain == "search" and subdomain == "workspace":
        mapping = {
            "resolve_search_scope": ["workspace.resolve_scope"],
            "run_workspace_search": ["workspace.search"],
            "summarize_results": ["report.summarize"],
        }
        return mapping.get(step, ["workspace.search"])
    if domain == "search" and subdomain == "browser":
        mapping = {
            "open_target_page": ["browser.navigate"],
            "inspect_page": ["browser.inspect"],
            "summarize_results": ["report.summarize"],
        }
        return mapping.get(step, ["browser.inspect"])
    if domain == "search":
        mapping = {
            "formulate_query": ["web.query"],
            "search_web": ["web.search"],
            "summarize_results": ["report.summarize"],
            "resolve_search_scope": ["search.resolve_scope"],
            "gather_results": ["search.gather"],
        }
        return mapping.get(step, ["web.search"])
    if domain == "email":
        mapping = {
            "select_mail_source": ["email.resolve_provider"],
            "retrieve_messages": ["email.read"],
            "find_target_email": ["email.search"],
            "draft_message": ["email.compose"],
            "send_message": ["email.send"],
            "save_or_send_message": ["email.update"],
            "delete_message": ["email.delete"],
            "summarize_results": ["report.summarize"],
            "confirm_target": ["report.confirm_target"],
        }
        return mapping.get(step, [f"email.{action or 'read'}"])
    if domain == "calendar":
        mapping = {
            "select_calendar_source": ["calendar.resolve_provider"],
            "retrieve_events": ["calendar.read"],
            "find_target_event": ["calendar.search"],
            "extract_event_details": ["calendar.parse_request"],
            "create_event": ["calendar.create"],
            "save_event": ["calendar.update"],
            "delete_event": ["calendar.delete"],
            "confirm_target": ["report.confirm_target"],
            "summarize_results": ["report.summarize"],
        }
        return mapping.get(step, [f"calendar.{action or 'read'}"])
    if domain == "code":
        mapping = {
            "inspect_codebase": ["code.inspect"],
            "plan_changes": ["code.plan"],
            "apply_changes": ["code.edit"],
            "verify_result": ["code.verify"],
        }
        return mapping.get(step, ["code.inspect"])
    return ["general.answer"]


def _preferred_tools_for_capability(capability: str) -> list[str]:
    mapping = {
        "filesystem.resolve_path": ["list_files", "mcp::filesystem::list_directory", "mcp_server::filesystem"],
        "filesystem.list": ["list_files", "mcp::filesystem::list_directory", "mcp_server::filesystem"],
        "filesystem.filter": ["list_files", "read_file", "mcp::filesystem::search_files"],
        "filesystem.inspect": ["list_files", "read_file", "mcp_server::filesystem"],
        "workspace.resolve_scope": ["search_workspace", "read_workspace_file"],
        "workspace.search": ["search_workspace", "read_workspace_file", "read_file"],
        "browser.navigate": ["browser_navigate", "browser_get_text"],
        "browser.inspect": ["browser_get_text", "browser_screenshot", "browser_eval"],
        "web.query": ["web_search"],
        "web.search": ["web_search", "fetch_url", "extract_source"],
        "search.resolve_scope": ["search_workspace", "web_search"],
        "search.gather": ["search_workspace", "web_search", "fetch_url"],
        "email.resolve_provider": ["outlook_get_inbox", "gmail_get_inbox"],
        "email.read": ["outlook_get_inbox", "gmail_get_inbox", "outlook_search_emails", "gmail_search_emails", "outlook_get_email", "gmail_get_email"],
        "email.search": ["outlook_search_emails", "gmail_search_emails", "outlook_get_email", "gmail_get_email"],
        "email.compose": ["outlook_save_draft_email", "gmail_save_draft_email"],
        "email.send": ["outlook_send_email", "gmail_send_email"],
        "email.update": ["outlook_update_email", "gmail_update_email", "outlook_reply_email", "gmail_reply_email"],
        "email.delete": ["outlook_delete_email", "gmail_delete_email"],
        "calendar.resolve_provider": ["outlook_read_calendar", "google_read_calendar"],
        "calendar.read": ["outlook_read_calendar", "google_read_calendar", "find_free_slots"],
        "calendar.search": ["outlook_read_calendar", "google_read_calendar"],
        "calendar.parse_request": ["outlook_read_calendar", "google_read_calendar"],
        "calendar.create": ["outlook_create_calendar_event", "google_create_calendar_event"],
        "calendar.update": ["outlook_update_calendar_event", "google_update_calendar_event"],
        "calendar.delete": ["outlook_delete_calendar_event", "google_delete_calendar_event"],
        "code.inspect": ["search_workspace", "read_workspace_file", "read_file"],
        "code.plan": ["search_workspace", "read_workspace_file"],
        "code.edit": ["write_web_files", "write_file", "run_python", "run_shell"],
        "code.verify": ["preview_web_app", "package_web_app", "run_python", "run_shell", "search_workspace"],
        "report.summarize": [],
        "report.confirm_target": [],
        "general.answer": [],
    }
    return list(mapping.get(capability, []))


def _select_available_tools(preferred_tools: list[str], catalog: dict[str, ToolCapability]) -> tuple[list[str], list[str]]:
    selected: list[str] = []
    unavailable: list[str] = []
    for tool_name in preferred_tools:
        capability = catalog.get(tool_name)
        if capability and capability.enabled and capability.available:
            selected.append(tool_name)
        else:
            unavailable.append(tool_name)
    return selected, unavailable


def build_intent_capability_plan(
    goal: str,
    *,
    route,
    catalog: dict[str, ToolCapability],
    classified_intent: dict | None = None,
) -> IntentCapabilityPlan:
    signal = _infer_plan_signal(goal, route=route, classified_intent=classified_intent)
    domain = str(signal.get("domain") or "general")
    subdomain = str(signal.get("subdomain") or "").strip().lower()
    goal_type = str(signal.get("goal_type") or "").strip().lower()
    action = str(signal.get("action") or "none").strip().lower()
    candidate_steps = [str(step or "").strip().lower() for step in (signal.get("candidate_steps") or []) if str(step or "").strip()]

    steps: list[PlannedCapabilityStep] = []
    all_preferred: list[str] = []
    all_selected: list[str] = []
    all_unavailable: list[str] = []
    for step in candidate_steps:
        capabilities = _capabilities_for_step(domain, subdomain, goal_type, action, step)
        preferred_tools: list[str] = []
        selected_tools: list[str] = []
        unavailable_tools: list[str] = []
        for capability_name in capabilities:
            preferred = _preferred_tools_for_capability(capability_name)
            preferred_tools.extend(tool for tool in preferred if tool not in preferred_tools)
            selected, unavailable = _select_available_tools(preferred, catalog)
            selected_tools.extend(tool for tool in selected if tool not in selected_tools)
            unavailable_tools.extend(tool for tool in unavailable if tool not in unavailable_tools)
        steps.append(PlannedCapabilityStep(
            step=step,
            capabilities=capabilities,
            preferred_tools=preferred_tools,
            selected_tools=selected_tools,
        ))
        all_preferred.extend(tool for tool in preferred_tools if tool not in all_preferred)
        all_selected.extend(tool for tool in selected_tools if tool not in all_selected)
        all_unavailable.extend(tool for tool in unavailable_tools if tool not in all_unavailable)

    return IntentCapabilityPlan(
        domain=domain,
        subdomain=subdomain,
        goal_type=goal_type,
        action=action,
        candidate_steps=candidate_steps,
        preferred_tools=all_preferred,
        selected_tools=all_selected,
        unavailable_tools=all_unavailable,
        steps=steps,
    )


def _calendar_record_identity(record: dict[str, Any]) -> tuple[str, str, str, str]:
    return (
        str(record.get("title") or "").strip().casefold(),
        _calendar_record_datetime_key(record, "start"),
        _calendar_record_datetime_key(record, "end"),
        str(record.get("location") or "").strip().casefold(),
    )


def _parse_calendar_record_datetime_value(value: Any, *, provider_scope: str = "") -> tuple[datetime | None, bool]:
    raw = str(value or "").strip()
    if not raw:
        return None, False

    iso_value = raw.replace("Z", "+00:00")
    if len(iso_value) == 10 and iso_value[4] == "-" and iso_value[7] == "-":
        try:
            parsed = datetime.strptime(iso_value, "%Y-%m-%d")
            return localize_calendar_match_datetime(parsed, provider_scope=provider_scope), True
        except ValueError:
            pass

    try:
        parsed = datetime.fromisoformat(iso_value)
        all_day = "T" not in iso_value
        return localize_calendar_match_datetime(parsed, provider_scope=provider_scope), all_day
    except ValueError:
        return None, False


def _calendar_record_datetime_key(record: dict[str, Any], field_name: str) -> str:
    provider_scope = str(record.get("provider_scope") or "")
    parsed, all_day = _parse_calendar_record_datetime_value(record.get(f"{field_name}_iso"), provider_scope=provider_scope)
    if parsed is None:
        parsed, all_day = _parse_calendar_record_datetime_value(record.get(field_name), provider_scope=provider_scope)
    if parsed is None:
        return str(record.get(f"{field_name}_iso") or record.get(field_name) or "").strip().casefold()
    return parsed.strftime("%Y-%m-%d" if all_day else "%Y-%m-%dT%H:%M:%S").casefold()


def _calendar_matches_represent_single_series(records: list[dict[str, Any]]) -> bool:
    identities = {_calendar_record_identity(record) for record in records if isinstance(record, dict)}
    identities.discard(("", "", "", ""))
    return len(identities) == 1 and bool(records)


def plan_direct_execution(
    goal: str,
    *,
    route,
    stored_task_state: dict | None = None,
    classified_intent: dict | None = None,
    is_action: bool | None = None,
    use_outlook: bool = False,
    gmail_available: bool = False,
    settings: dict | None = None,
    dynamic_tools: dict | None = None,
    mcp_cache: dict[str, dict] | None = None,
) -> PlannedExecution:
    _folder_profiles = {
        **(settings.get("outlook_folder_profiles") or {}),
        **(settings.get("google_folder_profiles") or {}),
    } if settings else None
    _route_domain = str(getattr(route, "domain", "") or "").strip().lower()
    _stored_task_state_for_direct = scope_task_state_to_domain(stored_task_state, _route_domain)
    direct_acceleration = resolve_direct_acceleration(
        goal,
        stored_task_state=_stored_task_state_for_direct,
        classified_intent=classified_intent,
        is_action=is_action,
        use_outlook=use_outlook,
        gmail_available=gmail_available,
        folder_profiles=_folder_profiles,
    )
    catalog = build_runtime_tool_catalog(
        settings,
        dynamic_tools=dynamic_tools,
        mcp_cache=mcp_cache,
        use_outlook=use_outlook,
        gmail_available=gmail_available,
    )
    capability_plan = build_intent_capability_plan(
        goal,
        route=route,
        catalog=catalog,
        classified_intent=classified_intent,
    )
    filtered_calls: list[dict[str, Any]] = []
    unavailable_tools: list[str] = []
    for call in list(direct_acceleration.get("calls") or []):
        tool_name = _normalize_tool_name(str(call.get("tool") or ""))
        capability = catalog.get(tool_name)
        if capability and capability.enabled and capability.available:
            filtered_calls.append({"tool": tool_name, "args": dict(call.get("args") or {})})
        else:
            unavailable_tools.append(tool_name)

    graph_route = refine_protocol_route(route, task_state=_stored_task_state_for_direct, direct_intent=direct_acceleration.get("intent"))
    task_graph = None
    if filtered_calls:
        graph_calls = [(call["tool"], dict(call["args"])) for call in filtered_calls]
        task_graph = task_graph_from_direct_calls(
            str(direct_acceleration.get("goal") or goal),
            protocol=graph_route.protocol,
            domain=graph_route.domain,
            calls=graph_calls,
            display_goal=str(direct_acceleration.get("display_goal") or ""),
            metadata={
                "reason": graph_route.reason,
                "followup_relation": graph_route.followup_relation,
                "planner": "direct_execution_planner",
                "capability_plan": capability_plan.to_dict(),
            },
        )

    return PlannedExecution(
        goal=str(direct_acceleration.get("goal") or goal),
        display_goal=str(direct_acceleration.get("display_goal") or ""),
        intent=direct_acceleration.get("intent"),
        calls=filtered_calls,
        task_graph=task_graph,
        graph_route=graph_route,
        catalog=catalog,
        unavailable_tools=unavailable_tools,
        metadata={
            "classified_intent": dict(classified_intent or {}),
            "capability_plan": capability_plan.to_dict(),
        },
    )


def plan_email_action_execution(
    goal: str,
    *,
    route,
    action: str,
    intent,
    session_email_records: list[dict],
    use_outlook: bool = False,
    gmail_available: bool = False,
    settings: dict | None = None,
    dynamic_tools: dict | None = None,
    mcp_cache: dict[str, dict] | None = None,
    explicit_provider: str = "",
    folder: str = "Inbox",
    requested_count: int = 1,
    selection_query: str = "",
    update_directives: dict[str, Any] | None = None,
    is_forward_request: bool = False,
    existing_records: list[dict] | None = None,
    select_email_records=None,
    resolve_forward_send_provider=None,
    email_forward_subject=None,
    build_forward_email_body=None,
    dutch: bool = False,
) -> PlannedEmailAction:
    catalog = build_runtime_tool_catalog(
        settings,
        dynamic_tools=dynamic_tools,
        mcp_cache=mcp_cache,
        use_outlook=use_outlook,
        gmail_available=gmail_available,
    )
    candidate_records = list(existing_records) if existing_records is not None else list(session_email_records or [])

    if action == "send":
        return _plan_email_send_action(
            goal,
            route=route,
            intent=intent,
            catalog=catalog,
            settings=settings,
            candidate_records=candidate_records,
            use_outlook=use_outlook,
            gmail_available=gmail_available,
            explicit_provider=explicit_provider,
            is_forward_request=is_forward_request,
            select_email_records=select_email_records,
            resolve_forward_send_provider=resolve_forward_send_provider,
            email_forward_subject=email_forward_subject,
            build_forward_email_body=build_forward_email_body,
            dutch=dutch,
        )

    return _plan_email_record_action(
        goal,
        route=route,
        action=action,
        intent=intent,
        catalog=catalog,
        candidate_records=candidate_records,
        explicit_provider=explicit_provider,
        folder=folder,
        requested_count=requested_count,
        selection_query=selection_query,
        update_directives=dict(update_directives or {}),
        existing_records_provided=existing_records is not None,
        use_outlook=use_outlook,
        gmail_available=gmail_available,
        select_email_records=select_email_records,
        dutch=dutch,
    )


def _plan_email_send_action(
    goal: str,
    *,
    route,
    intent,
    catalog: dict[str, ToolCapability],
    settings: dict | None,
    candidate_records: list[dict],
    use_outlook: bool,
    gmail_available: bool,
    explicit_provider: str,
    is_forward_request: bool,
    select_email_records,
    resolve_forward_send_provider,
    email_forward_subject,
    build_forward_email_body,
    dutch: bool,
) -> PlannedEmailAction:
    provider = explicit_provider
    if getattr(intent, "provider", "") in {"gmail", "outlook"}:
        provider = str(intent.provider)
    available_providers = _available_email_send_providers(catalog)

    if is_forward_request and not getattr(intent, "to", ""):
        return PlannedEmailAction(
            action="send",
            clarification=(
                "Ik kan deze e-mail doorsturen, maar ik mis nog het doeladres."
                if dutch else
                "I can forward this email, but I still need the destination address."
            ),
        )

    forward_record: dict[str, Any] | None = None
    if is_forward_request:
        matched_forward_records = list(select_email_records(
            candidate_records,
            goal,
            str(getattr(intent, "selection_query", "") or "").strip(),
            1,
            reference_kind=str(getattr(intent, "reference_kind", "") or ""),
            reference_ordinal=getattr(intent, "reference_ordinal", None),
            reference_temporal_hint=str(getattr(intent, "reference_temporal_hint", "") or ""),
        ))
        if not matched_forward_records:
            return PlannedEmailAction(
                action="send",
                clarification=(
                    "Ik weet nog niet welke eerdere e-mail je wilt doorsturen. Noem het nummer, onderwerp of de afzender."
                    if dutch else
                    "I still don't know which earlier email you want to forward. Please mention the list number, subject, or sender."
                ),
            )
        forward_record = matched_forward_records[0]
        provider = str(resolve_forward_send_provider(
            forward_record,
            explicit_provider=provider,
            use_outlook=use_outlook,
            gmail_available=gmail_available,
        ) or "")

    if not provider and len(available_providers) == 1:
        provider = available_providers[0]

    if not provider:
        return PlannedEmailAction(
            action="send",
            clarification=_email_access_disabled_clarification(settings, dutch=dutch),
        )

    if not is_forward_request and getattr(intent, "missing_fields", None):
        return PlannedEmailAction(action="send")

    payload = email_intent_to_send_payload(intent)
    recipient = str(payload.get("to") or "").strip()
    subject = str(payload.get("subject") or "").strip()
    body = str(payload.get("body") or "").strip()
    if is_forward_request and forward_record is not None:
        subject = subject or str(email_forward_subject(forward_record.get("subject", ""), dutch=dutch) or "")
        body = body or str(build_forward_email_body(forward_record, dutch=dutch) or "")

    tool_name = "gmail_send_email" if provider == "gmail" else "outlook_send_email"
    capability = catalog.get(tool_name)
    if capability is None or not capability.enabled or not capability.available:
        return PlannedEmailAction(
            action="send",
            clarification=_email_access_disabled_clarification(settings, provider=provider, dutch=dutch),
            unavailable_tools=[tool_name],
        )

    tool_args = {"to": recipient, "subject": subject, "body": body}
    cc = str(payload.get("cc") or "").strip()
    if cc:
        tool_args["cc"] = cc
    write_graph = task_graph_from_direct_calls(
        goal,
        protocol=route.protocol,
        domain=route.domain,
        calls=[(tool_name, tool_args)],
        side_effect=True,
        metadata={"planner": "email_action_planner", "action": "send"},
    )
    tool_details = {
        "tool_name": tool_name,
        "kind": "email_action",
        "recipient": recipient,
        "subject": subject,
        "provider": "Gmail" if provider == "gmail" else "Outlook",
    }
    if forward_record is not None:
        tool_details["forwarded_message_id"] = str(forward_record.get("message_id") or "").strip()
    return PlannedEmailAction(
        action="send",
        write_graphs=[write_graph],
        tool_details=[tool_details],
        metadata={"provider": provider, "payload": tool_args, "forward_record": dict(forward_record or {})},
    )


def _plan_email_record_action(
    goal: str,
    *,
    route,
    action: str,
    intent,
    catalog: dict[str, ToolCapability],
    candidate_records: list[dict],
    explicit_provider: str,
    folder: str,
    requested_count: int,
    selection_query: str,
    update_directives: dict[str, Any],
    existing_records_provided: bool,
    use_outlook: bool,
    gmail_available: bool,
    select_email_records,
    dutch: bool,
) -> PlannedEmailAction:
    if getattr(intent, "missing_fields", None):
        return PlannedEmailAction(action=action)

    providers = _resolve_email_providers(explicit_provider, use_outlook=use_outlook, gmail_available=gmail_available)
    if not providers:
        return PlannedEmailAction(
            action=action,
            clarification=(
                "Ik kan geen beschikbaar e-mailaccount vinden voor deze actie."
                if dutch else
                "I could not find an available email account for this action."
            ),
        )

    matched_records = list(select_email_records(
        candidate_records,
        goal,
        selection_query,
        requested_count,
        reference_kind=str(getattr(intent, "reference_kind", "") or ""),
        reference_ordinal=getattr(intent, "reference_ordinal", None),
        reference_temporal_hint=str(getattr(intent, "reference_temporal_hint", "") or ""),
    ))
    if explicit_provider:
        matched_records = [record for record in matched_records if str(record.get("provider") or "").strip() == explicit_provider]
    if action == "update":
        matched_records = [record for record in matched_records if str(record.get("folder") or "").strip() == "Drafts"]

    missing_message_ids = any(not str(record.get("message_id") or "").strip() for record in matched_records)
    if (not matched_records or missing_message_ids) and not existing_records_provided:
        read_graphs: list[TaskGraph] = []
        unavailable_tools: list[str] = []
        for provider in providers:
            if selection_query:
                read_tool = "outlook_search_emails" if provider == "outlook" else "gmail_search_emails"
                read_args = {"query": selection_query, "count": max(10, requested_count * 5), "folder": folder}
            else:
                read_tool = "outlook_get_inbox" if provider == "outlook" else "gmail_get_inbox"
                read_args = {"count": max(10, requested_count * 5), "folder": folder}
            capability = catalog.get(read_tool)
            if capability is None or not capability.enabled or not capability.available:
                unavailable_tools.append(read_tool)
                continue
            read_graphs.append(task_graph_from_direct_calls(
                goal,
                protocol="email_read",
                domain="email",
                calls=[(read_tool, read_args)],
                metadata={"planner": "email_action_planner", "action": action, "phase": "lookup"},
            ))
        return PlannedEmailAction(
            action=action,
            read_graphs=read_graphs,
            unavailable_tools=unavailable_tools,
            metadata={
                "providers": providers,
                "selection_query": selection_query,
                "requested_count": requested_count,
                "folder": folder,
            },
        )

    deduped_matches: list[dict] = []
    seen_match_ids: set[tuple[str, str]] = set()
    for record in matched_records:
        provider_name = str(record.get("provider") or "").strip()
        message_id = str(record.get("message_id") or "").strip()
        if action == "update" and str(record.get("folder") or "").strip() != "Drafts":
            continue
        key = (provider_name, message_id)
        if key in seen_match_ids:
            continue
        seen_match_ids.add(key)
        deduped_matches.append(record)
    matched_records = deduped_matches[:requested_count] if requested_count > 0 else deduped_matches

    if not matched_records:
        no_match = (
            f"Ik kon geen e-mail vinden die past bij '{selection_query or goal}'."
            if dutch else
            f"I couldn't find an email matching '{selection_query or goal}'."
        )
        return PlannedEmailAction(action=action, clarification=no_match)

    if requested_count == 1 and len(matched_records) > 1:
        ambiguous = (
            "Ik vond meerdere e-mails die hierbij kunnen horen. Noem het onderwerp, de afzender of het volgnummer."
            if dutch else
            "I found multiple matching emails. Please specify the subject, sender, or list number."
        )
        return PlannedEmailAction(action=action, clarification=ambiguous)

    write_graphs: list[TaskGraph] = []
    tool_details: list[dict[str, Any]] = []
    unavailable_tools: list[str] = []
    for record in matched_records:
        provider_name = str(record.get("provider") or "").strip()
        message_id = str(record.get("message_id") or "").strip()
        if not provider_name or not message_id:
            continue
        if action == "delete":
            tool_name = "outlook_delete_email" if provider_name == "outlook" else "gmail_delete_email"
            tool_args = {"message_id": message_id}
        else:
            tool_name = "outlook_update_email" if provider_name == "outlook" else "gmail_update_email"
            tool_args = {"message_id": message_id, **update_directives}
        capability = catalog.get(tool_name)
        if capability is None or not capability.enabled or not capability.available:
            unavailable_tools.append(tool_name)
            continue
        write_graphs.append(task_graph_from_direct_calls(
            goal,
            protocol=route.protocol,
            domain=route.domain,
            calls=[(tool_name, tool_args)],
            side_effect=True,
            metadata={"planner": "email_action_planner", "action": action},
        ))
        tool_details.append({
            "tool_name": tool_name,
            "kind": "email_action",
            "message_id": message_id,
            "provider": "Gmail" if provider_name == "gmail" else "Outlook",
            "action": action,
        })
    return PlannedEmailAction(
        action=action,
        write_graphs=write_graphs,
        tool_details=tool_details,
        unavailable_tools=unavailable_tools,
        metadata={"matched_records": matched_records, "action": action},
    )


def _resolve_email_providers(explicit_provider: str, *, use_outlook: bool, gmail_available: bool) -> list[str]:
    if explicit_provider:
        return [explicit_provider]
    providers: list[str] = []
    if use_outlook:
        providers.append("outlook")
    if gmail_available:
        providers.append("gmail")
    return providers


def plan_calendar_action_execution(
    goal: str,
    *,
    route,
    action: str,
    intent,
    providers: list[str],
    settings: dict | None = None,
    dynamic_tools: dict | None = None,
    mcp_cache: dict[str, dict] | None = None,
    use_outlook: bool = False,
    gmail_available: bool = False,
    requested_count: int = 1,
    days_ahead: int = 14,
    selection_query: str = "",
    directives: dict[str, Any] | None = None,
    existing_records: list[dict] | None = None,
    select_calendar_records=None,
    calendar_update_payload_from_record=None,
    dutch: bool = False,
) -> PlannedCalendarAction:
    catalog = build_runtime_tool_catalog(
        settings,
        dynamic_tools=dynamic_tools,
        mcp_cache=mcp_cache,
        use_outlook=use_outlook,
        gmail_available=gmail_available,
    )

    if action == "create":
        if getattr(intent, "missing_fields", None):
            return PlannedCalendarAction(action="create")
        payload = appointment_intent_to_create_payload(intent)
        write_graphs: list[TaskGraph] = []
        unavailable_tools: list[str] = []
        for provider in providers:
            tool_name = "outlook_create_calendar_event" if provider == "outlook" else "google_create_calendar_event"
            capability = catalog.get(tool_name)
            if capability is None or not capability.enabled or not capability.available:
                unavailable_tools.append(tool_name)
                continue
            write_graphs.append(task_graph_from_direct_calls(
                goal,
                protocol=route.protocol,
                domain=route.domain,
                calls=[(tool_name, payload)],
                side_effect=True,
                metadata={"planner": "calendar_action_planner", "action": "create"},
            ))
        return PlannedCalendarAction(
            action="create",
            write_graphs=write_graphs,
            unavailable_tools=unavailable_tools,
            metadata={"payload": payload},
        )

    if getattr(intent, "missing_fields", None):
        return PlannedCalendarAction(action=action)

    candidate_records = list(existing_records or [])
    if not candidate_records:
        read_graphs: list[TaskGraph] = []
        unavailable_tools: list[str] = []
        for provider in providers:
            read_tool = "outlook_read_calendar" if provider == "outlook" else "google_read_calendar"
            capability = catalog.get(read_tool)
            if capability is None or not capability.enabled or not capability.available:
                unavailable_tools.append(read_tool)
                continue
            read_graphs.append(task_graph_from_direct_calls(
                goal,
                protocol="calendar_read",
                domain="calendar",
                calls=[(read_tool, {"days_ahead": days_ahead})],
                metadata={"planner": "calendar_action_planner", "action": action, "phase": "lookup"},
            ))
        return PlannedCalendarAction(
            action=action,
            read_graphs=read_graphs,
            unavailable_tools=unavailable_tools,
            metadata={
                "providers": list(providers),
                "selection_query": selection_query,
                "requested_count": requested_count,
                "days_ahead": days_ahead,
            },
        )

    matched_records = list(select_calendar_records(
        candidate_records,
        goal,
        selection_query,
        requested_count,
        reference_kind=str(getattr(intent, "reference_kind", "") or ""),
        reference_ordinal=getattr(intent, "reference_ordinal", None),
        reference_temporal_hint=str(getattr(intent, "reference_temporal_hint", "") or ""),
    ))

    deduped_matches: list[dict] = []
    seen_match_ids: set[tuple[str, str]] = set()
    for record in matched_records:
        key = (str(record.get("provider_scope") or ""), str(record.get("event_id") or ""))
        if key in seen_match_ids:
            continue
        seen_match_ids.add(key)
        deduped_matches.append(record)
    matched_records = deduped_matches

    if not matched_records:
        no_match = (
            f"Ik kon geen agenda-afspraak vinden die past bij '{selection_query or goal}'."
            if dutch else
            f"I couldn't find a calendar event matching '{selection_query or goal}'."
        )
        return PlannedCalendarAction(action=action, clarification=no_match)

    if requested_count == 1 and len(matched_records) > 1 and not _calendar_matches_represent_single_series(matched_records):
        ambiguous = (
            "Ik vond meerdere afspraken die hierbij kunnen horen. Zeg welke je bedoelt door de titel of datum te noemen."
            if dutch else
            "I found multiple matching events. Please specify which one you mean by title or date."
        )
        return PlannedCalendarAction(action=action, clarification=ambiguous)

    if requested_count > 0 and (requested_count > 1 or not _calendar_matches_represent_single_series(matched_records)):
        matched_records = matched_records[:requested_count]

    write_graphs: list[TaskGraph] = []
    unavailable_tools: list[str] = []
    for record in matched_records:
        provider_scope = str(record.get("provider_scope") or "")
        event_id = str(record.get("event_id") or "").strip()
        if not event_id:
            continue
        if action == "delete":
            tool_name = "outlook_delete_calendar_event" if provider_scope == "outlook_only" else "google_delete_calendar_event"
            tool_args = {"event_id": event_id}
        else:
            tool_name = "outlook_update_calendar_event" if provider_scope == "outlook_only" else "google_update_calendar_event"
            tool_args = {"event_id": event_id, **dict(calendar_update_payload_from_record(record, dict(directives or {})))}
        capability = catalog.get(tool_name)
        if capability is None or not capability.enabled or not capability.available:
            unavailable_tools.append(tool_name)
            continue
        write_graphs.append(task_graph_from_direct_calls(
            goal,
            protocol=route.protocol,
            domain=route.domain,
            calls=[(tool_name, tool_args)],
            side_effect=True,
            metadata={"planner": "calendar_action_planner", "action": action},
        ))
    return PlannedCalendarAction(
        action=action,
        write_graphs=write_graphs,
        unavailable_tools=unavailable_tools,
        metadata={"matched_records": matched_records},
    )