"""
IDG Admin Extension — Indungi Romania Admin Panel (Phase 1).

Provides the following modules under /api/idg/*:
- Pages (CRUD with WYSIWYG content, SEO, status, slug)
- Groups (granular permissions, color/label, members)
- Group Legend (display config)
- Applications (toggle modules on/off)
- Appearance (logo, favicon, colors, fonts, custom CSS/JS)
- Extended Settings (SEO, social, email SMTP, security, maintenance)
- Members extended (suspend, ban, password reset, avatar from admin)

All classes prefixed with `IDG` and all data persisted in MongoDB.
"""
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional, Dict, Any
from datetime import datetime, timezone
import uuid
import re
import logging

logger = logging.getLogger("idg_admin")

# ---------------- IDG MODELS ----------------

class IDGPage(BaseModel):
    model_config = ConfigDict(extra="ignore")
    id: Optional[str] = None
    title: str = Field(min_length=1, max_length=200)
    slug: str = Field(min_length=1, max_length=200)
    template: str = "default"  # default, full-width, sidebar-right, sidebar-left
    content: str = ""  # HTML from WYSIWYG
    excerpt: str = ""
    status: str = "draft"  # draft, public, private
    meta_title: str = ""
    meta_description: str = ""
    meta_keywords: str = ""
    visibility_roles: List[str] = Field(default_factory=lambda: ["user", "mod", "admin", "root"])
    show_in_nav: bool = False
    order: int = 0


class IDGGroup(BaseModel):
    model_config = ConfigDict(extra="ignore")
    id: Optional[str] = None
    name: str = Field(min_length=1, max_length=60)
    slug: str = Field(min_length=1, max_length=60)
    description: str = ""
    color: str = "#71717a"
    label_icon: str = "users"
    group_style: Optional[str] = ""
    permissions: Dict[str, bool] = Field(default_factory=dict)
    # Permission keys: forum.read, forum.post, forum.delete_own, forum.delete_any,
    #                  moderation.approve, moderation.warn, admin.access, admin.users,
    #                  vip.features, etc.
    show_in_legend: bool = True
    legend_order: int = 0
    is_system: bool = False  # built-in groups can't be deleted
    parent_id: Optional[str] = None
    category: str = "Altele"


class IDGGroupAssign(BaseModel):
    user_id: str
    group_ids: List[str]


class IDGApplication(BaseModel):
    model_config = ConfigDict(extra="ignore")
    id: Optional[str] = None
    key: str  # forum, shop, vip, analytics, livechat, sso_discord, sso_steam
    name: str
    description: str = ""
    version: str = "1.0.0"
    status: str = "enabled"  # enabled, disabled, beta
    settings: Dict[str, Any] = Field(default_factory=dict)
    required_role: str = "admin"


class IDGAppearance(BaseModel):
    model_config = ConfigDict(extra="ignore")
    logo_url: Optional[str] = None
    logo_dark_url: Optional[str] = None
    favicon_url: Optional[str] = None
    background_url: Optional[str] = None
    color_primary: str = "#FF4F00"
    color_secondary: str = "#00F0FF"
    color_accent: str = "#FFD700"
    color_bg: str = "#050505"
    color_text: str = "#FFFFFF"
    color_link: str = "#FF4F00"
    font_primary: str = "Rajdhani"
    font_secondary: str = "IBM Plex Sans"
    base_font_size: int = 14
    custom_css: str = ""
    custom_js: str = ""
    header_tagline: str = ""
    header_social_facebook: str = "https://facebook.com"
    header_social_twitter: str = "https://twitter.com"
    header_social_youtube: str = "https://youtube.com"
    header_social_instagram: str = "https://instagram.com"
    header_social_twitch: str = "https://twitch.tv"
    header_laser_scanner: bool = True
    header_logo_height: int = 48
    header_logo_width: int = 160
    header_logo_text: str = "INDUNGI"
    header_logo_type: str = "geometric"




class IDGExtendedSettings(BaseModel):
    model_config = ConfigDict(extra="ignore")
    # SEO
    meta_title: Optional[str] = None
    meta_description: Optional[str] = None
    meta_keywords: Optional[str] = None
    google_analytics_id: Optional[str] = None
    # Social
    social_facebook: Optional[str] = None
    social_twitter: Optional[str] = None
    social_instagram: Optional[str] = None
    social_discord: Optional[str] = None
    social_youtube: Optional[str] = None
    social_tiktok: Optional[str] = None
    # Email SMTP
    smtp_host: Optional[str] = None
    smtp_port: Optional[int] = None
    smtp_username: Optional[str] = None
    smtp_password: Optional[str] = None
    smtp_encryption: Optional[str] = None  # tls, ssl, none
    smtp_from_email: Optional[str] = None
    smtp_from_name: Optional[str] = None
    # Security
    allow_registration: Optional[bool] = None
    require_email_confirmation: Optional[bool] = None
    csrf_protection: Optional[bool] = None
    rate_limit_enabled: Optional[bool] = None
    max_login_attempts: Optional[int] = None
    login_block_duration_min: Optional[int] = None
    # Advanced
    maintenance_mode: Optional[bool] = None
    maintenance_message: Optional[str] = None
    cache_enabled: Optional[bool] = None
    minify_assets: Optional[bool] = None
    # Forum Settings (IPS style)
    forums_rss: Optional[bool] = None
    forums_default_view: Optional[str] = None
    forums_fluid_pinned: Optional[bool] = None
    forums_default_view_choose: Optional[List[str]] = None
    forums_topics_per_page: Optional[int] = None
    forums_view_list_method: Optional[str] = None
    forums_view_list_choose: Optional[bool] = None
    forums_questions_downvote: Optional[bool] = None
    forums_answers_downvote: Optional[bool] = None
    forums_new_questions: Optional[str] = None
    forums_popular_now_posts: Optional[int] = None
    forums_popular_now_minutes: Optional[int] = None
    forums_posts_per_page: Optional[int] = None
    forums_topics_show_meta_time: Optional[bool] = None
    forums_topics_show_meta_moderation: Optional[bool] = None
    forums_mod_actions_anon: Optional[bool] = None
    forums_solved_topic_reengage: Optional[int] = None
    forums_topic_activity_desktop: Optional[bool] = None
    forums_topic_activity_mobile: Optional[bool] = None
    forums_topic_activity_desktop_pos: Optional[str] = None
    forums_topics_activity_pages_show: Optional[int] = None
    forums_topic_activity_features: Optional[List[str]] = None


