"""
Extended endpoints for INDUNGI ROMANIA forum:
- Object storage uploads (Emergent)
- Direct Messages (DMs) + Notifications
- Sub-forums + Full-text search
- Discord & Steam SSO
- Automated monthly winner cron
- Board reorder
"""
from fastapi import APIRouter, UploadFile, File, HTTPException, Depends, Request, Query, BackgroundTasks, Response, Header
from pydantic import BaseModel, ConfigDict, Field
from typing import List, Optional, Dict, Any
from datetime import datetime, timezone, timedelta
import os
import uuid
import requests
import asyncio
import logging
import urllib.parse
import re

logger = logging.getLogger("forum_ext")

STORAGE_URL = "https://integrations.emergentagent.com/objstore/api/v1/storage"
APP_NAME = "indungi-romania"
_storage_key: Optional[str] = None


LOCAL_STORAGE_DIR = os.path.join(os.path.dirname(__file__), "local_storage")

def init_storage():
    os.makedirs(LOCAL_STORAGE_DIR, exist_ok=True)
    return "local"


def put_object(path: str, data: bytes, content_type: str):
    init_storage()
    full_path = os.path.join(LOCAL_STORAGE_DIR, path)
    os.makedirs(os.path.dirname(full_path), exist_ok=True)
    with open(full_path, "wb") as f:
        f.write(data)
    return {"path": path, "size": len(data)}


def get_object(path: str):
    init_storage()
    full_path = os.path.join(LOCAL_STORAGE_DIR, path)
    if not os.path.exists(full_path):
        raise FileNotFoundError(f"File not found: {path}")
    with open(full_path, "rb") as f:
        content = f.read()
    import mimetypes
    ct, _ = mimetypes.guess_type(full_path)
    return content, ct or "application/octet-stream"


