import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import asyncio
from telegram import Update, BotCommand, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
    Application, CommandHandler, MessageHandler, CallbackQueryHandler,
    filters, ContextTypes, ConversationHandler
)
from agents.personal_agent import PersonalAgent
from agents.matching_agent import score_applicant as ai_score_applicant
from voice.elevenlabs_tts import generate_voice_summary, build_voice_text
from tools.upi_generator import UPIGeneratorTool
from features.base_feature import get_feature
import database as db
from config import TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B, VOICE_ID_AGENT_A, VOICE_ID_AGENT_B
# ─── Socket.IO emitters (optional — only active when api.py is co-running) ───
try:
    from api import emit_round_update, emit_negotiation_started, emit_negotiation_resolved
    _sio_available = True
except ImportError:
    _sio_available = False
    async def emit_round_update(*args, **kwargs): pass
    async def emit_negotiation_started(*args, **kwargs): pass
    async def emit_negotiation_resolved(*args, **kwargs): pass

_upi_tool = UPIGeneratorTool()

personal_agent = PersonalAgent()

# ─── Conversation states ───
AWAITING_PREFERENCES = 1           # User A describing their side
AWAITING_COUNTERPARTY_PREFS = 2    # User B describing their side
AWAITING_CONTRACT_DESCRIPTION = 3  # Poster describing an open contract
AWAITING_CONTRACT_APPLICATION = 4  # Seeker submitting their application

# ─── Shared in-memory store for pending coordination requests ───
# Key: counterparty username (lowercase), Value: coordination metadata
pending_coordinations = {}

# ─── Registry of running bot applications (for cross-bot messaging) ───
# Key: bot token, Value: Application instance
bot_apps: dict = {}

def register_bot(token: str, app: "Application") -> None:
    """Register a bot application so other bots can use it to send messages."""
    bot_apps[token] = app

async def send_to_user(user_id: int, text: str, parse_mode: str = "Markdown",
                       reply_markup=None) -> bool:
    """
    Send a message to any user via whichever registered bot can reach them.
    Returns True if sent, False if no bot could reach the user.
    """
    for app in bot_apps.values():
        try:
            await app.bot.send_message(
                chat_id=user_id, text=text, parse_mode=parse_mode,
                reply_markup=reply_markup
            )
            return True
        except Exception:
            continue
    return False

async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user = update.effective_user
    await db.create_user(user.id, user.username, user.first_name)
    await update.message.reply_text(
        f"🤖 *Welcome to negoT8, {user.first_name}!*\n\n"
        "I'm your personal AI agent. When you need to coordinate with someone, "
        "I'll talk to their agent and we'll figure it out together.\n\n"
        "🎭 Set your agent's style: /personality\n"
        "📅 Connect your calendar: /connectcalendar _(optional, for auto-availability)_\n"
        "🤝 Start coordinating: `/coordinate @friend`\n\n"
        "Then just tell me what you need in plain English.",
        parse_mode="Markdown"
    )


async def connect_calendar_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Send the Google OAuth URL so the user can connect their calendar."""
    from tools.google_calendar import GoogleCalendarTool
    user = update.effective_user
    cal = GoogleCalendarTool()

    # Check if already connected
    if await cal.is_connected(user.id):
        await update.message.reply_text(
            "📅 *Google Calendar already connected!*\n\n"
            "Your agent automatically checks your calendar for free slots "
            "whenever you schedule a meeting without specifying times.\n\n"
            "To disconnect, revoke access at myaccount.google.com → Security → Third-party apps.",
            parse_mode="Markdown",
        )
        return

    try:
        url = await cal.get_oauth_url(user.id)
    except Exception as e:
        await update.message.reply_text(
            f"⚠️ Could not generate calendar link: {e}\n"
            "Make sure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set in .env."
        )
        return

    keyboard = InlineKeyboardMarkup([
        [InlineKeyboardButton("🔗 Connect Google Calendar", url=url)]
    ])
    await update.message.reply_text(
        "📅 *Connect Your Google Calendar*\n\n"
        "Tap below to grant *read-only* access to your calendar.\n"
        "Your agent will use it to automatically find your free slots "
        "when scheduling meetings — no need to type out your availability.\n\n"
        "🔒 _Read-only. Revoke anytime from your Google account._",
        reply_markup=keyboard,
        parse_mode="Markdown",
    )

async def personality_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Let user choose their agent's negotiation personality."""
    keyboard = InlineKeyboardMarkup([
        [InlineKeyboardButton("😤 Aggressive Haggler", callback_data="personality_aggressive")],
        [InlineKeyboardButton("🤝 People Pleaser", callback_data="personality_people_pleaser")],
        [InlineKeyboardButton("📊 Data-Driven Analyst", callback_data="personality_analytical")],
        [InlineKeyboardButton("💚 Empathetic Mediator", callback_data="personality_empathetic")],
        [InlineKeyboardButton("⚖️ Balanced (Default)", callback_data="personality_balanced")],
    ])

    await update.message.reply_text(
        "🎭 *Choose your agent's negotiation personality:*\n\n"
        "This changes HOW your agent negotiates — not what it negotiates for.\n\n"
        "😤 *Aggressive* — Pushes hard, concedes slowly\n"
        "🤝 *People Pleaser* — Concedes fast, preserves relationship\n"
        "📊 *Analytical* — Cites data, logical arguments\n"
        "💚 *Empathetic* — Creative win-wins, understands both sides\n"
        "⚖️ *Balanced* — Middle-ground approach",
        reply_markup=keyboard,
        parse_mode="Markdown"
    )

