# backend/run.py — THE MAIN ENTRY POINT
import asyncio
import json
import os
import sys
import atexit

# ─── PID lock — prevents two copies of this process running simultaneously ───
import tempfile
_PID_FILE = os.path.join(tempfile.gettempdir(), "negot8_bots.pid")

def _acquire_pid_lock():
    """Write our PID to the lock file. If a previous PID is still alive, kill it first."""
    if os.path.exists(_PID_FILE):
        try:
            old_pid = int(open(_PID_FILE).read().strip())
            os.kill(old_pid, 9)          # kill the old copy unconditionally
            print(f"🔫 Killed previous bot process (PID {old_pid})")
        except (ValueError, ProcessLookupError, PermissionError):
            pass                          # already dead — no problem
    with open(_PID_FILE, "w") as f:
        f.write(str(os.getpid()))
    atexit.register(lambda: os.path.exists(_PID_FILE) and os.remove(_PID_FILE))

_acquire_pid_lock()
from telegram.ext import Application, CommandHandler, ConversationHandler, MessageHandler, CallbackQueryHandler, filters
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from agents.personal_agent import PersonalAgent
from agents.negotiation import run_negotiation
from voice.elevenlabs_tts import generate_voice_summary, build_voice_text
from tools.upi_generator import UPIGeneratorTool
import database as db
from config import *

# ─── 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

# ─── Blockchain (Polygon Amoy) ────────────────────────────────────────────────
try:
    from blockchain_web3.blockchain import register_agreement_on_chain
    _blockchain_available = True
except Exception as _bc_err:
    print(f"⚠️  Blockchain module unavailable: {_bc_err}")
    _blockchain_available = False
    async def register_agreement_on_chain(*args, **kwargs):
        return {"success": False, "mock": True, "tx_hash": "0xUNAVAILABLE",
                "block_number": 0, "agreement_hash": "0x0",
                "explorer_url": "", "gas_used": 0}

personal_agent = PersonalAgent()
upi_tool = UPIGeneratorTool()
pending_coordinations = {}
bot_apps = {}

async def send_to_user(bot, user_id, text, reply_markup=None):
    """Send a message to any user."""
    try:
        await bot.send_message(chat_id=user_id, text=text, parse_mode="Markdown", reply_markup=reply_markup)
    except Exception as e:
        print(f"Failed to send to {user_id}: {e}")

async def send_voice_to_user(bot, user_id, audio_path):
    """Send voice note to user."""
    try:
        with open(audio_path, "rb") as f:
            await bot.send_voice(chat_id=user_id, voice=f, caption="🎙 Voice summary from your agent")
    except Exception as e:
        print(f"Failed to send voice to {user_id}: {e}")

