import re
from datetime import datetime, timedelta
from email.utils import parsedate_to_datetime

from backend.copilot_briefing import (
    compose_followup_brief_report,
    compose_morning_brief_report,
    is_copilot_followup_goal,
    is_copilot_morning_brief_goal,
)


_KV_LINE_RE = re.compile(r"^([A-Za-z ]+):\s*(.*)$")
_DUTCH_HINT_RE = re.compile(
    r"\b(welke|mijn|waar|agenda|afspraak|afspraken|verplaats|wijzig|vandaag|gisteren|"
    r"verstuurd|verzonden|gekeken|gezocht|mail|mails|e-?mail|stuur|kun je|heb ik|heb je|"
    r"bestand|bestanden|folder|map|outlook|gmail)\b",
    re.IGNORECASE,
)
_SIDE_EFFECT_RE = re.compile(
    r"\b(send|reply|forward|delete|remove|create|schedule|write|post|draft|update|edit|change|move|reschedule|"
    r"verstuur\w*|stuur\w*|antwoord\w*|doorstuur\w*|verwijder\w*|maak\w*|plan\w*|"
    r"zet\w*|toevoeg\w*|voeg\s+toe|plaats\w*|wijzig\w*|bewerk\w*|verplaats\w*|verzet\w*)\b",
    re.IGNORECASE,
)
_EMAIL_DETAIL_HINT_RE = re.compile(
    r"\b(inhoud|inhoudelijk|samenvatting|samenvat|waar gaat|waarover|body|tekst|details?|"
    r"what does it say|what is it about|summary|content|advice|advies)\b",
    re.IGNORECASE,
)
_EMAIL_SUMMARY_OR_ANALYSIS_RE = re.compile(
    r"\b(samenvat(?:ting|ten)?|summary|summari[sz]e|analyse(?:er|)|analy[sz]e|"
    r"belangrijkste punten|key points|main points|kort|brief|duid(?:en|ing)?|"
    r"what (?:does|did) it say|what is in|waar staat|wat staat er|uitleg|leg uit|explain)\b",
    re.IGNORECASE,
)
_CALENDAR_OVERLAP_HINT_RE = re.compile(
    r"\b(overlap\w*|conflict\w*|dubbel\w*|tegelijk|same time|overlapping)\b",
    re.IGNORECASE,
)

_MONTH_NAMES_TO_NUMBER = {
    "jan": 1, "januari": 1, "january": 1,
    "feb": 2, "februari": 2, "february": 2,
    "mrt": 3, "maart": 3, "mar": 3, "march": 3,
    "apr": 4, "april": 4,
    "mei": 5, "may": 5,
    "jun": 6, "juni": 6, "june": 6,
    "jul": 7, "juli": 7, "july": 7,
    "aug": 8, "augustus": 8, "august": 8,
    "sep": 9, "sept": 9, "september": 9,
    "okt": 10, "oct": 10, "oktober": 10, "october": 10,
    "nov": 11, "november": 11,
    "dec": 12, "december": 12,
}

_EMAIL_LOOKUP_TOOLS = {
    "gmail_get_inbox",
    "gmail_search_emails",
    "outlook_get_inbox",
    "outlook_search_emails",
}
_EMAIL_FULL_READ_TOOLS = {
    "gmail_get_email",
    "outlook_get_email",
}
_EMAIL_ACTION_TOOLS = {
    "gmail_send_email",
    "gmail_update_email",
    "gmail_delete_email",
    "outlook_send_email",
    "outlook_update_email",
    "outlook_delete_email",
}
_CALENDAR_LOOKUP_TOOLS = {
    "google_read_calendar",
    "outlook_read_calendar",
}
_TASK_LOOKUP_TOOLS = {
    "outlook_get_tasks",
}
_IMAGE_DESCRIPTION_TOOLS = {
    "describe_image",
    "analyze_image",
    "image_describe",
}


def _compose_image_description(raw: str) -> str:
    text = str(raw or "").strip()
    if not text:
        return text
    lines = text.splitlines()
    if lines and lines[0].startswith("[Image:"):
        body = "\n".join(lines[1:]).strip()
        if body:
            return body
    return text
_CALENDAR_ACTION_TOOLS = {
    "outlook_create_calendar_event",
    "outlook_update_calendar_event",
    "outlook_delete_calendar_event",
    "google_create_calendar_event",
    "google_update_calendar_event",
    "google_delete_calendar_event",
}


def _calendar_provider_label(tool_name: str) -> str:
    normalized_tool = str(tool_name or "").strip().lower()
    if normalized_tool.startswith("outlook_"):
        return "Outlook"
    if normalized_tool.startswith("google_"):
        return "Google"
    return "calendar"


def _looks_dutch(*texts: str) -> bool:
    combined = " ".join(str(text or "") for text in texts)
    return bool(_DUTCH_HINT_RE.search(combined))


def _parse_kv_block(block: str) -> dict[str, str]:
    data: dict[str, str] = {}
    for line in str(block or "").splitlines():
        match = _KV_LINE_RE.match(line.strip())
        if match:
            data[match.group(1).strip()] = match.group(2).strip()
    return data


def _split_structured_blocks(text: str) -> list[str]:
    value = str(text or "").strip()
    if not value:
        return []
    parts = [part.strip() for part in value.split("\n\n---\n\n") if part.strip()]
    return parts or [value]


def _calendar_lookup_blocks(tool_result: str) -> list[str]:
    blocks = _split_structured_blocks(tool_result)
    if not blocks:
        return []
    first = blocks[0]
    if first.startswith(("Vandaag is ", "Today is ")):
        if "Summary:" not in first and "Subject:" not in first:
            return blocks[1:]
        stripped = re.sub(r"^(?:Vandaag is|Today is)[^\r\n]*(?:\r?\n)+", "", first, count=1).strip()
        if stripped:
            blocks[0] = stripped
        else:
            blocks = blocks[1:]
    return blocks


def _dedupe_preserve_order(items: list[str]) -> list[str]:
    seen: set[str] = set()
    out: list[str] = []
    for item in items:
        normalized = re.sub(r"\s+", " ", item).strip()
        if not normalized or normalized in seen:
            continue
        seen.add(normalized)
        out.append(item)
    return out


def _clean_email_preview(text: str, max_len: int = 120) -> str:
    value = str(text or "").strip()
    if not value:
        return ""
    value = re.sub(r"<https?://[^>]+>", "", value)
    value = re.sub(r"https?://\S+", "", value)
    value = re.sub(r"[\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180e\u2000-\u200f\u2028-\u202f\u205f\u2060-\u206f\ufeff]+", " ", value)
    value = re.sub(r"\s+", " ", value).strip(" -|,.;")
    if len(value) <= max_len:
        return value
    clipped = value[: max_len - 1].rstrip(" ,.;:-")
    return clipped + "..."


def _parse_email_datetime(value: str) -> datetime | None:
    raw = str(value or "").strip()
    if not raw or raw == "-":
        return None
    try:
        return datetime.fromisoformat(raw.replace("Z", "+00:00"))
    except ValueError:
        pass
    try:
        return parsedate_to_datetime(raw)
    except (TypeError, ValueError, IndexError, OverflowError):
        return None