async def personality_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle personality selection."""
    query = update.callback_query
    await query.answer()
    personality = query.data.replace("personality_", "")
    user_id = query.from_user.id
    await db.update_user_personality(user_id, personality)

    labels = {
        "aggressive": "😤 Aggressive Haggler",
        "people_pleaser": "🤝 People Pleaser",
        "analytical": "📊 Data-Driven Analyst",
        "empathetic": "💚 Empathetic Mediator",
        "balanced": "⚖️ Balanced"
    }
    await query.edit_message_text(
        f"✅ Agent personality set to: {labels[personality]}\n\n"
        "Your agent will use this style in all future negotiations.\n"
        "Change anytime with /personality"
    )

async def coordinate_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not context.args:
        await update.message.reply_text(
            "🤝 *Who do you want to coordinate with?*\n\n"
            "Usage: `/coordinate @username`\n\n"
            "Then describe what you need. Examples:\n"
            "• Find time for coffee next week\n"
            "• Split our trip expenses (include your UPI ID!)\n"
            "• Negotiate project scope and budget\n"
            "• Plan a weekend trip\n"
            "• Decide where to eat tonight\n"
            "• ...literally anything that needs agreement!",
            parse_mode="Markdown"
        )
        return ConversationHandler.END

    counterparty = context.args[0].replace("@", "")
    context.user_data["counterparty"] = counterparty

    await update.message.reply_text(
        f"🤖 Got it! I'll coordinate with *@{counterparty}*'s agent.\n\n"
        "Now tell me what you need — describe it naturally.\n"
        "Include any specific numbers, dates, or constraints.\n"
        "💡 For expenses: mention your UPI ID for auto-payment links!",
        parse_mode="Markdown"
    )
    return AWAITING_PREFERENCES

async def receive_preferences(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_message = update.message.text
    user = update.effective_user
    counterparty = context.user_data.get("counterparty", "unknown")

    await update.message.chat.send_action("typing")

    preferences = await personal_agent.extract_preferences(user_message, user.id)

    if "error" in preferences:
        await update.message.reply_text(
            "⚠️ I had trouble understanding that. Could you rephrase?\n"
            "Try to include specific details like dates, amounts, or preferences."
        )
        return AWAITING_PREFERENCES

    feature_type = preferences.get("feature_type", "generic")

    # Get user's personality
    user_data = await db.get_user(user.id)
    personality = dict(user_data).get("personality", "balanced") if user_data else "balanced"

    negotiation_id = await db.create_negotiation(feature_type, user.id)
    await db.add_participant(negotiation_id, user.id, preferences, personality_used=personality)

    pending_coordinations[negotiation_id] = {
        "negotiation_id": negotiation_id,
        "counterparty_username": counterparty.lower(),
        "initiator_id": user.id,
        "initiator_name": user.first_name,
        "feature_type": feature_type,
        "preferences_a": preferences,
        "personality_a": personality,
        "status": "pending"
    }

    feature_labels = {
        "scheduling": "📅 Meeting Scheduling", "expenses": "💰 Expense Splitting",
        "freelance": "💼 Project Negotiation", "roommate": "🏠 Roommate Decision",
        "trip": "✈️ Trip Planning", "marketplace": "🛒 Buy/Sell Deal",
        "collaborative": "🍕 Joint Decision", "conflict": "⚖️ Conflict Resolution",
        "generic": "🤝 Coordination"
    }
    personality_emoji = {"aggressive": "😤", "people_pleaser": "🤝", "analytical": "📊", "empathetic": "💚", "balanced": "⚖️"}

    await update.message.reply_text(
        f"✅ *Preferences captured!*\n\n"
        f"📋 Type: {feature_labels.get(feature_type, '🤝 Coordination')}\n"
        f"🤝 With: @{counterparty}\n"
        f"🎭 Agent style: {personality_emoji.get(personality, '⚖️')} {personality.replace('_', ' ').title()}\n"
        f"🆔 Negotiation: `{negotiation_id}`\n\n"
        f"I've notified @{counterparty}'s agent.\n"
        f"Once they share their side, our agents will negotiate automatically.\n\n"
        f"_I'll send you real-time updates + a voice summary at the end!_ 🤖↔️🤖🔊",
        parse_mode="Markdown"
    )
    return ConversationHandler.END

# ─────────────────────────────────────────────────────────────
# /pending — Counterparty (User B) checks for incoming requests
# /accept_<neg_id> callback — User B accepts and provides prefs
# ─────────────────────────────────────────────────────────────

async def pending_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    Show User B all pending coordination requests directed at their username.
    Works by looking up `pending_coordinations` by the user's Telegram username.
    """
    user = update.effective_user
    username = (user.username or "").lower()

    if not username:
        await update.message.reply_text(
            "⚠️ You need a Telegram username for negoT8 to work.\n"
            "Set one in Telegram Settings → Edit Profile → Username."
        )
        return

    # Collect all pending requests aimed at this user
    matching = {
        neg_id: data
        for neg_id, data in pending_coordinations.items()
        if data.get("counterparty_username", "").lower() == username
        and data.get("status", "pending") == "pending"
    }

    if not matching:
        await update.message.reply_text(
            "📭 *No pending coordination requests.*\n\n"
            "When someone does `/coordinate @you`, it'll appear here.\n"
            "Use /personality to set your style before accepting!",
            parse_mode="Markdown"
        )
        return

    feature_labels = {
        "scheduling": "📅 Meeting Scheduling", "expenses": "💰 Expense Splitting",
        "freelance": "💼 Project Negotiation", "roommate": "🏠 Roommate Decision",
        "trip": "✈️ Trip Planning",  "marketplace": "🛒 Buy/Sell Deal",
        "collaborative": "🍕 Joint Decision",  "conflict": "⚖️ Conflict Resolution",
        "generic": "🤝 Coordination"
    }

    for neg_id, data in matching.items():
        initiator = data.get("initiator_name", "Someone")
        feature   = feature_labels.get(data.get("feature_type", "generic"), "🤝 Coordination")
        # Quick summary of what initiator wants
        prefs   = data.get("preferences_a", {})
        goal    = prefs.get("goal", "coordinate with you")

        keyboard = InlineKeyboardMarkup([[
            InlineKeyboardButton("✅ Accept & Share My Side", callback_data=f"accept_{neg_id}"),
            InlineKeyboardButton("❌ Decline", callback_data=f"decline_{neg_id}"),
        ]])

        await update.message.reply_text(
            f"📬 *Incoming request from {initiator}*\n\n"
            f"📋 Type: {feature}\n"
            f"🎯 They want to: _{goal}_\n"
            f"🆔 ID: `{neg_id}`\n\n"
            "Tap *Accept* to share your preferences and let agents negotiate!",
            reply_markup=keyboard,
            parse_mode="Markdown"
        )


async def accept_pending_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    Inline-button handler: User B taps Accept → prompt for their preferences.
    Stores neg_id in user_data so the next text message is routed correctly.
    """
    query = update.callback_query
    await query.answer()
    action, neg_id = query.data.split("_", 1)

    if action == "decline":
        # Get initiator_id BEFORE popping
        data = pending_coordinations.get(neg_id, {})
        initiator_id = data.get("initiator_id")
        pending_coordinations.pop(neg_id, None)
        await query.edit_message_text(
            "❌ Request declined. The other party will be notified."
        )
        if initiator_id:
            await send_to_user(
                initiator_id,
                "❌ @{} declined the coordination request.".format(
                    (query.from_user.username or query.from_user.first_name)
                )
            )
        return

    # Accept path
    data = pending_coordinations.get(neg_id)
    if not data:
        await query.edit_message_text("⚠️ This request has expired or already been handled.")
        return

    # Mark as accepted so it doesn't show again in /pending
    pending_coordinations[neg_id]["status"] = "accepted"
    context.user_data["pending_neg_id"] = neg_id

    initiator     = data.get("initiator_name", "the other person")
    feature_type  = data.get("feature_type", "generic")
    feature_labels = {
        "scheduling": "meeting scheduling", "expenses": "expense splitting",
        "freelance": "project negotiation", "roommate": "roommate decision",
        "trip": "trip planning", "marketplace": "buy/sell deal",
        "collaborative": "joint decision", "conflict": "conflict resolution",
        "generic": "coordination"
    }
    feature_label = feature_labels.get(feature_type, "coordination")

    await query.edit_message_text(
        f"✅ *Accepted!*\n\n"
        f"Now tell me YOUR side for the {feature_label} with *{initiator}*.\n\n"
        "Describe your preferences, constraints, and goals in plain English.\n"
        "💡 Include specific numbers, dates, or your UPI ID if it's an expense split.",
        parse_mode="Markdown"
    )
    # Signal the ConversationHandler to move into the counterparty-prefs collection state
    context.user_data["awaiting_counterparty_prefs"] = True
    return AWAITING_COUNTERPARTY_PREFS


async def receive_counterparty_preferences(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    User B's free-text preferences after accepting.
    1. Extract structured preferences via PersonalAgent
    2. Persist both participants in DB
    3. Run negotiation engine with live Telegram callbacks
    """
    if not context.user_data.get("awaiting_counterparty_prefs"):
        return  # Not in counterparty flow

    user = update.effective_user
    user_message = update.message.text
    neg_id = context.user_data.get("pending_neg_id")

    if not neg_id or neg_id not in pending_coordinations:
        await update.message.reply_text(
            "⚠️ Couldn't find the negotiation. It may have expired. Ask the other person to resend."
        )
        context.user_data.pop("awaiting_counterparty_prefs", None)
        return

    await update.message.chat.send_action("typing")

    data = pending_coordinations[neg_id]
    feature_type = data.get("feature_type", "generic")

    # ── Extract User B's preferences ──
    preferences_b = await personal_agent.extract_preferences(user_message, user.id)

    if "error" in preferences_b:
        await update.message.reply_text(
            "⚠️ I had trouble understanding that. Please rephrase and try again.\n"
            "Tip: include specific details like dates, amounts, or preferences."
        )
        return  # Stay in counterparty pref state

    # ── Ensure User B is registered ──
    await db.create_user(user.id, user.username, user.first_name)

    # ── Get User B's personality ──
    user_b_data  = await db.get_user(user.id)
    personality_b = dict(user_b_data).get("personality", "balanced") if user_b_data else "balanced"

    # ── Persist User B as participant ──
    await db.add_participant(
        neg_id, user.id, preferences_b, personality_used=personality_b
    )

    # Mark negotiation as active
    pending_coordinations[neg_id]["status"] = "running"
    pending_coordinations[neg_id]["user_b_id"] = user.id

    personality_emoji = {
        "aggressive": "😤", "people_pleaser": "🤝",
        "analytical": "📊", "empathetic": "💚", "balanced": "⚖️"
    }

    await update.message.reply_text(
        f"🤖 *Got it! Starting negotiation…*\n\n"
        f"🎭 Your agent style: {personality_emoji.get(personality_b, '⚖️')} "
        f"{personality_b.replace('_', ' ').title()}\n\n"
        f"🤖↔️🤖 *Agents are negotiating — stand by for updates!*",
        parse_mode="Markdown"
    )

    # ── Clear conversation state ──
    context.user_data.pop("awaiting_counterparty_prefs", None)
    context.user_data.pop("pending_neg_id", None)

    # ── Emit negotiation_started to dashboard ──
    try:
        participants_info = [
            {"user_id": initiator_id, "personality": personality_a},
            {"user_id": user.id, "personality": personality_b},
        ]
        await emit_negotiation_started(neg_id, feature_type, participants_info)
    except Exception as e:
        print(f"[Socket.IO] emit_negotiation_started failed (non-critical): {e}")

    # ── Run negotiation with live Telegram callbacks ──
    initiator_id  = data["initiator_id"]
    # Shallow-copy preferences so we don't mutate the stored dict in pending_coordinations
    preferences_a = dict(data["preferences_a"])
    preferences_b = dict(preferences_b)
    # Inject real names so the NegotiatorAgent refers to users by name in reasoning
    preferences_a["human_name"] = data.get("initiator_name") or "User A"
    preferences_b["human_name"] = (user.first_name or user.username or "User B")
    personality_a = data.get("personality_a", "balanced")

    asyncio.create_task(
        run_negotiation_with_telegram_updates(
            negotiation_id=neg_id,
            preferences_a=preferences_a,
            preferences_b=preferences_b,
            user_a_id=initiator_id,
            user_b_id=user.id,
            feature_type=feature_type,
            personality_a=personality_a,
            personality_b=personality_b,
        )
    )
    return ConversationHandler.END