# ─── Resolution handler with voice + UPI ───
async def handle_resolution(negotiation_id, resolution, feature_type,
                             user_a_id, user_b_id, bot_a, bot_b,
                             preferences_a, preferences_b):
    """Post-resolution: generate UPI link, voice summary, analytics, send to users."""

    status = resolution["status"]
    proposal = resolution.get("final_proposal", {})
    emoji = "✅" if status == "resolved" else "⚠️"

    summary_text = (
        f"{emoji} *Negotiation {'Complete' if status == 'resolved' else 'Needs Input'}!*\n\n"
        f"📊 Resolved in {resolution['rounds_taken']} rounds\n\n"
        f"📋 *Agreement:*\n{proposal.get('summary', 'See details')}\n\n"
        f"*For A:* {proposal.get('for_party_a', 'See details')}\n"
        f"*For B:* {proposal.get('for_party_b', 'See details')}"
    )

    # ─── UPI link (for expense-related features) ───
    upi_markup = None
    if feature_type in ("expenses", "freelance", "marketplace", "roommate"):
        # Try to extract settlement from proposal details
        details = proposal.get("details", {})
        settlement = details.get("settlement", {})
        upi_id = (preferences_a.get("raw_details", {}).get("upi_id") or
                  preferences_b.get("raw_details", {}).get("upi_id"))

        if upi_id and settlement.get("amount"):
            upi_result = await upi_tool.execute(
                payee_upi=upi_id,
                payee_name=settlement.get("payee_name", "User"),
                amount=float(settlement["amount"]),
                note=f"negoT8: {feature_type} settlement"
            )
            upi_link = upi_result["upi_link"]
            summary_text += f"\n\n💳 *Tap to pay:* ₹{settlement['amount']:,.0f}"

            upi_markup = InlineKeyboardMarkup([
                [InlineKeyboardButton(
                    f"💳 Pay ₹{settlement['amount']:,.0f}",
                    url=upi_link
                )]
            ])

    # ─── Send text summary to both ───
    await send_to_user(bot_a, user_a_id, summary_text, reply_markup=upi_markup)
    await send_to_user(bot_b, user_b_id, summary_text, reply_markup=upi_markup)

    # ─── Voice summary ───
    voice_text = build_voice_text(feature_type, {
        "rounds": resolution["rounds_taken"],
        "summary": proposal.get("summary", "resolved"),
        **proposal.get("details", {}),
        **{k: v for k, v in proposal.items() if k != "details"}
    })

    voice_path = await generate_voice_summary(voice_text, negotiation_id, VOICE_ID_AGENT_A)
    if voice_path:
        await send_voice_to_user(bot_a, user_a_id, voice_path)
        # Generate with different voice for User B
        voice_path_b = await generate_voice_summary(voice_text, f"{negotiation_id}_b", VOICE_ID_AGENT_B)
        if voice_path_b:
            await send_voice_to_user(bot_b, user_b_id, voice_path_b)

    # ─── Compute & store analytics ───
    timeline = resolution.get("satisfaction_timeline", [])
    concession_log = []
    rounds = await db.get_rounds(negotiation_id)
    for r in rounds:
        concessions = json.loads(r["concessions_made"]) if r["concessions_made"] else []
        for c in concessions:
            concession_log.append({"round": r["round_number"], "by": "A" if r["proposer_id"] == user_a_id else "B", "gave_up": c})

    final_sat_a = timeline[-1]["score_a"] if timeline else 50
    final_sat_b = timeline[-1]["score_b"] if timeline else 50
    fairness = 100 - abs(final_sat_a - final_sat_b)

    await db.store_analytics({
        "negotiation_id": negotiation_id,
        "satisfaction_timeline": json.dumps(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"),
    })

    # ─── Register agreement on Polygon Amoy (invisible to user) ──────────────
    blockchain_text = ""
    if status == "resolved":
        try:
            blockchain_result = await register_agreement_on_chain(
                negotiation_id  = negotiation_id,
                feature_type    = feature_type,
                summary         = proposal.get("summary", "Agreement reached"),
                resolution_data = resolution,
            )
            await db.store_blockchain_proof(
                negotiation_id = negotiation_id,
                tx_hash        = blockchain_result["tx_hash"],
                block_number   = blockchain_result.get("block_number", 0),
                agreement_hash = blockchain_result["agreement_hash"],
                explorer_url   = blockchain_result["explorer_url"],
                gas_used       = blockchain_result.get("gas_used", 0),
            )
            if blockchain_result.get("success") and not blockchain_result.get("mock"):
                blockchain_text = (
                    f"\n\n🔗 *Verified on Blockchain*\n"
                    f"This agreement is permanently recorded on Polygon\\.\n"
                    f"[View Proof on PolygonScan]({blockchain_result['explorer_url']})"
                )
            else:
                blockchain_text = "\n\n🔗 _Blockchain proof pending\\.\\.\\._"
        except Exception as _bc_exc:
            print(f"[Blockchain] Non-critical error for {negotiation_id}: {_bc_exc}")
            blockchain_text = "\n\n🔗 _Blockchain proof pending\\.\\.\\._"

    if blockchain_text:
        await send_to_user(bot_a, user_a_id, blockchain_text)
        await send_to_user(bot_b, user_b_id, blockchain_text)

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


# ─────────────────────────────────────────────────────────────
# Entry point — starts Bot A + Bot B concurrently
# Per PTB docs (Context7): use context-manager pattern so
# initialize() and shutdown() are always called correctly.
# Source: https://github.com/python-telegram-bot/python-telegram-bot/wiki/Frequently-requested-design-patterns
# ─────────────────────────────────────────────────────────────

async def _reset_telegram_session(name: str, token: str) -> None:
    """
    Forcefully clear any stale Telegram polling session for this token.
    Steps:
      1. deleteWebhook  — removes any webhook and drops pending updates
      2. getUpdates(offset=-1, timeout=0) — forces the server to close
         any open long-poll held by a previously killed process
    Both calls are made via asyncio-friendly httpx so we don't block.
    """
    import httpx
    base = f"https://api.telegram.org/bot{token}"
    async with httpx.AsyncClient(timeout=10) as client:
        # Step 1 — delete webhook
        try:
            r = await client.post(f"{base}/deleteWebhook",
                                  json={"drop_pending_updates": True})
            desc = r.json().get("description", "ok")
            print(f"  Bot {name} deleteWebhook: {desc}")
        except Exception as e:
            print(f"  Bot {name} deleteWebhook failed (non-critical): {e}")

        # Step 2 — drain pending updates; this causes the Telegram server
        # to close any open long-poll connection from a previous process.
        try:
            r = await client.get(f"{base}/getUpdates",
                                 params={"offset": -1, "timeout": 0,
                                         "limit": 1})
            print(f"  Bot {name} session drained ✓")
        except Exception as e:
            print(f"  Bot {name} drain failed (non-critical): {e}")


async def _run_single_bot(name: str, app, stop_event: asyncio.Event) -> None:
    """
    Run one bot for its full lifecycle using the PTB context-manager pattern.
    Context7 source:
      async with application:          # calls initialize() + shutdown()
          await application.start()
          await application.updater.start_polling(...)
          # … keep alive …
          await application.updater.stop()
          await application.stop()
    """
    from telegram import Update
    async with app:                                     # initialize + shutdown
        await app.start()
        await app.updater.start_polling(
            allowed_updates=Update.ALL_TYPES,
            drop_pending_updates=True,                  # ignore stale messages
            poll_interval=0.5,                          # fast reconnect
        )
        print(f"▶️  Bot {name} polling...")
        await stop_event.wait()                         # block until Ctrl+C
        await app.updater.stop()
        await app.stop()
    print(f"  Bot {name} stopped.")


async def run_bots():
    sys.path.insert(0, os.path.join(os.path.dirname(__file__), "telegram-bots"))
    from bot import create_bot

    # ── 1. Database ───────────────────────────────────────────────────────────
    await db.init_db()
    print("✅ Database initialized")

    # ── 2. Collect bot tokens ─────────────────────────────────────────────────
    bots_to_run = []
    if TELEGRAM_BOT_TOKEN_A:
        bots_to_run.append(("A", TELEGRAM_BOT_TOKEN_A))
    if TELEGRAM_BOT_TOKEN_B:
        bots_to_run.append(("B", TELEGRAM_BOT_TOKEN_B))

    if not bots_to_run:
        print("❌ No bot tokens found in .env")
        return

    # ── 3. Reset Telegram sessions ────────────────────────────────────────────
    # This clears any long-poll held by a previously killed process.
    print("🧹 Resetting Telegram sessions...")
    for name, token in bots_to_run:
        await _reset_telegram_session(name, token)

    # PTB long-poll timeout is 10s — a brief pause lets Telegram's servers
    # acknowledge the deleteWebhook + drain before we start polling.
    await asyncio.sleep(2)

    # ── 4. Signal handler → shared stop event ────────────────────────────────
    stop_event = asyncio.Event()
    loop = asyncio.get_running_loop()
    try:
        import signal
        loop.add_signal_handler(signal.SIGINT,  stop_event.set)
        loop.add_signal_handler(signal.SIGTERM, stop_event.set)
    except NotImplementedError:
        pass  # Windows fallback

    # ── 5. Build & run all bots concurrently ─────────────────────────────────
    apps = [(name, create_bot(token)) for name, token in bots_to_run]
    print(f"\n🚀 Starting {len(apps)} bot(s)...\n")

    try:
        await asyncio.gather(
            *[_run_single_bot(name, app, stop_event) for name, app in apps]
        )
    except (KeyboardInterrupt, asyncio.CancelledError):
        stop_event.set()

    print("👋 Done.")


if __name__ == "__main__":
    asyncio.run(run_bots())
