"""Tools for local-network Mugi Mesh collaboration."""
from __future__ import annotations

import json
import re
import uuid
from typing import Any

from langchain_core.tools import tool
from pydantic import BaseModel, Field
from tools._logging import _emit_input_status, should_stop_tool

from mesh import get_mesh_manager
from mesh.logical_message_dedupe import merged_payload_with_logical_id
from mesh.protocol import MeshMessage
from mesh.relationship_ledger import contains_peer_sensitive_content, sanitize_peer_visible_text
from utils.handoff_envelope import envelope_from_json_str


def _mesh_contact_peer_timeout_defaults() -> tuple[int, int]:
    """(default_seconds, max_seconds) from [mesh] in mugi_config.toml."""
    try:
        from core.config import get_mesh_config

        m = get_mesh_config()
        default = int(m.get("contact_peer_default_timeout_seconds", 300) or 300)
        cap = int(m.get("contact_peer_max_timeout_seconds", 3600) or 3600)
    except Exception:
        default, cap = 300, 3600
    default = max(60, default)
    cap = max(default, cap)
    return default, cap


def _default_contact_peer_timeout_seconds() -> int:
    return _mesh_contact_peer_timeout_defaults()[0]


def _clamp_contact_peer_timeout_seconds(raw: int) -> int:
    _, cap = _mesh_contact_peer_timeout_defaults()
    try:
        v = int(raw)
    except (TypeError, ValueError):
        v = _default_contact_peer_timeout_seconds()
    return max(1, min(v, cap))

_MESH_HANDSHAKE_HINT_RE = re.compile(
    r"(?:\b(?:are\s+you\s+up|up\s+for\s+it|formal\s+handoff|ready\s+for\s+handoff|"
    r"ping\s+first|confirm\s+first|handshake\s+only)\b|"
    r"i(?:'|')?ll\s+send\s+(?:the\s+)?(?:a\s+)?formal\s+handoff)",
    re.IGNORECASE,
)


def _looks_like_mesh_task_handshake_only(message: str) -> bool:
    """True when *message* looks like a social ping instead of a full task spec."""
    t = (message or "").strip()
    if not t or len(t) > 1200:
        return False
    if not _MESH_HANDSHAKE_HINT_RE.search(t):
        return False
    if re.search(
        r"(?i)\b(research|investigate|compile|analyze|write\b.+\breport|deliverable|"
        r"requirements?|objective|steps?\s*:|task\s*[:#]|\d+\.\s+\S{4,})\b",
        t,
    ):
        return False
    return True


def _normalize_peer_name(name: str) -> str:
    n = (name or "").strip().lower().replace(" ", "")
    for suffix in (".local", ".local."):
        if n.endswith(suffix):
            n = n[: -len(suffix)]
    return n


def _peer_name_matches_session(sess: dict, target: str, *, prefix_match: bool = False) -> bool:
    """True if normalized *target* matches peer_nickname, mesh_intro_hint, or peer_hostname."""
    if not target:
        return False
    for key in ("peer_nickname", "mesh_intro_hint", "peer_hostname"):
        raw = str(sess.get(key) or "").strip()
        if not raw:
            continue
        norm = _normalize_peer_name(raw)
        if not norm:
            continue
        if norm == target:
            return True
        if prefix_match and (norm.startswith(target) or target.startswith(norm)):
            return True
    return False


def _mesh_session_disambiguation_label(sess: dict) -> str:
    host = str(sess.get("peer_hostname") or "peer")
    nick = str(sess.get("peer_nickname") or "").strip()
    return f"{host} ({nick})" if nick else host


def _resolve_mesh_session_for_peer(manager: Any, peer_name: str) -> tuple[dict | None, str | None]:
    """Return (session_dict, error_message). On success error_message is None."""
    from core.config import get_mesh_config

    target = _normalize_peer_name(peer_name)
    prefix_match = bool(get_mesh_config().get("peer_name_prefix_match", False))
    peers = manager.list_sessions()
    matches: list[dict] = []
    for sess in peers:
        sid = str(sess.get("session_id") or "")
        if not sid:
            continue
        if _peer_name_matches_session(sess, target, prefix_match=prefix_match):
            matches.append(sess)
    if not matches:
        return None, f"No active session found for peer '{peer_name}'."
    if len(matches) > 1:
        labels = ", ".join(_mesh_session_disambiguation_label(s) for s in matches)
        return None, (
            f"Multiple peers match '{peer_name}': {labels}. "
            "Use a more specific nickname, intro hint, or hostname."
        )
    return matches[0], None