async def run_negotiation_with_telegram_updates(
    negotiation_id: str,
    preferences_a: dict,
    preferences_b: dict,
    user_a_id: int,
    user_b_id: int,
    feature_type: str,
    personality_a: str = "balanced",
    personality_b: str = "balanced",
):
    """
    Runs the negotiation engine and pushes real-time round updates + resolution
    to both users via Telegram messages.
    """
    from agents.negotiation import run_negotiation

    # ── Pre-fetch feature-specific context (Tavily, Calculator, etc.) ──
    feature = get_feature(feature_type)
    try:
        await send_to_user(user_a_id, "🔍 _Gathering real-world data for your negotiation…_")
        feature_context = await feature.get_context(
            preferences_a, preferences_b,
            user_a_id=user_a_id, user_b_id=user_b_id,
        )
    except Exception as e:
        print(f"Feature context fetch failed (non-fatal): {e}")
        feature_context = ""

    personality_emoji = {
        "aggressive": "😤", "people_pleaser": "🤝",
        "analytical": "📊", "empathetic": "💚", "balanced": "⚖️"
    }
    action_labels = {
        "propose": "📤 Proposed",
        "counter":  "🔄 Counter-proposed",
        "accept":   "✅ Accepted",
        "escalate": "⚠️ Escalated to humans",
    }

    async def on_round(data: dict):
        round_num   = data["round_number"]
        action      = data.get("action", "counter")
        reasoning   = data.get("reasoning", "")
        sat_a       = data.get("satisfaction_a", 0)
        sat_b       = data.get("satisfaction_b", 0)
        label       = action_labels.get(action, action)

        name_a = preferences_a.get("human_name") or "Party A"
        name_b = preferences_b.get("human_name") or "Party B"

        # Telegram hard limit is 4096 chars — truncate reasoning there with a note
        max_reasoning = 3800
        if len(reasoning) > max_reasoning:
            reasoning = reasoning[:max_reasoning] + "… _(truncated)_"

        msg = (
            f"🤖 *Round {round_num}* — {label}\n\n"
            f"_{reasoning}_\n\n"
            f"📊 *{name_a}:* {sat_a:.0f}%  |  *{name_b}:* {sat_b:.0f}%"
        )
        await send_to_user(user_a_id, msg)
        await send_to_user(user_b_id, msg)

        # ── Emit to dashboard via Socket.IO ──
        try:
            await emit_round_update(negotiation_id, data)
        except Exception as e:
            print(f"[Socket.IO] emit_round_update failed (non-critical): {e}")

    async def on_resolution(data: dict):
        import json
        status        = data.get("status", "resolved")
        rounds_taken  = data.get("rounds_taken", "?")
        summary       = data.get("summary", "Agreement reached")
        final         = data.get("final_proposal", {})

        sat_timeline = data.get("satisfaction_timeline", [])
        final_sat_a  = sat_timeline[-1]["score_a"] if sat_timeline else 0
        final_sat_b  = sat_timeline[-1]["score_b"] if sat_timeline else 0

        # ── Use feature-specific formatted resolution message ──
        try:
            details_text = feature.format_resolution(data, preferences_a, preferences_b)
        except Exception as e:
            print(f"Feature format_resolution failed (non-fatal): {e}")
            if status == "resolved":
                details_text = f"✅ *Agreement Reached!*\n\n_{summary}_"
            else:
                details_text = f"⚠️ *Escalated — Human decision needed*\n\n_{summary}_"

        # ── UPI button (for payment-related features) ──
        upi_markup = None
        if feature_type in ("expenses", "freelance", "marketplace", "roommate"):
            details    = final.get("details", {})
            settlement = details.get("settlement", {})

            # Resolve UPI ID: cast a wide net over every key the LLM might use
            raw_details_a = preferences_a.get("raw_details", {})
            raw_details_b = preferences_b.get("raw_details", {})
            upi_id = (
                raw_details_a.get("upi_id") or raw_details_a.get("upi") or raw_details_a.get("upi_address")
                or raw_details_b.get("upi_id") or raw_details_b.get("upi") or raw_details_b.get("upi_address")
                or details.get("upi_id") or details.get("upi") or details.get("upiid")
                or details.get("payee_upi") or details.get("receiver_upi") or details.get("pay_to")
                or settlement.get("upi_id") or settlement.get("upi") or settlement.get("payee_upi")
                or final.get("upi_id") or final.get("upi")
            )

            # Resolve amount: settlement > details > final proposal top-level
            raw_amount = (
                settlement.get("amount") or settlement.get("total")
                or details.get("amount") or details.get("price") or details.get("total_amount")
                or details.get("split_amount") or details.get("owed_amount")
                or final.get("amount") or final.get("price")
            )

            if upi_id and raw_amount:
                try:
                    from telegram import InlineKeyboardButton, InlineKeyboardMarkup
                    pay_amount = float(raw_amount)
                    payee_name = (
                        settlement.get("payee_name")
                        or details.get("seller") or details.get("payee_name")
                        or "User"
                    )
                    upi_result = await _upi_tool.execute(
                        payee_upi=upi_id,
                        payee_name=payee_name,
                        amount=pay_amount,
                        note=f"negoT8: {feature_type} settlement"
                    )
                    upi_link = upi_result.get("upi_link", "")
                    if upi_link:
                        details_text += f"\n\n💳 *Tap to pay:* ₹{pay_amount:,.0f}\n🔗 UPI: `{upi_id}`"
                        upi_markup = InlineKeyboardMarkup([[
                            InlineKeyboardButton(
                                f"💳 Pay ₹{pay_amount:,.0f}",
                                url=upi_link
                            )
                        ]])
                except Exception as e:
                    print(f"UPI link generation failed: {e}")

        name_a = preferences_a.get("human_name") or "Party A"
        name_b = preferences_b.get("human_name") or "Party B"
        msg = (
            f"{details_text}\n\n"
            f"📊 *{name_a}:* {final_sat_a:.0f}%  |  *{name_b}:* {final_sat_b:.0f}%"
        )
        await send_to_user(user_a_id, msg, reply_markup=upi_markup)
        await send_to_user(user_b_id, msg, reply_markup=upi_markup)

        # ── Voice summary (different voices for A and B) ──
        try:
            voice_context = {
                "rounds": rounds_taken, "summary": summary,
                **final.get("details", {}),
                **{k: v for k, v in final.items() if k not in ("details", "summary")}
            }
            voice_text = build_voice_text(feature_type, voice_context)
            voice_path_a = await generate_voice_summary(
                voice_text, negotiation_id, VOICE_ID_AGENT_A
            )
            if voice_path_a:
                for app in bot_apps.values():
                    try:
                        with open(voice_path_a, "rb") as f:
                            await app.bot.send_voice(
                                chat_id=user_a_id, voice=f,
                                caption="🎙 Voice summary from your agent"
                            )
                        break
                    except Exception:
                        continue
            voice_path_b = await generate_voice_summary(
                voice_text, f"{negotiation_id}_b", VOICE_ID_AGENT_B
            )
            if voice_path_b:
                for app in bot_apps.values():
                    try:
                        with open(voice_path_b, "rb") as f:
                            await app.bot.send_voice(
                                chat_id=user_b_id, voice=f,
                                caption="🎙 Voice summary from your agent"
                            )
                        break
                    except Exception:
                        continue
        except Exception as e:
            print(f"Voice summary failed (non-fatal): {e}")

        # ── Store analytics ──
        try:
            rounds_db = await db.get_rounds(negotiation_id)
            concession_log = []
            for r in rounds_db:
                concessions = json.loads(r["concessions_made"]) if r["concessions_made"] else []
                for c in concessions:
                    party = "A" if r["proposer_id"] == user_a_id else "B"
                    concession_log.append({"round": r["round_number"], "by": party, "gave_up": c})
            fairness = 100 - abs(final_sat_a - final_sat_b)
            await db.store_analytics({
                "negotiation_id": negotiation_id,
                "satisfaction_timeline": json.dumps(sat_timeline),
                "concession_log": json.dumps(concession_log),
                "fairness_score": fairness,
                "total_concessions_a": sum(1 for c in concession_log if c["by"] == "A"),
                "total_concessions_b": sum(1 for c in concession_log if c["by"] == "B"),
            })
        except Exception as e:
            print(f"Analytics storage failed (non-fatal): {e}")

        # ── Register on Polygon Amoy + send blockchain badge ──
        if status == "resolved":
            proof = None
            try:
                from blockchain_web3.blockchain import register_agreement_on_chain
                print(f"[Blockchain] Registering {negotiation_id} on-chain...")
                proof = await register_agreement_on_chain(
                    negotiation_id  = negotiation_id,
                    feature_type    = feature_type,
                    summary         = final.get("summary", summary),
                    resolution_data = data,
                )
                print(f"[Blockchain] Result: success={proof.get('success')} mock={proof.get('mock')} tx={proof.get('tx_hash','')[:20]}")
            except Exception as e:
                print(f"[Blockchain] register_agreement_on_chain FAILED: {e}")
                import traceback; traceback.print_exc()

            # Always store to DB (even mock proofs) so /proof always works
            if proof:
                try:
                    await db.store_blockchain_proof(
                        negotiation_id = negotiation_id,
                        tx_hash        = proof["tx_hash"],
                        block_number   = proof.get("block_number", 0),
                        agreement_hash = proof["agreement_hash"],
                        explorer_url   = proof.get("explorer_url", ""),
                        gas_used       = proof.get("gas_used", 0),
                    )
                    print(f"[Blockchain] DB proof stored for {negotiation_id}")
                except Exception as e:
                    print(f"[Blockchain] store_blockchain_proof FAILED: {e}")
                    import traceback; traceback.print_exc()

            # Build the badge message
            if proof and proof.get("success") and not proof.get("mock"):
                explorer_url = proof["explorer_url"]
                badge = (
                    f"🔗 *Agreement Verified on Blockchain*\n\n"
                    f"Permanently recorded on *Polygon Amoy*\.\n\n"
                    f"🏷 Negotiation: `{negotiation_id}`\n"
                    f"⛓ Block: `{proof['block_number']}`\n"
                    f"🔐 Hash: `{str(proof['agreement_hash'])[:22]}\.\.\.`\n\n"
                    f"[🔍 View Proof on PolygonScan]({explorer_url})\n\n"
                    f"_Use /proof {negotiation_id} anytime to retrieve this\._"
                )
            elif proof and proof.get("mock"):
                badge = (
                    f"🔗 *Agreement Recorded*\n\n"
                    f"🏷 Negotiation: `{negotiation_id}`\n"
                    f"🔐 Hash: `{str(proof.get('agreement_hash',''))[:22]}\.\.\.`\n\n"
                    f"_Blockchain proof stored\. Use /proof {negotiation_id} to view\._"
                )
            else:
                badge = (
                    f"🔗 *Agreement Saved*\n\n"
                    f"🏷 Negotiation ID: `{negotiation_id}`\n\n"
                    f"_Use /proof {negotiation_id} to check blockchain status\._"
                )

            await send_to_user(user_a_id, badge)
            await send_to_user(user_b_id, badge)

            # ── Generate & send Deal Agreement PDF (freelance / marketplace) ──
            if feature_type in ("freelance", "marketplace"):
                try:
                    from tools.pdf_generator import generate_deal_pdf
                    user_a_row = await db.get_user(user_a_id)
                    user_b_row = await db.get_user(user_b_id)
                    user_a_info = {
                        "id":       user_a_id,
                        "name":     user_a_row["display_name"] if user_a_row else "",
                        "username": user_a_row["username"] if user_a_row else "",
                    }
                    user_b_info = {
                        "id":       user_b_id,
                        "name":     user_b_row["display_name"] if user_b_row else "",
                        "username": user_b_row["username"] if user_b_row else "",
                    }
                    pdf_path = await generate_deal_pdf(
                        negotiation_id  = negotiation_id,
                        feature_type    = feature_type,
                        final_proposal  = final,
                        user_a          = user_a_info,
                        user_b          = user_b_info,
                        rounds_taken    = rounds_taken,
                        sat_a           = final_sat_a,
                        sat_b           = final_sat_b,
                        preferences_a   = preferences_a,
                        preferences_b   = preferences_b,
                        blockchain_proof= proof,
                    )
                    caption = (
                        f"📄 *Deal Agreement — {negotiation_id[:8].upper()}*\n"
                        f"Your official deal document. Keep this for your records."
                    )
                    for uid in (user_a_id, user_b_id):
                        for app in bot_apps.values():
                            try:
                                with open(pdf_path, "rb") as f:
                                    await app.bot.send_document(
                                        chat_id     = uid,
                                        document    = f,
                                        filename    = f"negoT8_Deal_{negotiation_id[:8].upper()}.pdf",
                                        caption     = caption,
                                        parse_mode  = "Markdown",
                                    )
                                break
                            except Exception:
                                continue
                    # Clean up temp file
                    try:
                        os.remove(pdf_path)
                    except OSError:
                        pass
                except Exception as e:
                    print(f"[PDF] Deal agreement generation failed (non-fatal): {e}")
                    import traceback; traceback.print_exc()

        # ── Emit final resolution to dashboard via Socket.IO ──
        try:
            await emit_negotiation_resolved(negotiation_id, data)
        except Exception as e:
            print(f"[Socket.IO] emit_negotiation_resolved failed (non-critical): {e}")

    try:
        if feature_type == "trip":
            # Trip planning: use group negotiation (supports 2+ people)
            from features.trip import run_group_negotiation
            await run_group_negotiation(
                negotiation_id=negotiation_id,
                all_preferences=[preferences_a, preferences_b],
                all_user_ids=[user_a_id, user_b_id],
                feature_type=feature_type,
                personalities=[personality_a, personality_b],
                on_round_update=on_round,
                on_resolution=on_resolution,
            )
        else:
            await run_negotiation(
                negotiation_id=negotiation_id,
                preferences_a=preferences_a,
                preferences_b=preferences_b,
                user_a_id=user_a_id,
                user_b_id=user_b_id,
                feature_type=feature_type,
                personality_a=personality_a,
                personality_b=personality_b,
                on_round_update=on_round,
                on_resolution=on_resolution,
                feature_context=feature_context,
            )
        # Clean up pending_coordinations entry
        pending_coordinations.pop(negotiation_id, None)
    except Exception as e:
        err_msg = f"❌ Negotiation error: {e}\nPlease try again."
        await send_to_user(user_a_id, err_msg)
        await send_to_user(user_b_id, err_msg)



