"""SimpleX Manager — FastAPI app."""

import asyncio
import json
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path

import mimetypes

from fastapi import FastAPI, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

import db
import profiles as pm

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger(__name__)

BASE = Path(__file__).parent
TEMPLATES = Jinja2Templates(directory=str(BASE / "templates"))

AUTH_TOKEN = os.environ.get("MANAGER_TOKEN", "changeme")


@asynccontextmanager
async def lifespan(app: FastAPI):
    db.init_db()
    for profile in db.list_profiles():
        if profile.get("address"):
            await pm.start_bot(profile, _save_address)
    yield
    for pid in list(pm._running):
        await pm.stop_bot(pid)


app = FastAPI(title="SimpleX Manager", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=str(BASE / "static")), name="static")
# Generated directory-bot websites (one folder per directory bot) — preview/host.
WEB_DIR = BASE.parent / "web"
WEB_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/directory", StaticFiles(directory=str(WEB_DIR), html=True), name="directory")


async def _save_address(profile_id: int, address: str) -> None:
    db.update_address(profile_id, address)


def _check_auth(request: Request) -> bool:
    token = request.cookies.get("token") or request.headers.get("X-Token")
    return token == AUTH_TOKEN


def _require_auth(request: Request) -> None:
    if not _check_auth(request):
        raise HTTPException(status_code=401, detail="Unauthorized")


def _redirect_if_unauth(request: Request):
    if not _check_auth(request):
        return RedirectResponse("/login", status_code=303)
    return None


def _enrich(profiles: list[dict]) -> list[dict]:
    for p in profiles:
        p["running"] = pm.is_running(p["id"])
        p["config"] = json.loads(p.get("config") or "{}")
    return profiles


def _category(bot_type: str) -> str:
    """Which sidebar category a profile belongs to: users / businesses / rss-bots / bots."""
    if bot_type in pm.BUSINESS_TYPES:
        return "businesses"
    if bot_type in pm.RSS_TYPES:
        return "rss-bots"
    if bot_type in pm.USER_TYPES:
        return "users"
    return "bots"


# ── Auth ──────────────────────────────────────────────────────────────────────

@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
    return TEMPLATES.TemplateResponse(request, "login.html")


@app.post("/login")
async def login(request: Request, token: str = Form(...)):
    if token != AUTH_TOKEN:
        return TEMPLATES.TemplateResponse(request, "login.html", {"error": "Invalid token"})
    resp = RedirectResponse("/", status_code=303)
    resp.set_cookie("token", token, httponly=True, samesite="lax")
    return resp


@app.get("/logout")
async def logout():
    resp = RedirectResponse("/login", status_code=303)
    resp.delete_cookie("token")
    return resp


# ── Pages ─────────────────────────────────────────────────────────────────────

@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    return TEMPLATES.TemplateResponse(request, "home.html", {"nav_active": "home"})


@app.get("/users", response_class=HTMLResponse)
async def users_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    items = _enrich([p for p in db.list_profiles() if p["bot_type"] in pm.USER_TYPES])
    return TEMPLATES.TemplateResponse(request, "list.html", {
        "tab": "users", "items": items, "create_types": pm.USER_TYPES,
        "nav_active": "users",
    })


@app.get("/businesses", response_class=HTMLResponse)
async def businesses_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    items = _enrich([p for p in db.list_profiles() if p["bot_type"] in pm.BUSINESS_TYPES])
    return TEMPLATES.TemplateResponse(request, "list.html", {
        "tab": "businesses", "items": items, "create_types": pm.BUSINESS_TYPES,
        "nav_active": "businesses",
    })


@app.get("/bots", response_class=HTMLResponse)
async def bots_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    items = _enrich([p for p in db.list_profiles() if p["bot_type"] in pm.BOT_TYPES])
    return TEMPLATES.TemplateResponse(request, "list.html", {
        "tab": "bots", "items": items, "create_types": pm.BOT_TYPES,
        "nav_active": "bots",
    })


@app.get("/rss-bots", response_class=HTMLResponse)
async def rss_bots_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    items = _enrich([p for p in db.list_profiles() if p["bot_type"] in pm.RSS_TYPES])
    return TEMPLATES.TemplateResponse(request, "list.html", {
        "tab": "rss-bots", "items": items, "create_types": pm.RSS_TYPES,
        "nav_active": "rss-bots",
    })