# ------- Build the extension router -------
def build_router(db, get_current_user, get_user_optional, require_admin, now_iso, award_points, clear_boards_cache=None):
    api = APIRouter()

    # ============== FILE UPLOAD ==============
    @api.post("/upload")
    async def upload_file(file: UploadFile = File(...), kind: str = "avatar", user=Depends(get_current_user)):
        ext = (file.filename or "bin").rsplit(".", 1)[-1].lower()
        allowed_exts = {"png", "jpg", "jpeg", "gif", "webp", "mp4", "webm"}
        if ext not in allowed_exts:
            raise HTTPException(400, "Only image or video formats allowed (png/jpg/gif/webp/mp4/webm)")
        data = await file.read()
        max_size = 20 * 1024 * 1024 if (ext in {"mp4", "webm"} or kind == "proof") else 5 * 1024 * 1024
        if len(data) > max_size:
            raise HTTPException(400, f"File too large (max {max_size // (1024 * 1024)}MB)")
            
        # Pillow Media Compression: scale maximum width to 1200px, compress JPEG uploads to 85% quality.
        if ext in {"jpg", "jpeg", "png", "webp"} and kind != "proof":
            try:
                from PIL import Image
                import io
                img = Image.open(io.BytesIO(data))
                width, height = img.size
                changed = False
                if width > 1200:
                    ratio = 1200 / float(width)
                    new_height = int(float(height) * ratio)
                    img = img.resize((1200, new_height), Image.Resampling.LANCZOS)
                    changed = True
                
                if ext in {"jpg", "jpeg"}:
                    out = io.BytesIO()
                    if img.mode in ("RGBA", "P"):
                        img = img.convert("RGB")
                    img.save(out, format="JPEG", quality=85)
                    data = out.getvalue()
                elif changed:
                    out = io.BytesIO()
                    img.save(out, format=img.format or "PNG")
                    data = out.getvalue()
            except Exception as pe:
                logger.error(f"Pillow image processing failed: {pe}")

        file_id = uuid.uuid4().hex
        path = f"{APP_NAME}/uploads/{user['id']}/{file_id}.{ext}"
        content_type = file.content_type or {
            "png": "image/png", 
            "jpg": "image/jpeg", 
            "jpeg": "image/jpeg", 
            "gif": "image/gif", 
            "webp": "image/webp",
            "mp4": "video/mp4",
            "webm": "video/webm"
        }.get(ext, "application/octet-stream")
        try:
            result = put_object(path, data, content_type)
        except Exception as e:
            logger.exception("upload failed")
            raise HTTPException(500, f"Storage failed: {e}")
        await db.files.insert_one({
            "id": file_id,
            "storage_path": result["path"],
            "owner_id": user["id"],
            "kind": kind,
            "original_filename": file.filename,
            "content_type": content_type,
            "size": result.get("size", len(data)),
            "is_deleted": False,
            "created_at": now_iso(),
        })
        public_url = f"/api/files/{file_id}"
        return {"id": file_id, "url": public_url, "path": result["path"]}

    @api.get("/files/{file_id}")
    async def serve_file(file_id: str):
        rec = await db.files.find_one({"id": file_id, "is_deleted": False}, {"_id": 0})
        if not rec:
            raise HTTPException(404, "File not found")
        data, ct = get_object(rec["storage_path"])
        return Response(content=data, media_type=rec.get("content_type", ct))

    # ============== DIRECT MESSAGES ==============
    class DMSendIn(BaseModel):
        to_username: str
        content: str = Field(min_length=1, max_length=4000)

    async def _get_or_create_thread(user_a: str, user_b: str):
        key = "::".join(sorted([user_a, user_b]))
        thread = await db.dm_threads.find_one({"key": key}, {"_id": 0})
        if thread:
            return thread
        thread = {
            "id": uuid.uuid4().hex,
            "key": key,
            "participants": sorted([user_a, user_b]),
            "last_message_at": now_iso(),
            "last_message_preview": "",
            "last_sender_id": "",
            "created_at": now_iso(),
            "unread": {user_a: 0, user_b: 0},
        }
        await db.dm_threads.insert_one(thread)
        thread.pop("_id", None)
        return thread

    @api.post("/dms/send")
    async def dm_send(body: DMSendIn, user=Depends(get_current_user)):
        clean_target = body.to_username.lstrip("@").strip()
        recipient = await db.users.find_one({"username_lower": clean_target.lower()}, {"_id": 0, "password_hash": 0})
        if not recipient:
            recipient = await db.users.find_one({"username": {"$regex": f"^{re.escape(clean_target)}$", "$options": "i"}}, {"_id": 0, "password_hash": 0})
        if not recipient:
            raise HTTPException(404, f"Utilizatorul @{clean_target} nu a fost găsit.")
        if recipient["id"] == user["id"]:
            raise HTTPException(400, "Nu îți poți trimite un mesaj privat ție însuți.")
        thread = await _get_or_create_thread(user["id"], recipient["id"])
        msg_id = uuid.uuid4().hex
        msg = {
            "id": msg_id,
            "thread_id": thread["id"],
            "sender_id": user["id"],
            "sender_username": user["username"],
            "recipient_id": recipient["id"],
            "content": body.content,
            "created_at": now_iso(),
            "read": False,
        }
        await db.dms.insert_one(msg)
        unread = thread.get("unread", {})
        unread[recipient["id"]] = unread.get(recipient["id"], 0) + 1
        unread[user["id"]] = 0
        await db.dm_threads.update_one(
            {"id": thread["id"]},
            {"$set": {
                "last_message_at": now_iso(),
                "last_message_preview": body.content[:120],
                "last_sender_id": user["id"],
                "unread": unread,
            }},
        )
        # Notification
        await db.notifications.insert_one({
            "id": uuid.uuid4().hex,
            "user_id": recipient["id"],
            "type": "dm",
            "title": f"New message from {user['username']}",
            "body": body.content[:120],
            "link": f"/messages/{thread['id']}",
            "read": False,
            "created_at": now_iso(),
        })
        return {"id": msg_id, "thread_id": thread["id"]}

    @api.get("/dms/threads")
    async def list_threads(user=Depends(get_current_user)):
        cursor = db.dm_threads.find({"participants": user["id"]}, {"_id": 0}).sort("last_message_at", -1).limit(100)
        threads = await cursor.to_list(100)
        for t in threads:
            other_id = [p for p in t["participants"] if p != user["id"]][0] if t["participants"] else None
            other = await db.users.find_one({"id": other_id}, {"_id": 0, "password_hash": 0}) if other_id else None
            t["other_user"] = other
            t["unread_count"] = t.get("unread", {}).get(user["id"], 0)
        return threads

    @api.get("/dms/threads/{thread_id}")
    async def get_thread(thread_id: str, user=Depends(get_current_user)):
        thread = await db.dm_threads.find_one({"id": thread_id}, {"_id": 0})
        if not thread or user["id"] not in thread["participants"]:
            raise HTTPException(404, "Thread not found")
        msgs = await db.dms.find({"thread_id": thread_id}, {"_id": 0}).sort("created_at", 1).limit(500).to_list(500)
        # mark read
        unread = thread.get("unread", {})
        unread[user["id"]] = 0
        await db.dm_threads.update_one({"id": thread_id}, {"$set": {"unread": unread}})
        await db.dms.update_many({"thread_id": thread_id, "recipient_id": user["id"], "read": False}, {"$set": {"read": True}})
        other_id = [p for p in thread["participants"] if p != user["id"]][0]
        other = await db.users.find_one({"id": other_id}, {"_id": 0, "password_hash": 0})
        return {"thread": thread, "messages": msgs, "other_user": other}

    @api.post("/dms/threads/{thread_id}/reply")
    async def reply_thread(thread_id: str, body: dict, user=Depends(get_current_user)):
        thread = await db.dm_threads.find_one({"id": thread_id}, {"_id": 0})
        if not thread or user["id"] not in thread["participants"]:
            raise HTTPException(404, "Thread not found")
        other_id = [p for p in thread["participants"] if p != user["id"]][0]
        other = await db.users.find_one({"id": other_id})
        if not other:
            raise HTTPException(404, "Recipient missing")
        return await dm_send(DMSendIn(to_username=other["username"], content=body.get("content", "")), user)

    # ============== NOTIFICATIONS ==============
    @api.get("/notifications")
    async def list_notifications(user=Depends(get_current_user)):
        items = await db.notifications.find({"user_id": user["id"]}, {"_id": 0}).sort("created_at", -1).limit(50).to_list(50)
        unread = await db.notifications.count_documents({"user_id": user["id"], "read": False})
        return {"items": items, "unread": unread}

    @api.post("/notifications/{notif_id}/read")
    async def mark_read(notif_id: str, user=Depends(get_current_user)):
        await db.notifications.update_one({"id": notif_id, "user_id": user["id"]}, {"$set": {"read": True}})
        return {"ok": True}

    @api.post("/notifications/read-all")
    async def mark_all_read(user=Depends(get_current_user)):
        await db.notifications.update_many({"user_id": user["id"]}, {"$set": {"read": True}})
        return {"ok": True}

    # ============== SEARCH ==============
    @api.get("/search")
    async def search(q: str = Query(..., min_length=2), limit: int = 20):
        rx = {"$regex": re.escape(q), "$options": "i"}
        topics = await db.topics.find({"$or": [{"title": rx}]}, {"_id": 0}).limit(limit).to_list(limit)
        posts = await db.posts.find({"content": rx}, {"_id": 0}).limit(limit).to_list(limit)
        users = await db.users.find({"username_lower": {"$regex": q.lower()}}, {"_id": 0, "password_hash": 0}).limit(limit).to_list(limit)
        return {"topics": topics, "posts": posts, "users": users}

    # ============== ADMIN: REORDER BOARDS ==============
    class ReorderIn(BaseModel):
        ordered_ids: List[str]

    @api.post("/admin/boards/reorder")
    async def reorder_boards(body: ReorderIn, admin=Depends(require_admin)):
        for idx, bid in enumerate(body.ordered_ids):
            await db.boards.update_one({"id": bid}, {"$set": {"order": idx}})
        if clear_boards_cache:
            clear_boards_cache()
        return {"ok": True}

    # ============== SSO: DISCORD ==============
    @api.get("/auth/discord/login")
    async def discord_login():
        client_id = os.environ.get("DISCORD_CLIENT_ID")
        redirect_uri = os.environ.get("DISCORD_REDIRECT_URI") or f"{os.environ.get('FRONTEND_URL', '')}/auth/discord/callback"
        if not client_id:
            raise HTTPException(503, "Discord SSO not configured. Admin must add DISCORD_CLIENT_ID + DISCORD_CLIENT_SECRET.")
        scope = "identify email"
        url = (
            f"https://discord.com/oauth2/authorize?client_id={client_id}"
            f"&redirect_uri={urllib.parse.quote(redirect_uri, safe='')}"
            f"&response_type=code&scope={urllib.parse.quote(scope)}"
        )
        return {"url": url}

    @api.post("/auth/discord/callback")
    async def discord_callback(body: dict):
        code = body.get("code")
        if not code:
            raise HTTPException(400, "Missing code")
        client_id = os.environ.get("DISCORD_CLIENT_ID")
        client_secret = os.environ.get("DISCORD_CLIENT_SECRET")
        redirect_uri = os.environ.get("DISCORD_REDIRECT_URI") or f"{os.environ.get('FRONTEND_URL', '')}/auth/discord/callback"
        if not client_id or not client_secret:
            raise HTTPException(503, "Discord SSO not configured")
        try:
            tok = requests.post(
                "https://discord.com/api/oauth2/token",
                data={
                    "client_id": client_id,
                    "client_secret": client_secret,
                    "grant_type": "authorization_code",
                    "code": code,
                    "redirect_uri": redirect_uri,
                },
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                timeout=15,
            )
            tok.raise_for_status()
            access = tok.json()["access_token"]
            me = requests.get("https://discord.com/api/users/@me", headers={"Authorization": f"Bearer {access}"}, timeout=15)
            me.raise_for_status()
            d = me.json()
        except Exception as e:
            raise HTTPException(400, f"Discord auth failed: {e}")
        return await _link_or_create_sso_user(db, "discord", d.get("id"), d.get("username") or d.get("global_name"), d.get("email"), now_iso)

    # ============== SSO: STEAM (OpenID) ==============
    @api.get("/auth/steam/login")
    async def steam_login():
        return_url = os.environ.get("STEAM_RETURN_URL") or f"{os.environ.get('FRONTEND_URL', '')}/auth/steam/callback"
        params = {
            "openid.ns": "http://specs.openid.net/auth/2.0",
            "openid.mode": "checkid_setup",
            "openid.return_to": return_url,
            "openid.realm": os.environ.get("FRONTEND_URL", ""),
            "openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
            "openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select",
        }
        url = "https://steamcommunity.com/openid/login?" + urllib.parse.urlencode(params)
        return {"url": url}

    @api.post("/auth/steam/callback")
    async def steam_callback(body: dict):
        # body is the openid.* params returned to the frontend
        params = dict(body)
        params["openid.mode"] = "check_authentication"
        try:
            r = requests.post("https://steamcommunity.com/openid/login", data=params, timeout=15)
            if "is_valid:true" not in r.text:
                raise HTTPException(400, "Steam auth failed")
            claimed_id = body.get("openid.claimed_id", "")
            m = re.search(r"/openid/id/(\d+)$", claimed_id)
            if not m:
                raise HTTPException(400, "Invalid Steam ID")
            steam_id = m.group(1)
            username = f"steam_{steam_id[-6:]}"
            return await _link_or_create_sso_user(db, "steam", steam_id, username, None, now_iso)
        except HTTPException:
            raise
        except Exception as e:
            raise HTTPException(400, f"Steam auth failed: {e}")

    # ============== PRESENCE / HEARTBEAT ==============
    class HeartbeatReq(BaseModel):
        path: Optional[str] = None

    @api.post("/heartbeat")
    async def heartbeat(req: Request, body: Optional[HeartbeatReq] = None, user=Depends(get_current_user)):
        db = req.app.state.db
        import datetime
        now = datetime.datetime.now(datetime.timezone.utc)
        date_str = now.strftime("%Y-%m-%d")
        
        update_data = {"last_seen": now.isoformat()}
        if body and body.path:
            update_data["current_path"] = body.path
            
        await db.users.update_one({"id": user["id"]}, {"$set": update_data})
        
        await db.idg_online_logs.update_one(
            {"user_id": user["id"], "date": date_str},
            {"$inc": {"seconds": 60}},
            upsert=True
        )
        return {"ok": True, "ts": now.isoformat()}

    @api.get("/posts/recent")
    async def recent_posts(limit: int = 5):
        items = await db.posts.find({"is_approved": {"$ne": False}}, {"_id": 0}).sort("created_at", -1).limit(min(limit, 50)).to_list(min(limit, 50))
        # Hydrate author info AND topic title
        for p in items:
            if p.get("author_id"):
                u = await db.users.find_one({"id": p["author_id"]}, {"_id": 0, "id": 1, "username": 1, "avatar_url": 1, "loadout": 1, "is_vip": 1, "vip_tier": 1, "role": 1, "group_color": 1, "group_name": 1, "group_style": 1})
                p["author"] = u
            if p.get("topic_id"):
                t = await db.topics.find_one({"id": p["topic_id"]}, {"_id": 0, "id": 1, "title": 1, "board_id": 1, "created_at": 1})
                p["topic"] = t
        return items

    @api.get("/presence/online")
    async def users_online():
        cutoff = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
        users = await db.users.find(
            {"last_seen": {"$gt": cutoff}},
            {"_id": 0, "id": 1, "username": 1, "avatar_url": 1, "loadout": 1, "is_vip": 1, "vip_tier": 1, "role": 1, "last_seen": 1, "current_path": 1}
        ).sort("last_seen", -1).limit(200).to_list(200)
        return {"count": len(users), "users": users}

    # ============== TYPING INDICATOR (DM) ==============
    @api.post("/dms/threads/{thread_id}/typing")
    async def set_typing(thread_id: str, user=Depends(get_current_user)):
        thread = await db.dm_threads.find_one({"id": thread_id}, {"_id": 0})
        if not thread or user["id"] not in thread["participants"]:
            raise HTTPException(404, "Thread not found")
        typing = thread.get("typing") or {}
        typing[user["id"]] = datetime.now(timezone.utc).timestamp()
        await db.dm_threads.update_one({"id": thread_id}, {"$set": {"typing": typing}})
        return {"ok": True}

    @api.get("/dms/threads/{thread_id}/typing")
    async def get_typing(thread_id: str, user=Depends(get_current_user)):
        thread = await db.dm_threads.find_one({"id": thread_id}, {"_id": 0})
        if not thread or user["id"] not in thread["participants"]:
            raise HTTPException(404, "Thread not found")
        typing = thread.get("typing") or {}
        now_ts = datetime.now(timezone.utc).timestamp()
        # 5 second TTL — only count "other party" typing
        active = []
        for uid, ts in typing.items():
            if uid != user["id"] and (now_ts - float(ts)) < 5:
                active.append(uid)
        return {"typing_user_ids": active}

    return api