# ─────────────────────────────────────────────────────────────────────────────
# OPEN CONTRACTS — Public Opportunity Marketplace
# /opencontract   — Poster creates a public opportunity
# /open_opportunities — Seeker browses & applies
# /mycontracts    — Poster reviews applicants + triggers negotiation
# ─────────────────────────────────────────────────────────────────────────────

_CONTRACT_TYPE_LABELS = {
    "freelance":   "💼 Freelance Project",
    "scheduling":  "📅 Job Interview / Meeting",
    "marketplace": "🛒 Buy / Sell",
    "generic":     "🤝 General Opportunity",
    "expenses":    "💰 Shared Expense",
    "collaborative": "🍕 Collaborative Decision",
    "conflict":    "⚖️ Conflict Resolution",
    "roommate":    "🏠 Roommate / Living",
    "trip":        "✈️ Trip",
}


async def opencontract_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Poster kicks off a new open contract listing."""
    await update.message.reply_text(
        "📋 *Post an Open Contract*\n\n"
        "Describe your opportunity in plain English. Include all relevant details:\n\n"
        "💼 *Freelance project:* tech stack, deliverables, budget, timeline\n"
        "📅 *Job / Interview:* role, company, skills needed, interview format\n"
        "🛒 *Buy / Sell:* item description, condition, asking price, location\n"
        "🤝 *Other:* anything two people need to agree on publicly\n\n"
        "The more detail you give, the better your agent can match you!",
        parse_mode="Markdown",
    )
    return AWAITING_CONTRACT_DESCRIPTION


async def receive_contract_description(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Extract structured requirements from the poster's description and store the contract."""
    user = update.effective_user
    user_message = update.message.text

    await update.message.chat.send_action("typing")

    # Always use the real PersonalAgent — mock mode is for the negotiation engine, not user input
    requirements = await personal_agent.extract_preferences(user_message, user.id)

    if "error" in requirements:
        await update.message.reply_text(
            "⚠️ I had trouble understanding that. Please add more detail and try again."
        )
        return AWAITING_CONTRACT_DESCRIPTION

    feature_type  = requirements.get("feature_type", "generic")
    goal          = requirements.get("goal", "Open opportunity")

    # Build a short title from the goal
    title = goal[:80] if len(goal) <= 80 else goal[:77] + "..."

    await db.create_user(user.id, user.username, user.first_name)
    contract_id = await db.create_open_contract(
        poster_id     = user.id,
        contract_type = feature_type,
        title         = title,
        description   = user_message,
        requirements  = requirements,
    )

    type_label = _CONTRACT_TYPE_LABELS.get(feature_type, "🤝 General")
    await update.message.reply_text(
        f"✅ *Contract Posted!*\n\n"
        f"📋 Type: {type_label}\n"
        f"🎯 Goal: _{goal}_\n"
        f"🆔 Contract ID: `{contract_id}`\n\n"
        f"Anyone can now apply via */open_opportunities*.\n"
        f"Use */mycontracts* to review applicants and pick the best match.\n\n"
        f"_Your agent will score every applicant automatically before you decide!_ 🤖",
        parse_mode="Markdown",
    )
    return ConversationHandler.END