class IDGAdminMemberUpdate(BaseModel):
    model_config = ConfigDict(extra="ignore")
    role: Optional[str] = None
    is_vip: Optional[bool] = None
    vip_tier: Optional[str] = None
    vip_expires_at: Optional[str] = None
    vip_duration_days: Optional[int] = None
    vip_purchase_count: Optional[int] = None
    points: Optional[int] = None
    banned: Optional[bool] = None
    suspended_until: Optional[str] = None
    username: Optional[str] = None
    email: Optional[str] = None
    bio: Optional[str] = None
    avatar_url: Optional[str] = None
    banner_url: Optional[str] = None
    group_ids: Optional[List[str]] = None
    new_password: Optional[str] = None  # admin-initiated reset
    steam_id: Optional[str] = None
    discord_username: Optional[str] = None
    cs2_rank: Optional[str] = None
    favorite_game: Optional[str] = None


class IDGSavedAction(BaseModel):
    model_config = ConfigDict(extra="ignore")
    id: Optional[str] = None
    name: str
    reply_content: Optional[str] = ""
    lock_topic: Optional[bool] = False
    move_to_board_id: Optional[str] = ""


class IDGWarnIn(BaseModel):
    user_id: str
    reason: str = Field(min_length=3, max_length=500)
    points: int = Field(default=1, ge=1, le=5)


class IDGSubscription(BaseModel):
    subscription: Dict[str, Any]


class IDGRssFeed(BaseModel):
    model_config = ConfigDict(extra="ignore")
    id: Optional[str] = None
    feed_url: str
    target_board_id: str
    author_id: Optional[str] = "system"



# ---------------- DEFAULT DATA SEEDS ----------------

DEFAULT_GROUPS = [
    {"name": "Root", "slug": "root", "description": "Super-administrator", "color": "#A855F7", "label_icon": "crown",
     "permissions": {"forum.read": True, "forum.post": True, "forum.delete_any": True, "moderation.approve": True,
                     "moderation.warn": True, "admin.access": True, "admin.users": True, "admin.settings": True, "vip.features": True},
     "show_in_legend": True, "legend_order": 1, "is_system": True},
    {"name": "Administrator", "slug": "admin", "description": "Administrator total", "color": "#DC2626", "label_icon": "shield",
     "permissions": {"forum.read": True, "forum.post": True, "forum.delete_any": True, "moderation.approve": True,
                     "moderation.warn": True, "admin.access": True, "admin.users": True, "admin.settings": True, "vip.features": True},
     "show_in_legend": True, "legend_order": 2, "is_system": True},
    {"name": "Moderator", "slug": "mod", "description": "Echipa de moderare", "color": "#2563EB", "label_icon": "shield-check",
     "permissions": {"forum.read": True, "forum.post": True, "forum.delete_any": True,
                     "moderation.approve": True, "moderation.warn": True, "admin.access": False},
     "show_in_legend": True, "legend_order": 3, "is_system": True},
    {"name": "VIP", "slug": "vip", "description": "Membri VIP", "color": "#FF4F00", "label_icon": "crown",
     "permissions": {"forum.read": True, "forum.post": True, "vip.features": True},
     "show_in_legend": True, "legend_order": 4, "is_system": True},
    {"name": "Member", "slug": "member", "description": "Membri obișnuiți", "color": "#71717A", "label_icon": "user",
     "permissions": {"forum.read": True, "forum.post": True},
     "show_in_legend": True, "legend_order": 5, "is_system": True},
]

DEFAULT_APPS = [
    {"key": "forum", "name": "Forum", "description": "Sistem forum cu sub-boards & topic-uri", "version": "2.0.0", "status": "enabled"},
    {"key": "downloads", "name": "Downloads & Resurse", "description": "Hub complet de descărcare kituri CS, CFG-uri, hărți, pluginuri și modele", "version": "2.0.0", "status": "enabled"},
    {"key": "vip", "name": "VIP", "description": "Sistem VIP cu loadout customizat", "version": "1.5.0", "status": "enabled"},
    {"key": "messaging", "name": "Direct Messages", "description": "Sistem privat de mesagerie", "version": "1.2.0", "status": "enabled"},
    {"key": "trophies", "name": "Trophies", "description": "Sistem trofee & milestones", "version": "1.0.0", "status": "enabled"},
    {"key": "shop", "name": "Shop", "description": "Magazin pentru rame VIP & boost-uri (puncte)", "version": "0.9.0", "status": "beta"},
    {"key": "analytics", "name": "Analytics", "description": "Google Analytics & on-site tracking", "version": "1.0.0", "status": "disabled"},
    {"key": "livechat", "name": "Live Chat", "description": "Chat live pe forum (Discord/Twitch widget)", "version": "0.5.0", "status": "disabled"},
    {"key": "chatbox", "name": "Chatbox (Rooms)", "description": "Sistem de Chatbox avansat cu camere, permisiuni și asistent AI", "version": "2.0.0", "status": "enabled"},
    {"key": "sso_discord", "name": "Discord SSO", "description": "Login cu Discord OAuth", "version": "1.0.0", "status": "enabled"},
    {"key": "sso_steam", "name": "Steam SSO", "description": "Login cu Steam OpenID", "version": "1.0.0", "status": "enabled"},
]