def _format_localized_email_datetime(value: str, dutch: bool) -> str:
    raw = str(value or "").strip()
    parsed = _parse_email_datetime(raw)
    if parsed is None:
        return raw or "-"

    local_now = datetime.now().astimezone()
    local_tz = local_now.tzinfo
    if parsed.tzinfo is None:
        local_dt = parsed.replace(tzinfo=local_tz)
    else:
        local_dt = parsed.astimezone(local_tz)

    today = local_now.date()
    date_value = local_dt.date()
    time_text = local_dt.strftime("%H:%M")
    if date_value == today:
        return f"vandaag om {time_text}" if dutch else f"today at {time_text}"
    if date_value == today - timedelta(days=1):
        return f"gisteren om {time_text}" if dutch else f"yesterday at {time_text}"

    if dutch:
        weekday_names = ["ma", "di", "wo", "do", "vr", "za", "zo"]
        month_names = ["jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"]
        return f"{weekday_names[local_dt.weekday()]} {local_dt.day:02d} {month_names[local_dt.month - 1]} {time_text}"
    return local_dt.strftime("%a %d %b %H:%M")


def _email_lookup_wants_detailed_preview(goal: str) -> bool:
    value = str(goal or "")
    if not value:
        return False
    return bool(_EMAIL_DETAIL_HINT_RE.search(value) or _email_lookup_is_specific_request(value))


def _email_lookup_wants_summary(goal: str) -> bool:
    value = str(goal or "")
    if not value:
        return False
    return bool(re.search(
        r"\b(samenvat(?:ten|ting)?|vat\s+.*\s+samen|waar\s+gaan\s+.*\s+over|summary|summari[sz]e|what\s+.*\s+about)\b",
        value,
        re.IGNORECASE,
    ))


def _email_topic_summary(subject: str, preview: str) -> str:
    subj = re.sub(r"^(?:(?:re|fw|fwd|sv|antw)\s*:\s*)+", "", str(subject or ""), flags=re.IGNORECASE).strip()
    snippet = str(preview or "").strip()
    if snippet:
        snippet = re.sub(r"^(?:hoi|beste|hi|hello|dear)\b[^.!?]*[.!?]?\s*", "", snippet, flags=re.IGNORECASE).strip()
        snippet = re.sub(r"\s+", " ", snippet).strip(" -|,.;")
    if snippet and len(snippet) >= 12:
        return snippet[:220].rstrip(" ,.;:-") + ("..." if len(snippet) > 220 else "")
    if subj:
        return subj
    return "(geen duidelijke inhoud gevonden)"


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

    lowered = raw.lower()
    all_day = "hele dag" in lowered or "all day" in lowered
    cleaned = re.sub(r"\s*\((?:hele dag|all day)\)", "", raw, flags=re.IGNORECASE).strip().replace(",", "")

    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d"):
        try:
            parsed = datetime.strptime(cleaned, fmt)
            return parsed, all_day or fmt == "%Y-%m-%d"
        except ValueError:
            continue

    match = re.search(
        r"(?:(?:[A-Za-z\u00C0-\u017F]+)\s+)?(\d{1,2})\s+([A-Za-z\u00C0-\u017F]+)(?:\s+(\d{4}))?(?:\s+(\d{1,2}):(\d{2}))?$",
        cleaned,
        re.IGNORECASE,
    )
    if not match:
        return None, all_day

    day = int(match.group(1))
    month = _MONTH_NAMES_TO_NUMBER.get(str(match.group(2) or "").lower())
    if not month:
        return None, all_day
    year = int(match.group(3)) if match.group(3) else datetime.now().year
    hour = int(match.group(4) or 0)
    minute = int(match.group(5) or 0)
    return datetime(year, month, day, hour, minute), all_day or match.group(4) is None


def _calendar_event_timing(event: dict[str, str]) -> dict | None:
    start_raw = str(event.get("start") or "").strip()
    end_raw = str(event.get("end") or "").strip()
    start_dt, start_all_day = _parse_calendar_rendered_datetime(start_raw)
    end_dt, end_all_day = _parse_calendar_rendered_datetime(end_raw)
    if start_dt is None:
        return None
    all_day = start_all_day or end_all_day
    if end_dt is None or end_dt <= start_dt:
        end_dt = start_dt + (timedelta(days=1) if all_day else timedelta(hours=1))
    return {
        "title": str(event.get("title") or "").strip() or "(no title)",
        "start_dt": start_dt,
        "end_dt": end_dt,
        "all_day": all_day,
    }


def _calendar_overlap_pairs(events: list[dict], *, include_all_day: bool) -> list[tuple[dict, dict]]:
    pairs: list[tuple[dict, dict]] = []
    for index, first in enumerate(events):
        for second in events[index + 1:]:
            if not include_all_day and (bool(first.get("all_day")) or bool(second.get("all_day"))):
                continue
            first_start = first.get("start_dt")
            first_end = first.get("end_dt")
            second_start = second.get("start_dt")
            second_end = second.get("end_dt")
            if not isinstance(first_start, datetime) or not isinstance(first_end, datetime):
                continue
            if not isinstance(second_start, datetime) or not isinstance(second_end, datetime):
                continue
            if first_start < second_end and second_start < first_end:
                pairs.append((first, second))
    return pairs