# ─── /open_opportunities — Seeker browses open contracts ─────────────────────

# How many contracts to show per page
_PAGE_SIZE = 6


async def open_opportunities_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show a paginated list of open contracts."""
    await _show_opportunities_page(update, context, page=0)


async def _show_opportunities_page(update, context, page: int = 0):
    contracts = await db.get_open_contracts(status="open")

    if not contracts:
        text = (
            "📭 *No open contracts right now.*\n\n"
            "Be the first to post one with /opencontract!"
        )
        if update.callback_query:
            await update.callback_query.edit_message_text(text, parse_mode="Markdown")
        else:
            await update.message.reply_text(text, parse_mode="Markdown")
        return

    total         = len(contracts)
    total_pages   = max(1, (total + _PAGE_SIZE - 1) // _PAGE_SIZE)
    page          = max(0, min(page, total_pages - 1))
    start         = page * _PAGE_SIZE
    page_contracts = contracts[start : start + _PAGE_SIZE]

    buttons = []
    for c in page_contracts:
        type_icon = _CONTRACT_TYPE_LABELS.get(c["contract_type"], "🤝").split()[0]
        apps      = c.get("application_count", 0)
        poster    = c.get("poster_username") or c.get("poster_name") or "Anonymous"
        label     = f"{type_icon} {c['title'][:35]} | {apps} app(s) | @{poster}"
        buttons.append([InlineKeyboardButton(label, callback_data=f"contract_select_{c['id']}")])

    # Pagination row
    nav = []
    if page > 0:
        nav.append(InlineKeyboardButton("⬅️ Prev", callback_data=f"opps_page_{page - 1}"))
    if page < total_pages - 1:
        nav.append(InlineKeyboardButton("Next ➡️", callback_data=f"opps_page_{page + 1}"))
    if nav:
        buttons.append(nav)

    keyboard = InlineKeyboardMarkup(buttons)
    text = (
        f"🏪 *Open Contracts* ({total} available — page {page + 1}/{total_pages})\n\n"
        "Tap any contract to see details and apply."
    )

    if update.callback_query:
        await update.callback_query.edit_message_text(
            text, reply_markup=keyboard, parse_mode="Markdown"
        )
    else:
        await update.message.reply_text(
            text, reply_markup=keyboard, parse_mode="Markdown"
        )


async def opportunities_page_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle pagination buttons."""
    query = update.callback_query
    await query.answer()
    page = int(query.data.split("_")[-1])
    await _show_opportunities_page(update, context, page=page)