class ContactPeerInput(BaseModel):
    peer_name: str = Field(
        description=(
            "Peer label to contact: nickname, intro hint, or hostname (as shown in list_mesh_peers). "
            "Use only after the local user's main-chat request is handled, or when they explicitly asked "
            "you to contact this peer — do not prioritize mesh chit-chat over answering the user."
        )
    )
    message: str = Field(
        description=(
            "Message to send. When is_task and wait_for_result are both true, this must be the **complete task "
            "specification in one message** (goals, constraints, expected output). Do not send a 'confirm first' "
            "handshake unless you will add a second contact_peer in the same run."
        ),
    )
    is_task: bool = Field(default=False, description="True when delegating a task. Task conversations do not count against social budget.")
    wait_for_result: bool = Field(
        default=False,
        description="When true with is_task=true, waits for the peer's task result. Requires a full task body in message, not a handshake ping.",
    )
    timeout_seconds: int = Field(
        default_factory=_default_contact_peer_timeout_seconds,
        description=(
            "Maximum wait when wait_for_result=true (seconds). Peer runs a full agent loop; 120s is often too short. "
            "Default comes from [mesh] contact_peer_default_timeout_seconds in mugi_config.toml."
        ),
    )
    delegation_job_id: str = Field(default="", description="Optional opaque job id included in payload for correlation/debugging.")
    handoff_envelope_json: str = Field(
        default="",
        description=(
            "Optional JSON handoff envelope (delegation/mesh seam only). "
            "Used for intent/observation hints, not as a tool output format."
        ),
    )


