"""Web UI for HiveMind — Chat, Admin, Training, Sessions."""
from __future__ import annotations

import json
import logging
import base64
from pathlib import Path
from typing import Any

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, Form
from fastapi.responses import HTMLResponse, JSONResponse

log = logging.getLogger(__name__)

# Load HTML from file or fallback
_HTML_PATH = Path(__file__).parent / "dashboard.html"


def create_web_app(node: Any, private_key_path: Path | None = None) -> FastAPI:
    """Create the web UI application."""
    app = FastAPI(title=f"HiveMind: {node.name}")

    @app.get("/", response_class=HTMLResponse)
    async def index():
        if _HTML_PATH.exists():
            return _HTML_PATH.read_text(encoding="utf-8")
        return "<h1>dashboard.html not found</h1>"

    # ─── API: Status ─────────────────────────────────────────────
    @app.get("/api/status")
    async def status():
        return node.status

    @app.post("/api/chat")
    async def chat(data: dict):
        msg = data.get("message", "")
        if not msg:
            return {"response": ""}
        response = await node.chat(msg)
        return {"response": response}

    @app.post("/api/command")
    async def command(data: dict):
        cmd = data.get("command", "")
        if cmd == "clear":
            # Clear active session history
            s = node.sessions.active
            s.messages.clear()
            node.sessions.save_active()
            return {"result": "Chat geleert."}
        elif cmd == "status":
            return {"result": json.dumps(node.status, indent=2, ensure_ascii=False)}
        elif cmd == "plugins":
            caps = [{"plugin": c.plugin, "name": c.name, "desc": c.description}
                    for c in node.plugins.all_capabilities()]
            return {"result": json.dumps(caps, indent=2, ensure_ascii=False)}
        elif cmd == "help":
            return {"result": (
                "Befehle:\\n"
                "  /help      - Diese Hilfe\\n"
                "  /status    - Node-Status\\n"
                "  /plugins   - Geladene Plugins\\n"
                "  /peers     - Verbundene Peers\\n"
                "  /addpeer host:port - Peer hinzufuegen\\n"
                "  /clear     - Chat leeren\\n"
                "  /memory    - Gespeicherte Fakten"
            )}
        elif cmd == "memory":
            facts = node.memory.all_facts
            custom = node.memory.all_custom
            if not facts and not custom:
                return {"result": "Kein Memory gespeichert."}
            lines = ["Gespeicherte Fakten:"]
            for k, v in facts.items():
                lines.append(f"  {k}: {v}")
            if custom:
                lines.append("Notizen:")
                for n in custom:
                    lines.append(f"  - {n}")
            return {"result": "\\n".join(lines)}
        elif cmd == "peers":
            if not node.network:
                return {"result": "Netzwerk nicht aktiviert."}
            peers = [{"name": p.name or p.node_id, "address": p.address,
                       "online": p.online, "hours_ago": round(p.hours_since_seen, 1)}
                     for p in node.network.peers.all_peers]
            return {"result": json.dumps(peers, indent=2, ensure_ascii=False)}
        elif cmd.startswith("addpeer "):
            addr = cmd.split(" ", 1)[1].strip()
            try:
                from hivemind.network.protocol import PeerInfo as _PI
                host, port = _PI.parse_address(addr)
                if node.network:
                    node.network.peers.add_manual(host, port)
                    import asyncio
                    asyncio.create_task(node.network.connect_to(host, int(port)))
                    return {"result": f"Peer hinzugefuegt: {addr}"}
                return {"result": "Netzwerk nicht aktiviert."}
            except Exception as e:
                return {"result": f"Fehler: {e}"}
        return {"result": f"Unbekannter Befehl: {cmd}"}

    # ─── API: Sessions ───────────────────────────────────────────
    @app.get("/api/sessions")
    async def list_sessions():
        return {"sessions": node.sessions.list_sessions(), "active": node.sessions.active_id}

    @app.post("/api/sessions/create")
    async def create_session(data: dict = None):
        name = (data or {}).get("name", "")
        s = node.sessions.create(name)
        return {"session": s.summary_info}

    @app.post("/api/sessions/switch")
    async def switch_session(data: dict):
        s = node.sessions.switch(data.get("id", ""))
        if s:
            return {"session": s.summary_info, "messages": s.messages}
        return JSONResponse({"error": "Session nicht gefunden"}, 404)

    @app.post("/api/sessions/rename")
    async def rename_session(data: dict):
        ok = node.sessions.rename(data.get("id", ""), data.get("name", ""))
        return {"success": ok}

    @app.delete("/api/sessions/{session_id}")
    async def delete_session(session_id: str):
        ok = node.sessions.delete(session_id)
        return {"success": ok}

    @app.get("/api/sessions/history")
    async def session_history():
        return {"messages": node.sessions.get_history(), "session": node.sessions.active.summary_info}

    # ─── API: Memory ─────────────────────────────────────────────
    @app.get("/api/memory")
    async def get_memory():
        return {"facts": node.memory.all_facts, "custom": node.memory.all_custom, "stats": node.memory.stats}

    @app.post("/api/memory/fact")
    async def add_memory_fact(data: dict):
        node.memory.add_fact(data.get("category", ""), data.get("value", ""))
        return {"success": True}

    @app.delete("/api/memory/fact/{category}")
    async def del_memory_fact(category: str):
        return {"success": node.memory.remove_fact(category)}

    @app.post("/api/memory/custom")
    async def add_memory_custom(data: dict):
        node.memory.add_custom(data.get("note", ""))
        return {"success": True}

    @app.delete("/api/memory/custom/{index}")
    async def del_memory_custom(index: int):
        return {"success": node.memory.remove_custom(index)}

    # ─── API: Public IP ──────────────────────────────────────────
    @app.get("/api/network/myaddress")
    async def my_address():
        import asyncio
        from urllib.request import urlopen, Request
        port = node.config.network.listen_port if hasattr(node.config, 'network') else 9420

        def _fetch_ip(url, timeout=5):
            try:
                return urlopen(Request(url, headers={"User-Agent": "HiveMind"}), timeout=timeout).read().decode().strip()
            except Exception:
                return None

        loop = asyncio.get_event_loop()
        # Fetch IPv4 and IPv6 in parallel
        ipv4_task = loop.run_in_executor(None, lambda: _fetch_ip("https://api4.ipify.org"))
        ipv6_task = loop.run_in_executor(None, lambda: _fetch_ip("https://api6.ipify.org"))
        ipv4 = await ipv4_task
        ipv6 = await ipv6_task

        # Format addresses
        from hivemind.network.protocol import PeerInfo as _PI
        result = {"port": port, "ipv4": ipv4 or None, "ipv6": ipv6 or None}
        if ipv4:
            result["address_v4"] = f"{ipv4}:{port}"
        if ipv6:
            result["address_v6"] = _PI.format_address(ipv6, port)
        # Primary address: prefer IPv6 if available (DS-Lite compatibility)
        if ipv6:
            result["address"] = result["address_v6"]
            result["ip"] = ipv6
        elif ipv4:
            result["address"] = result["address_v4"]
            result["ip"] = ipv4
        else:
            result["address"] = "unbekannt"
            result["ip"] = "unbekannt"
        # DS-Lite detection: if IPv4 looks like CGN (100.64.x.x-range or shared)
        if ipv4 and ipv6 and ipv4 != ipv6:
            result["ds_lite"] = True
            result["hint"] = "DS-Lite erkannt: IPv4 ist geteilt (CGN). Nutze die IPv6-Adresse!"
        return result

    # ─── API: Profile ────────────────────────────────────────────
    @app.get("/api/profile")
    async def get_profile():
        return {
            "name": node.name,
            "specialization": node.config.node.specialization,
            "expertise_tags": node.config.node.expertise_tags,
            "model": Path(node.config.model.path).stem if node.config.model.path else "",
        }

    @app.post("/api/profile")
    async def save_profile(data: dict):
        import yaml
        node.config.node.specialization = data.get("specialization", "")
        node.config.node.expertise_tags = data.get("expertise_tags", [])
        from hivemind.confidence import ConfidenceScorer
        node.scorer = ConfidenceScorer(
            expertise_tags=node.config.node.expertise_tags,
            specialization=node.config.node.specialization,
        )
        config_path = node.base_dir / "config.yaml"
        cfg = {}
        if config_path.exists():
            cfg = yaml.safe_load(config_path.read_text()) or {}
        if "node" not in cfg:
            cfg["node"] = {}
        cfg["node"]["specialization"] = node.config.node.specialization
        cfg["node"]["expertise_tags"] = node.config.node.expertise_tags
        config_path.write_text(yaml.dump(cfg, allow_unicode=True, default_flow_style=False))
        return {"success": True}

    # ─── API: Training ───────────────────────────────────────────
    @app.get("/api/training/stats")
    async def training_stats():
        return node.training.stats

    @app.post("/api/training/export")
    async def training_export(data: dict = None):
        fmt = (data or {}).get("format", "chatml")
        path = node.training.export_jsonl(format_type=fmt)
        return {"path": str(path), "format": fmt}

    @app.get("/api/training/topics")
    async def training_topics():
        return node.training.topic_analysis()

    @app.get("/api/training/lora")
    async def list_lora():
        return {"adapters": node.training.get_lora_adapters()}

    @app.post("/api/training/lora/upload")
    async def upload_lora(file: UploadFile = File(...)):
        data = await file.read()
        path = node.training.save_lora(file.filename or "adapter.gguf", data)
        return {"success": True, "path": str(path), "size_mb": round(len(data) / 1048576, 1)}

    # ─── API: RAG ────────────────────────────────────────────────
    @app.get("/api/rag/documents")
    async def rag_documents():
        return {"documents": node.rag.document_list, "stats": node.rag.stats}

    @app.post("/api/rag/upload")
    async def rag_upload(file: UploadFile = File(...)):
        content = (await file.read()).decode("utf-8", errors="replace")
        name = file.filename or "document.txt"
        chunks = node.rag.add_document(name, content=content)
        return {"success": True, "name": name, "chunks": chunks}

    @app.delete("/api/rag/document/{name}")
    async def rag_delete(name: str):
        return {"success": node.rag.remove_document(name)}

    @app.post("/api/rag/import/huggingface")
    async def rag_import_hf(data: dict):
        url = data.get("url", "")
        if not url:
            return JSONResponse({"error": "URL fehlt"}, 400)
        max_rows = data.get("max_rows", 5000)
        text_fields = data.get("text_fields", None)
        split = data.get("split", "train")
        import asyncio
        result = await asyncio.get_event_loop().run_in_executor(
            None, lambda: node.rag.import_huggingface(url, max_rows, text_fields, split))
        if "error" in result:
            return JSONResponse(result, 400)
        return result

    @app.post("/api/rag/search")
    async def rag_search(data: dict):
        query = data.get("query", "")
        return {"results": node.rag.search(query, top_k=data.get("top_k", 5))}

    # ─── API: Keys & Updates ─────────────────────────────────────
    @app.get("/api/keys")
    async def get_keys():
        from hivemind.network.updater import PUBLISHER_PUBLIC_KEY
        priv = ""
        if private_key_path and private_key_path.exists():
            priv = private_key_path.read_text().strip()
        return {"public_key": PUBLISHER_PUBLIC_KEY, "private_key_available": bool(priv),
                "private_key_path": str(private_key_path) if private_key_path else ""}

    @app.post("/api/update/publish")
    async def publish_update(file: UploadFile = File(...), version: str = Form(...), changelog: str = Form("")):
        from hivemind.network.updater import sign_update
        from hivemind.network.protocol import Message, MsgType
        if not private_key_path or not private_key_path.exists():
            return JSONResponse({"error": "Private Key nicht gefunden."}, 400)
        private_key = private_key_path.read_text().strip()
        updates_dir = node.base_dir / "updates"
        updates_dir.mkdir(exist_ok=True)
        zip_path = updates_dir / f"hivemind-{version}.zip"
        content = await file.read()
        zip_path.write_bytes(content)
        try:
            manifest = sign_update(zip_path, private_key, version, changelog)
        except Exception as e:
            return JSONResponse({"error": str(e)}, 500)
        (updates_dir / f"hivemind-{version}.json").write_text(json.dumps(manifest, indent=2))
        sent = 0
        update_payload = {"manifest": manifest, "data_b64": base64.b64encode(content).decode()}
        # Send via direct P2P
        if node.network:
            sent += await node.network.broadcast(Message(type=MsgType.UPDATE_DATA, sender_id=node.id,
                payload=update_payload))
        # Send via Relay
        if node.relay and node.relay.connected:
            relay_ok = await node.relay.broadcast({
                "type": "UPDATE_DATA",
                "manifest": manifest,
                "data_b64": update_payload["data_b64"],
            })
            if relay_ok:
                sent += node.relay.status.get("relay_nodes", 0) or 1
        return {"success": True, "version": version, "sha256": manifest["sha256"],
                "signature": manifest["signature"][:40] + "...", "peers_notified": sent}

    # ─── API: Pending Update ───────────────────────────────────────
    @app.get("/api/update/pending")
    async def update_pending():
        if node.updater and node.updater.pending_update:
            return {"pending": True, **node.updater.pending_update}
        return {"pending": False}

    @app.post("/api/update/apply")
    async def update_apply():
        if not node.updater:
            return JSONResponse({"error": "Updater nicht verfuegbar"}, 400)
        result = await node.updater.apply_pending()
        if "error" in result:
            return JSONResponse(result, 400)
        return result

    @app.post("/api/update/dismiss")
    async def update_dismiss():
        if node.updater:
            node.updater.dismiss_pending()
        return {"success": True}

    # ─── API: PDF Download ─────────────────────────────────────────
    @app.get("/api/pdf/{filename}")
    async def pdf_download(filename: str):
        from fastapi.responses import FileResponse
        pdf_dir = node.base_dir / "data" / "pdf"
        path = pdf_dir / filename
        if not path.exists() or not filename.endswith(".pdf"):
            return JSONResponse({"error": "PDF nicht gefunden"}, 404)
        return FileResponse(path, media_type="application/pdf", filename=filename)

    # ─── API: Network Mode (Online/Offline) ────────────────────
    @app.get("/api/network/mode")
    async def network_mode():
        online = bool(node.network or node.relay)
        return {"online": online, "network_enabled": node.config.network.enabled}

    @app.post("/api/network/mode")
    async def set_network_mode(data: dict):
        import yaml
        go_online = data.get("online", True)
        if go_online and not node.network and not node.relay:
            # Start network + relay
            node.config.network.enabled = True
            try:
                from hivemind.network.peerlist import PeerList
                from hivemind.network.peer import P2PNetwork
                from hivemind.network.updater import AutoUpdater
                peers = PeerList(node.base_dir / "data" / "peers.json")
                for addr in (node.config.network.bootstrap_nodes or []):
                    from hivemind.network.protocol import PeerInfo as _PI
                    host, port = _PI.parse_address(addr)
                    peers.add_manual(host, port)
                network = P2PNetwork(
                    node=node, peer_list=peers,
                    listen_port=node.config.network.listen_port,
                )
                updater = AutoUpdater(node, node.base_dir)
                node.updater = updater
                updater.register_handlers(network)
                from hivemind.network.protocol import MsgType
                network.on_message(MsgType.QUERY, node._handle_network_query)
                network.on_message(MsgType.RESPONSE, node._handle_network_response)
                await network.start()
                node.network = network
            except Exception as e:
                log.warning("Failed to start P2P: %s", e)
            # Relay
            relay_urls = getattr(node.config.network, 'relay_servers', []) or []
            if relay_urls and not node.relay:
                try:
                    from hivemind.network.relay_client import RelayConnection
                    node.relay = RelayConnection(
                        relay_url=relay_urls[0], node=node,
                        on_message=node._handle_relay_message,
                        on_peers=node._handle_relay_peers,
                    )
                    await node.relay.start()
                except Exception as e:
                    log.warning("Failed to start relay: %s", e)
        elif not go_online:
            # Stop network + relay
            node.config.network.enabled = False
            if node.relay:
                await node.relay.stop()
                node.relay = None
            if node.network:
                await node.network.stop()
                node.network = None
        # Persist to config.yaml
        config_path = node.base_dir / "config.yaml"
        cfg = {}
        if config_path.exists():
            cfg = yaml.safe_load(config_path.read_text()) or {}
        if "network" not in cfg:
            cfg["network"] = {}
        cfg["network"]["enabled"] = go_online
        config_path.write_text(yaml.dump(cfg, allow_unicode=True, default_flow_style=False))
        return {"online": go_online, "success": True}

    # ─── API: Relay Status ───────────────────────────────────────
    @app.get("/api/relay/info")
    async def relay_info():
        result = {"relay_client": None}
        if node.relay:
            result["relay_client"] = node.relay.status
        return result

    # ─── WebSocket ───────────────────────────────────────────────
    @app.websocket("/ws/chat")
    async def ws_chat(ws: WebSocket):
        await ws.accept()
        try:
            while True:
                msg = await ws.receive_text()
                response = await node.chat(msg)
                await ws.send_text(response)
        except WebSocketDisconnect:
            pass

    return app