async def contract_selected_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show full contract details and an Apply button."""
    query = update.callback_query
    await query.answer()
    try:
        contract_id = query.data.replace("contract_select_", "")
        contract    = await db.get_open_contract(contract_id)

        if not contract:
            await query.edit_message_text("⚠️ Contract not found or no longer available.")
            return

        reqs         = contract.get("requirements") or {}
        goal         = reqs.get("goal", contract["title"])
        constraints  = reqs.get("constraints", [])
        poster       = contract.get("poster_username") or contract.get("poster_name") or "Anonymous"
        type_label   = _CONTRACT_TYPE_LABELS.get(contract["contract_type"], "🤝 General")
        apps         = await db.get_applications(contract_id)

        # Build constraint summary (plain text — no Markdown, user content could break it)
        constraint_lines = [
            f"  • {c.get('description', c.get('value', ''))}"
            for c in constraints[:4] if c.get("hard")
        ]
        constraints_text = "\n".join(constraint_lines) if constraint_lines else "  • (none specified)"

        keyboard = InlineKeyboardMarkup([
            [InlineKeyboardButton("🚀 Apply to This Contract", callback_data=f"contract_apply_{contract_id}")],
            [InlineKeyboardButton("⬅️ Back to List", callback_data="opps_page_0")],
        ])

        await query.edit_message_text(
            f"📋 {contract['title']}\n"
            f"{'─' * 30}\n"
            f"📌 Type: {type_label}\n"
            f"👤 Posted by: @{poster}\n"
            f"📬 Applications: {len(apps)}\n\n"
            f"🎯 What they need:\n{goal}\n\n"
            f"⛔ Hard requirements:\n{constraints_text}\n\n"
            f"🆔 Contract ID: {contract_id}\n\n"
            f"Tap Apply to submit your preferences and get a match score!",
            reply_markup=keyboard,
        )
    except Exception as e:
        print(f"[contract_selected_callback] error: {e}")
        await query.edit_message_text(f"⚠️ Error loading contract: {e}")


async def contract_apply_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """User taps Apply — prompt them for their preferences."""
    query = update.callback_query
    await query.answer()
    try:
        contract_id = query.data.replace("contract_apply_", "")
        contract    = await db.get_open_contract(contract_id)

        if not contract:
            await query.edit_message_text("⚠️ Contract not found or no longer available.")
            return

        # Check if they're the poster
        if contract["poster_id"] == query.from_user.id:
            await query.answer("⚠️ You can't apply to your own contract!", show_alert=True)
            return

        # Check for duplicate application
        existing_apps = await db.get_applications(contract_id)
        if any(a["applicant_id"] == query.from_user.id for a in existing_apps):
            await query.answer("You've already applied to this contract.", show_alert=True)
            return

        context.user_data["applying_contract_id"] = contract_id
        context.user_data["applying_contract_type"] = contract["contract_type"]
        context.user_data["awaiting_contract_application"] = True

        hints = {
            "freelance":   "your skills, hourly rate or project rate, availability, and relevant experience",
            "scheduling":  "your availability windows, timezone, and preferred meeting format",
            "marketplace": "your offer price, pickup preference, and any conditions",
            "generic":     "your goals, constraints, and what you bring to the table",
        }
        hint = hints.get(contract["contract_type"], hints["generic"])

        await query.edit_message_text(
            f"📝 Applying to: {contract['title'][:60]}\n"
            f"{'─' * 30}\n\n"
            f"Tell me your side in plain English.\n"
            f"Include: {hint}\n\n"
            f"Your agent will score your match automatically.",
        )
        return AWAITING_CONTRACT_APPLICATION
    except Exception as e:
        print(f"[contract_apply_callback] error: {e}")
        await query.edit_message_text(f"⚠️ Error: {e}")


async def receive_contract_application(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Receive applicant's preferences + store + fire async scoring."""
    if not context.user_data.get("awaiting_contract_application"):
        return

    user            = update.effective_user
    user_message    = update.message.text
    contract_id     = context.user_data.get("applying_contract_id")
    contract_type   = context.user_data.get("applying_contract_type", "generic")

    if not contract_id:
        await update.message.reply_text("⚠️ Session expired. Try /open_opportunities again.")
        context.user_data.pop("awaiting_contract_application", None)
        return

    contract = await db.get_open_contract(contract_id)
    if not contract or contract["status"] != "open":
        await update.message.reply_text("⚠️ This contract is no longer open.")
        context.user_data.pop("awaiting_contract_application", None)
        return

    await update.message.chat.send_action("typing")

    # Always use the real PersonalAgent for contract applications
    preferences = await personal_agent.extract_preferences(user_message, user.id)

    if "error" in preferences:
        await update.message.reply_text(
            "⚠️ Couldn't parse your preferences. Please try again with more detail."
        )
        return AWAITING_CONTRACT_APPLICATION

    await db.create_user(user.id, user.username, user.first_name)
    app_id = await db.add_application(contract_id, user.id, preferences)

    # Confirm immediately, then score in background
    await update.message.reply_text(
        f"✅ *Application submitted!*\n\n"
        f"📋 Contract: `{contract_id}`\n"
        f"🤖 Your agent is computing your match score...\n\n"
        f"_The poster will see ranked applicants in /mycontracts._\n"
        f"_If selected, you'll receive a Telegram message to start negotiation._",
        parse_mode="Markdown",
    )

    # Fire background scoring
    asyncio.create_task(
        _score_and_notify(
            app_id        = app_id,
            contract_id   = contract_id,
            contract_type = contract_type,
            requirements  = contract.get("requirements") or {},
            preferences   = preferences,
            applicant_id  = user.id,
        )
    )

    context.user_data.pop("awaiting_contract_application", None)
    context.user_data.pop("applying_contract_id", None)
    context.user_data.pop("applying_contract_type", None)
    return ConversationHandler.END


async def _score_and_notify(
    app_id: int,
    contract_id: str,
    contract_type: str,
    requirements: dict,
    preferences: dict,
    applicant_id: int,
):
    """Background task: AI-score the application, then notify the applicant."""
    try:
        result = await ai_score_applicant(requirements, preferences, contract_type)
        score     = result.get("match_score", 0)
        reasoning = result.get("match_reasoning", "")
        await db.update_application_match_score(app_id, score, reasoning)

        # Score badge
        if score >= 80:
            badge = "🟢 Strong match"
        elif score >= 55:
            badge = "🟡 Moderate match"
        else:
            badge = "🔴 Weak match"

        alignments = result.get("key_alignments", [])
        gaps       = result.get("key_gaps", [])
        align_text = "\n".join(f"  ✅ {a}" for a in alignments[:3]) or "  (none)"
        gap_text   = "\n".join(f"  ⚠️ {g}" for g in gaps[:3]) or "  (none)"

        await send_to_user(
            applicant_id,
            f"📊 Match Score: {score}/100 {badge}\n\n"
            f"{reasoning}\n\n"
            f"Alignments:\n{align_text}\n\n"
            f"Gaps:\n{gap_text}\n\n"
            f"Contract: {contract_id}\n"
            f"If the poster selects you, your agents will negotiate automatically!",
            parse_mode=None,
        )
    except Exception as e:
        print(f"[_score_and_notify] failed for app {app_id}: {e}")


# ─── /mycontracts — Poster reviews + picks applicant ─────────────────────────

async def mycontracts_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show the poster all their contracts with application counts."""
    user      = update.effective_user
    contracts = await db.get_contracts_by_poster(user.id)

    if not contracts:
        await update.message.reply_text(
            "📭 *You have no posted contracts.*\n\n"
            "Post one with /opencontract!",
            parse_mode="Markdown",
        )
        return

    status_icons = {
        "open": "🟢", "negotiating": "🔵", "resolved": "✅", "closed": "⬛"
    }
    buttons = []
    for c in contracts:
        icon  = status_icons.get(c["status"], "⬜")
        apps  = c.get("application_count", 0)
        best  = c.get("best_score") or 0
        label = f"{icon} {c['title'][:30]} | {apps} apps | top {int(best)}/100"
        buttons.append([InlineKeyboardButton(label, callback_data=f"mycontract_{c['id']}")])

    await update.message.reply_text(
        f"📋 *Your Contracts* ({len(contracts)} total)\n\n"
        "Tap a contract to review applicants and start negotiation.",
        reply_markup=InlineKeyboardMarkup(buttons),
        parse_mode="Markdown",
    )


async def mycontract_detail_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show ranked applicants for a specific contract with action buttons."""
    query = update.callback_query
    await query.answer()
    try:
        contract_id = query.data.replace("mycontract_", "")
        contract    = await db.get_open_contract(contract_id)

        if not contract or contract["poster_id"] != query.from_user.id:
            await query.edit_message_text("⚠️ Contract not found or you don't own it.")
            return

        apps = await db.get_applications(contract_id)  # already sorted by match_score DESC

        if not apps:
            await query.edit_message_text(
                f"📋 {contract['title'][:60]}\n"
                f"{'─' * 30}\n\n"
                f"📭 No applications yet.\n"
                f"Contract ID: {contract_id} | Status: {contract['status']}\n"
                f"Share it so others can apply via /open_opportunities.",
            )
            return

        # Build applicant summary lines
        lines = []
        for i, a in enumerate(apps[:8], 1):
            name  = a.get("username") or a.get("display_name") or f"User {a['applicant_id']}"
            score = int(a.get("match_score") or 0)
            lines.append(f"{i}. @{name} — {score}/100")
        applicants_text = "\n".join(lines)

        buttons = [
            [InlineKeyboardButton("🤖 Auto-pick Best Match", callback_data=f"autopick_{contract_id}")],
            [InlineKeyboardButton("📋 Select Manually",      callback_data=f"manualselect_{contract_id}")],
            [InlineKeyboardButton("⬅️ Back",                  callback_data="mycontracts_back")],
        ]

        if contract["status"] != "open":
            buttons = [[InlineKeyboardButton("⬅️ Back", callback_data="mycontracts_back")]]

        await query.edit_message_text(
            f"📋 {contract['title'][:60]}\n"
            f"{'─' * 30}\n\n"
            f"📬 {len(apps)} Applicant(s) — ranked by AI match score:\n\n"
            f"{applicants_text}\n\n"
            f"How do you want to select who to negotiate with?",
            reply_markup=InlineKeyboardMarkup(buttons),
        )
    except Exception as e:
        print(f"[mycontract_detail_callback] error: {e}")
        await query.edit_message_text(f"⚠️ Error loading contract details: {e}")