# ---------------- ROUTER BUILDER ----------------

def build_idg_router(db, get_current_user, require_admin, now_iso, hash_password, clear_groups_cache=None):
    """Build the /idg admin router (mounted under /api/idg/*)."""
    api = APIRouter(prefix="/idg")

    # ---- Helper: slugify ----
    def _slugify(s: str) -> str:
        s = s.lower().strip()
        s = re.sub(r"[^a-z0-9]+", "-", s)
        return s.strip("-")[:80] or uuid.uuid4().hex[:8]

    # ==================== PAGES ====================
    @api.get("/pages")
    async def list_pages(status: Optional[str] = None, admin=Depends(require_admin)):
        q = {} if not status else {"status": status}
        items = await db.idg_pages.find(q, {"_id": 0}).sort([("order", 1), ("created_at", -1)]).limit(500).to_list(500)
        return items

    @api.get("/pages/public")
    async def list_public_pages():
        items = await db.idg_pages.find({"status": "public"}, {"_id": 0, "content": 0}).sort("order", 1).to_list(200)
        return items

    @api.get("/pages/by-slug/{slug}")
    async def get_page_by_slug(slug: str):
        page = await db.idg_pages.find_one({"slug": slug}, {"_id": 0})
        if not page:
            raise HTTPException(404, "Page not found")
        if page.get("status") != "public":
            raise HTTPException(404, "Page not available")
        return page

    @api.post("/pages")
    async def create_page(body: IDGPage, admin=Depends(require_admin)):
        slug = _slugify(body.slug or body.title)
        if await db.idg_pages.find_one({"slug": slug}):
            raise HTTPException(400, "Slug already in use")
        doc = body.model_dump()
        doc["id"] = uuid.uuid4().hex
        doc["slug"] = slug
        doc["created_at"] = now_iso()
        doc["updated_at"] = now_iso()
        doc["author_id"] = admin["id"]
        await db.idg_pages.insert_one(doc)
        doc.pop("_id", None)
        return doc

    @api.patch("/pages/{page_id}")
    async def update_page(page_id: str, body: IDGPage, admin=Depends(require_admin)):
        updates = {k: v for k, v in body.model_dump().items() if v is not None and k != "id"}
        if "slug" in updates:
            updates["slug"] = _slugify(updates["slug"])
            other = await db.idg_pages.find_one({"slug": updates["slug"], "id": {"$ne": page_id}})
            if other:
                raise HTTPException(400, "Slug already in use")
        updates["updated_at"] = now_iso()
        result = await db.idg_pages.update_one({"id": page_id}, {"$set": updates})
        if result.matched_count == 0:
            raise HTTPException(404, "Page not found")
        return await db.idg_pages.find_one({"id": page_id}, {"_id": 0})

    @api.delete("/pages/{page_id}")
    async def delete_page(page_id: str, admin=Depends(require_admin)):
        result = await db.idg_pages.delete_one({"id": page_id})
        if result.deleted_count == 0:
            raise HTTPException(404, "Page not found")
        return {"ok": True}

    # ==================== GROUPS ====================
    @api.get("/groups")
    async def list_groups(admin=Depends(require_admin)):
        items = await db.idg_groups.find({}, {"_id": 0}).sort("legend_order", 1).to_list(200)
        
        # Pre-fetch role counts efficiently
        role_counts = {}
        for role in ["root", "admin", "mod", "user"]:
            role_counts[role] = await db.users.count_documents({"role": role})
        
        # VIP users who are not staff
        vip_non_staff_count = await db.users.count_documents({
            "is_vip": True,
            "role": "user"
        })
        # Regular members = user role AND is_vip is False
        regular_member_count = await db.users.count_documents({
            "role": "user",
            "is_vip": False
        })

        for g in items:
            slug = g.get("slug", "")
            # Check explicit group_ids first
            explicit = await db.users.count_documents({"group_ids": g["id"]})
            if explicit > 0:
                g["member_count"] = explicit
            elif slug == "root":
                g["member_count"] = role_counts.get("root", 0)
            elif slug == "admin":
                g["member_count"] = role_counts.get("admin", 0)
            elif slug == "mod":
                g["member_count"] = role_counts.get("mod", 0)
            elif slug == "vip":
                g["member_count"] = vip_non_staff_count
            elif slug == "member":
                g["member_count"] = regular_member_count
            else:
                g["member_count"] = explicit
        return items

    @api.get("/groups/legend")
    async def public_legend():
        items = await db.idg_groups.find(
            {"show_in_legend": True},
            {"_id": 0, "id": 1, "name": 1, "slug": 1, "color": 1, "label_icon": 1, "legend_order": 1, "description": 1, "parent_id": 1, "category": 1}
        ).sort("legend_order", 1).to_list(50)
        return items

    @api.post("/groups")
    async def create_group(body: IDGGroup, admin=Depends(require_admin)):
        slug = _slugify(body.slug or body.name)
        if await db.idg_groups.find_one({"slug": slug}):
            raise HTTPException(400, "Slug already in use")
        doc = body.model_dump()
        doc["id"] = uuid.uuid4().hex
        doc["slug"] = slug
        doc["is_system"] = False
        doc["created_at"] = now_iso()
        await db.idg_groups.insert_one(doc)
        doc.pop("_id", None)
        if clear_groups_cache:
            clear_groups_cache()
        return doc

    @api.patch("/groups/{group_id}")
    @api.put("/groups/{group_id}")
    async def update_group(group_id: str, body: IDGGroup, admin=Depends(require_admin)):
        existing = await db.idg_groups.find_one({"id": group_id})
        if not existing:
            raise HTTPException(404, "Group not found")
        updates = {k: v for k, v in body.model_dump().items() if v is not None and k != "id"}
        if existing.get("is_system") and admin.get("role") != "root":
            # System groups: non-root admins can only edit limited fields
            allowed = {"color", "label_icon", "description", "show_in_legend", "legend_order", "permissions"}
            updates = {k: v for k, v in updates.items() if k in allowed}
        updates["updated_at"] = now_iso()
        await db.idg_groups.update_one({"id": group_id}, {"$set": updates})
        if clear_groups_cache:
            clear_groups_cache()
        return await db.idg_groups.find_one({"id": group_id}, {"_id": 0})

    @api.delete("/groups/{group_id}")
    async def delete_group(group_id: str, admin=Depends(require_admin)):
        g = await db.idg_groups.find_one({"id": group_id})
        if not g:
            raise HTTPException(404, "Group not found")
        if g.get("is_system") and admin.get("role") != "root":
            raise HTTPException(403, "System groups cannot be deleted")
        await db.idg_groups.delete_one({"id": group_id})
        # remove from users
        await db.users.update_many({"group_ids": group_id}, {"$pull": {"group_ids": group_id}})
        if clear_groups_cache:
            clear_groups_cache()
        return {"ok": True}

    @api.post("/groups/assign")
    async def assign_groups(body: IDGGroupAssign, admin=Depends(require_admin)):
        await db.users.update_one({"id": body.user_id}, {"$set": {"group_ids": body.group_ids}})
        if clear_groups_cache:
            clear_groups_cache()
        return {"ok": True}

    @api.post("/groups/upload-icon")
    async def upload_group_icon(file: UploadFile = File(...), admin=Depends(require_admin)):
        import os
        import uuid
        from forum_ext import put_object, APP_NAME
        
        ext = (file.filename or "bin").rsplit(".", 1)[-1].lower()
        allowed_exts = {"png", "jpg", "jpeg", "gif", "webp"}
        if ext not in allowed_exts:
            raise HTTPException(400, "Only image formats allowed (png/jpg/jpeg/gif/webp)")
            
        data = await file.read()
        file_id = uuid.uuid4().hex
        
        path = f"{APP_NAME}/uploads/groups/{file_id}.{ext}"
        content_type = file.content_type or f"image/{ext}"
        
        try:
            result = put_object(path, data, content_type)
        except Exception as e:
            raise HTTPException(500, f"Storage failed: {e}")
            
        await db.files.insert_one({
            "id": file_id,
            "storage_path": result["path"],
            "owner_id": admin["id"],
            "kind": "group_icon",
            "original_filename": file.filename,
            "content_type": content_type,
            "size": len(data),
            "is_deleted": False,
            "created_at": now_iso(),
        })
        
        return {"url": f"/api/files/{file_id}"}


    # ==================== APPLICATIONS ====================
    @api.get("/apps")
    async def list_apps(admin=Depends(require_admin)):
        for default_app in DEFAULT_APPS:
            existing = await db.idg_apps.find_one({"key": default_app["key"]})
            if not existing:
                doc = dict(default_app)
                doc["id"] = uuid.uuid4().hex
                doc["created_at"] = now_iso()
                doc["updated_at"] = now_iso()
                await db.idg_apps.insert_one(doc)

        items = await db.idg_apps.find({}, {"_id": 0}).sort("key", 1).to_list(50)
        # Auto-repair: ensure every app has an 'id' field
        for item in items:
            if not item.get("id"):
                new_id = uuid.uuid4().hex
                await db.idg_apps.update_one({"key": item["key"]}, {"$set": {"id": new_id}})
                item["id"] = new_id
        return items

    @api.post("/import-archive-data")
    async def import_archive_data(data: dict):
        archive_root_id = uuid.uuid4().hex
        await db.idg_boards.update_one(
            {"name": "Arhiva 2019"},
            {"$setOnInsert": {
                "id": archive_root_id,
                "name": "Arhiva 2019",
                "description": "Recuperat din Web Archive",
                "game": "general",
                "icon": "archive",
                "order": 999,
                "parent_id": None,
                "min_role": "user",
                "vip_only": False,
                "archived": True
            }},
            upsert=True
        )
        root_board = await db.idg_boards.find_one({"name": "Arhiva 2019"})
        parent_id = root_board["id"]
        
        for i, forum in enumerate(data.get("forums", [])):
            name = forum.get("name", "").strip()
            if name.startswith("["): name = name.replace("[", "").strip()
            await db.idg_boards.update_one(
                {"name": name, "parent_id": parent_id},
                {"$setOnInsert": {
                    "id": uuid.uuid4().hex, "name": name, "description": f"Archived: {forum.get('url')}",
                    "game": "general", "icon": "folder", "order": i, "parent_id": parent_id,
                    "min_role": "user", "vip_only": False, "archived": True
                }}, upsert=True
            )
        
        for topic in data.get("topics", []):
            name = topic.get("name", "").strip()
            await db.idg_topics.update_one(
                {"title": name},
                {"$setOnInsert": {
                    "id": uuid.uuid4().hex, "board_id": parent_id, "title": name,
                    "author_id": "system", "author_username": "ArchiveBot", "content": f"Topic arhiva: {topic.get('url')}",
                    "created_at": now_iso(), "updated_at": now_iso(), "pinned": False, "locked": True, "archived": True, "views": 0, "reply_count": 0, "reactions": {}
                }}, upsert=True
            )
            
        for member in data.get("members", []):
            username = member.get("name", "").strip()
            if not username: continue
            await db.idg_users.update_one(
                {"username": username},
                {"$setOnInsert": {
                    "id": uuid.uuid4().hex, "username": username, "email": f"{username.lower().replace(' ', '')}@archive.local",
                    "password_hash": "ARCHIVED", "role": "user", "points": 0,
                    "created_at": now_iso(), "bio": f"Recuperat: {member.get('url')}", "is_vip": False
                }}, upsert=True
            )
        return {"status": "ok", "imported": True}

    @api.get("/apps/public/{key}")
    async def app_public_status(key: str):
        a = await db.idg_apps.find_one({"key": key}, {"_id": 0, "key": 1, "status": 1, "name": 1})
        return a or {"key": key, "status": "disabled"}

    @api.patch("/apps/{app_id}")
    async def update_app(app_id: str, body: IDGApplication, admin=Depends(require_admin)):
        updates = {k: v for k, v in body.model_dump().items() if v is not None and k != "id"}
        updates["updated_at"] = now_iso()
        # Try by id first, fallback to key
        result = await db.idg_apps.update_one({"id": app_id}, {"$set": updates})
        if result.matched_count == 0:
            result = await db.idg_apps.update_one({"key": app_id}, {"$set": updates})
        if result.matched_count == 0:
            raise HTTPException(404, "App not found")
        a = await db.idg_apps.find_one({"$or": [{"id": app_id}, {"key": app_id}]}, {"_id": 0})
        return a

    @api.post("/apps/{app_id}/toggle")
    async def toggle_app(app_id: str, admin=Depends(require_admin)):
        # Try by id first, fallback to key
        a = await db.idg_apps.find_one({"$or": [{"id": app_id}, {"key": app_id}]})
        if not a:
            raise HTTPException(404, "App not found")
        new_status = "disabled" if a.get("status") == "enabled" else "enabled"
        await db.idg_apps.update_one({"_id": a["_id"]}, {"$set": {"status": new_status, "updated_at": now_iso()}})
        return {"id": a.get("id", app_id), "status": new_status}

    # ==================== APPEARANCE ====================
    @api.get("/appearance")
    async def get_appearance():
        doc = await db.idg_appearance.find_one({"id": "global"}, {"_id": 0})
        if not doc:
            doc = IDGAppearance().model_dump()
            doc["id"] = "global"
            await db.idg_appearance.insert_one(doc)
            doc.pop("_id", None)
        else:
            default_doc = IDGAppearance().model_dump()
            for k, v in default_doc.items():
                doc.setdefault(k, v)
        return doc


    @api.patch("/appearance")
    async def update_appearance(body: IDGAppearance, admin=Depends(require_admin)):
        updates = {k: v for k, v in body.model_dump().items() if v is not None}
        updates["updated_at"] = now_iso()
        await db.idg_appearance.update_one({"id": "global"}, {"$set": updates}, upsert=True)
        return await db.idg_appearance.find_one({"id": "global"}, {"_id": 0})

    # ==================== EXTENDED SETTINGS ====================
    @api.get("/settings/extended")
    async def get_extended_settings():
        doc = await db.idg_settings.find_one({"id": "extended"}, {"_id": 0})
        if not doc:
            doc = IDGExtendedSettings().model_dump()
            doc["id"] = "extended"
            # sane defaults
            doc.setdefault("allow_registration", True)
            doc.setdefault("require_email_confirmation", False)
            doc.setdefault("csrf_protection", True)
            doc.setdefault("rate_limit_enabled", True)
            doc.setdefault("max_login_attempts", 5)
            doc.setdefault("login_block_duration_min", 15)
            doc.setdefault("maintenance_mode", False)
            doc.setdefault("cache_enabled", True)
            doc.setdefault("minify_assets", False)
            await db.idg_settings.insert_one(doc)
            doc.pop("_id", None)
        return doc

    @api.patch("/settings/extended")
    async def update_extended_settings(body: IDGExtendedSettings, admin=Depends(require_admin)):
        updates = {k: v for k, v in body.model_dump().items() if v is not None}
        updates["updated_at"] = now_iso()
        await db.idg_settings.update_one({"id": "extended"}, {"$set": updates}, upsert=True)
        return await db.idg_settings.find_one({"id": "extended"}, {"_id": 0})

    # Public read for the parts the frontend needs (for SEO etc.)
    @api.get("/settings/public")
    async def get_public_extended_settings():
        doc = await db.idg_settings.find_one({"id": "extended"}, {"_id": 0}) or {}
        # Strip sensitive fields
        for k in ["smtp_password", "smtp_username", "smtp_host", "smtp_port", "smtp_encryption"]:
            doc.pop(k, None)
        return doc

    # ==================== MEMBERS (extended) ====================
    @api.get("/members")
    async def list_members(
        q: Optional[str] = None,
        role: Optional[str] = None,
        status: Optional[str] = None,
        group_id: Optional[str] = None,
        limit: int = 100,
        admin=Depends(require_admin),
    ):
        query: Dict[str, Any] = {}
        if q:
            query["$or"] = [
                {"username_lower": {"$regex": q.lower()}},
                {"email": {"$regex": q.lower()}},
            ]
        if role:
            query["role"] = role
        if status == "banned":
            query["banned"] = True
        elif status == "active":
            query["banned"] = {"$ne": True}
        if group_id:
            query["group_ids"] = group_id
        items = await db.users.find(query, {"_id": 0, "password_hash": 0}).sort("joined_at", -1).limit(limit).to_list(limit)
        return items

    @api.patch("/members/{user_id}")
    async def admin_update_member(user_id: str, body: IDGAdminMemberUpdate, admin=Depends(require_admin)):
        updates = {k: v for k, v in body.model_dump().items() if v is not None and k != "new_password"}
        if "username" in updates:
            updates["username_lower"] = updates["username"].lower()
        if "email" in updates:
            updates["email"] = updates["email"].lower()
        # password reset
        if body.new_password:
            updates["password_hash"] = hash_password(body.new_password)
        updates["updated_at"] = now_iso()
        result = await db.users.update_one({"id": user_id}, {"$set": updates})
        if result.matched_count == 0:
            raise HTTPException(404, "User not found")
        return await db.users.find_one({"id": user_id}, {"_id": 0, "password_hash": 0})

    @api.post("/members/{user_id}/suspend")
    async def suspend_member(user_id: str, days: int = 7, admin=Depends(require_admin)):
        until = (datetime.now(timezone.utc) + __import__("datetime").timedelta(days=days)).isoformat()
        await db.users.update_one({"id": user_id}, {"$set": {"suspended_until": until, "banned": True}})
        return {"ok": True, "suspended_until": until}

    @api.post("/members/{user_id}/unban")
    async def unban_member(user_id: str, admin=Depends(require_admin)):
        await db.users.update_one({"id": user_id}, {"$set": {"banned": False, "suspended_until": None}})
        return {"ok": True}

    # ==================== ADMIN DASHBOARD SUMMARY ====================
    @api.get("/dashboard")
    async def admin_dashboard(admin=Depends(require_admin)):
        from datetime import timedelta
        now = datetime.now(timezone.utc)
        last_24 = (now - timedelta(hours=24)).isoformat()
        last_7d = (now - timedelta(days=7)).isoformat()
        # Filter test fixtures out of "real" user counts where useful
        not_test = {"username": {"$not": {"$regex": "^TEST_", "$options": "i"}}}
        return {
            "total_users": await db.users.count_documents(not_test),
            "total_topics": await db.topics.count_documents({}),
            "total_posts": await db.posts.count_documents({}),
            "online_now": await db.users.count_documents({**not_test, "last_seen": {"$gt": (now - timedelta(minutes=15)).isoformat()}}),
            "new_users_24h": await db.users.count_documents({**not_test, "joined_at": {"$gt": last_24}}),
            "new_topics_7d": await db.topics.count_documents({"created_at": {"$gt": last_7d}}),
            "new_posts_7d": await db.posts.count_documents({"created_at": {"$gt": last_7d}}),
            "pending_reports": await db.reports.count_documents({"status": "pending"}),
            "pending_mod_queue": await db.posts.count_documents({"is_approved": False}),
            "vip_count": await db.users.count_documents({**not_test, "is_vip": True}),
            "banned_count": await db.users.count_documents({**not_test, "banned": True}),
        }

    # ==================== LOG VIEWER ====================
    @api.get("/admin/logs")
    async def get_logs(admin=Depends(require_admin)):
        import os
        log_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
        stderr_path = os.path.join(log_dir, "stderr.log")
        system_path = os.path.join(log_dir, "system.log")
        
        stderr_content = ""
        if os.path.exists(stderr_path):
            try:
                with open(stderr_path, "r", encoding="utf-8", errors="ignore") as f:
                    stderr_content = f.read()[-50000:]
            except Exception as e:
                stderr_content = f"Error reading stderr.log: {e}"
                
        system_content = ""
        if os.path.exists(system_path):
            try:
                with open(system_path, "r", encoding="utf-8", errors="ignore") as f:
                    system_content = f.read()[-50000:]
            except Exception as e:
                system_content = f"Error reading system.log: {e}"
                
        return {"stderr": stderr_content, "system": system_content}

    @api.delete("/admin/logs")
    async def clear_logs(admin=Depends(require_admin)):
        import os
        log_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
        for log_name in ["stderr.log", "system.log"]:
            path = os.path.join(log_dir, log_name)
            if os.path.exists(path):
                try:
                    with open(path, "w", encoding="utf-8") as f:
                        f.write("")
                except Exception as e:
                    raise HTTPException(500, f"Failed to clear {log_name}: {e}")
        return {"ok": True}

    # ==================== SAVED ACTIONS ====================
    @api.get("/admin/saved-actions")
    async def list_saved_actions(admin=Depends(require_admin)):
        return await db.idg_saved_actions.find({}, {"_id": 0}).to_list(100)

    @api.post("/admin/saved-actions")
    async def create_saved_action(body: IDGSavedAction, admin=Depends(require_admin)):
        doc = body.model_dump()
        doc["id"] = uuid.uuid4().hex
        doc["created_at"] = now_iso()
        await db.idg_saved_actions.insert_one(doc)
        doc.pop("_id", None)
        return doc

    @api.patch("/admin/saved-actions/{action_id}")
    async def update_saved_action(action_id: str, body: IDGSavedAction, admin=Depends(require_admin)):
        updates = {k: v for k, v in body.model_dump().items() if v is not None and k != "id"}
        await db.idg_saved_actions.update_one({"id": action_id}, {"$set": updates})
        return await db.idg_saved_actions.find_one({"id": action_id}, {"_id": 0})

    @api.delete("/admin/saved-actions/{action_id}")
    async def delete_saved_action(action_id: str, admin=Depends(require_admin)):
        await db.idg_saved_actions.delete_one({"id": action_id})
        return {"ok": True}

    @api.post("/moderation/topics/{topic_id}/apply-action/{action_id}")
    async def apply_saved_action(topic_id: str, action_id: str, user=Depends(get_current_user)):
        if user.get("role") not in ("mod", "admin", "root"):
            raise HTTPException(403, "Moderator only")
        action = await db.idg_saved_actions.find_one({"id": action_id})
        if not action:
            raise HTTPException(404, "Saved action not found")
        topic = await db.topics.find_one({"id": topic_id})
        if not topic:
            raise HTTPException(404, "Topic not found")
        
        if action.get("reply_content"):
            post_id = uuid.uuid4().hex
            post_doc = {
                "id": post_id,
                "topic_id": topic_id,
                "author_id": user["id"],
                "author_username": user["username"],
                "content": action["reply_content"],
                "created_at": now_iso(),
                "reactions": {},
                "is_approved": True
            }
            await db.posts.insert_one(post_doc)
            await db.topics.update_one({"id": topic_id}, {"$inc": {"reply_count": 1}, "$set": {"last_post_at": now_iso(), "last_post_user_id": user["id"]}})
            await db.users.update_one({"id": user["id"]}, {"$inc": {"message_count": 1}})
            
        sets = {}
        if action.get("lock_topic"):
            sets["locked"] = True
        if action.get("move_to_board_id"):
            board = await db.boards.find_one({"id": action["move_to_board_id"]})
            if board:
                sets["board_id"] = action["move_to_board_id"]
                
        if sets:
            sets["updated_at"] = now_iso()
            await db.topics.update_one({"id": topic_id}, {"$set": sets})
            
        return {"ok": True}

    # ==================== WARNINGS ====================
    @api.post("/moderation/warn")
    async def warn_user(body: IDGWarnIn, moderator=Depends(get_current_user)):
        from datetime import timedelta
        if moderator.get("role") not in ("mod", "admin", "root"):
            raise HTTPException(403, "Moderator only")
        user_to_warn = await db.users.find_one({"id": body.user_id})
        if not user_to_warn:
            raise HTTPException(404, "User not found")
        
        warn_id = uuid.uuid4().hex
        warn_doc = {
            "id": warn_id,
            "user_id": body.user_id,
            "moderator_id": moderator["id"],
            "moderator_username": moderator["username"],
            "reason": body.reason,
            "points": body.points,
            "created_at": now_iso()
        }
        await db.idg_warnings.insert_one(warn_doc)
        
        pipeline = [
            {"$match": {"user_id": body.user_id}},
            {"$group": {"_id": "$user_id", "total_points": {"$sum": "$points"}}}
        ]
        res = await db.idg_warnings.aggregate(pipeline).to_list(1)
        total_points = res[0]["total_points"] if res else body.points
        
        susp_until = None
        banned = False
        import datetime as dt_mod
        if total_points >= 5:
            susp_until = (dt_mod.datetime.now(dt_mod.timezone.utc) + timedelta(days=7)).isoformat()
            banned = True
        elif total_points >= 3:
            susp_until = (dt_mod.datetime.now(dt_mod.timezone.utc) + timedelta(days=1)).isoformat()
            banned = True
            
        updates = {"warning_points": total_points}
        if susp_until:
            updates["suspended_until"] = susp_until
            updates["banned"] = banned
            
        await db.users.update_one({"id": body.user_id}, {"$set": updates})
        
        await db.notifications.insert_one({
            "id": uuid.uuid4().hex,
            "user_id": body.user_id,
            "type": "warning",
            "title": f"⚠️ Avertisment primit: {body.points} puncte",
            "body": f"Motiv: {body.reason}. Total puncte: {total_points}",
            "link": "/profile",
            "read": False,
            "created_at": now_iso()
        })
        
        # Trigger Web Push notification if subscribed
        try:
            trigger_push_notification(
                db,
                body.user_id,
                f"⚠️ Avertisment primit: {body.points} puncte",
                f"Motiv: {body.reason}. Total puncte: {total_points}"
            )
        except Exception:
            pass
            
        return {"ok": True, "total_points": total_points, "suspended_until": susp_until}

    @api.get("/moderation/warnings/{user_id}")
    async def list_warnings(user_id: str, user=Depends(get_current_user)):
        if user["id"] != user_id and user.get("role") not in ("mod", "admin", "root"):
            raise HTTPException(403, "Access denied")
        return await db.idg_warnings.find({"user_id": user_id}, {"_id": 0}).sort("created_at", -1).to_list(100)

    # ==================== WEB PUSH NOTIFICATIONS ====================
    @api.get("/push/keys")
    async def get_push_keys():
        import os
        public_key = os.environ.get("VAPID_PUBLIC_KEY") or "BJaMkpKHrEu40KK4fP1HCahq4NIXq8EGsxo5bLkyMYFiuWskfuR9ZRwoCJ8VaavC2FPY8v2LWcPpOIy6Ydnnsks"
        return {"publicKey": public_key}

    @api.post("/push/subscribe")
    async def subscribe_push(body: IDGSubscription, user=Depends(get_current_user)):
        await db.users.update_one(
            {"id": user["id"]},
            {"$set": {"push_subscription": body.subscription}}
        )
        return {"ok": True}

    # ==================== AUTO-RSS IMPORTER CONFIG ====================
    @api.get("/admin/rss-feeds")
    async def list_rss_feeds(admin=Depends(require_admin)):
        return await db.idg_rss_feeds.find({}, {"_id": 0}).to_list(100)

    @api.post("/admin/rss-feeds")
    async def create_rss_feed(body: IDGRssFeed, admin=Depends(require_admin)):
        doc = body.model_dump()
        doc["id"] = uuid.uuid4().hex
        doc["last_checked_guid"] = ""
        doc["created_at"] = now_iso()
        await db.idg_rss_feeds.insert_one(doc)
        doc.pop("_id", None)
        return doc

    @api.delete("/admin/rss-feeds/{feed_id}")
    async def delete_rss_feed(feed_id: str, admin=Depends(require_admin)):
        await db.idg_rss_feeds.delete_one({"id": feed_id})
        return {"ok": True}

    return api