RELAY_KINDS = {"chat": "Chat Relay", "file": "File Relay", "message": "Message Relay"}


@app.get("/relays/{kind}", response_class=HTMLResponse)
async def relay_page(request: Request, kind: str):
    if redir := _redirect_if_unauth(request):
        return redir
    if kind not in RELAY_KINDS:
        raise HTTPException(status_code=404, detail="Unknown relay")
    return TEMPLATES.TemplateResponse(request, "relay.html", {
        "nav_active": "relays", "kind": kind, "title": RELAY_KINDS[kind],
    })


@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    return TEMPLATES.TemplateResponse(request, "settings.html", {
        "nav_active": "settings",
        "network": await pm.get_network_config(),
    })


@app.get("/network", response_class=HTMLResponse)
async def network_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    return TEMPLATES.TemplateResponse(request, "network.html", {
        "detail": await pm.get_servers_detail(),
        "nav_active": "network",
    })


@app.get("/notifications", response_class=HTMLResponse)
async def notifications_page(request: Request):
    if redir := _redirect_if_unauth(request):
        return redir
    return TEMPLATES.TemplateResponse(request, "notifications.html", {
        "items": pm.get_notifications(100),
        "nav_active": "notifications",
    })


@app.get("/api/status")
async def api_status(request: Request):
    _require_auth(request)
    status = await pm.global_status()
    status["profiles_total"] = len(db.list_profiles())
    status["online"] = status["profiles_running"] > 0
    return JSONResponse(status)


@app.get("/api/notifications")
async def api_notifications(request: Request):
    _require_auth(request)
    return JSONResponse({"unread": pm.unread_count(), "items": pm.get_notifications(50)})


@app.post("/api/notifications/read")
async def api_notifications_read(request: Request):
    _require_auth(request)
    pm.mark_all_read()
    return JSONResponse({"ok": True})


@app.get("/profile/{profile_id}", response_class=HTMLResponse)
async def profile_page(request: Request, profile_id: int):
    if redir := _redirect_if_unauth(request):
        return redir
    profile = db.get_profile(profile_id)
    if not profile:
        raise HTTPException(404, "Profile not found")
    profile["config"] = json.loads(profile.get("config") or "{}")
    profile["running"] = pm.is_running(profile_id)
    # Refresh cached lists so member counts / contacts are current on view
    await pm.refresh_lists(profile_id)
    bot = pm.get_running(profile_id)
    contacts = bot.contacts if bot else []
    groups = bot.groups if bot else []
    log_lines = bot.log_lines[-50:] if bot else []
    cat = _category(profile["bot_type"])
    # Split groups by their link role: channels (observer) vs regular groups (member).
    # The is_channel flag is set during the bot's group refresh (see _classify_group).
    channels = [g for g in groups if g.get("is_channel")]
    plain_groups = [g for g in groups if not g.get("is_channel")]
    return TEMPLATES.TemplateResponse(request, "profile.html", {
        "profile": profile,
        "contacts": contacts,
        "groups": plain_groups,
        "channels": channels,
        "log_lines": log_lines,
        "back": "/" + cat,
        "nav_active": cat,
    })


def _find_chat_name(bot, chat_type: str, chat_id: int) -> str:
    """Resolve a chat's display name from the running bot's cached lists."""
    if not bot:
        return ""
    if chat_type == "direct":
        for c in bot.contacts:
            if c["contactId"] == chat_id:
                return c["localDisplayName"]
    elif chat_type == "group":
        for g in bot.groups:
            if pm.group_id(g) == chat_id:
                return pm.group_name(g)
    return ""


@app.get("/profile/{profile_id}/chat/{chat_type}/{chat_id}", response_class=HTMLResponse)
async def chat_room(request: Request, profile_id: int, chat_type: str, chat_id: int):
    if redir := _redirect_if_unauth(request):
        return redir
    profile = db.get_profile(profile_id)
    if not profile:
        raise HTTPException(404, "Profile not found")
    if chat_type not in ("direct", "group"):
        raise HTTPException(400, "chat_type must be 'direct' or 'group'")
    bot = pm.get_running(profile_id)
    name = _find_chat_name(bot, chat_type, chat_id) or f"#{chat_id}"
    # Is this group a channel? (affects send affordance: broadcast vs reply)
    is_channel = False
    if chat_type == "group" and bot:
        for g in bot.groups:
            if pm.group_id(g) == chat_id:
                is_channel = bool(g.get("is_channel"))
                break
    return TEMPLATES.TemplateResponse(request, "chat.html", {
        "profile": profile,
        "running": pm.is_running(profile_id),
        "chat_type": chat_type,
        "chat_id": chat_id,
        "chat_name": name,
        "is_channel": is_channel,
        "nav_active": _category(profile["bot_type"]),
    })