async def mycontracts_back_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Navigate back to mycontracts list."""
    query = update.callback_query
    await query.answer()
    await query.message.delete()
    await mycontracts_command(update, context)


async def autopick_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Auto-select the highest-scored applicant and start negotiation."""
    query = update.callback_query
    await query.answer()

    contract_id = query.data.replace("autopick_", "")
    contract    = await db.get_open_contract(contract_id)

    if not contract or contract["poster_id"] != query.from_user.id:
        await query.edit_message_text("⚠️ Contract not found or you don't own it.")
        return

    apps = await db.get_applications(contract_id)
    if not apps:
        await query.edit_message_text("⚠️ No applicants yet.")
        return

    best = apps[0]  # already sorted by match_score DESC
    name = best.get("username") or best.get("display_name") or f"User {best['applicant_id']}"

    await query.edit_message_text(
        f"🤖 Auto-picked best match: @{name} ({int(best.get('match_score', 0))}/100)\n\n"
        f"Starting negotiation..."
    )
    await _start_open_contract_negotiation(contract_id, best["applicant_id"], query.from_user.id)


async def manual_select_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show individual applicants as selectable buttons."""
    query = update.callback_query
    await query.answer()

    contract_id = query.data.replace("manualselect_", "")
    apps        = await db.get_applications(contract_id)

    if not apps:
        await query.edit_message_text("⚠️ No applicants yet.")
        return

    buttons = []
    for a in apps[:8]:
        name  = a.get("username") or a.get("display_name") or f"User {a['applicant_id']}"
        score = int(a.get("match_score") or 0)
        reasoning = (a.get("match_reasoning") or "")[:60]
        label = f"@{name} — {score}/100 — {reasoning}"
        buttons.append([InlineKeyboardButton(
            label[:64],  # Telegram button label max 64 chars
            callback_data=f"selectapp_{contract_id}_{a['applicant_id']}"
        )])
    buttons.append([InlineKeyboardButton("⬅️ Back", callback_data=f"mycontract_{contract_id}")])

    await query.edit_message_text(
        "📋 *Select an Applicant to Negotiate With:*\n\n"
        "(Ranked by AI match score — highest first)",
        reply_markup=InlineKeyboardMarkup(buttons),
        parse_mode="Markdown",
    )


async def select_applicant_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Poster manually selects a specific applicant."""
    query = update.callback_query
    await query.answer()

    _, contract_id, applicant_id_str = query.data.split("_", 2)
    applicant_id = int(applicant_id_str)
    contract = await db.get_open_contract(contract_id)

    if not contract or contract["poster_id"] != query.from_user.id:
        await query.edit_message_text("⚠️ Contract not found or you don't own it.")
        return

    await query.edit_message_text(
        f"✅ *Selected applicant {applicant_id}*\n\n_Starting negotiation..._",
        parse_mode="Markdown",
    )
    await _start_open_contract_negotiation(contract_id, applicant_id, query.from_user.id)


async def _start_open_contract_negotiation(
    contract_id: str,
    applicant_id: int,
    poster_id: int,
):
    """
    Load preferences from DB, create a negotiation, and kick off the engine.
    Poster = user_a (made the contract = their requirements).
    Applicant = user_b (their application preferences).
    """
    contract = await db.get_open_contract(contract_id)
    if not contract:
        await send_to_user(poster_id, f"⚠️ Contract `{contract_id}` not found.")
        return

    # Load preferences
    preferences_a = contract.get("requirements") or {}
    apps = await db.get_applications(contract_id)
    app  = next((a for a in apps if a["applicant_id"] == applicant_id), None)
    if not app:
        await send_to_user(poster_id, "⚠️ Could not find that applicant's data.")
        return
    preferences_b = app.get("preferences") or {}

    feature_type = contract.get("contract_type", "generic")

    # Get personalities
    poster_row    = await db.get_user(poster_id)
    applicant_row = await db.get_user(applicant_id)
    personality_a = dict(poster_row).get("personality", "balanced")    if poster_row    else "balanced"
    personality_b = dict(applicant_row).get("personality", "balanced") if applicant_row else "balanced"

    # Inject human names
    preferences_a["human_name"] = (
        (dict(poster_row).get("display_name") or dict(poster_row).get("username") or "Poster")
        if poster_row else "Poster"
    )
    preferences_b["human_name"] = (
        (dict(applicant_row).get("display_name") or dict(applicant_row).get("username") or "Applicant")
        if applicant_row else "Applicant"
    )

    # Create a standard negotiation in DB
    neg_id = await db.create_negotiation(feature_type, poster_id)
    await db.add_participant(neg_id, poster_id,    preferences_a, personality_used=personality_a)
    await db.add_participant(neg_id, applicant_id, preferences_b, personality_used=personality_b)

    # Link contract → negotiation
    await db.claim_contract(contract_id, applicant_id, neg_id)

    applicant_name = preferences_b["human_name"]
    poster_name    = preferences_a["human_name"]

    # Notify both parties that negotiation is starting
    await send_to_user(
        poster_id,
        f"🤖↔️🤖 *Negotiation starting!*\n\n"
        f"Your agent is negotiating with *{applicant_name}* on contract `{contract_id}`.\n"
        f"Negotiation ID: `{neg_id}`\n\n"
        f"_Stand by for real-time updates!_",
    )
    await send_to_user(
        applicant_id,
        f"🎉 *You've been selected!*\n\n"
        f"*{poster_name}* picked your application on contract `{contract_id}`.\n"
        f"Your agent is starting negotiation now...\n"
        f"Negotiation ID: `{neg_id}`\n\n"
        f"_Stand by for real-time updates!_ 🤖↔️🤖",
    )

    # Run negotiation using the existing engine
    asyncio.create_task(
        run_negotiation_with_telegram_updates(
            negotiation_id = neg_id,
            preferences_a  = preferences_a,
            preferences_b  = preferences_b,
            user_a_id      = poster_id,
            user_b_id      = applicant_id,
            feature_type   = feature_type,
            personality_a  = personality_a,
            personality_b  = personality_b,
        )
    )

    # Mark contract as resolved after negotiation task is queued
    # (actual status update to 'resolved' happens via close_contract call in on_resolution)


async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "🤖 *negoT8 — All Commands*\n"
        "━━━━━━━━━━━━━━━━━━━━━━\n\n"

        "⚙️ *Setup*\n"
        "/start — Register & see a welcome message\n"
        "/personality — Choose your agent's negotiation style\n"
        "  _(Aggressive · People Pleaser · Analytical · Empathetic · Balanced)_\n"
        "/connectcalendar — Link your Google Calendar so your agent knows your free slots automatically\n\n"

        "🤝 *Negotiation*\n"
        "/coordinate @username — Kick off a negotiation with someone\n"
        "  _Then describe what you need in plain English_\n"
        "/pending — See & accept incoming coordination requests from others\n\n"

        "📋 *Marketplace (Open Contracts)*\n"
        "/opencontract — Post a public job, project, or buy-sell opportunity\n"
        "/open\\_opportunities — Browse all open contracts and apply to one\n"
        "/mycontracts — View contracts you've posted, see applicants, and pick the best one\n\n"

        "🔗 *Blockchain & Proof*\n"
        "/proof — Show blockchain proof for your latest resolved negotiation\n"
        "/proof <id> — Show proof for a specific negotiation ID\n"
        "  _Every resolved deal is permanently recorded on Polygon Amoy_\n\n"

        "ℹ️ *Other*\n"
        "/help — Show this command reference\n\n"

        "━━━━━━━━━━━━━━━━━━━━━━\n"
        "💡 *Quick start:*\n"
        "1. /personality → pick your style\n"
        "2. `/coordinate @friend` → describe what you need\n"
        "3. Your agents negotiate — you get a resolution + payment link + proof ✅\n\n"
        "_Works for: scheduling · expenses · trips · buying/selling · roommates · conflicts · and more!_",
        parse_mode="Markdown"
    )


