# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Contacts Skill

Unified address book that cross-references across all other skills.
Data stored in ~/.familiar/data/contacts.json

Each contact has a unique ID. Donor records, email senders, calendar
attendees, and task assignees reference contacts by ID or email address.
The contact_history tool aggregates across all skills by querying each
skill's data for matching contact IDs or email addresses.
"""

import csv
import io
import json
import logging
from datetime import datetime
from pathlib import Path


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


# Use centralized safe I/O and ID generation
try:
    from familiar.core.utils import atomic_write_json, generate_id, safe_load_json
except ImportError:
    atomic_write_json = None
    safe_load_json = None
    generate_id = None

logger = logging.getLogger(__name__)


def _get_data_file():
    """Get data file path (re-evaluates for tenant scoping)."""
    return _get_data_dir() / "contacts.json"


DATA_FILE = _get_data_file()  # Default for backward compat

_DEFAULT_DATA = {"contacts": [], "version": 1}


def _load_data() -> dict:
    """Load contacts data with automatic backup recovery."""
    if safe_load_json:
        return safe_load_json(_get_data_file(), default=lambda: dict(_DEFAULT_DATA))
    if _get_data_file().exists():
        try:
            with open(_get_data_file(), encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError, OSError) as e:
            logger.warning(f"Failed to load contacts data: {e}")
    return dict(_DEFAULT_DATA)


def _save_data(data: dict):
    """Save contacts data atomically with backup rotation."""
    if atomic_write_json:
        atomic_write_json(_get_data_file(), data)
    else:
        _get_data_file().parent.mkdir(parents=True, exist_ok=True)
        with open(_get_data_file(), "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)


def _generate_id() -> str:
    if generate_id:
        return generate_id()
    import secrets

    return secrets.token_hex(4)


def _normalize_email(email: str) -> str:
    """Normalize email for comparison."""
    return email.strip().lower() if email else ""


def _match_contact(contact: dict, query: str) -> bool:
    """Check if a contact matches a search query across all fields."""
    q = query.lower()
    searchable = [
        contact.get("name", ""),
        contact.get("email", ""),
        contact.get("phone", ""),
        contact.get("organization", ""),
        contact.get("role", ""),
        contact.get("notes", ""),
    ]
    searchable.extend(contact.get("tags", []))
    return any(q in field.lower() for field in searchable if field)


# === Tool Handlers ===


def add_contact(data: dict) -> str:
    """Create or update a contact record."""
    name = data.get("name", "").strip()
    if not name:
        return "Error: Please provide a name for the contact."

    db = _load_data()
    email = _normalize_email(data.get("email", ""))

    # Check for existing contact by name or email
    existing = None
    for c in db["contacts"]:
        if c["name"].lower() == name.lower():
            existing = c
            break
        if email and _normalize_email(c.get("email", "")) == email:
            existing = c
            break

    now = datetime.now().isoformat()

    if existing:
        # Update existing fields (only overwrite if new value provided)
        for field in ["email", "phone", "organization", "role", "notes"]:
            val = data.get(field, "").strip() if isinstance(data.get(field), str) else ""
            if val:
                existing[field] = val
        if data.get("tags"):
            # Merge tags, deduplicate
            existing_tags = set(existing.get("tags", []))
            existing_tags.update(data["tags"])
            existing["tags"] = sorted(existing_tags)
        existing["updated_at"] = now
        _save_data(db)
        return f"✅ Updated contact: {existing['name']} [{existing['id']}]"

    # Create new contact
    contact = {
        "id": _generate_id(),
        "name": name,
        "email": email,
        "phone": data.get("phone", "").strip(),
        "organization": data.get("organization", "").strip(),
        "role": data.get("role", "").strip(),
        "notes": data.get("notes", "").strip(),
        "tags": sorted(set(data.get("tags", []))),
        "links": [],  # Cross-skill references: {"skill": "nonprofit", "type": "donor", "id": "abc123"}
        "created_at": now,
        "updated_at": now,
    }

    db["contacts"].append(contact)
    _save_data(db)
    return f"✅ Created contact: {name} [{contact['id']}]"


def search_contacts(data: dict) -> str:
    """Full-text search across all contact fields, with tag/org/role filters."""
    db = _load_data()
    query = data.get("query", "").strip()
    tag_filter = data.get("tag", "").strip().lower()
    org_filter = data.get("organization", "").strip().lower()
    role_filter = data.get("role", "").strip().lower()

    results = []
    for c in db["contacts"]:
        if query and not _match_contact(c, query):
            continue
        if tag_filter and tag_filter not in [t.lower() for t in c.get("tags", [])]:
            continue
        if org_filter and org_filter not in c.get("organization", "").lower():
            continue
        if role_filter and role_filter not in c.get("role", "").lower():
            continue
        results.append(c)

    if not results:
        return "No contacts found matching criteria."

    results.sort(key=lambda x: x.get("name", "").lower())

    lines = [f"👥 Contacts ({len(results)}):\n"]
    for c in results[:25]:
        parts = [f"  [{c['id']}] {c['name']}"]
        if c.get("email"):
            parts.append(f"<{c['email']}>")
        if c.get("organization"):
            parts.append(f"@ {c['organization']}")
        if c.get("role"):
            parts.append(f"({c['role']})")
        lines.append(" ".join(parts))

    if len(results) > 25:
        lines.append(f"\n  ... and {len(results) - 25} more")

    return "\n".join(lines)


def get_contact(data: dict) -> str:
    """Retrieve full contact record by name, email, or ID."""
    db = _load_data()
    contact_id = data.get("id", "").strip().lower()
    name = data.get("name", "").strip().lower()
    email = _normalize_email(data.get("email", ""))

    contact = None
    for c in db["contacts"]:
        if contact_id and c["id"].lower() == contact_id:
            contact = c
            break
        if name and c["name"].lower() == name:
            contact = c
            break
        if email and _normalize_email(c.get("email", "")) == email:
            contact = c
            break

    if not contact:
        return "Contact not found."

    lines = [
        f"👤 {contact['name']}",
        f"   ID: {contact['id']}",
    ]
    if contact.get("email"):
        lines.append(f"   Email: {contact['email']}")
    if contact.get("phone"):
        lines.append(f"   Phone: {contact['phone']}")
    if contact.get("organization"):
        lines.append(f"   Organization: {contact['organization']}")
    if contact.get("role"):
        lines.append(f"   Role: {contact['role']}")
    if contact.get("tags"):
        lines.append(f"   Tags: {', '.join(contact['tags'])}")
    if contact.get("notes"):
        lines.append(f"   Notes: {contact['notes']}")
    if contact.get("links"):
        lines.append(f"\n   Cross-references ({len(contact['links'])}):")
        for link in contact["links"][:10]:
            lines.append(f"     → {link['skill']}/{link['type']}: {link['id']}")
    lines.append(f"\n   Created: {contact.get('created_at', 'N/A')[:10]}")
    lines.append(f"   Updated: {contact.get('updated_at', 'N/A')[:10]}")

    return "\n".join(lines)


def link_contact(data: dict) -> str:
    """Link a contact to a donor record, email thread, or calendar event."""
    db = _load_data()
    contact_id = data.get("contact_id", "").strip()
    skill = data.get("skill", "").strip()
    link_type = data.get("type", "").strip()
    link_id = data.get("link_id", "").strip()

    if not all([contact_id, skill, link_type, link_id]):
        return "Please provide contact_id, skill, type, and link_id."

    contact = None
    for c in db["contacts"]:
        if c["id"] == contact_id:
            contact = c
            break

    if not contact:
        return f"Contact {contact_id} not found."

    # Check for duplicate link
    for link in contact.get("links", []):
        if link["skill"] == skill and link["type"] == link_type and link["id"] == link_id:
            return f"Link already exists: {skill}/{link_type}/{link_id}"

    if "links" not in contact:
        contact["links"] = []

    contact["links"].append(
        {"skill": skill, "type": link_type, "id": link_id, "linked_at": datetime.now().isoformat()}
    )
    contact["updated_at"] = datetime.now().isoformat()

    _save_data(db)
    return f"✅ Linked {contact['name']} → {skill}/{link_type}/{link_id}"


def list_contacts(data: dict) -> str:
    """List all contacts, optionally filtered by tag, org, or recent interaction."""
    db = _load_data()
    tag_filter = data.get("tag", "").strip().lower()
    org_filter = data.get("organization", "").strip().lower()
    limit = min(data.get("limit", 50), 100)

    contacts = db.get("contacts", [])

    if tag_filter:
        contacts = [c for c in contacts if tag_filter in [t.lower() for t in c.get("tags", [])]]
    if org_filter:
        contacts = [c for c in contacts if org_filter in c.get("organization", "").lower()]

    contacts.sort(key=lambda x: x.get("updated_at", ""), reverse=True)
    contacts = contacts[:limit]

    if not contacts:
        return "No contacts found."

    lines = [f"👥 Contacts ({len(contacts)}):\n"]
    for c in contacts:
        org = f" @ {c['organization']}" if c.get("organization") else ""
        email = f" <{c['email']}>" if c.get("email") else ""
        lines.append(f"  [{c['id']}] {c['name']}{org}{email}")

    return "\n".join(lines)


def import_contacts(data: dict) -> str:
    """Import contacts from a vCard (.vcf) file or CSV file."""
    filepath = data.get("filepath", "").strip()
    if not filepath:
        return "Please provide a filepath to import from."

    path = Path(filepath).expanduser()
    if not path.exists():
        return f"File not found: {filepath}"

    # add_contact() handles its own load/save cycle
    imported = 0
    updated = 0

    ext = path.suffix.lower()

    if ext == ".vcf":
        # Parse vCard format
        content = path.read_text(encoding="utf-8", errors="replace")
        vcards = content.split("BEGIN:VCARD")

        for vcard in vcards[1:]:  # Skip first empty split
            name = ""
            email = ""
            phone = ""
            org = ""

            for line in vcard.split("\n"):
                line = line.strip()
                if line.startswith("FN:") or line.startswith("FN;"):
                    name = line.split(":", 1)[1].strip()
                elif line.startswith("EMAIL") and ":" in line:
                    email = line.split(":", 1)[1].strip()
                elif line.startswith("TEL") and ":" in line:
                    phone = line.split(":", 1)[1].strip()
                elif line.startswith("ORG:") or line.startswith("ORG;"):
                    org = line.split(":", 1)[1].strip().rstrip(";")

            if name:
                result = add_contact(
                    {"name": name, "email": email, "phone": phone, "organization": org}
                )
                if "Created" in result:
                    imported += 1
                elif "Updated" in result:
                    updated += 1

    elif ext == ".csv":
        content = path.read_text(encoding="utf-8", errors="replace")
        reader = csv.DictReader(io.StringIO(content))

        # Flexible column name matching
        for row in reader:
            name = (
                row.get("name")
                or row.get("Name")
                or row.get("full_name")
                or row.get("Full Name")
                or ""
            ).strip()
            if not name:
                # Try first + last
                first = (row.get("first_name") or row.get("First Name") or "").strip()
                last = (row.get("last_name") or row.get("Last Name") or "").strip()
                name = f"{first} {last}".strip()

            if not name:
                continue

            email = (
                row.get("email")
                or row.get("Email")
                or row.get("email_address")
                or row.get("Email Address")
                or ""
            ).strip()
            phone = (
                row.get("phone")
                or row.get("Phone")
                or row.get("phone_number")
                or row.get("Phone Number")
                or ""
            ).strip()
            org = (
                row.get("organization")
                or row.get("Organization")
                or row.get("company")
                or row.get("Company")
                or ""
            ).strip()
            role = (
                row.get("role") or row.get("Role") or row.get("title") or row.get("Title") or ""
            ).strip()

            result = add_contact(
                {"name": name, "email": email, "phone": phone, "organization": org, "role": role}
            )
            if "Created" in result:
                imported += 1
            elif "Updated" in result:
                updated += 1
    else:
        return f"Unsupported format: {ext}. Use .vcf or .csv"

    return f"✅ Import complete: {imported} new, {updated} updated"


def export_contacts(data: dict) -> str:
    """Export contacts to vCard (.vcf) or CSV format."""
    db = _load_data()
    fmt = data.get("format", "csv").lower()
    filepath = data.get("filepath", "").strip()

    contacts = db.get("contacts", [])
    if not contacts:
        return "No contacts to export."

    if not filepath:
        ext = ".vcf" if fmt == "vcf" else ".csv"
        filepath = str(
            Path.home()
            / ".familiar"
            / "documents"
            / f"contacts_export_{datetime.now().strftime('%Y%m%d')}{ext}"
        )

    path = Path(filepath).expanduser()
    path.parent.mkdir(parents=True, exist_ok=True)

    if fmt == "vcf":
        lines = []
        for c in contacts:
            lines.append("BEGIN:VCARD")
            lines.append("VERSION:3.0")
            lines.append(f"FN:{c['name']}")
            if c.get("email"):
                lines.append(f"EMAIL:{c['email']}")
            if c.get("phone"):
                lines.append(f"TEL:{c['phone']}")
            if c.get("organization"):
                lines.append(f"ORG:{c['organization']}")
            if c.get("role"):
                lines.append(f"TITLE:{c['role']}")
            if c.get("notes"):
                lines.append(f"NOTE:{c['notes']}")
            lines.append("END:VCARD")
            lines.append("")
        path.write_text("\n".join(lines), encoding="utf-8")

    elif fmt == "csv":
        output = io.StringIO()
        writer = csv.DictWriter(
            output,
            fieldnames=[
                "id",
                "name",
                "email",
                "phone",
                "organization",
                "role",
                "tags",
                "notes",
                "created_at",
                "updated_at",
            ],
        )
        writer.writeheader()
        for c in contacts:
            writer.writerow(
                {
                    "id": c.get("id", ""),
                    "name": c.get("name", ""),
                    "email": c.get("email", ""),
                    "phone": c.get("phone", ""),
                    "organization": c.get("organization", ""),
                    "role": c.get("role", ""),
                    "tags": ";".join(c.get("tags", [])),
                    "notes": c.get("notes", ""),
                    "created_at": c.get("created_at", ""),
                    "updated_at": c.get("updated_at", ""),
                }
            )
        path.write_text(output.getvalue(), encoding="utf-8")
    else:
        return f"Unsupported format: {fmt}. Use 'vcf' or 'csv'."

    return f"✅ Exported {len(contacts)} contacts to {path}"


def merge_contacts(data: dict) -> str:
    """Merge duplicate contact records. Keeps the first as primary, merges fields from second."""
    db = _load_data()
    primary_id = data.get("primary_id", "").strip()
    merge_id = data.get("merge_id", "").strip()

    if not primary_id or not merge_id:
        return "Please provide primary_id and merge_id."

    if not data.get("_confirmed"):
        from familiar.core.confirmations import needs_confirmation

        return needs_confirmation(
            tool_name="merge_contacts",
            tool_input=data,
            preview=f"Merge contact '{merge_id}' into '{primary_id}'. The secondary record will be deleted.",
            risk="high",
        )

    if primary_id == merge_id:
        return "Cannot merge a contact with itself."

    primary = None
    merge_source = None
    merge_idx = None

    for i, c in enumerate(db["contacts"]):
        if c["id"] == primary_id:
            primary = c
        if c["id"] == merge_id:
            merge_source = c
            merge_idx = i

    if not primary:
        return f"Primary contact {primary_id} not found."
    if not merge_source:
        return f"Merge contact {merge_id} not found."

    # Fill empty fields in primary from merge_source
    for field in ["email", "phone", "organization", "role", "notes"]:
        if not primary.get(field) and merge_source.get(field):
            primary[field] = merge_source[field]

    # Merge tags
    primary_tags = set(primary.get("tags", []))
    primary_tags.update(merge_source.get("tags", []))
    primary["tags"] = sorted(primary_tags)

    # Merge links
    existing_links = {
        (entry["skill"], entry["type"], entry["id"]) for entry in primary.get("links", [])
    }
    for link in merge_source.get("links", []):
        key = (link["skill"], link["type"], link["id"])
        if key not in existing_links:
            primary.setdefault("links", []).append(link)

    primary["updated_at"] = datetime.now().isoformat()

    # Remove merge source
    db["contacts"].pop(merge_idx)
    _save_data(db)

    return f"✅ Merged [{merge_id}] into [{primary_id}] ({primary['name']}). Duplicate removed."


def contact_history(data: dict) -> str:
    """Show all interactions with a contact across email, donor logs, calendar, tasks."""
    db = _load_data()
    contact_id = data.get("id", "").strip()
    name = data.get("name", "").strip().lower()
    email = _normalize_email(data.get("email", ""))

    # Find the contact
    contact = None
    for c in db["contacts"]:
        if contact_id and c["id"] == contact_id:
            contact = c
            break
        if name and c["name"].lower() == name:
            contact = c
            break
        if email and _normalize_email(c.get("email", "")) == email:
            contact = c
            break

    if not contact:
        return "Contact not found."

    lines = [f"📋 Interaction History: {contact['name']}\n"]
    contact_email = _normalize_email(contact.get("email", ""))
    interactions = []

    # Check nonprofit/donor data
    try:
        np_file = Path.home() / ".familiar" / "data" / "nonprofit.json"
        if np_file.exists():
            with open(np_file, encoding="utf-8") as f:
                np_data = json.load(f)
            for donor in np_data.get("donors", []):
                if donor["name"].lower() == contact["name"].lower() or (
                    contact_email and _normalize_email(donor.get("email", "")) == contact_email
                ):
                    for interaction in donor.get("interactions", []):
                        interactions.append(
                            {
                                "date": interaction.get("date", ""),
                                "source": "nonprofit",
                                "type": interaction.get("type", "note"),
                                "detail": interaction.get("description", "")
                                or (
                                    f"${interaction.get('amount', '')}"
                                    if interaction.get("amount")
                                    else ""
                                ),
                            }
                        )
    except (json.JSONDecodeError, IOError, OSError) as e:
        logger.debug("JSON parse error suppressed: %s", e)
    # Check tasks data
    try:
        tasks_file = Path.home() / ".familiar" / "data" / "tasks.json"
        if tasks_file.exists():
            with open(tasks_file, encoding="utf-8") as f:
                tasks_data = json.load(f)
            for task in tasks_data.get("tasks", []):
                assignee = task.get("assignee", "").lower()
                if assignee == contact["name"].lower() or assignee == contact["id"]:
                    interactions.append(
                        {
                            "date": task.get("created_at", ""),
                            "source": "tasks",
                            "type": "task",
                            "detail": task.get("title", "")[:60],
                        }
                    )
    except (json.JSONDecodeError, IOError, OSError) as e:
        logger.debug("JSON parse error suppressed: %s", e)
    # Sort by date descending
    interactions.sort(key=lambda x: x.get("date", ""), reverse=True)

    if not interactions:
        lines.append("  No cross-skill interactions found.")
        if contact.get("links"):
            lines.append(f"\n  Linked references: {len(contact['links'])}")
            for link in contact["links"][:5]:
                lines.append(f"    → {link['skill']}/{link['type']}: {link['id']}")
    else:
        for item in interactions[:20]:
            date = item["date"][:10] if item["date"] else "N/A"
            lines.append(f"  {date} [{item['source']}] {item['type']}: {item['detail']}")

        if len(interactions) > 20:
            lines.append(f"\n  ... and {len(interactions) - 20} more interactions")

    return "\n".join(lines)


# === Tool Definitions ===

TOOLS = [
    {
        "name": "add_contact",
        "description": (
            "Create a new contact record or update an existing one by matching name or "
            "email address. Use this when the user wants to save someone's contact info, "
            "add a phone number, or tag a person. Fields like email, phone, organization, "
            "role, and tags are merged into the existing record if one is found."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Contact full name"},
                "email": {"type": "string", "description": "Email address"},
                "phone": {"type": "string", "description": "Phone number"},
                "organization": {"type": "string", "description": "Organization or company"},
                "role": {"type": "string", "description": "Job title or role"},
                "notes": {"type": "string", "description": "Free-text notes"},
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Tags (e.g. board, donor, volunteer)",
                },
            },
            "required": ["name"],
        },
        "handler": add_contact,
        "category": "contacts",
    },
    {
        "name": "search_contacts",
        "description": (
            "Full-text search across all contact fields including name, email, phone, "
            "organization, role, notes, and tags. Use this when the user wants to find "
            "a contact but may not know the exact name or ID. Supports optional filters "
            "for tag, organization, and role. Returns up to 25 matching contacts."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search term (matches name, email, phone, org, role, notes, tags)",
                },
                "tag": {"type": "string", "description": "Filter by tag"},
                "organization": {"type": "string", "description": "Filter by organization"},
                "role": {"type": "string", "description": "Filter by role"},
            },
        },
        "handler": search_contacts,
        "category": "contacts",
    },
    {
        "name": "get_contact",
        "description": (
            "Retrieve the full contact record by name, email address, or internal ID. "
            "Use this when the user asks for details about a specific person, such as "
            "their phone number, organization, or linked records. Returns all stored "
            "fields including cross-skill references and timestamps."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "id": {"type": "string", "description": "Contact ID"},
                "name": {"type": "string", "description": "Contact name"},
                "email": {"type": "string", "description": "Contact email"},
            },
        },
        "handler": get_contact,
        "category": "contacts",
    },
    {
        "name": "link_contact",
        "description": (
            "Link a contact to a record in another skill such as a donor entry, email "
            "thread, or calendar event. Use this to build cross-skill references so that "
            "contact_history can aggregate interactions. Duplicate links are detected and "
            "rejected automatically."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "contact_id": {"type": "string", "description": "Contact ID to link"},
                "skill": {
                    "type": "string",
                    "description": "Skill name (e.g. nonprofit, calendar, email)",
                },
                "type": {
                    "type": "string",
                    "description": "Link type (e.g. donor, attendee, thread)",
                },
                "link_id": {"type": "string", "description": "ID of the linked record"},
            },
            "required": ["contact_id", "skill", "type", "link_id"],
        },
        "handler": link_contact,
        "category": "contacts",
    },
    {
        "name": "list_contacts",
        "description": (
            "List all contacts sorted by most recently updated, with optional filters "
            "for tag and organization. Use this when the user wants to browse the address "
            "book or see contacts in a specific group. Returns up to 100 contacts with "
            "name, organization, and email."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "tag": {"type": "string", "description": "Filter by tag"},
                "organization": {"type": "string", "description": "Filter by organization"},
                "limit": {
                    "type": "integer",
                    "default": 50,
                    "description": "Max results (up to 100)",
                },
            },
        },
        "handler": list_contacts,
        "category": "contacts",
    },
    {
        "name": "import_contacts",
        "description": (
            "Import contacts from a vCard (.vcf) file or a CSV file with flexible "
            "column name matching. Use this when the user wants to bulk-load contacts "
            "from an export or backup. Existing contacts matched by name or email are "
            "updated; new contacts are created. Returns import and update counts."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "filepath": {"type": "string", "description": "Path to .vcf or .csv file"}
            },
            "required": ["filepath"],
        },
        "handler": import_contacts,
        "category": "contacts",
    },
    {
        "name": "export_contacts",
        "description": (
            "Export all contacts to a vCard (.vcf) or CSV file. Use this when the user "
            "wants to back up their address book or share contacts with another system. "
            "If no filepath is provided, a timestamped file is created in "
            "~/.familiar/documents/. Returns the output file path."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "format": {"type": "string", "enum": ["vcf", "csv"], "default": "csv"},
                "filepath": {
                    "type": "string",
                    "description": "Output file path (optional, auto-generated if omitted)",
                },
            },
        },
        "handler": export_contacts,
        "category": "contacts",
    },
    {
        "name": "merge_contacts",
        "description": (
            "Merge two duplicate contact records by absorbing fields, tags, and links "
            "from the secondary into the primary. Use this when the user has duplicate "
            "entries for the same person. The secondary record is permanently deleted "
            "after merging. Requires confirmation before proceeding."
        ),
        "requires_confirmation": True,
        "input_schema": {
            "type": "object",
            "properties": {
                "primary_id": {"type": "string", "description": "ID of contact to keep"},
                "merge_id": {
                    "type": "string",
                    "description": "ID of contact to merge in and remove",
                },
            },
            "required": ["primary_id", "merge_id"],
        },
        "handler": merge_contacts,
        "category": "contacts",
    },
    {
        "name": "contact_history",
        "description": (
            "Show all interactions with a contact aggregated across email, donor logs, "
            "calendar events, and tasks. Use this when the user asks about their history "
            "with a person or wants a relationship summary. Returns a chronological list "
            "of interactions sorted by date, plus any cross-skill links."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "id": {"type": "string", "description": "Contact ID"},
                "name": {"type": "string", "description": "Contact name"},
                "email": {"type": "string", "description": "Contact email"},
            },
        },
        "handler": contact_history,
        "category": "contacts",
    },
]
