"""FastAPI routes for mesh endpoints.

Mesh endpoints are intentionally unauthenticated (peer auth uses ECDH key
exchange), but are rate-limited per IP to prevent abuse from LAN clients.
"""
from __future__ import annotations
import asyncio
import logging
import time
from collections import defaultdict, deque

import json

from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel, Field

from mesh import MeshManager
from mesh.protocol import HandshakeRequest, MeshMessage

_log = logging.getLogger(__name__)

# ── Simple per-IP sliding-window rate limiter ─────────────────────────────────
_RATE_WINDOW = 60.0  # seconds
_RATE_MAX_HANDSHAKE = 10
_RATE_MAX_MESSAGE = 30
_serve_hits: dict[str, list[float]] = defaultdict(list)

_hits: dict[str, list[float]] = defaultdict(list)


def _is_rate_limited(ip: str, limit: int) -> bool:
    """Return True if *ip* has exceeded *limit* requests in the sliding window."""
    now = time.monotonic()
    bucket = _hits[ip]
    bucket[:] = [t for t in bucket if now - t < _RATE_WINDOW]
    if len(bucket) >= limit:
        return True
    bucket.append(now)
    return False


def _is_serve_rate_limited(ip: str, limit: int) -> bool:
    now = time.monotonic()
    bucket = _serve_hits[ip]
    bucket[:] = [t for t in bucket if now - t < _RATE_WINDOW]
    if len(bucket) >= limit:
        return True
    bucket.append(now)
    return False


_MESH_EXPORT_RATE_WINDOW_SEC = 60.0
_mesh_export_hits_ip: defaultdict[str, deque[float]] = defaultdict(lambda: deque(maxlen=512))


def _mesh_export_rate_limited(remote_ip: str, limit_per_minute: int) -> bool:
    """Return True if *ip* exceeded export requests in the sliding window."""
    now = time.monotonic()
    window = _mesh_export_hits_ip[remote_ip]
    cutoff = now - _MESH_EXPORT_RATE_WINDOW_SEC
    while window and window[0] < cutoff:
        window.popleft()
    if len(window) >= max(1, limit_per_minute):
        return True
    window.append(now)
    return False


class PendingAction(BaseModel):
    action: str


class PeerTaskTrust(BaseModel):
    trusted_for_tasks: bool


class PeerNicknameBody(BaseModel):
    nickname: str = ""
    clear_intro_hint: bool = False


class PeerRelationshipSettingsBody(BaseModel):
    mesh_rapport_memory_enabled: bool | None = None
    mesh_rapport_digest_in_main_chat: bool | None = None
    mesh_rapport_digest_style: str | None = None


class RapportMuteBody(BaseModel):
    fingerprint: str = Field(min_length=1)
    muted: bool = True


class FileTransferPrepareBody(BaseModel):
    session_id: str = Field(min_length=1)
    receiver_fingerprint: str = Field(min_length=1)
    paths: list[str] = Field(default_factory=list)
    client_source: str | None = None


class FileTransferTransferSessionBody(BaseModel):
    transfer_id: str = Field(min_length=1)
    session_id: str = Field(min_length=1)


class FileTransferOutboundCancelBody(BaseModel):
    transfer_id: str = Field(min_length=1)
    session_id: str = Field(min_length=1)
    notify_peer: bool = True


class FileTransferAcceptBody(BaseModel):
    transfer_id: str = Field(min_length=1)
    destination_dir: str = Field(min_length=1)
    session_id: str | None = None


class FileTransferAcceptFileBody(BaseModel):
    transfer_id: str = Field(min_length=1)
    file_id: str = Field(min_length=1)
    session_id: str | None = None


class FileTransferDeclineBody(BaseModel):
    transfer_id: str = Field(min_length=1)
    session_id: str | None = None


class ProposalPromoteBody(BaseModel):
    proposal_id: str = Field(min_length=1)
    # Optional overrides for drive description at promotion time.
    drive_description: str | None = None
    drive_type: str | None = None


class ProposalDismissBody(BaseModel):
    proposal_id: str = Field(min_length=1)