# ─── /proof command — Retrieve blockchain proof for a negotiation ─────────────
async def proof_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    /proof [negotiation_id]
    Shows the blockchain proof for a negotiation.
    If no ID given, uses the user's most recent resolved negotiation.
    """
    user = update.effective_user

    if context.args:
        neg_id = context.args[0].strip()
    else:
        neg_id = await db.get_latest_negotiation_for_user(user.id)
        if not neg_id:
            await update.message.reply_text(
                "❓ *No negotiation found.*\n\n"
                "Usage: /proof <negotiation\\_id>\n"
                "The ID is shown after every negotiation resolves.\n\n"
                "Example: /proof aa271ee7",
                parse_mode="Markdown"
            )
            return

    await update.message.chat.send_action("typing")

    proof = await db.get_blockchain_proof(neg_id)

    if not proof:
        # Check if the negotiation itself exists (wrong ID vs not-yet-resolved)
        neg = await db.get_negotiation(neg_id)
        if not neg:
            await update.message.reply_text(
                f"❌ *Negotiation* `{neg_id}` *not found.*\n\n"
                "Double-check the ID from your negotiation summary.",
                parse_mode="Markdown"
            )
        else:
            status = dict(neg).get("status", "unknown")
            await update.message.reply_text(
                f"⏳ *No blockchain proof yet* for `{neg_id}`\n\n"
                f"Negotiation status: *{status}*\n\n"
                "The proof is registered automatically when the negotiation resolves.\n"
                "If it just finished, wait a few seconds and try again.",
                parse_mode="Markdown"
            )
        return

    tx     = proof.get("tx_hash", "")
    blk    = proof.get("block_number", 0)
    ahash  = str(proof.get("agreement_hash", ""))
    url    = proof.get("explorer_url", "")
    gas    = proof.get("gas_used", 0)
    is_mock = "MOCK" in tx.upper() or "FAILED" in tx.upper() or blk == 0

    # Truncate for display
    short_tx    = (tx[:12] + "..." + tx[-8:]) if len(tx) > 22 else tx
    short_hash  = (ahash[:18] + "...") if len(ahash) > 20 else ahash

    if is_mock:
        # Mock/fallback proof — still show it, but clearly labelled
        await update.message.reply_text(
            f"🔗 *Agreement Proof* — `{neg_id}`\n\n"
            f"🌐 Network: Polygon Amoy\n"
            f"🔐 Agreement Hash: `{short_hash}`\n\n"
            f"⚠️ _On-chain TX pending or unavailable._\n"
            f"The agreement data is stored securely.\n\n"
            f"_Hash uniquely identifies your agreement content._",
            parse_mode="Markdown"
        )
        return

    # Real on-chain proof
    keyboard = None
    if url and url.startswith("http"):
        keyboard = InlineKeyboardMarkup([[
            InlineKeyboardButton("🔍 View on PolygonScan", url=url)
        ]])

    await update.message.reply_text(
        f"🔗 *Blockchain Proof of Agreement*\n\n"
        f"🏷 Negotiation: `{neg_id}`\n"
        f"🌐 Network: *Polygon Amoy*\n"
        f"⛓ Block: `{blk}`\n"
        f"📜 TX: `{short_tx}`\n"
        f"🔐 Hash: `{short_hash}`\n"
        f"⛽ Gas Used: `{gas:,}`\n\n"
        f"This agreement is *permanently and immutably* recorded on "
        f"the Polygon blockchain — neither party can alter or delete it.",
        reply_markup=keyboard,
        parse_mode="Markdown"
    )

async def _set_bot_commands(app: Application) -> None:
    """Register all commands in Telegram's bot-menu (the '/' popup)."""
    await app.bot.set_my_commands([
        BotCommand("start",              "Register & see welcome message"),
        BotCommand("help",               "Show all available commands"),
        BotCommand("personality",        "Choose your agent's negotiation style"),
        BotCommand("connectcalendar",    "Link your Google Calendar"),
        BotCommand("coordinate",         "Start a negotiation with @username"),
        BotCommand("pending",            "View & accept incoming coordination requests"),
        BotCommand("opencontract",       "Post a public job / buy-sell opportunity"),
        BotCommand("open_opportunities", "Browse & apply to open contracts"),
        BotCommand("mycontracts",        "View your contracts & pick the best applicant"),
        BotCommand("proof",              "Get blockchain proof for a negotiation"),
    ])


def create_bot(token: str) -> Application:
    """Create a bot application with all handlers."""
    app = Application.builder().token(token).post_init(_set_bot_commands).build()

    # Register this app so send_to_user() can reach users on either bot
    register_bot(token, app)

    # ── Initiator flow (User A): /coordinate → preferences ──
    initiator_conv = ConversationHandler(
        entry_points=[CommandHandler("coordinate", coordinate_command)],
        states={
            AWAITING_PREFERENCES: [
                MessageHandler(filters.TEXT & ~filters.COMMAND, receive_preferences)
            ]
        },
        fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)],
        name="initiator_conv",
        per_user=True,
    )

    # ── Counterparty flow (User B): Accept inline button → preferences ──
    # per_message=False (default) + per_chat=True so the ConversationHandler tracks
    # state per-user across any chat, which is correct for CallbackQueryHandler
    # entry_points (suppresses the PTBUserWarning about per_message).
    counterparty_conv = ConversationHandler(
        entry_points=[
            CallbackQueryHandler(accept_pending_callback, pattern=r"^accept_"),
            CallbackQueryHandler(accept_pending_callback, pattern=r"^decline_"),
        ],
        states={
            # After accepting, User B's next plain-text message is their preferences
            AWAITING_COUNTERPARTY_PREFS: [
                MessageHandler(filters.TEXT & ~filters.COMMAND, receive_counterparty_preferences)
            ]
        },
        fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)],
        name="counterparty_conv",
        per_user=True,
        per_chat=False,
        per_message=False,
    )

    app.add_handler(CommandHandler("start",             start_command))
    app.add_handler(CommandHandler("personality",       personality_command))
    app.add_handler(CommandHandler("pending",           pending_command))
    app.add_handler(CommandHandler("connectcalendar",   connect_calendar_command))
    app.add_handler(CommandHandler("help",              help_command))
    app.add_handler(CommandHandler("proof",             proof_command))
    # Open contracts marketplace — simple command handlers (no conversation needed)
    app.add_handler(CommandHandler("open_opportunities", open_opportunities_command))
    app.add_handler(CommandHandler("mycontracts",       mycontracts_command))
    app.add_handler(CallbackQueryHandler(personality_callback,        pattern=r"^personality_"))
    app.add_handler(CallbackQueryHandler(opportunities_page_callback, pattern=r"^opps_page_"))
    app.add_handler(CallbackQueryHandler(contract_selected_callback,  pattern=r"^contract_select_"))
    app.add_handler(CallbackQueryHandler(mycontract_detail_callback,  pattern=r"^mycontract_[0-9a-f]"))
    app.add_handler(CallbackQueryHandler(mycontracts_back_callback,   pattern=r"^mycontracts_back$"))
    app.add_handler(CallbackQueryHandler(autopick_callback,           pattern=r"^autopick_"))
    app.add_handler(CallbackQueryHandler(manual_select_callback,      pattern=r"^manualselect_"))
    app.add_handler(CallbackQueryHandler(select_applicant_callback,   pattern=r"^selectapp_"))
    app.add_handler(initiator_conv)
    app.add_handler(counterparty_conv)

    # ── Open Contract poster ConversationHandler ──
    opencontract_conv = ConversationHandler(
        entry_points=[CommandHandler("opencontract", opencontract_command)],
        states={
            AWAITING_CONTRACT_DESCRIPTION: [
                MessageHandler(filters.TEXT & ~filters.COMMAND, receive_contract_description)
            ]
        },
        fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)],
        name="opencontract_conv",
        per_user=True,
    )

    # ── Contract application ConversationHandler (seeker) ──
    contract_apply_conv = ConversationHandler(
        entry_points=[CallbackQueryHandler(contract_apply_callback, pattern=r"^contract_apply_")],
        states={
            AWAITING_CONTRACT_APPLICATION: [
                MessageHandler(filters.TEXT & ~filters.COMMAND, receive_contract_application)
            ]
        },
        fallbacks=[CommandHandler("cancel", lambda u, c: ConversationHandler.END)],
        name="contract_apply_conv",
        per_user=True,
        per_chat=False,
        per_message=False,
    )

    app.add_handler(opencontract_conv)
    app.add_handler(contract_apply_conv)

    # Fallback: catch User B's preference text when NOT inside a ConversationHandler
    # (e.g., they tapped Accept but the conv state was lost after a bot restart)
    app.add_handler(
        MessageHandler(
            filters.TEXT & ~filters.COMMAND,
            receive_counterparty_preferences
        )
    )

    return app