# Helper that creates or links an SSO user, returns JWT token
async def _link_or_create_sso_user(db, provider: str, provider_id: str, username: str, email: Optional[str], now_iso):
    import jwt as _jwt
    from datetime import datetime as _dt, timezone as _tz, timedelta as _td

    sso_key = f"{provider}:{provider_id}"
    user = await db.users.find_one({"sso_id": sso_key})
    if not user and email:
        user = await db.users.find_one({"email": email.lower()})
        if user:
            await db.users.update_one({"id": user["id"]}, {"$set": {"sso_id": sso_key, "sso_provider": provider}})
    if not user:
        # Create new
        base = re.sub(r"[^a-zA-Z0-9_]", "", username or f"user{provider_id[-6:]}")[:20] or f"user{provider_id[-6:]}"
        candidate = base
        i = 1
        while await db.users.find_one({"username_lower": candidate.lower()}):
            candidate = f"{base}{i}"
            i += 1
        new_user = {
            "id": uuid.uuid4().hex,
            "email": email.lower() if email else f"{candidate.lower()}@{provider}.sso",
            "username": candidate,
            "username_lower": candidate.lower(),
            "password_hash": "!sso",
            "role": "user",
            "is_vip": False,
            "vip_tier": None,
            "vip_until": None,
            "points": 0,
            "message_count": 0,
            "reaction_score": 0,
            "banned": False,
            "avatar_url": "",
            "bio": f"Conectat prin {provider.capitalize()}",
            "location": "",
            "signature": "",
            "favorite_game": "cs2",
            "favorite_weapon": "AK-47",
            "joined_at": now_iso(),
            "last_seen": now_iso(),
            "sso_id": sso_key,
            "sso_provider": provider,
            "loadout": {"username_style": "default", "avatar_frame": "none", "banner_badge": "none", "card_background": "default", "favorite_weapon": "AK-47"},
        }
        await db.users.insert_one(new_user)
        user = new_user

    payload = {
        "sub": user["id"],
        "exp": _dt.now(_tz.utc) + _td(hours=24 * 7),
        "iat": _dt.now(_tz.utc),
    }
    token = _jwt.encode(payload, os.environ["JWT_SECRET"], algorithm="HS256")
    clean = {k: v for k, v in user.items() if k not in ("password_hash", "_id")}
    return {"token": token, "user": clean}