@tool(args_schema=ContactPeerInput)
def contact_peer(
    peer_name: str,
    message: str,
    is_task: bool = False,
    wait_for_result: bool = False,
    timeout_seconds: int = 300,
    delegation_job_id: str = "",
    handoff_envelope_json: str = "",
) -> str:
    """Send a message to a connected peer Mugi instance.

    **Order of work:** finish the local user's main-chat request first unless they explicitly
    asked you to contact this peer. Avoid long peer exchanges while the user is waiting.

    For task handoffs, optional handoff_envelope_json carries compact context hints.
    When ``is_task`` and ``wait_for_result`` are both true, ``message`` must contain the
    full task in one shot — peers cannot read your mind after a handshake ping.

    When wait_for_result is enabled, this tool returns text only in v1.
    """
    manager = get_mesh_manager()
    if manager is None:
        return "Mesh is not available yet."
    sess_match, err = _resolve_mesh_session_for_peer(manager, peer_name)
    if err:
        return err
    assert sess_match is not None
    session_id = str(sess_match.get("session_id") or "")
    peers = manager.list_sessions()
    if is_task and not manager.can_delegate_task_to_session(session_id):
        return (
            f"Task offload is not trusted for peer '{peer_name}'. "
            "Enable it in Settings → Mesh → Known Peers → Allow task offload for that peer."
        )
    if is_task and wait_for_result and _looks_like_mesh_task_handshake_only(message):
        return (
            "Refused: with is_task=true and wait_for_result=true you must send the **complete task** in this single "
            "`message` (objectives, constraints, expected output) — not a 'confirm first' or 'formal handoff' ping. "
            "Re-call contact_peer with the full specification, or set wait_for_result=false for a two-step social ping."
        )
    if contains_peer_sensitive_content(message):
        return (
            "Refused: this mesh message appears to include personal or sensitive local-user information "
            "(for example an email, local path, secret, long identifier, USER.md, or MEMORY.md reference). "
            "Send only broad, peer-safe themes or ask the user to share private details directly."
        )
    convo_type = "task" if is_task else "social"
    conversation_id = uuid.uuid4().hex
    payload: dict[str, Any] = {"text": sanitize_peer_visible_text(message, max_chars=8000)}
    payload = merged_payload_with_logical_id(payload)
    if delegation_job_id.strip():
        payload["job_id"] = delegation_job_id.strip()
    if is_task and handoff_envelope_json.strip():
        env = envelope_from_json_str(handoff_envelope_json)
        if env is not None:
            try:
                from core.config import get_mesh_config
                from utils.handoff_envelope import truncate_envelope_for_wire

                max_chars = int(get_mesh_config().get("handoff_envelope_max_json_chars", 2000) or 2000)
            except Exception:
                max_chars = 2000
            payload["envelope"] = truncate_envelope_for_wire(env, max_chars)
    max_message_bytes = int(manager._get_message_max_bytes())  # Intentional private read for deterministic truncation.
    preview = {
        "type": "task_delegate" if is_task else "prompt",
        "conversation_id": conversation_id,
        "conversation_type": convo_type,
        "message_number": 1,
        "payload": payload,
        "ttl": 300,
    }
    try:
        preview_size = len(json.dumps(preview, ensure_ascii=False).encode("utf-8"))
    except Exception:
        preview_size = 0
    if preview_size > max_message_bytes and "envelope" in payload:
        payload.pop("envelope", None)
    msg = MeshMessage(
        type="task_delegate" if is_task else "prompt",
        conversation_id=conversation_id,
        conversation_type=convo_type,
        message_number=1,
        payload=payload,
    )
    pending_key = ""
    wait_budget = _clamp_contact_peer_timeout_seconds(timeout_seconds)
    if is_task and wait_for_result:
        reg = manager.register_pending_result(
            conversation_id=conversation_id,
            session_id=session_id,
            peer_fingerprint=str(
                next((s.get("peer_fingerprint") for s in peers if str(s.get("session_id") or "") == session_id), "")
            ),
            timeout_seconds=wait_budget,
        )
        if not reg.get("ok"):
            err = str(reg.get("error") or "unable to register pending mesh task")
            return f"Could not start wait-capable task for {peer_name}: {err}"
        pending_key = str(reg.get("key") or "")
    _emit_input_status(f"Contacting {peer_name}…")
    out = manager.send_message(session_id=session_id, message=msg)
    if not out.get("ok") and pending_key:
        manager.unregister_pending_result(pending_key)
    if out.get("ok") and is_task and wait_for_result and pending_key:
        _emit_input_status(f"Waiting for {peer_name}…")
        waited = manager.wait_for_pending_result(
            key=pending_key,
            timeout_seconds=wait_budget,
            stop_check=should_stop_tool,
        )
        _emit_input_status("")
        if waited.get("ok"):
            text = str(waited.get("text") or "").strip()
            return text or f"{peer_name} completed the task with no text result."
        err = str(waited.get("error") or "task failed")
        if err == "timeout":
            return (
                f"Timed out waiting for {peer_name} (>{wait_budget}s). "
                "The peer may still be running the task (tools / LLM). Retry with a larger `timeout_seconds`, "
                "or raise `[mesh].contact_peer_default_timeout_seconds` in mugi_config.toml."
            )
        if err == "cancelled":
            return f"Stopped waiting for {peer_name}."
        return f"{peer_name} task failed: {err}"
    _emit_input_status("")
    if out.get("ok"):
        return f"Sent to {peer_name}. Status {out.get('status_code')}."
    data = out.get("data") or {}
    retry_after = data.get("retry_after_seconds")
    if retry_after:
        return f"{peer_name}'s Mugi is busy right now. Try again in about {retry_after} seconds."
    return f"Could not send to {peer_name}: {out.get('error') or data}"


