# backend/api.py — FastAPI + Socket.IO API server for negoT8 Dashboard (Milestone 6)
import json
import asyncio
from datetime import datetime

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
import socketio

import database as db
from tools.google_calendar import GoogleCalendarTool

# ─── Socket.IO server ──────────────────────────────────────────────────────────
sio = socketio.AsyncServer(
    async_mode="asgi",
    cors_allowed_origins="*",
    logger=False,
    engineio_logger=False,
)

# ─── FastAPI app ───────────────────────────────────────────────────────────────
app = FastAPI(title="negoT8 API", version="2.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ─── Helpers ──────────────────────────────────────────────────────────────────

def _row_to_dict(row) -> dict:
    """Convert an aiosqlite Row or plain dict, parsing JSON string fields."""
    if row is None:
        return {}
    d = dict(row)
    for field in (
        "preferences", "proposal", "response", "concessions_made",
        "resolution", "satisfaction_timeline", "concession_log",
    ):
        if field in d and isinstance(d[field], str):
            try:
                d[field] = json.loads(d[field])
            except (json.JSONDecodeError, TypeError):
                pass
    return d


async def _build_negotiation_detail(negotiation_id: str) -> dict:
    """Assemble full negotiation object: meta + participants + rounds + analytics."""
    import aiosqlite
    from config import DATABASE_PATH

    async with aiosqlite.connect(DATABASE_PATH) as conn:
        conn.row_factory = aiosqlite.Row

        # Negotiation meta
        async with conn.execute(
            "SELECT * FROM negotiations WHERE id = ?", (negotiation_id,)
        ) as cur:
            neg = await cur.fetchone()

    if neg is None:
        return None

    neg_dict = _row_to_dict(neg)

    # Participants (with user display info)
    import aiosqlite
    from config import DATABASE_PATH
    async with aiosqlite.connect(DATABASE_PATH) as conn:
        conn.row_factory = aiosqlite.Row
        async with conn.execute(
            """SELECT p.*, u.username, u.display_name, u.personality, u.voice_id
               FROM participants p
               LEFT JOIN users u ON u.telegram_id = p.user_id
               WHERE p.negotiation_id = ?""",
            (negotiation_id,),
        ) as cur:
            participants = [_row_to_dict(r) for r in await cur.fetchall()]

        # Rounds
        async with conn.execute(
            "SELECT * FROM rounds WHERE negotiation_id = ? ORDER BY round_number",
            (negotiation_id,),
        ) as cur:
            rounds = [_row_to_dict(r) for r in await cur.fetchall()]

        # Analytics
        async with conn.execute(
            "SELECT * FROM negotiation_analytics WHERE negotiation_id = ?",
            (negotiation_id,),
        ) as cur:
            analytics_row = await cur.fetchone()

    analytics = _row_to_dict(analytics_row) if analytics_row else {}
    if analytics.get("satisfaction_timeline") and isinstance(
        analytics["satisfaction_timeline"], str
    ):
        try:
            analytics["satisfaction_timeline"] = json.loads(
                analytics["satisfaction_timeline"]
            )
        except Exception:
            analytics["satisfaction_timeline"] = []
    if analytics.get("concession_log") and isinstance(
        analytics["concession_log"], str
    ):
        try:
            analytics["concession_log"] = json.loads(analytics["concession_log"])
        except Exception:
            analytics["concession_log"] = []

    return {
        **neg_dict,
        "participants": participants,
        "rounds": rounds,
        "analytics": analytics,
    }


# ─── REST Endpoints ────────────────────────────────────────────────────────────

@app.get("/")
async def root():
    return {"status": "ok", "message": "negoT8 API v2 running"}


# ─── Google Calendar OAuth Callback ───────────────────────────────────────────

@app.get("/api/auth/google/callback", response_class=HTMLResponse)
async def google_calendar_callback(request: Request):
    """
    Handles the redirect from Google after the user authorises calendar access.
    - `code`  — OAuth authorisation code from Google
    - `state` — the user's telegram_id (set when building the auth URL)
    """
    params = dict(request.query_params)
    code = params.get("code")
    state = params.get("state")   # telegram_id

    if not code or not state:
        return HTMLResponse(
            "<h2>❌ Missing code or state. Please try /connectcalendar again.</h2>",
            status_code=400,
        )

    try:
        telegram_id = int(state)
    except ValueError:
        return HTMLResponse("<h2>❌ Invalid state parameter.</h2>", status_code=400)

    cal = GoogleCalendarTool()
    success = await cal.exchange_code(telegram_id, code)

    # Notify the user in Telegram (best-effort via direct Bot API call)
    try:
        import httpx
        from config import TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B
        msg = (
            "✅ *Google Calendar connected!*\n\n"
            "Your agent will now automatically use your real availability "
            "when scheduling meetings — no need to mention times manually.\n\n"
            "_Read-only access. Revoke anytime from myaccount.google.com → Security → Third-party apps._"
            if success else
            "❌ Failed to connect Google Calendar. Please try /connectcalendar again."
        )
        async with httpx.AsyncClient() as client:
            for token in (TELEGRAM_BOT_TOKEN_A, TELEGRAM_BOT_TOKEN_B):
                if not token:
                    continue
                resp = await client.post(
                    f"https://api.telegram.org/bot{token}/sendMessage",
                    json={"chat_id": telegram_id, "text": msg, "parse_mode": "Markdown"},
                    timeout=8.0,
                )
                if resp.status_code == 200:
                    break
    except Exception as e:
        print(f"[OAuth] Could not send Telegram confirmation: {e}")

    if success:
        return HTMLResponse("""
        <html><body style="font-family:sans-serif;text-align:center;padding:60px">
          <h1>✅ Google Calendar Connected!</h1>
          <p>You can close this tab and return to Telegram.</p>
          <p style="color:#666">negoT8 now has read-only access to your calendar.</p>
        </body></html>
        """)
    else:
        return HTMLResponse("""
        <html><body style="font-family:sans-serif;text-align:center;padding:60px">
          <h1>❌ Connection Failed</h1>
          <p>Please go back to Telegram and try <code>/connectcalendar</code> again.</p>
        </body></html>
        """, status_code=500)


@app.get("/api/negotiations")
async def list_negotiations():
    """Return all negotiations with participant count and latest status."""
    import aiosqlite
    from config import DATABASE_PATH

    async with aiosqlite.connect(DATABASE_PATH) as conn:
        conn.row_factory = aiosqlite.Row
        async with conn.execute(
            """SELECT n.*,
                      COUNT(p.user_id) AS participant_count
               FROM negotiations n
               LEFT JOIN participants p ON p.negotiation_id = n.id
               GROUP BY n.id
               ORDER BY n.created_at DESC"""
        ) as cur:
            rows = await cur.fetchall()

    negotiations = []
    for row in rows:
        d = _row_to_dict(row)
        # Lightweight — don't embed full rounds here
        negotiations.append(d)

    return {"negotiations": negotiations, "total": len(negotiations)}


@app.get("/api/negotiations/{negotiation_id}")
async def get_negotiation(negotiation_id: str):
    """Return full negotiation detail: meta + participants + rounds + analytics."""
    detail = await _build_negotiation_detail(negotiation_id)
    if detail is None:
        raise HTTPException(status_code=404, detail=f"Negotiation '{negotiation_id}' not found")
    return detail


@app.get("/api/negotiations/{negotiation_id}/rounds")
async def get_negotiation_rounds(negotiation_id: str):
    """Return just the rounds for a negotiation (useful for live updates)."""
    rounds = await db.get_rounds(negotiation_id)
    parsed = [_row_to_dict(r) for r in rounds]
    return {"negotiation_id": negotiation_id, "rounds": parsed, "count": len(parsed)}


@app.get("/api/negotiations/{negotiation_id}/analytics")
async def get_negotiation_analytics(negotiation_id: str):
    """Return analytics for a negotiation."""
    analytics = await db.get_analytics(negotiation_id)
    if analytics is None:
        raise HTTPException(status_code=404, detail="Analytics not yet available for this negotiation")

    # Parse JSON strings
    for field in ("satisfaction_timeline", "concession_log"):
        if isinstance(analytics.get(field), str):
            try:
                analytics[field] = json.loads(analytics[field])
            except Exception:
                analytics[field] = []

    return analytics


@app.get("/api/users/{telegram_id}")
async def get_user(telegram_id: int):
    """Return a single user by Telegram ID."""
    user = await db.get_user(telegram_id)
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return dict(user)


@app.get("/api/stats")
async def get_stats():
    """High-level stats for the dashboard overview page."""
    import aiosqlite
    from config import DATABASE_PATH

    async with aiosqlite.connect(DATABASE_PATH) as conn:
        conn.row_factory = aiosqlite.Row

        async with conn.execute("SELECT COUNT(*) AS c FROM negotiations") as cur:
            total_neg = (await cur.fetchone())["c"]

        async with conn.execute(
            "SELECT COUNT(*) AS c FROM negotiations WHERE status = 'resolved'"
        ) as cur:
            resolved = (await cur.fetchone())["c"]

        async with conn.execute(
            "SELECT COUNT(*) AS c FROM negotiations WHERE status = 'active'"
        ) as cur:
            active = (await cur.fetchone())["c"]

        async with conn.execute("SELECT COUNT(*) AS c FROM users") as cur:
            total_users = (await cur.fetchone())["c"]

        async with conn.execute(
            "SELECT AVG(fairness_score) AS avg_fs FROM negotiation_analytics"
        ) as cur:
            row = await cur.fetchone()
            avg_fairness = round(row["avg_fs"] or 0, 1)

        async with conn.execute(
            """SELECT feature_type, COUNT(*) AS c
               FROM negotiations
               GROUP BY feature_type
               ORDER BY c DESC"""
        ) as cur:
            feature_breakdown = [dict(r) for r in await cur.fetchall()]

    return {
        "total_negotiations": total_neg,
        "resolved": resolved,
        "active": active,
        "escalated": total_neg - resolved - active,
        "total_users": total_users,
        "avg_fairness_score": avg_fairness,
        "feature_breakdown": feature_breakdown,
    }


# ─── Open Contracts REST Endpoints ────────────────────────────────────────────

@app.get("/api/open-contracts")
async def list_open_contracts(status: str = "open"):
    """
    Return open contracts (default: status=open).
    Pass ?status=all to get every contract regardless of status.
    """
    if status == "all":
        import aiosqlite
        from config import DATABASE_PATH
        async with aiosqlite.connect(DATABASE_PATH) as conn:
            conn.row_factory = aiosqlite.Row
            async with conn.execute(
                """SELECT oc.*,
                          u.username AS poster_username,
                          u.display_name AS poster_name,
                          COUNT(ca.id) AS application_count
                   FROM open_contracts oc
                   LEFT JOIN users u ON u.telegram_id = oc.poster_id
                   LEFT JOIN contract_applications ca ON ca.contract_id = oc.id
                   GROUP BY oc.id
                   ORDER BY oc.created_at DESC"""
            ) as cur:
                rows = await cur.fetchall()
        contracts = []
        for r in rows:
            d = dict(r)
            if isinstance(d.get("requirements"), str):
                try:
                    import json as _json
                    d["requirements"] = _json.loads(d["requirements"])
                except Exception:
                    pass
            contracts.append(d)
    else:
        contracts = await db.get_open_contracts(status=status)

    return {"contracts": contracts, "total": len(contracts)}


@app.get("/api/open-contracts/{contract_id}")
async def get_open_contract(contract_id: str):
    """
    Return full detail for a single open contract including ranked applicants.
    """
    contract = await db.get_open_contract(contract_id)
    if contract is None:
        raise HTTPException(status_code=404, detail=f"Contract '{contract_id}' not found")

    applications = await db.get_applications(contract_id)
    # applications are already sorted by match_score DESC from the DB helper

    # Parse preferences in each application
    for app_row in applications:
        if isinstance(app_row.get("preferences"), str):
            try:
                app_row["preferences"] = json.loads(app_row["preferences"])
            except Exception:
                pass

    return {
        **contract,
        "applications": applications,
        "application_count": len(applications),
    }


# ─── Socket.IO Events ──────────────────────────────────────────────────────────

@sio.event
async def connect(sid, environ):
    print(f"[Socket.IO] Client connected: {sid}")


@sio.event
async def disconnect(sid):
    print(f"[Socket.IO] Client disconnected: {sid}")


@sio.event
async def join_negotiation(sid, data):
    """
    Client emits: { negotiation_id: "abc123" }
    Server joins the socket into a room named after the negotiation_id.
    Then immediately sends the current state.
    """
    if isinstance(data, str):
        negotiation_id = data
    else:
        negotiation_id = data.get("negotiation_id") or data.get("id", "")

    if not negotiation_id:
        await sio.emit("error", {"message": "negotiation_id required"}, to=sid)
        return

    await sio.enter_room(sid, negotiation_id)
    print(f"[Socket.IO] {sid} joined room: {negotiation_id}")

    # Send current state immediately
    detail = await _build_negotiation_detail(negotiation_id)
    if detail:
        await sio.emit("negotiation_state", detail, to=sid)
    else:
        await sio.emit("error", {"message": f"Negotiation '{negotiation_id}' not found"}, to=sid)


@sio.event
async def leave_negotiation(sid, data):
    """Client emits: { negotiation_id: "abc123" }"""
    if isinstance(data, str):
        negotiation_id = data
    else:
        negotiation_id = data.get("negotiation_id", "")

    if negotiation_id:
        await sio.leave_room(sid, negotiation_id)
        print(f"[Socket.IO] {sid} left room: {negotiation_id}")


# ─── Socket.IO emit helpers (called from run.py / negotiation engine) ──────────

async def emit_round_update(negotiation_id: str, round_data: dict):
    """
    Called by the negotiation engine after each round completes.
    Broadcasts to all dashboard clients watching this negotiation.
    """
    await sio.emit(
        "round_update",
        {
            "negotiation_id": negotiation_id,
            "round": round_data,
            "timestamp": datetime.utcnow().isoformat(),
        },
        room=negotiation_id,
    )


async def emit_negotiation_started(negotiation_id: str, feature_type: str, participants: list):
    """Broadcast when a new negotiation kicks off."""
    await sio.emit(
        "negotiation_started",
        {
            "negotiation_id": negotiation_id,
            "feature_type": feature_type,
            "participants": participants,
            "timestamp": datetime.utcnow().isoformat(),
        },
        room=negotiation_id,
    )


async def emit_negotiation_resolved(negotiation_id: str, resolution: dict):
    """Broadcast the final resolution to all watchers."""
    await sio.emit(
        "negotiation_resolved",
        {
            "negotiation_id": negotiation_id,
            "resolution": resolution,
            "timestamp": datetime.utcnow().isoformat(),
        },
        room=negotiation_id,
    )


# ─── ASGI app (wraps FastAPI with Socket.IO) ───────────────────────────────────
# This is what uvicorn runs — it combines both the REST API and the WS server.
socket_app = socketio.ASGIApp(sio, other_asgi_app=app)