def _calendar_tight_handoff_pairs(events: list[dict], *, max_gap_minutes: int = 30) -> list[tuple[dict, dict, int]]:
    timed_events = [event for event in events if not bool(event.get("all_day"))]
    timed_events.sort(key=lambda item: (item.get("start_dt") or datetime.max, item.get("end_dt") or datetime.max))
    pairs: list[tuple[dict, dict, int]] = []
    for first, second in zip(timed_events, timed_events[1:]):
        first_end = first.get("end_dt")
        second_start = second.get("start_dt")
        if not isinstance(first_end, datetime) or not isinstance(second_start, datetime):
            continue
        gap_minutes = int((second_start - first_end).total_seconds() // 60)
        if 0 < gap_minutes <= max_gap_minutes:
            pairs.append((first, second, gap_minutes))
    return pairs


def _calendar_overlap_lines(goal: str, events: list[dict[str, str]], dutch: bool) -> list[str]:
    enriched = [item for item in (_calendar_event_timing(event) for event in events) if item]
    if len(enriched) < 2:
        return []

    timed_pairs = _calendar_overlap_pairs(enriched, include_all_day=False)
    all_pairs = _calendar_overlap_pairs(enriched, include_all_day=True)
    all_day_pairs = [pair for pair in all_pairs if pair not in timed_pairs and (pair[0].get("all_day") or pair[1].get("all_day"))]
    tight_pairs = _calendar_tight_handoff_pairs(enriched)
    overlap_requested = bool(_CALENDAR_OVERLAP_HINT_RE.search(str(goal or "")))

    def _format_pairs(pairs: list[tuple[dict, dict]]) -> str:
        labels = [f"{first['title']} × {second['title']}" for first, second in pairs[:3]]
        if len(pairs) > 3:
            labels.append(f"+{len(pairs) - 3}")
        return ", ".join(labels)

    def _format_tight_pairs(pairs: list[tuple[dict, dict, int]]) -> str:
        labels = [f"{first['title']} -> {second['title']} ({gap} min)" for first, second, gap in pairs[:3]]
        if len(pairs) > 3:
            labels.append(f"+{len(pairs) - 3}")
        return ", ".join(labels)

    lines: list[str] = []
    if timed_pairs:
        if dutch:
            lines.append(f"Harde overlap: {len(timed_pairs)} conflict{'en' if len(timed_pairs) != 1 else ''} tussen tijdsgebonden afspraken ({_format_pairs(timed_pairs)}).")
        else:
            lines.append(f"Hard overlap: {len(timed_pairs)} conflict{'s' if len(timed_pairs) != 1 else ''} between timed events ({_format_pairs(timed_pairs)}).")
    elif overlap_requested and len([event for event in enriched if not bool(event.get("all_day"))]) >= 2:
        lines.append("Overlapcheck: geen overlap tussen tijdsgebonden afspraken." if dutch else "Overlap check: no overlap between timed events.")

    if tight_pairs:
        if dutch:
            lines.append(f"Strak op elkaar: {len(tight_pairs)} overgang{'en' if len(tight_pairs) != 1 else ''} met weinig ruimte ertussen ({_format_tight_pairs(tight_pairs)}).")
        else:
            lines.append(f"Tight handoff: {len(tight_pairs)} transition{'s' if len(tight_pairs) != 1 else ''} with little room in between ({_format_tight_pairs(tight_pairs)}).")

    if all_day_pairs:
        if dutch:
            lines.append(f"Hele dag blokkeert mogelijk focus: {len(all_day_pairs)} samenloop{'gevallen' if len(all_day_pairs) != 1 else 'geval'} met een hele dag afspraak ({_format_pairs(all_day_pairs)}), maar dat is niet altijd een hard tijdsconflict.")
        else:
            lines.append(f"All-day blocking risk: {len(all_day_pairs)} case{'s' if len(all_day_pairs) != 1 else ''} where an all-day event overlaps the day ({_format_pairs(all_day_pairs)}), but that is not always a hard time conflict.")
    elif overlap_requested and any(bool(event.get("all_day")) for event in enriched):
        lines.append("Hele dag afspraken geven geen extra overlap met de rest." if dutch else "All-day events do not add extra overlaps with the rest.")

    return lines


def _email_lookup_time_label(goal: str, details: dict, dutch: bool) -> str:
    goal_text = str(goal or "")
    hint = str(details.get("time_filter_hint") or "").strip().lower()
    if hint == "today" or re.search(r"\b(vandaag|today)\b", goal_text, re.IGNORECASE):
        return "van vandaag" if dutch else "from today"
    if re.search(r"\b(gisteren|yesterday)\b", goal_text, re.IGNORECASE):
        return "van gisteren" if dutch else "from yesterday"
    return ""


def extract_email_lookup_records(tool_result: str) -> list[dict[str, str]]:
    raw = str(tool_result or "").strip()
    lowered = raw.lower()
    if not raw or lowered.startswith("no emails") or "no emails matching" in lowered:
        return []

    records: list[dict[str, str]] = []
    for data in [_parse_kv_block(block) for block in _split_structured_blocks(raw)]:
        subject = str(data.get("Subject", "") or "").strip()
        preview = _clean_email_preview(data.get("Preview", ""), max_len=220)
        counterparty = str(data.get("To") or data.get("From") or "").strip()
        if not subject and not preview and not counterparty:
            continue
        records.append({
            "subject": subject,
            "preview": preview,
            "from": str(data.get("From", "") or "").strip(),
            "to": str(data.get("To", "") or "").strip(),
            "date": str(data.get("Date", "") or "").strip(),
            "mailbox": str(data.get("Mailbox", "") or "").strip(),
            "folder": str(data.get("Folder", "") or "").strip(),
            "message_id": str(data.get("Message ID", "") or "").strip(),
            "thread_id": str(data.get("Thread ID", "") or "").strip(),
            "attachments": str(data.get("Attachments", "") or "").strip(),
            "attachment_names": str(data.get("Attachment Names", "") or "").strip(),
        })
    return records


def extract_calendar_lookup_records(tool_result: str) -> list[dict[str, str]]:
    raw = str(tool_result or "").strip()
    lowered = raw.lower()
    if not raw or _is_empty_calendar_lookup_text(raw):
        return []

    blocks = _calendar_lookup_blocks(raw)

    records: list[dict[str, str]] = []
    for data in [_parse_kv_block(block) for block in blocks]:
        title = str(data.get("Summary") or data.get("Subject") or "").strip()
        start = str(data.get("Start") or "").strip()
        end = str(data.get("End") or "").strip()
        start_iso = str(data.get("Start ISO") or "").strip()
        end_iso = str(data.get("End ISO") or "").strip()
        location = str(data.get("Location") or "").strip()
        event_id = str(data.get("Event ID") or "").strip()
        if not title and not start and not end:
            continue
        records.append({
            "title": title,
            "start": start,
            "end": end,
            "start_iso": start_iso,
            "end_iso": end_iso,
            "location": location,
            "event_id": event_id,
        })
    return records


def extract_task_lookup_records(tool_result: str) -> list[dict[str, str]]:
    raw = str(tool_result or "").strip()
    if not raw or raw.lower().startswith("no tasks"):
        return []
    records: list[dict[str, str]] = []
    for data in [_parse_kv_block(block) for block in _split_structured_blocks(raw)]:
        subject = str(data.get("Subject", "") or "").strip()
        if not subject:
            continue
        records.append({
            "subject": subject,
            "status": str(data.get("Status", "") or "").strip(),
            "due": str(data.get("Due", "") or "").strip(),
            "importance": str(data.get("Importance", "") or "").strip(),
            "notes": str(data.get("Notes", "") or "").strip()[:200],
        })
    return records


def build_tool_result_details(tool_name: str, tool_result: str) -> dict:
    normalized_tool = str(tool_name or "").strip()
    raw = str(tool_result or "").strip()
    lowered = raw.lower()

    details: dict = {
        "tool_name": normalized_tool,
        "kind": "raw",
        "is_error": raw.startswith(("Error", "POLICY_DENIED", "HITL_REQUIRED", "TOOL_DISABLED")),
        "text_preview": raw[:300],
    }

    if normalized_tool in _EMAIL_LOOKUP_TOOLS:
        records = extract_email_lookup_records(raw)
        folders = _dedupe_preserve_order([
            str(record.get("folder") or "").strip()
            for record in records
            if str(record.get("folder") or "").strip()
        ])
        mailboxes = _dedupe_preserve_order([
            str(record.get("mailbox") or "").strip()
            for record in records
            if str(record.get("mailbox") or "").strip()
        ])
        provider = _email_provider_label(normalized_tool, records)
        provenance_parts = [provider]
        if len(folders) == 1:
            provenance_parts.append(f"folder {folders[0]}")
        elif folders:
            provenance_parts.append("multiple folders")
        if len(mailboxes) == 1 and mailboxes[0] not in {provider, "Gmail", "Outlook"}:
            provenance_parts.append(mailboxes[0])
        details.update({
            "kind": "email_lookup",
            "provider": provider,
            "count": len(records),
            "has_hits": bool(records),
            "records": records[:25],
            "folders": folders,
            "mailboxes": mailboxes,
            "provenance_label": ", ".join(provenance_parts),
            "is_empty": _is_empty_email_lookup_text(raw),
            "time_filter_hint": "today" if "newer_than:1d" in lowered else "",
        })
        return details

    if normalized_tool in _CALENDAR_LOOKUP_TOOLS:
        events = extract_calendar_lookup_records(raw)
        provider = _calendar_provider_label(normalized_tool)
        details.update({
            "kind": "calendar_lookup",
            "provider": provider,
            "count": len(events),
            "events": events[:25],
            "has_hits": bool(events),
            "provenance_label": provider,
        })
        return details

    if normalized_tool in _TASK_LOOKUP_TOOLS:
        tasks = extract_task_lookup_records(raw)
        details.update({
            "kind": "task_lookup",
            "provider": "Outlook",
            "count": len(tasks),
            "tasks": tasks[:25],
            "has_hits": bool(tasks),
            "provenance_label": "Outlook Tasks",
        })
        return details

    if normalized_tool in _EMAIL_ACTION_TOOLS:
        match = re.match(r"Email sent to\s+(.+?)\s+[—-]\s+Subject:\s+(.+)$", raw)
        data = _parse_kv_block(raw)
        if lowered.startswith("email deleted successfully"):
            status = "deleted"
        elif lowered.startswith("email updated successfully"):
            status = "updated"
        else:
            status = "sent" if raw.startswith("Email sent") else "unknown"
        details.update({
            "kind": "email_action",
            "status": status,
            "recipient": match.group(1).strip() if match else str(data.get("To") or "").strip(),
            "subject": match.group(2).strip() if match else str(data.get("Subject") or "").strip(),
            "message_id": str(data.get("Message ID") or "").strip(),
            "folder": str(data.get("Folder") or "").strip(),
            "provider": _email_provider_label(normalized_tool, []),
        })
        return details

    if normalized_tool in _CALENDAR_ACTION_TOOLS:
        data = _parse_kv_block(raw)
        details.update({
            "kind": "calendar_action",
            "status": "updated" if lowered.startswith("calendar event updated") else (
                "created" if lowered.startswith("calendar event created") else (
                    "deleted" if "deleted successfully" in lowered else "unknown"
                )
            ),
            "title": str(data.get("Title") or "").strip(),
            "start": str(data.get("Start") or "").strip(),
            "end": str(data.get("End") or "").strip(),
            "event_id": str(data.get("Event ID") or "").strip(),
            "provider": _calendar_provider_label(normalized_tool),
        })
        return details

    return details


def compact_tool_result_for_context(tool_name: str, tool_result: str, tool_details: dict | None = None) -> str:
    normalized_tool = str(tool_name or "").strip()
    raw = str(tool_result or "").strip()
    details = dict(tool_details or build_tool_result_details(normalized_tool, raw))

    if not raw:
        return raw

    if normalized_tool in _EMAIL_FULL_READ_TOOLS:
        if len(raw) <= 5000:
            return raw
        clipped = raw[:4500].rstrip()
        omitted = max(0, len(raw) - len(clipped))
        return clipped + f"\n\n[compacted: {omitted} characters omitted from context]"

    if details.get("kind") == "email_lookup":
        records = list(details.get("records") or [])
        provider = str(details.get("provider") or "mail").strip()
        folders = [str(item).strip() for item in (details.get("folders") or []) if str(item).strip()]
        header = f"Source: {provider}"
        if len(folders) == 1:
            header += f" | Folder: {folders[0]}"
        header += f" | Count: {int(details.get('count') or len(records))}"
        if not records:
            return raw[:600]
        lines = [header]
        preview_records = records[:6]
        for index, record in enumerate(preview_records, start=1):
            party = str(record.get("from") or record.get("to") or "-").strip()
            date_value = str(record.get("date") or "-").strip()
            subject = str(record.get("subject") or "(no subject)").strip()
            lines.append(f"{index}. {subject} | {party} | {date_value}")
        omitted = max(0, int(details.get("count") or len(records)) - len(preview_records))
        if omitted:
            lines.append(f"[compacted: {omitted} more email records omitted from context]")
        return "\n".join(lines)

    if details.get("kind") == "calendar_lookup":
        events = list(details.get("events") or [])
        provider = str(details.get("provider") or "calendar").strip()
        header = f"Source: {provider} | Events: {int(details.get('count') or len(events))}"
        if not events:
            return raw[:600]
        lines = [header]
        preview_events = events[:6]
        for index, event in enumerate(preview_events, start=1):
            title = str(event.get("title") or "(no title)").strip()
            start = str(event.get("start") or "-").strip()
            lines.append(f"{index}. {title} | {start}")
        omitted = max(0, int(details.get("count") or len(events)) - len(preview_events))
        if omitted:
            lines.append(f"[compacted: {omitted} more calendar events omitted from context]")
        return "\n".join(lines)

    if normalized_tool in {"list_files", "search_workspace"}:
        lines = raw.splitlines()
        if len(lines) <= 250:
            return raw
        kept = lines[:250]
        omitted = len(lines) - len(kept)
        kept.append(f"[compacted: {omitted} more lines omitted from context]")
        return "\n".join(kept)

    if len(raw) <= 1200:
        return raw
    clipped = raw[:900].rstrip()
    omitted = max(0, len(raw) - len(clipped))
    return clipped + f"\n\n[compacted: {omitted} characters omitted from context]"


def _format_provenance_line(tool_name: str, details: dict, dutch: bool) -> str:
    kind = str(details.get("kind") or "")
    if kind == "email_lookup":
        provider = str(details.get("provider") or _email_provider_label(tool_name, []))
        folders = [str(item).strip() for item in (details.get("folders") or []) if str(item).strip()]
        line = f"Bron: {provider}" if dutch else f"Source: {provider}"
        if len(folders) == 1:
            line += f", {'map' if dutch else 'folder'} {folders[0]}"
        return line
    if kind == "calendar_lookup":
        provider = str(details.get("provider") or _calendar_provider_label(tool_name))
        return f"Bron: {provider}-agenda" if dutch else f"Source: {provider} calendar"
    return ""


# ── Source attribution ─────────────────────────────────────────────────────────

_SOURCE_SKIP_TOOLS = (
    _EMAIL_ACTION_TOOLS | _CALENDAR_ACTION_TOOLS
    | {"run_python", "write_file", "run_shell", "list_files", "schedule_goal", "create_skill", "create_tool", "update_skill", "update_tool"}
)


def _collect_sources(memory: list[dict], dutch: bool = False) -> list[str]:
    """Extract unique source labels from memory items for provenance footer."""
    sources: list[str] = []
    seen: set[str] = set()

    for item in memory:
        tool_name = str(item.get("tool_name") or "").strip()
        if not tool_name or tool_name in _SOURCE_SKIP_TOOLS:
            continue

        tool_details = item.get("tool_details") if isinstance(item.get("tool_details"), dict) else {}
        tool_args = item.get("tool_args") if isinstance(item.get("tool_args"), dict) else {}
        tool_result = str(item.get("tool_result") or "").strip()

        label = ""

        # Email / calendar / task — use existing provenance_label
        provenance = str(tool_details.get("provenance_label") or "").strip()
        if provenance:
            label = provenance
        elif tool_name == "web_search":
            label = "Web"
        elif tool_name == "fetch_url":
            url = str(tool_args.get("url") or "").strip()
            if url:
                try:
                    from urllib.parse import urlparse
                    domain = urlparse(url).netloc or url[:60]
                    label = f"Web: {domain}"
                except Exception:
                    label = "Web"
            else:
                label = "Web"
        elif tool_name == "extract_source":
            src = str(tool_args.get("source") or "").strip()
            label = f"Bron: {src[:60]}" if (dutch and src) else (f"Source: {src[:60]}" if src else "Content extraction")
        elif tool_name == "fetch_webhook":
            wh_name = str(tool_args.get("webhook_name") or "").strip()
            label = f"Webhook: {wh_name}" if wh_name else "Webhook"
        elif tool_name in ("read_workspace_file", "read_file"):
            path = str(tool_args.get("path") or tool_args.get("filename") or "").strip()
            label = f"File: {path}" if path else ("Bestand" if dutch else "File")
        elif tool_name == "search_workspace":
            label = "Workspace"
        elif tool_name == "recall_memory":
            label = "Agent geheugen" if dutch else "Agent memory"
        elif tool_name == "http_request":
            url = str(tool_args.get("url") or "").strip()
            if url:
                try:
                    from urllib.parse import urlparse
                    domain = urlparse(url).netloc or url[:60]
                    label = f"HTTP: {domain}"
                except Exception:
                    label = "HTTP"
            else:
                label = "HTTP"
        elif tool_name in _EMAIL_LOOKUP_TOOLS:
            label = _email_provider_label(tool_name, [])
        elif tool_name in _CALENDAR_LOOKUP_TOOLS:
            label = _calendar_provider_label(tool_name)
        elif tool_name in _TASK_LOOKUP_TOOLS:
            label = "Outlook Tasks"
        else:
            # Custom / unknown tool — show name
            label = tool_name

        if label and label not in seen:
            seen.add(label)
            sources.append(label)

    return sources


def _format_sources_footer(memory: list[dict], dutch: bool = False) -> str:
    """Build a '---\\nBronnen: ...' footer from collected sources, or empty string."""
    sources = _collect_sources(memory, dutch)
    if not sources:
        return ""
    header = "Bronnen" if dutch else "Sources"
    return f"\n\n---\n{header}: {', '.join(sources)}"


def _email_lookup_is_specific_request(goal: str) -> bool:
    value = str(goal or "")
    if not value:
        return False
    return bool(re.search(
        r"\b(van|from|over|about|subject|onderwerp|afzender|sender|specific|specifieke|welke|which)\b|@|\"[^\"]+\"|'[^']+'",
        value,
        re.IGNORECASE,
    ))


def _is_empty_email_lookup_text(text: str) -> bool:
    lowered = str(text or "").strip().lower()
    if not lowered:
        return False
    return (
        lowered.startswith("no emails")
        or lowered.startswith("no unread emails")
        or "no emails matching" in lowered
        or lowered.startswith("ik heb geen e-mails gevonden")
        or lowered.startswith("i found no emails")
    )


def _email_lookup_has_hits(text: str) -> bool:
    return bool(extract_email_lookup_records(text))


def _is_empty_calendar_lookup_text(text: str) -> bool:
    lowered = str(text or "").strip().lower()
    if not lowered:
        return False
    return (
        lowered.startswith("no upcoming events")
        or lowered.startswith("no events found for the requested period")
        or lowered.startswith("ik zie geen komende afspraken")
        or lowered.startswith("i see no upcoming events")
    )


def _email_provider_label(tool_name: str, records: list[dict[str, str]]) -> str:
    normalized_tool = str(tool_name or "").strip().lower()
    if normalized_tool.startswith("outlook_"):
        return "Outlook"
    if normalized_tool.startswith("gmail_"):
        return "Gmail"
    mailbox = next((str(item.get("mailbox") or "").strip() for item in records if str(item.get("mailbox") or "").strip()), "")
    if mailbox:
        return mailbox
    return "mail"


def _compose_multi_source_email_summary(results: list[tuple[str, str]], dutch: bool, goal: str = "") -> str:
    """Return a single combined table (with a Source column) when 2+ providers have results."""
    email_results = [
        (tool_name, tool_result)
        for tool_name, tool_result in results
        if tool_name in _EMAIL_LOOKUP_TOOLS and extract_email_lookup_records(tool_result)
    ]

    if len(email_results) < 2:
        return ""

    detailed_preview = _email_lookup_wants_detailed_preview(goal)
    wants_summary = _email_lookup_wants_summary(goal)
    preview_limit = 100 if detailed_preview else 80

    # Collect all parsed rows tagged with their provider
    all_rows: list[tuple[str, dict]] = []  # (provider_label, parsed_kv)
    for tool_name, tool_result in email_results:
        raw = str(tool_result or "").strip()
        blocks = _split_structured_blocks(raw)
        parsed = [_parse_kv_block(b) for b in blocks]
        normalized = str(tool_name or "").strip().lower()
        if normalized.startswith("outlook_"):
            label = "Outlook"
        elif normalized.startswith("gmail_"):
            label = "Gmail"
        else:
            label = str(tool_name or "").split("_")[0].capitalize()
        for p in parsed:
            all_rows.append((label, p))

    # Sort by date descending (newest first); rows without a parseable date go to the bottom
    def _row_sort_key(item: tuple[str, dict]):
        dt = _parse_email_datetime(item[1].get("Date", ""))
        if dt is None:
            return datetime.min
        try:
            return dt.replace(tzinfo=None)
        except Exception:
            return datetime.min
    all_rows.sort(key=_row_sort_key, reverse=True)

    total = len(all_rows)
    if total == 0:
        return ""

    # Derive time label from first provider's details
    first_tool, first_result = email_results[0]
    first_details = build_tool_result_details(first_tool, first_result)
    time_label = _email_lookup_time_label(goal, first_details, dutch)

    # Intro line
    if wants_summary:
        intro = (
            f"Hier is een samenvatting van {total} e-mail{'s' if total != 1 else ''}{' ' + time_label if time_label else ''}."
            if dutch else
            f"Here is a summary of {total} email{'s' if total != 1 else ''}{' ' + time_label if time_label else ''}."
        )
    else:
        intro = (
            f"Ik heb {total} e-mail{'s' if total != 1 else ''}{' ' + time_label if time_label else ''} gevonden."
            if dutch else
            f"I found {total} email{'s' if total != 1 else ''}{' ' + time_label if time_label else ''}."
        )

    has_to = any(data.get("To") for _, data in all_rows)
    col_source = "Bron" if dutch else "Source"
    col_from = ("Aan" if has_to else "Van") if dutch else ("To" if has_to else "From")
    col_status = "Status"
    col_subject = "Onderwerp" if dutch else "Subject"
    col_date = "Datum" if dutch else "Date"
    col_preview = "Inhoud" if dutch else "Preview"

    header = f"| # | {col_source} | {col_status} | {col_from} | {col_subject} | {col_date} | {col_preview} | _message_id | _provider |"
    sep    = "|---|---|---|---|---|---|---|---|---|"
    rows: list[str] = []
    for index, (provider_label, data) in enumerate(all_rows, start=1):
        subject = _email_table_cell(data.get("Subject", "(no subject)"), 55)
        counterparty = _email_table_cell(data.get("To") or data.get("From") or "-", 30)
        raw_date = data.get("Date", "-")
        date_val = _email_table_cell(_format_localized_email_datetime(raw_date, dutch), 20)
        preview_raw = _clean_email_preview(data.get("Preview", ""), max_len=preview_limit)
        if wants_summary:
            preview_raw = _email_topic_summary(data.get("Subject", ""), preview_raw)[:preview_limit]
        preview = _email_table_cell(preview_raw, preview_limit)
        status_raw = data.get("Status", "")
        if dutch:
            status = {"Unread": "Ongelezen", "Read": "Gelezen", "Replied": "Beantwoord"}.get(status_raw, status_raw)
        else:
            status = status_raw or "-"
        status = _email_table_cell(status, 12)
        message_id = str(data.get("Message ID", "") or "").strip().replace("|", " ") or "-"
        provider_value = provider_label.strip().lower().replace("|", " ") or "-"
        rows.append(f"| {index} | {provider_label} | {status} | {counterparty} | {subject} | {date_val} | {preview} | {message_id} | {provider_value} |")

    body = "\n".join([header, sep] + rows)
    tip = ("Klik op een rij om die e-mail te openen." if dutch else "Click a row to open that email.")
    return intro + "\n\n" + body + "\n\n" + tip


def _email_table_cell(value: str, max_len: int) -> str:
    """Truncate and escape a value for use inside a markdown table cell."""
    v = str(value or "").strip().replace("|", "·").replace("\n", " ").replace("\r", "")
    if len(v) > max_len:
        v = v[: max_len - 1].rstrip(" ,.;:-") + "…"
    return v or "-"


def _compose_email_lookup(goal: str, tool_result: str, dutch: bool, tool_name: str = "", tool_details: dict | None = None, start_index: int = 1) -> str:
    raw = str(tool_result or "").strip()
    lowered = raw.lower()
    if lowered.startswith("no emails") or lowered.startswith("no unread emails"):
        return "Ik heb geen e-mails gevonden." if dutch else "I found no emails."
    if "no emails matching" in lowered:
        return "Ik heb geen e-mails gevonden die aan die zoekvraag voldoen." if dutch else "I found no emails matching that query."

    blocks = _split_structured_blocks(raw)
    parsed_blocks = [_parse_kv_block(block) for block in blocks]
    details = dict(tool_details or build_tool_result_details(tool_name, raw))
    mailboxes = {data.get("Mailbox", "").strip() for data in parsed_blocks if data.get("Mailbox", "").strip()}
    folders = {data.get("Folder", "").strip() for data in parsed_blocks if data.get("Folder", "").strip()}
    show_mailbox = len(mailboxes) > 1
    detailed_preview = _email_lookup_wants_detailed_preview(goal)
    wants_summary = _email_lookup_wants_summary(goal)
    preview_limit = 100 if detailed_preview else 80

    count = len(parsed_blocks)
    if count == 0:
        return "Ik heb geen e-mails gevonden." if dutch else "I found no emails."

    normalized_tool = str(tool_name or "").strip().lower()
    if normalized_tool.startswith("outlook_"):
        single_mailbox = "Outlook"
    elif normalized_tool.startswith("gmail_"):
        single_mailbox = "Gmail"
    else:
        single_mailbox = next(iter(mailboxes), "") if len(mailboxes) == 1 else ""

    time_label = _email_lookup_time_label(goal, details, dutch)
    if single_mailbox:
        intro = (
            f"Hier is een samenvatting van {count} e-mail{'s' if count != 1 else ''} {time_label + ' ' if time_label else ''}uit {single_mailbox}."
            if (dutch and wants_summary) else
            f"Here is a summary of {count} email{'s' if count != 1 else ''} {time_label + ' ' if time_label else ''}from {single_mailbox}."
            if wants_summary else
            f"Ik heb {count} e-mail{'s' if count != 1 else ''} {time_label + ' ' if time_label else ''}in {single_mailbox} gevonden."
            if dutch else
            f"I found {count} email{'s' if count != 1 else ''} {time_label + ' ' if time_label else ''}in {single_mailbox}."
        )
    else:
        intro = (
            f"Hier is een samenvatting van {count} e-mail{'s' if count != 1 else ''} {time_label}.".replace("  ", " ").strip()
            if (dutch and wants_summary) else
            f"Here is a summary of {count} email{'s' if count != 1 else ''} {time_label}.".replace("  ", " ").strip()
            if wants_summary else
            f"Ik heb {count} e-mail{'s' if count != 1 else ''} {time_label} gevonden.".replace("  ", " ").strip()
            if dutch else
            f"I found {count} email{'s' if count != 1 else ''} {time_label}.".replace("  ", " ").strip()
        )

    has_to = any(data.get("To") for data in parsed_blocks)
    col_from = ("Aan" if has_to else "Van") if dutch else ("To" if has_to else "From")
    col_status = "Status"
    col_subject = "Onderwerp" if dutch else "Subject"
    col_date = "Datum" if dutch else "Date"
    col_preview = "Inhoud" if dutch else "Preview"

    # Build markdown table — rows are clickable in the UI via data-email-index
    header = f"| # | {col_status} | {col_from} | {col_subject} | {col_date} | {col_preview} | _message_id | _provider |"
    sep = "|---|---|---|---|---|---|---|---|"
    rows: list[str] = []
    provider_value = "outlook" if normalized_tool.startswith("outlook_") else "gmail" if normalized_tool.startswith("gmail_") else "-"
    for index, data in enumerate(parsed_blocks, start=start_index):
        subject = _email_table_cell(data.get("Subject", "(no subject)"), 55)
        counterparty = _email_table_cell(data.get("To") or data.get("From") or "-", 30)
        raw_date = data.get("Date", "-")
        date_val = _email_table_cell(_format_localized_email_datetime(raw_date, dutch), 20)
        preview_raw = _clean_email_preview(data.get("Preview", ""), max_len=preview_limit)
        if wants_summary:
            preview_raw = _email_topic_summary(data.get("Subject", ""), preview_raw)[:preview_limit]
        preview = _email_table_cell(preview_raw, preview_limit)
        status_raw = data.get("Status", "")
        if dutch:
            status = {"Unread": "Ongelezen", "Read": "Gelezen", "Replied": "Beantwoord"}.get(status_raw, status_raw)
        else:
            status = status_raw or "-"
        status = _email_table_cell(status, 12)
        message_id = str(data.get("Message ID", "") or "").strip().replace("|", " ") or "-"
        if show_mailbox:
            mb = _email_table_cell(data.get("Mailbox", ""), 18)
            rows.append(f"| {index} | {status} | {counterparty} ({mb}) | {subject} | {date_val} | {preview} | {message_id} | {provider_value} |")
        else:
            rows.append(f"| {index} | {status} | {counterparty} | {subject} | {date_val} | {preview} | {message_id} | {provider_value} |")

    body = "\n".join([header, sep] + rows)
    tip = ("Klik op een rij om die e-mail te openen." if dutch else "Click a row to open that email.")
    provenance = _format_provenance_line(tool_name, details, dutch)
    if provenance:
        return intro + "\n" + provenance + "\n\n" + body + "\n\n" + tip
    return intro + "\n\n" + body + "\n\n" + tip


def _compose_calendar_lookup(goal: str, tool_result: str, dutch: bool, tool_name: str = "", tool_details: dict | None = None) -> str:
    raw = str(tool_result or "").strip()
    details = dict(tool_details or build_tool_result_details(tool_name, raw))
    provenance = _format_provenance_line(tool_name, details, dutch)
    if _is_empty_calendar_lookup_text(raw):
        base = "Ik zie geen komende afspraken." if dutch else "I see no upcoming events."
        return (base + "\n" + provenance) if provenance else base

    events = list(details.get("events") or extract_calendar_lookup_records(raw))
    count = len(events)
    if count == 0:
        base = "Ik zie geen komende afspraken." if dutch else "I see no upcoming events."
        return (base + "\n" + provenance) if provenance else base

    intro = (
        f"Ik zie {count} {'afspraak' if count == 1 else 'afspraken'}."
        if dutch else
        f"I found {count} upcoming event{'s' if count != 1 else ''}."
    )

    # Determine calendar provider from tool name (used as hidden column for the inline reader)
    provider = "outlook" if str(tool_name or "").startswith("outlook_") else "google"

    # Build clickable markdown table (hidden _event_id and _provider columns power the inline reader)
    if dutch:
        header_row = "# | Titel | Start | Einde | Locatie | _event_id | _provider | _start_iso | _end_iso"
    else:
        header_row = "# | Title | Start | End | Location | _event_id | _provider | _start_iso | _end_iso"
    sep_row = "--- | --- | --- | --- | --- | --- | --- | --- | ---"

    rows: list[str] = []
    overview_lines: list[str] = ["Overzicht:" if dutch else "Overview:"]
    for index, event in enumerate(events, start=1):
        title = str(event.get("title") or ("[geen titel]" if dutch else "[no title]")).replace("|", "–")
        start = str(event.get("start") or "-").replace("|", " ")
        end = str(event.get("end") or "-").replace("|", " ")
        location = str(event.get("location") or "-").replace("|", " ")
        event_id = str(event.get("event_id") or "").replace("|", "").strip()
        start_iso = str(event.get("start_iso") or "").replace("|", "").strip()
        end_iso = str(event.get("end_iso") or "").replace("|", "").strip()
        overview_lines.append(f"**[{index}] {title}**")
        detail_parts: list[str] = []
        if start and start != "-":
            detail_parts.append(("Start" if dutch else "Start") + f": {start}")
        if end and end != "-":
            detail_parts.append(("Einde" if dutch else "End") + f": {end}")
        if location and location != "-":
            detail_parts.append(("Locatie" if dutch else "Location") + f": {location}")
        if detail_parts:
            overview_lines.append(" | ".join(detail_parts))
        rows.append(f"{index} | {title} | {start} | {end} | {location} | {event_id} | {provider} | {start_iso} | {end_iso}")

    table = header_row + "\n" + sep_row + "\n" + "\n".join(rows)
    overview = "\n".join(overview_lines)

    overlap_lines = _calendar_overlap_lines(goal, events, dutch)
    overlap_text = ("\n".join(overlap_lines) + "\n\n") if overlap_lines else ""

    if provenance:
        return intro + "\n" + provenance + "\n\n" + overlap_text + overview + "\n\n" + table
    return intro + "\n\n" + overlap_text + overview + "\n\n" + table


def _compose_calendar_action(tool_result: str, dutch: bool) -> str:
    raw = str(tool_result or "").strip()
    lowered = raw.lower()
    if lowered.startswith("calendar event ") and " deleted successfully" in lowered:
        event_id_match = re.search(r"calendar event\s+(.+?)\s+deleted successfully\.", raw, re.IGNORECASE)
        event_id = event_id_match.group(1).strip() if event_id_match else ""
        if dutch:
            return f"De agenda-afspraak is verwijderd.\nEvent ID: {event_id}" if event_id else "De agenda-afspraak is verwijderd."
        return f"The calendar event was deleted.\nEvent ID: {event_id}" if event_id else "The calendar event was deleted."

    data = _parse_kv_block(raw)
    title = data.get("Title", "")
    start = data.get("Start", "")
    end = data.get("End", "")
    link = data.get("Link", "")
    event_id = data.get("Event ID", "")

    if lowered.startswith("calendar event created successfully"):
        header = "De agenda-afspraak is aangemaakt." if dutch else "The calendar event was created."
    elif lowered.startswith("calendar event updated successfully"):
        header = "De agenda-afspraak is bijgewerkt." if dutch else "The calendar event was updated."
    else:
        header = raw

    if header == raw:
        return raw

    lines = [header]
    if title:
        lines.append(f"{'Titel' if dutch else 'Title'}: {title}")
    if start:
        lines.append(f"Start: {start}")
    if end:
        lines.append(f"{'Einde' if dutch else 'End'}: {end}")
    if link and link != "-":
        lines.append(f"Link: {link}")
    if event_id:
        lines.append(f"Event ID: {event_id}")
    return "\n".join(lines)


def _compose_email_action(tool_result: str, dutch: bool) -> str:
    raw = str(tool_result or "").strip()
    if raw.startswith("Email sent to "):
        match = re.match(r"Email sent to\s+(.+?)\s+[—-]\s+Subject:\s+(.+)$", raw)
        if match:
            recipient = match.group(1).strip()
            subject = match.group(2).strip()
            if dutch:
                return f"De e-mail is verzonden.\nAan: {recipient}\nOnderwerp: {subject}"
            return f"The email was sent.\nTo: {recipient}\nSubject: {subject}"
    if raw.startswith("Email sent successfully"):
        return "De e-mail is verzonden." if dutch else "The email was sent successfully."
    if raw.startswith("Email deleted successfully"):
        data = _parse_kv_block(raw)
        subject = str(data.get("Subject") or "").strip()
        message_id = str(data.get("Message ID") or "").strip()
        header = "De e-mail is verwijderd." if dutch else "The email was deleted."
        lines = [header]
        if subject:
            lines.append(f"{'Onderwerp' if dutch else 'Subject'}: {subject}")
        if message_id:
            lines.append(f"Message ID: {message_id}")
        return "\n".join(lines)
    if raw.startswith("Email updated successfully"):
        data = _parse_kv_block(raw)
        recipient = str(data.get("To") or "").strip()
        subject = str(data.get("Subject") or "").strip()
        message_id = str(data.get("Message ID") or "").strip()
        header = "De conceptmail is bijgewerkt." if dutch else "The draft email was updated."
        lines = [header]
        if recipient:
            lines.append(f"{'Aan' if dutch else 'To'}: {recipient}")
        if subject:
            lines.append(f"{'Onderwerp' if dutch else 'Subject'}: {subject}")
        if message_id:
            lines.append(f"Message ID: {message_id}")
        return "\n".join(lines)
    return raw


def _compose_email_full_read(goal: str, step_desc: str, tool_result: str, fallback_response: str = "") -> str:
    raw = str(tool_result or "").strip()
    fallback = str(fallback_response or "").strip()
    combined_goal = " ".join(part for part in (str(goal or "").strip(), str(step_desc or "").strip()) if part)
    if fallback and fallback != raw and _EMAIL_SUMMARY_OR_ANALYSIS_RE.search(combined_goal):
        return fallback
    return raw


def compose_tool_response(
    goal: str,
    step_desc: str,
    tool_name: str,
    tool_result: str,
    fallback_response: str = "",
    tool_details: dict | None = None,
) -> tuple[str, bool]:
    normalized_tool = str(tool_name or "").strip()
    raw = str(tool_result or "").strip()
    if not raw:
        return fallback_response, False
    if raw.startswith(("Error", "POLICY_DENIED", "HITL_REQUIRED", "TOOL_DISABLED")):
        return fallback_response or raw, False

    dutch = _looks_dutch(goal, step_desc, fallback_response)
    if normalized_tool in _IMAGE_DESCRIPTION_TOOLS:
        return _compose_image_description(raw), True
    if normalized_tool in _EMAIL_FULL_READ_TOOLS:
        return _compose_email_full_read(goal, step_desc, raw, fallback_response), True
    if normalized_tool in _EMAIL_LOOKUP_TOOLS:
        return _compose_email_lookup(goal, raw, dutch, normalized_tool, tool_details), True
    if normalized_tool in _CALENDAR_LOOKUP_TOOLS:
        return _compose_calendar_lookup(goal, raw, dutch, normalized_tool, tool_details), True
    if normalized_tool in _CALENDAR_ACTION_TOOLS:
        return _compose_calendar_action(raw, dutch), True
    if normalized_tool in _EMAIL_ACTION_TOOLS:
        return _compose_email_action(raw, dutch), True
    return fallback_response, False


def build_tool_result_envelope(
    goal: str,
    step_desc: str,
    tool_name: str,
    tool_result: str,
    fallback_response: str = "",
    tool_details: dict | None = None,
) -> dict[str, object]:
    normalized_tool = str(tool_name or "").strip()
    raw = str(tool_result or "").strip()
    details = dict(tool_details or build_tool_result_details(normalized_tool, raw))
    context_result = compact_tool_result_for_context(normalized_tool, raw, details)
    rendered_result, was_composed = compose_tool_response(
        goal,
        step_desc,
        normalized_tool,
        raw,
        fallback_response or context_result,
        details,
    )
    if not str(rendered_result or "").strip():
        rendered_result = fallback_response or context_result or raw
    return {
        "tool_name": normalized_tool,
        "raw_result": raw,
        "details": details,
        "context_result": context_result,
        "rendered_result": str(rendered_result or "").strip(),
        "was_composed": bool(was_composed),
        "is_error": bool(details.get("is_error", False)),
        "kind": str(details.get("kind") or "raw"),
    }


def compose_direct_results(goal: str, results: list[tuple[str, str]], fallback_report: str) -> str:
    if not results:
        return fallback_report
    dutch = _looks_dutch(goal, fallback_report)
    multi_source_summary = _compose_multi_source_email_summary(results, dutch, goal)
    if multi_source_summary:
        return multi_source_summary
    has_positive_email_lookup = any(
        tool_name in _EMAIL_LOOKUP_TOOLS and _email_lookup_has_hits(tool_result)
        for tool_name, tool_result in results
    )
    has_positive_calendar_lookup = any(
        tool_name in _CALENDAR_LOOKUP_TOOLS and not _is_empty_calendar_lookup_text(tool_result)
        for tool_name, tool_result in results
    )
    composed: list[str] = []
    used_composer = False
    for tool_name, tool_result in results:
        rendered, was_composed = compose_tool_response(goal, goal, tool_name, tool_result, tool_result)
        if (
            has_positive_email_lookup
            and tool_name in _EMAIL_LOOKUP_TOOLS
            and _is_empty_email_lookup_text(tool_result)
        ):
            used_composer = used_composer or was_composed
            continue
        if (
            has_positive_calendar_lookup
            and tool_name in _CALENDAR_LOOKUP_TOOLS
            and _is_empty_calendar_lookup_text(tool_result)
        ):
            used_composer = used_composer or was_composed
            continue
        if rendered.strip():
            composed.append(rendered.strip())
        used_composer = used_composer or was_composed
    if not used_composer:
        return fallback_report
    unique = _dedupe_preserve_order(composed)
    return unique[0] if len(unique) == 1 else "\n\n---\n\n".join(unique)


def compose_final_report(goal: str, memory: list[dict], fallback_report: str) -> str:
    if not memory:
        return fallback_report

    if is_copilot_morning_brief_goal(goal):
        try:
            from backend.copilot_briefing import load_contact_profiles
            _profiles = load_contact_profiles()
        except (ImportError, Exception):
            _profiles = {}
        try:
            morning_brief = compose_morning_brief_report(goal, memory, contact_profiles=_profiles)
        except TypeError:
            morning_brief = compose_morning_brief_report(goal, memory)
        if morning_brief:
            return morning_brief

    if is_copilot_followup_goal(goal):
        try:
            from backend.copilot_briefing import load_contact_profiles
            _profiles = load_contact_profiles()
        except (ImportError, Exception):
            _profiles = {}
        try:
            followup_brief = compose_followup_brief_report(goal, memory, contact_profiles=_profiles)
        except TypeError:
            followup_brief = compose_followup_brief_report(goal, memory)
        if followup_brief:
            return followup_brief

    deterministic = [
        item for item in memory
        if (item.get("result") or "").strip() and item.get("deterministic_result")
    ]
    if not deterministic:
        return fallback_report

    last_deterministic = deterministic[-1]
    last_tool = str(last_deterministic.get("tool_name") or "")
    if last_tool in _CALENDAR_ACTION_TOOLS | _EMAIL_ACTION_TOOLS:
        return str(last_deterministic.get("result") or fallback_report).strip() or fallback_report

    if _SIDE_EFFECT_RE.search(str(goal or "")):
        return str(last_deterministic.get("result") or fallback_report).strip() or fallback_report

    if len(memory) == 1:
        return str(memory[0].get("result") or fallback_report).strip() or fallback_report

    if all(item.get("deterministic_result") for item in memory if (item.get("result") or "").strip()):
        dutch = _looks_dutch(goal, fallback_report)
        multi_source_summary = _compose_multi_source_email_summary([
            (str(item.get("tool_name") or ""), str(item.get("tool_result") or ""))
            for item in memory
            if str(item.get("tool_name") or "").strip() in _EMAIL_LOOKUP_TOOLS
        ], dutch, goal)
        if multi_source_summary:
            return multi_source_summary
        has_positive_email_lookup = any(
            str(item.get("tool_name") or "") in _EMAIL_LOOKUP_TOOLS
            and _email_lookup_has_hits(str(item.get("tool_result") or item.get("result") or ""))
            for item in memory
        )
        has_positive_calendar_lookup = any(
            str(item.get("tool_name") or "") in _CALENDAR_LOOKUP_TOOLS
            and not _is_empty_calendar_lookup_text(str(item.get("tool_result") or item.get("result") or ""))
            for item in memory
        )
        joined = _dedupe_preserve_order([
            str(item.get("result") or "").strip()
            for item in memory
            if (item.get("result") or "").strip()
            and not (
                has_positive_email_lookup
                and str(item.get("tool_name") or "") in _EMAIL_LOOKUP_TOOLS
                and _is_empty_email_lookup_text(str(item.get("tool_result") or item.get("result") or ""))
            )
            and not (
                has_positive_calendar_lookup
                and str(item.get("tool_name") or "") in _CALENDAR_LOOKUP_TOOLS
                and _is_empty_calendar_lookup_text(str(item.get("tool_result") or item.get("result") or ""))
            )
        ])
        return joined[0] if len(joined) == 1 else "\n\n---\n\n".join(joined)

    return fallback_report