@tool
def list_mesh_peers() -> str:
    """List known and active mesh peers."""
    manager = get_mesh_manager()
    if manager is None:
        return "Mesh is not available yet."
    sessions = manager.list_sessions()
    peers = manager.list_peers()
    lines = [f"Known peers: {len(peers)}", f"Active sessions: {len(sessions)}"]
    peer_by_fp = {
        str(p.get("fingerprint") or ""): p
        for p in peers
        if isinstance(p, dict)
    }
    session_fps = {str(s.get("peer_fingerprint") or "") for s in sessions}
    for sess in sessions:
        fp = str(sess.get("peer_fingerprint") or "")
        trust = bool((peer_by_fp.get(fp) or {}).get("trusted_for_tasks", False))
        nick = str(sess.get("peer_nickname") or "").strip()
        host = str(sess.get("peer_hostname") or "peer")
        hint = str(sess.get("mesh_intro_hint") or "").strip()
        label = f"{nick} ({host})" if nick else host
        extra = f", intro_hint={hint!r}" if hint else ""
        lines.append(
            f"- {label} @ {sess.get('peer_address')}:{sess.get('peer_port')} "
            f"(session {str(sess.get('session_id'))[:8]}, task_trust={'on' if trust else 'off'}{extra})"
        )
    if not sessions:
        lines.append("- No active peer sessions.")
    for p in peers:
        if not isinstance(p, dict):
            continue
        fp = str(p.get("fingerprint") or "")
        if not fp or fp in session_fps:
            continue
        nick = str(p.get("nickname") or "").strip()
        host = str(p.get("hostname") or "peer")
        hint = str(p.get("mesh_intro_hint") or "").strip()
        label = f"{nick} ({host})" if nick else host
        extra = f", intro_hint={hint!r}" if hint else ""
        short_fp = fp if len(fp) <= 20 else fp[:18] + "…"
        lines.append(f"- {label} (not connected){extra} · {short_fp}")
    return "\n".join(lines)


@tool
def mesh_conversation_status() -> str:
    """Show active mesh conversations and budgets."""
    manager = get_mesh_manager()
    if manager is None:
        return "Mesh is not available yet."
    sessions = manager.list_sessions()
    lines: list[str] = []
    for sess in sessions:
        nick = str(sess.get("peer_nickname") or "").strip()
        host = str(sess.get("peer_hostname") or "peer")
        label = f"{nick} ({host})" if nick else host
        conversations = sess.get("conversations") or []
        if not conversations:
            lines.append(f"- {label}: no active conversations")
            continue
        for c in conversations:
            ctype = c.get("conversation_type")
            cnt = int(c.get("message_count") or 0)
            maxn = int(c.get("max_messages") or 0)
            lines.append(f"- {label} [{ctype}] {cnt}/{maxn} messages, state={c.get('state')}")
    if not lines:
        return "No mesh conversations are active."
    return "\n".join(lines)


class MeshSendFilesInput(BaseModel):
    peer_name: str = Field(
        description=(
            "Connected peer to receive the files: nickname, intro hint, or hostname "
            "(same matching rules as list_mesh_peers / contact_peer)."
        )
    )
    paths: list[str] = Field(
        description=(
            "One or more filesystem paths and/or chat **attachment_id** strings (e.g. att_1). "
            "Prefer attachment_id when the user attached files in this conversation."
        )
    )


@tool(args_schema=MeshSendFilesInput)
def mesh_send_files(peer_name: str, paths: list[str]) -> str:
    """Send files to a connected mesh peer over the encrypted LAN file-transfer channel.

    Use this when the user asks to send attached files (or specific paths) to another Mac that
    already has an **active** mesh session. Do **not** try to ship file bytes through
    **contact_peer** — that tool is for prompts/tasks, not bulk file payloads.

    Requires ``[mesh] file_transfer_enabled`` and an established session (user must have connected
    in Mesh). If there is no session, tell the user to connect first or use **/mesh-send** from the UI.
    """
    manager = get_mesh_manager()
    if manager is None:
        return "Mesh is not available yet."
    sess_match, err = _resolve_mesh_session_for_peer(manager, peer_name)
    if err:
        return err
    assert sess_match is not None
    session_id = str(sess_match.get("session_id") or "")
    recv_fp = str(sess_match.get("peer_fingerprint") or "").strip()
    if not recv_fp:
        return "Active session is missing peer fingerprint; reconnect in Mesh and retry."

    if not paths:
        return "No files to send (paths is empty)."

    _emit_input_status(f"Sending files to {peer_name}…")
    try:
        out = manager.file_transfers.send_outbound(
            session_id=session_id,
            receiver_fingerprint=recv_fp,
            paths=list(paths),
            client_source="agent",
        )
    finally:
        _emit_input_status("")

    if not out.get("ok"):
        return str(out.get("error") or out.get("data") or "Mesh file send failed.")

    tid = str(out.get("transfer_id") or "")
    files = out.get("files")
    n = len(files) if isinstance(files, list) else 0
    host = str(out.get("host") or "")
    port = out.get("port")
    return (
        f"Mesh file offer sent to {peer_name} (transfer_id={tid}, {n} file(s)). "
        f"The peer should see a receive prompt. Sender serve endpoint: {host}:{port}."
    )