@app.get("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/messages")
async def chat_messages(request: Request, profile_id: int, chat_type: str, chat_id: int):
    _require_auth(request)
    count = int(request.query_params.get("count", 50))
    try:
        messages = await pm.get_chat_history(profile_id, chat_type, chat_id, count)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"messages": messages})


@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/send")
async def chat_send(request: Request, profile_id: int, chat_type: str, chat_id: int):
    _require_auth(request)
    data = await request.json()
    text = data.get("text", "").strip()
    if not text:
        raise HTTPException(400, "text required")
    reply_to_id = data.get("reply_to_id")
    try:
        await pm.send_to_chat(profile_id, chat_type, chat_id, text, reply_to_id=reply_to_id)
    except Exception as e:
        log.error("chat send failed (profile=%s %s/%s): %s", profile_id, chat_type, chat_id, e)
        return JSONResponse({"ok": False, "error": str(e)})
    return JSONResponse({"ok": True})


@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/react")
async def chat_react(request: Request, profile_id: int, chat_type: str, chat_id: int):
    _require_auth(request)
    data = await request.json()
    item_id = data.get("item_id")
    emoji = data.get("emoji", "")
    add = data.get("add", True)
    if not item_id or not emoji:
        raise HTTPException(400, "item_id and emoji required")
    try:
        await pm.send_reaction(profile_id, chat_type, chat_id, item_id, emoji, add)
    except Exception as e:
        log.error("react failed: %s", e)
        return JSONResponse({"ok": False, "error": str(e)})
    return JSONResponse({"ok": True})


@app.post("/api/profiles/{profile_id}/file/{file_id}/receive")
async def file_receive(request: Request, profile_id: int, file_id: int):
    _require_auth(request)
    try:
        await pm.accept_file(profile_id, file_id)
    except Exception as e:
        log.error("file receive failed: %s", e)
        return JSONResponse({"ok": False, "error": str(e)})
    return JSONResponse({"ok": True})


@app.get("/api/profiles/{profile_id}/file/{file_id}/download")
async def file_download(request: Request, profile_id: int, file_id: int):
    _require_auth(request)
    try:
        data, filename = await pm.read_file_bytes(profile_id, file_id)
    except FileNotFoundError as e:
        raise HTTPException(404, str(e))
    except Exception as e:
        log.error("file download failed: %s", e)
        raise HTTPException(500, str(e))
    mime = mimetypes.guess_type(filename)[0] or "application/octet-stream"
    return Response(
        content=data,
        media_type=mime,
        headers={"Content-Disposition": f'inline; filename="{filename}"'},
    )