def trigger_push_notification(db, user_id: str, title: str, body_text: str, link: str = ""):
    import asyncio
    try:
        loop = asyncio.get_running_loop()
        loop.create_task(send_web_push_async(db, user_id, title, body_text, link))
    except Exception:
        pass


async def send_web_push_async(db, user_id: str, title: str, body_text: str, link: str):
    import os
    import json
    
    user_doc = await db.users.find_one({"id": user_id})
    if not user_doc or "push_subscription" not in user_doc:
        return
    sub = user_doc["push_subscription"]
    if not sub:
        return
        
    from pywebpush import webpush, WebPushException
    
    vapid_private = os.environ.get("VAPID_PRIVATE_KEY") or "8FC2NrCkSuYJSq_JuLtIFnva-RwZiY3vYEAZnTSCxv4"
    vapid_public = os.environ.get("VAPID_PUBLIC_KEY") or "BJaMkpKHrEu40KK4fP1HCahq4NIXq8EGsxo5bLkyMYFiuWskfuR9ZRwoCJ8VaavC2FPY8v2LWcPpOIy6Ydnnsks"
    
    payload = {
        "title": title,
        "body": body_text,
        "link": link
    }
    
    try:
        webpush(
            subscription_info=sub,
            data=json.dumps(payload),
            vapid_private_key=vapid_private,
            vapid_claims={"sub": "mailto:admin@indungi.pro"},
            timeout=5
        )
    except WebPushException as ex:
        if ex.response and ex.response.status_code in (404, 410):
            await db.users.update_one({"id": user_id}, {"$unset": {"push_subscription": ""}})
    except Exception:
        pass