# ============== Monthly winner cron ==============
async def auto_archive_cron(db, now_iso):
    """Background task: every hour, archive topics that have been inactive too long."""
    while True:
        try:
            settings = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
            days = int(settings.get("archive_after_days") or 0)
            if days > 0:
                cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
                result = await db.topics.update_many(
                    {"last_post_at": {"$lt": cutoff}, "archived": {"$ne": True}, "pinned": {"$ne": True}},
                    {"$set": {"archived": True, "archived_at": now_iso()}},
                )
                if result.modified_count:
                    logger.info(f"Auto-archived {result.modified_count} topics older than {days}d")
        except Exception as e:
            logger.exception(f"auto-archive cron error: {e}")
        await asyncio.sleep(3600)


async def monthly_winner_cron(db, now_iso, award_points):
    """Background task: every minute, check if it's day 1 00:00-00:10 UTC and award last month's winner if not done."""
    while True:
        try:
            now = datetime.now(timezone.utc)
            if now.day == 1 and now.hour == 0 and now.minute < 10:
                # Award for previous month
                prev = (now.replace(day=1) - timedelta(days=1))
                month_key = prev.strftime("%Y-%m")
                already = await db.monthly_winners.find_one({"month_key": month_key})
                if not already:
                    pipeline = [
                        {"$match": {"month_key": month_key}},
                        {"$group": {"_id": "$user_id", "monthly_points": {"$sum": "$amount"}}},
                        {"$sort": {"monthly_points": -1}},
                        {"$limit": 1},
                    ]
                    top = await db.points_log.aggregate(pipeline).to_list(1)
                    if top:
                        winner_id = top[0]["_id"]
                        settings = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
                        bonus = settings.get("monthly_winner_points", 500)
                        await award_points(winner_id, bonus, f"Monthly winner {month_key}")
                        await db.users.update_one({"id": winner_id}, {"$set": {"is_vip": True, "vip_tier": "monthly_winner"}})
                        await db.monthly_winners.insert_one({
                            "id": uuid.uuid4().hex,
                            "month_key": month_key,
                            "user_id": winner_id,
                            "monthly_points": top[0]["monthly_points"],
                            "awarded_at": now_iso(),
                        })
                        await db.notifications.insert_one({
                            "id": uuid.uuid4().hex,
                            "user_id": winner_id,
                            "type": "monthly_winner",
                            "title": f"🏆 You won {month_key}!",
                            "body": f"+{bonus} bonus points + VIP",
                            "link": "/leaderboard",
                            "read": False,
                            "created_at": now_iso(),
                        })
                        logger.info(f"Monthly winner crowned for {month_key}: {winner_id}")
        except Exception as e:
            logger.exception(f"monthly cron error: {e}")
        await asyncio.sleep(300)  # check every 5 min