@app.post("/api/profiles/{profile_id}/chat/{chat_type}/{chat_id}/clear")
async def chat_clear(request: Request, profile_id: int, chat_type: str, chat_id: int):
    _require_auth(request)
    try:
        ok = await pm.clear_chat(profile_id, chat_type, chat_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": ok})


@app.delete("/api/profiles/{profile_id}/contacts/{contact_id}")
async def contact_delete(request: Request, profile_id: int, contact_id: int):
    _require_auth(request)
    try:
        ok = await pm.delete_contact(profile_id, contact_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": ok})


# ── Profile API ───────────────────────────────────────────────────────────────

@app.post("/api/profiles")
async def create_profile(request: Request):
    _require_auth(request)
    data = await request.json()
    name = data.get("name", "").strip()
    bot_type = data.get("bot_type", "echo")
    config = data.get("config", {})
    if not name:
        raise HTTPException(400, "name required")
    if bot_type not in pm.ALL_TYPES:
        raise HTTPException(400, f"bot_type must be one of {pm.ALL_TYPES}")
    try:
        profile = db.create_profile(name, bot_type, config)
    except Exception as e:
        raise HTTPException(400, str(e))
    # Directory bots get their own auto-generated listing website
    if bot_type == "directory":
        try:
            profile["site"] = pm.generate_directory_site(name)
        except Exception as e:
            log.error("directory site generation failed: %s", e)
    return JSONResponse(profile, status_code=201)


@app.post("/api/profiles/{profile_id}/profile")
async def edit_profile(request: Request, profile_id: int):
    _require_auth(request)
    if not db.get_profile(profile_id):
        raise HTTPException(404, "Profile not found")
    data = await request.json()
    # Keys absent from the body are left unchanged; avatar="" removes the avatar.
    full_name = data.get("full_name")
    bio = data.get("bio")
    avatar = data.get("avatar")  # None = unchanged, "" = remove, str = replace
    try:
        config = await pm.update_profile(profile_id, full_name, bio, avatar)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": True, "config": config})


@app.delete("/api/profiles/{profile_id}")
async def delete_profile(request: Request, profile_id: int):
    _require_auth(request)
    await pm.stop_bot(profile_id)
    db.delete_profile(profile_id)
    return JSONResponse({"ok": True})


@app.post("/api/profiles/{profile_id}/start")
async def start_profile(request: Request, profile_id: int):
    _require_auth(request)
    profile = db.get_profile(profile_id)
    if not profile:
        raise HTTPException(404, "Profile not found")
    await pm.start_bot(profile, _save_address)
    return JSONResponse({"ok": True, "status": "starting"})


@app.post("/api/profiles/{profile_id}/stop")
async def stop_profile(request: Request, profile_id: int):
    _require_auth(request)
    await pm.stop_bot(profile_id)
    return JSONResponse({"ok": True, "status": "stopped"})


@app.get("/api/profiles/{profile_id}/status")
async def profile_status(request: Request, profile_id: int):
    _require_auth(request)
    profile = db.get_profile(profile_id)
    if not profile:
        raise HTTPException(404)
    bot = pm.get_running(profile_id)
    return JSONResponse({
        "running": bot is not None,
        "address": bot.address if bot else profile.get("address", ""),
        "contacts": len(bot.contacts) if bot else 0,
        "groups": len(bot.groups) if bot else 0,
        "log": bot.log_lines[-20:] if bot else [],
        "poll_next": bot.poll_next if bot else 0,
    })


@app.post("/api/profiles/{profile_id}/send")
async def send_message(request: Request, profile_id: int):
    _require_auth(request)
    data = await request.json()
    to = data.get("to", "")
    text = data.get("text", "")
    if not to or not text:
        raise HTTPException(400, "to and text required")
    ok = await pm.send_message(profile_id, to, text)
    return JSONResponse({"ok": ok})


# ── Group / Channel API ───────────────────────────────────────────────────────

@app.post("/api/profiles/{profile_id}/groups")
async def create_group(request: Request, profile_id: int):
    """Create a group (kind='group', 2-way) or channel (kind='channel', broadcast)."""
    _require_auth(request)
    profile = db.get_profile(profile_id)
    if not profile:
        raise HTTPException(404, "Profile not found")
    data = await request.json()
    name = data.get("name", "").strip()
    kind = data.get("kind", "group")
    if not name:
        raise HTTPException(400, "name required")
    try:
        if kind == "channel":
            link = await pm.create_channel(profile_id, name)
        else:
            link = await pm.create_group(profile_id, name)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": True, "link": link})


@app.post("/api/profiles/{profile_id}/groups/{group_id}/join")
async def group_join(request: Request, profile_id: int, group_id: int):
    _require_auth(request)
    try:
        ok = await pm.join_group(profile_id, group_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": ok})


@app.post("/api/profiles/{profile_id}/groups/{group_id}/leave")
async def group_leave(request: Request, profile_id: int, group_id: int):
    _require_auth(request)
    try:
        ok = await pm.leave_group(profile_id, group_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": ok})


@app.delete("/api/profiles/{profile_id}/groups/{group_id}")
async def group_delete(request: Request, profile_id: int, group_id: int):
    _require_auth(request)
    try:
        ok = await pm.delete_group(profile_id, group_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"ok": ok})


@app.get("/api/profiles/{profile_id}/groups/{group_id}/members")
async def group_members(request: Request, profile_id: int, group_id: int):
    _require_auth(request)
    try:
        members = await pm.get_group_members(profile_id, group_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"members": members})


@app.get("/api/profiles/{profile_id}/groups/{group_id}/link")
async def group_link(request: Request, profile_id: int, group_id: int):
    _require_auth(request)
    try:
        link = await pm.get_group_link(profile_id, group_id)
    except Exception as e:
        raise HTTPException(400, str(e))
    return JSONResponse({"link": link})


if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