async def rss_importer_cron(db, now_iso):
    import feedparser
    import asyncio
    import uuid
    import logging
    logger = logging.getLogger("rss_importer")
    
    while True:
        try:
            feeds = await db.idg_rss_feeds.find().to_list(500)
            for feed in feeds:
                try:
                    d = feedparser.parse(feed["feed_url"])
                    if not d.entries:
                        continue
                    
                    entries = list(d.entries)
                    entries.reverse()
                    
                    last_guid = feed.get("last_checked_guid") or ""
                    new_last_guid = last_guid
                    
                    for entry in entries:
                         guid = entry.get("id") or entry.get("link") or ""
                         if not guid:
                             continue
                         
                         if last_guid and guid == last_guid:
                             new_last_guid = guid
                             continue
                             
                         if not last_guid:
                             if entry not in entries[-3:]:
                                 continue
                                 
                         topic_id = uuid.uuid4().hex
                         title = entry.get("title", "RSS Feed Topic")[:150]
                         content = entry.get("summary") or entry.get("description") or entry.get("title", "")
                         content += f"<br/><br/><a href='{entry.get('link')}' target='_blank'>Citește articolul complet</a>"
                         
                         author_id = feed.get("author_id") or "system"
                         author_user = await db.users.find_one({"id": author_id}) or await db.users.find_one({"role": "root"}) or {"username": "System", "id": "system"}
                         
                         topic_doc = {
                             "id": topic_id,
                             "board_id": feed["target_board_id"],
                             "title": title,
                             "author_id": author_user["id"],
                             "author_username": author_user["username"],
                             "content": content,
                             "created_at": now_iso(),
                             "updated_at": now_iso(),
                             "pinned": False,
                             "locked": False,
                             "views": 0,
                             "reply_count": 0,
                             "reactions": {},
                             "last_post_at": now_iso(),
                             "last_post_user_id": author_user["id"]
                         }
                         await db.topics.insert_one(topic_doc)
                         new_last_guid = guid
                         
                    if new_last_guid != last_guid:
                         await db.idg_rss_feeds.update_one({"id": feed["id"]}, {"$set": {"last_checked_guid": new_last_guid}})
                         
                except Exception as fe:
                    logger.error(f"Error parsing feed {feed.get('feed_url')}: {fe}")
        except Exception as e:
            logger.error(f"RSS Importer cron error: {e}")
        await asyncio.sleep(300)