def build_mesh_router(manager: MeshManager) -> APIRouter:
    router = APIRouter(prefix="/mesh")

    @router.post("/handshake")
    async def handshake(request: Request, body: HandshakeRequest):
        remote_ip = ""
        try:
            remote_ip = str(request.client.host or "")
        except Exception:
            remote_ip = ""
        if _is_rate_limited(remote_ip, _RATE_MAX_HANDSHAKE):
            _log.warning("Rate-limited handshake from %s", remote_ip)
            return JSONResponse(status_code=429, content={"error": "rate_limited"})
        out = manager.handle_handshake(
            static_pub=body.static_pub,
            ephemeral_pub=body.ephemeral_pub,
            salt=body.salt,
            hostname=body.hostname,
            mesh_port=body.mesh_port,
            remote_addr=remote_ip,
        )
        status = str(out.get("status") or "")
        if status == "pending":
            return JSONResponse(status_code=202, content=out)
        if status == "denied":
            return JSONResponse(status_code=403, content=out)
        if status == "error":
            return JSONResponse(status_code=500, content=out)
        return JSONResponse(content=out)

    @router.post("/message")
    async def message(request: Request, body: dict):
        remote_ip = ""
        try:
            remote_ip = str(request.client.host or "")
        except Exception:
            pass
        if _is_rate_limited(remote_ip, _RATE_MAX_MESSAGE):
            _log.warning("Rate-limited message from %s", remote_ip)
            return JSONResponse(status_code=429, content={"error": "rate_limited"})
        status, payload = await manager.enqueue_encrypted_message(body if isinstance(body, dict) else {})
        return JSONResponse(status_code=status, content=payload)

    @router.get("/status")
    async def status():
        return JSONResponse(content=manager.status())

    @router.get("/relationships/settings")
    async def relationship_settings():
        from core.config import get_mesh_config

        from mesh.relationship_ledger import list_rapport_mutes

        cfg = get_mesh_config()
        return JSONResponse(
            content={
                "mesh_rapport_memory_enabled": bool(cfg.get("mesh_rapport_memory_enabled", True)),
                "mesh_rapport_digest_in_main_chat": bool(cfg.get("mesh_rapport_digest_in_main_chat", True)),
                "mesh_rapport_digest_style": str(cfg.get("mesh_rapport_digest_style") or "normal"),
                "rapport_muted_fingerprints": list_rapport_mutes(),
                "stats": manager.status(),
            }
        )

    @router.put("/relationships/settings")
    async def update_relationship_settings(body: PeerRelationshipSettingsBody):
        from core.config import get_config, reload_config, save_config

        cfg = get_config()
        mesh_cfg = cfg.setdefault("mesh", {})
        if body.mesh_rapport_memory_enabled is not None:
            mesh_cfg["mesh_rapport_memory_enabled"] = bool(body.mesh_rapport_memory_enabled)
        if body.mesh_rapport_digest_in_main_chat is not None:
            mesh_cfg["mesh_rapport_digest_in_main_chat"] = bool(body.mesh_rapport_digest_in_main_chat)
        if body.mesh_rapport_digest_style is not None:
            st = str(body.mesh_rapport_digest_style).strip().lower()
            if st in {"brief", "normal"}:
                mesh_cfg["mesh_rapport_digest_style"] = st
        save_config(cfg)
        reload_config()
        try:
            manager._refresh_prompt_block()
        except Exception:
            pass
        return JSONResponse(
            content={
                "ok": True,
                "mesh_rapport_memory_enabled": bool(mesh_cfg.get("mesh_rapport_memory_enabled", True)),
                "mesh_rapport_digest_in_main_chat": bool(mesh_cfg.get("mesh_rapport_digest_in_main_chat", True)),
                "mesh_rapport_digest_style": str(mesh_cfg.get("mesh_rapport_digest_style") or "normal"),
            }
        )

    @router.delete("/relationships")
    async def clear_relationships():
        from mesh.relationship_ledger import clear_all_relationships

        count = clear_all_relationships()
        try:
            manager._refresh_prompt_block()
        except Exception:
            pass
        return JSONResponse(content={"ok": True, "cleared": count})

    @router.delete("/relationships/{fingerprint:path}")
    async def delete_one_relationship(fingerprint: str):
        """Remove rapport ledger entry for one fingerprint (same as deleting peer rapport slice)."""
        fp = fingerprint.strip()
        try:
            from mesh.relationship_ledger import delete_relationship

            ok = delete_relationship(fp)
        except Exception:
            ok = False
        try:
            manager._refresh_prompt_block()
        except Exception:
            pass
        return JSONResponse(content={"ok": bool(ok), "fingerprint": fp})

    @router.get("/relationships/export")
    async def relationships_export(request: Request, max_records: int = 80, omit_fingerprint: bool = True):
        """Sanitized rapport export (auth required via global middleware when token is enabled)."""
        from core.config import get_mesh_config

        from mesh.relationship_ledger import rapport_export_bundle, record_rapport_export_event

        mc = get_mesh_config()
        lim = int(mc.get("mesh_rapport_export_per_minute") or 30)
        cap_records = min(int(max_records or 80), int(mc.get("mesh_rapport_export_max_records") or 200))
        max_bytes = max(65_536, int(mc.get("mesh_rapport_export_max_bytes") or 4 * 1024 * 1024))

        remote_ip = ""
        try:
            remote_ip = str(request.client.host or "")
        except Exception:
            remote_ip = ""

        if _mesh_export_rate_limited(remote_ip, lim):
            _log.warning("Rate-limited rapport export from %s", remote_ip)
            return JSONResponse(status_code=429, content={"error": "rate_limited", "detail": "relationships_export"})

        blob = rapport_export_bundle(max_records=max(1, cap_records), omit_full_fingerprint=omit_fingerprint)
        raw = json.dumps(blob, ensure_ascii=False)
        if len(raw.encode("utf-8")) > max_bytes:
            return JSONResponse(
                status_code=413,
                content={"error": "export_too_large", "max_bytes": max_bytes},
            )
        try:
            record_rapport_export_event()
        except Exception:
            pass
        _log.info(
            "rapport_export bytes=%d records=%d",
            len(raw.encode("utf-8")),
            int((blob or {}).get("exported_count") or 0),
            extra={"event": "mesh_rapport_export", "ledger_action": "export", "bytes": len(raw.encode("utf-8"))},
        )
        return JSONResponse(content=blob)

    @router.post("/relationships/rapport_mute")
    async def rapport_mute_route(body: RapportMuteBody):
        from mesh.relationship_ledger import set_rapport_muted

        ok = set_rapport_muted(body.fingerprint.strip(), bool(body.muted))
        try:
            manager._refresh_prompt_block()
        except Exception:
            pass
        return JSONResponse(content={"ok": ok, "fingerprint": body.fingerprint.strip(), "muted": bool(body.muted)})

    @router.post("/disconnect")
    async def disconnect(body: dict):
        sid = str((body or {}).get("session_id") or "")
        ok = manager.close_session(sid)
        return JSONResponse(content={"ok": ok})

    @router.get("/peers")
    async def peers():
        return JSONResponse(
            content={
                "peers": manager.list_peers(),
                "sessions": manager.list_sessions(),
                "discovered": manager.discovered_peers(),
                "muted": manager.peers.list_muted(),
            }
        )

    @router.delete("/peers/{fingerprint:path}")
    async def delete_peer(fingerprint: str):
        fp = fingerprint.strip()
        removed = manager.peers.remove_peer(fp)
        try:
            from mesh.relationship_ledger import delete_relationship

            delete_relationship(fp)
        except Exception:
            pass
        return JSONResponse(content={"ok": removed or True})

    @router.post("/peers/{fingerprint:path}/task_trust")
    async def set_peer_task_trust(fingerprint: str, body: PeerTaskTrust):
        ok = manager.peers.set_trusted_for_tasks(fingerprint.strip(), body.trusted_for_tasks)
        code = 200 if ok else 404
        return JSONResponse(status_code=code, content={"ok": ok, "fingerprint": fingerprint, "trusted_for_tasks": bool(body.trusted_for_tasks)})

    @router.post("/peers/{fingerprint:path}/nickname")
    async def set_peer_nickname_route(fingerprint: str, body: PeerNicknameBody):
        fp = fingerprint.strip()
        ok = manager.peers.set_peer_nickname(
            fp,
            body.nickname,
            clear_intro_hint=bool(body.clear_intro_hint),
        )
        if ok:
            try:
                manager._refresh_prompt_block()
            except Exception:
                pass
        code = 200 if ok else 404
        return JSONResponse(
            status_code=code,
            content={"ok": ok, "fingerprint": fp, "nickname": body.nickname.strip()},
        )

    @router.get("/peers/pending")
    async def pending_peers():
        return JSONResponse(content={"pending": manager.list_pending()})

    @router.post("/peers/pending/{request_id}")
    async def resolve_pending(request_id: str, body: PendingAction):
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None, lambda: manager.resolve_pending_request(request_id=request_id, action=body.action)
        )
        code = 200 if out.get("ok") else 404
        return JSONResponse(status_code=code, content=out)

    @router.get("/audit")
    async def audit(limit: int = 100, offset: int = 0):
        rows = manager.audit_rows(limit=limit, offset=offset)
        return JSONResponse(content={"rows": rows, "count": len(rows)})

    @router.delete("/audit")
    async def clear_audit_route():
        from mesh.audit import clear_audit
        count = clear_audit()
        return JSONResponse(content={"ok": True, "cleared": count})

    @router.get("/debug/discovery")
    async def debug_discovery():
        return JSONResponse(content=manager.discovery_debug())

    @router.get("/peers/blocked")
    async def list_blocked():
        return JSONResponse(content={"blocked": manager.peers.list_blocked()})

    @router.post("/peers/unblock")
    async def unblock_peers(body: dict):
        fp = str((body or {}).get("fingerprint") or "").strip()
        if fp:
            ok = manager.peers.unblock_peer(fp)
            return JSONResponse(content={"ok": ok, "unblocked": fp})
        count = manager.peers.clear_blocked()
        return JSONResponse(content={"ok": True, "cleared": count})

    @router.get("/peers/muted")
    async def muted_list():
        return JSONResponse(content={"muted": manager.peers.list_muted()})

    @router.post("/peers/mute")
    async def mute_peer_route(body: dict):
        fp = str((body or {}).get("fingerprint") or "").strip()
        if not fp:
            return JSONResponse(status_code=400, content={"ok": False, "error": "fingerprint required"})
        manager.peers.mute_peer(fp)
        try:
            manager._refresh_prompt_block()
        except Exception:
            pass
        return JSONResponse(content={"ok": True, "muted": fp})

    @router.post("/peers/unmute")
    async def unmute_peer_route(body: dict):
        fp = str((body or {}).get("fingerprint") or "").strip()
        if not fp:
            return JSONResponse(status_code=400, content={"ok": False, "error": "fingerprint required"})
        ok = manager.peers.unmute_peer(fp)
        try:
            manager._refresh_prompt_block()
        except Exception:
            pass
        code = 200 if ok else 404
        return JSONResponse(status_code=code, content={"ok": ok, "unmuted": fp})

    @router.post("/connect")
    async def connect(body: dict):
        host = str((body or {}).get("address") or "")
        port = int((body or {}).get("port") or 0)
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(None, lambda: manager.connect_to_peer(address=host, port=port))
        status = str(out.get("status") or "")
        if out.get("ok"):
            code = 200
        elif status == "pending":
            code = 202
        else:
            code = 400
        return JSONResponse(status_code=code, content=out)

    @router.post("/send_plain")
    async def send_plain(body: dict):
        sid = str((body or {}).get("session_id") or "")
        payload = (body or {}).get("message") or {}
        try:
            msg = MeshMessage(**payload)
        except Exception as exc:
            return JSONResponse(status_code=400, content={"ok": False, "error": str(exc)})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(None, lambda: manager.send_message(session_id=sid, message=msg))
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    # --- Encrypted LAN file transfer (auth: same as other /mesh/* except serve GET) ---
    @router.post("/file_transfers/prepare")
    async def file_transfer_prepare(body: FileTransferPrepareBody):
        from core.config import get_mesh_config

        if not bool(get_mesh_config().get("file_transfer_enabled", True)):
            return JSONResponse(status_code=403, content={"ok": False, "error": "file_transfer disabled"})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.prepare_outbound(
                session_id=body.session_id,
                receiver_fingerprint=body.receiver_fingerprint,
                paths=body.paths,
                client_source=body.client_source,
            ),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.post("/file_transfers/commit")
    async def file_transfer_commit(body: FileTransferTransferSessionBody):
        from core.config import get_mesh_config

        if not bool(get_mesh_config().get("file_transfer_enabled", True)):
            return JSONResponse(status_code=403, content={"ok": False, "error": "file_transfer disabled"})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.commit_outbound(transfer_id=body.transfer_id, session_id=body.session_id),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.post("/file_transfers/send")
    async def file_transfer_send(body: FileTransferPrepareBody):
        """Prepare + commit in one request (user-facing "send" without a manual prepare step)."""
        from core.config import get_mesh_config

        if not bool(get_mesh_config().get("file_transfer_enabled", True)):
            return JSONResponse(status_code=403, content={"ok": False, "error": "file_transfer disabled"})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.send_outbound(
                session_id=body.session_id,
                receiver_fingerprint=body.receiver_fingerprint,
                paths=body.paths,
                client_source=body.client_source,
            ),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.post("/file_transfers/outbound/cancel")
    async def file_transfer_outbound_cancel(body: FileTransferOutboundCancelBody):
        loop = asyncio.get_running_loop()
        np = bool(body.notify_peer)
        tid = body.transfer_id
        sid = body.session_id
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.cancel_outbound(
                transfer_id=tid, session_id=sid, notify_peer=np
            ),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.get("/file_transfers/inbound/pending")
    async def file_transfer_inbound_pending():
        rows = manager.file_transfers.list_inbound_pending()
        return JSONResponse(content={"pending": rows})

    @router.post("/file_transfers/inbound/accept")
    async def file_transfer_inbound_accept(body: FileTransferAcceptBody):
        from core.config import get_mesh_config

        if not bool(get_mesh_config().get("file_transfer_enabled", True)):
            return JSONResponse(status_code=403, content={"ok": False, "error": "file_transfer disabled"})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.accept_inbound_impl(
                transfer_id=body.transfer_id,
                destination_dir=body.destination_dir,
                session_id=body.session_id,
            ),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.post("/file_transfers/inbound/accept_prepare")
    async def file_transfer_inbound_accept_prepare(body: FileTransferAcceptBody):
        from core.config import get_mesh_config

        if not bool(get_mesh_config().get("file_transfer_enabled", True)):
            return JSONResponse(status_code=403, content={"ok": False, "error": "file_transfer disabled"})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.accept_inbound_prepare(
                transfer_id=body.transfer_id,
                destination_dir=body.destination_dir,
                session_id=body.session_id,
            ),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.post("/file_transfers/inbound/accept_file")
    async def file_transfer_inbound_accept_file(body: FileTransferAcceptFileBody):
        from core.config import get_mesh_config

        if not bool(get_mesh_config().get("file_transfer_enabled", True)):
            return JSONResponse(status_code=403, content={"ok": False, "error": "file_transfer disabled"})
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.file_transfers.accept_inbound_fetch_one(
                transfer_id=body.transfer_id,
                file_id=body.file_id,
                session_id=body.session_id,
            ),
        )
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.post("/file_transfers/inbound/decline")
    async def file_transfer_inbound_decline(body: FileTransferDeclineBody):
        out = manager.file_transfers.decline_inbound(transfer_id=body.transfer_id, session_id=body.session_id)
        return JSONResponse(status_code=200 if out.get("ok") else 400, content=out)

    @router.get("/proposals")
    async def list_proposals_route(state: str | None = None, limit: int = 200):
        from mesh import proposals as _proposals

        rows = _proposals.list_proposals(state=state, limit=int(limit or 200))
        return JSONResponse(
            content={
                "proposals": rows,
                "count": len(rows),
                "feature_enabled": bool(manager._get_mesh_config().get("peer_proposals_enabled", False)),
            }
        )

    @router.post("/proposals/promote")
    async def promote_proposal_route(body: ProposalPromoteBody):
        loop = asyncio.get_running_loop()
        out = await loop.run_in_executor(
            None,
            lambda: manager.promote_peer_proposal(
                proposal_id=body.proposal_id,
                drive_description=body.drive_description,
                drive_type=body.drive_type,
            ),
        )
        code = 200 if out.get("ok") else (out.get("status_code") or 400)
        return JSONResponse(status_code=code, content=out)

    @router.post("/proposals/dismiss")
    async def dismiss_proposal_route(body: ProposalDismissBody):
        from mesh import proposals as _proposals

        ok = _proposals.set_proposal_state(body.proposal_id, _proposals.STATE_DISMISSED)
        return JSONResponse(status_code=200 if ok else 404, content={"ok": ok, "proposal_id": body.proposal_id})

    @router.delete("/proposals")
    async def clear_proposals_route():
        from mesh import proposals as _proposals

        count = _proposals.clear_all()
        return JSONResponse(content={"ok": True, "cleared": count})

    @router.get("/proposals/export")
    async def export_proposals_route():
        from mesh import proposals as _proposals

        rows = _proposals.export_all()
        return JSONResponse(content={"proposals": rows, "count": len(rows)})

    @router.get("/file_transfers/serve/{transfer_id}/{file_id}")
    async def file_transfer_serve(transfer_id: str, file_id: str, request: Request):
        from core.config import get_mesh_config
        from mesh.file_transfer import parse_transfer_auth_header

        remote_ip = ""
        try:
            remote_ip = str(request.client.host or "")
        except Exception:
            remote_ip = ""
        per_min = int(get_mesh_config().get("file_transfer_serve_rate_per_minute", 240))
        if _is_serve_rate_limited(remote_ip, per_min):
            _log.warning("Rate-limited file transfer serve from %s", remote_ip)
            return JSONResponse(status_code=429, content={"error": "rate_limited"})
        ticket = parse_transfer_auth_header(request.headers.get("authorization") or request.headers.get("Authorization"))
        code, stream_it, err = manager.file_transfers.serve_file_stream(
            transfer_id=transfer_id, file_id=file_id, ticket_hex=ticket or ""
        )
        if stream_it is None:
            return JSONResponse(status_code=code, content={"error": err})
        return StreamingResponse(stream_it, media_type="application/octet-stream")

    return router