# ==================== SEEDER ====================

async def seed_idg(db, now_iso):
    """Seed default groups & apps if they don't exist."""
    # Default groups
    for g in DEFAULT_GROUPS:
        doc = {**g, "id": uuid.uuid4().hex, "created_at": now_iso()}
        await db.idg_groups.update_one({"slug": g["slug"]}, {"$setOnInsert": doc}, upsert=True)

    # Default apps
    for a in DEFAULT_APPS:
        doc = {**a, "id": uuid.uuid4().hex, "settings": a.get("settings", {}), "required_role": a.get("required_role", "admin"), "created_at": now_iso()}
        await db.idg_apps.update_one({"key": a["key"]}, {"$setOnInsert": doc}, upsert=True)

    # Default appearance
    if not await db.idg_appearance.find_one({"id": "global"}):
        doc = IDGAppearance().model_dump()
        doc["id"] = "global"
        doc["created_at"] = now_iso()
        await db.idg_appearance.insert_one(doc)

    # Default chat room
    if not await db.idg_chat_rooms.find_one({}):
        await db.idg_chat_rooms.insert_one({
            "id": uuid.uuid4().hex,
            "name": "General Chat",
            "description": "Lobby",
            "order": 0,
            "enabled": True,
            "created_at": now_iso()
        })

    # Default extended settings
    if not await db.idg_settings.find_one({"id": "extended"}):
        doc = IDGExtendedSettings().model_dump()
        doc["id"] = "extended"
        doc["allow_registration"] = True
        doc["require_email_confirmation"] = False
        doc["csrf_protection"] = True
        doc["rate_limit_enabled"] = True
        doc["max_login_attempts"] = 5
        doc["login_block_duration_min"] = 15
        doc["maintenance_mode"] = False
        doc["cache_enabled"] = True
        doc["minify_assets"] = False
        # Forum defaults
        doc["forums_rss"] = False
        doc["forums_default_view"] = "table"
        doc["forums_fluid_pinned"] = True
        doc["forums_default_view_choose"] = ["table", "grid", "fluid"]
        doc["forums_topics_per_page"] = 25
        doc["forums_view_list_method"] = "list"
        doc["forums_view_list_choose"] = True
        doc["forums_questions_downvote"] = False
        doc["forums_answers_downvote"] = True
        doc["forums_new_questions"] = "0"
        doc["forums_popular_now_posts"] = 10
        doc["forums_popular_now_minutes"] = 60
        doc["forums_posts_per_page"] = 25
        doc["forums_topics_show_meta_time"] = True
        doc["forums_topics_show_meta_moderation"] = True
        doc["forums_mod_actions_anon"] = False
        doc["forums_solved_topic_reengage"] = 14
        doc["forums_topic_activity_desktop"] = True
        doc["forums_topic_activity_mobile"] = True
        doc["forums_topic_activity_desktop_pos"] = "sidebar"
        doc["forums_topics_activity_pages_show"] = 3
        doc["forums_topic_activity_features"] = ["popularDays", "topPost", "uploads"]
        
        doc["created_at"] = now_iso()
        await db.idg_settings.insert_one(doc)
