"""FastAPI app for Armorer self-hosted UI."""

from __future__ import annotations

import asyncio

import httpx
import websockets
from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response, WebSocket, WebSocketDisconnect
from websockets.exceptions import ConnectionClosed

from armorer.cli.core_lifecycle import ensure_armorer_core
from armorer.config import ConfigManager, resolve_armorer_config_dir
from armorer.core.response import _normalize_core_response
from armorer.messaging import InboxOutboxRelay
from armorer.messaging.bridge import build_local_cli_task_metadata
from armorer.tools import orchestrator
from armorer.version import get_version
from armorer.web.auth import require_ui_token
from armorer.web.schemas import (
    ChatRequest,
    InstallActionRequest,
    OpenClawSetupApplyRequest,
    OpenClawChatRequest,
    RunActionRequest,
    SettingsUpdateRequest,
    UninstallActionRequest,
)
from armorer.web.services import (
    approve_openclaw_native_ui_browser,
    alerts_summary,
    apply_openclaw_setup,
    installed_agents_summary,
    influencerpy_dashboard_upstream,
    openclaw_native_ui_upstream,
    openclaw_setup_summary,
    openclaw_chat_history,
    read_recent_jobs,
    reveal_secret_value,
    runtime_events_summary,
    run_openclaw_chat_turn,
    sanitize_job_payload,
    settings_summary,
    sync_telegram_setup,
    sync_telegram_shortcuts,
    update_settings,
    status_summary,
    supported_agents_summary,
)


app = FastAPI(title="Armorer UI API", version=get_version())

_HOP_BY_HOP_HEADERS = {
    "connection",
    "keep-alive",
    "proxy-authenticate",
    "proxy-authorization",
    "te",
    "trailers",
    "transfer-encoding",
    "upgrade",
    "host",
    "content-length",
}


def _request_origin(request: Request) -> str:
    forwarded_proto = request.headers.get("x-forwarded-proto")
    forwarded_host = request.headers.get("x-forwarded-host")
    host = forwarded_host or request.headers.get("host") or request.url.netloc
    proto = forwarded_proto or request.url.scheme or "http"
    return f"{proto}://{host}"


def _header_origin(headers, *, fallback_scheme: str = "http") -> str:
    origin = str(headers.get("origin") or "").strip()
    if origin:
        return origin
    forwarded_proto = str(headers.get("x-forwarded-proto") or "").strip()
    forwarded_host = str(headers.get("x-forwarded-host") or headers.get("host") or "").strip()
    if forwarded_host:
        return f"{forwarded_proto or fallback_scheme}://{forwarded_host}"
    return ""


async def _proxy_influencerpy_dashboard_http(request: Request, agent: str, rest: str = "") -> Response:
    target = influencerpy_dashboard_upstream(agent)
    if not target.get("ok"):
        raise HTTPException(status_code=404, detail=target.get("message", "InfluencerPy dashboard is unavailable"))
    suffix = f"/{rest or ''}".replace("//", "/")
    query = f"?{request.url.query}" if request.url.query else ""
    if suffix == "/api" or suffix.startswith("/api/"):
        api_suffix = suffix[4:] or "/"
        upstream_base = str(target.get("api_base") or "")
        if not upstream_base:
            raise HTTPException(status_code=502, detail="InfluencerPy API upstream is unavailable")
        upstream_url = f"{upstream_base}{api_suffix}{query}"
    else:
        mount_path = str(target.get("mount_path") or "").rstrip("/")
        normalized_suffix = "" if suffix == "/" else suffix
        upstream_url = f"{target['http_base']}{mount_path}{normalized_suffix}{query}"
    headers = _openclaw_proxy_headers(request.headers)
    headers["accept-encoding"] = "identity"
    body = await request.body()
    timeout = httpx.Timeout(30.0, connect=10.0)
    async with httpx.AsyncClient(follow_redirects=False, timeout=timeout) as client:
        upstream = await client.request(
            request.method,
            upstream_url,
            headers=headers,
            content=body,
        )
    response_headers = {
        key: value
        for key, value in upstream.headers.items()
        if key.lower() not in _HOP_BY_HOP_HEADERS
    }
    return Response(content=upstream.content, status_code=upstream.status_code, headers=response_headers)


@app.api_route("/api/influencerpy/dashboard/{agent}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
@app.api_route("/api/influencerpy/dashboard/{agent}/{rest:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
async def proxy_influencerpy_dashboard_http(request: Request, agent: str, rest: str = "") -> Response:
    return await _proxy_influencerpy_dashboard_http(request, agent, rest)


@app.websocket("/api/influencerpy/dashboard/{agent}")
@app.websocket("/api/influencerpy/dashboard/{agent}/{rest:path}")
async def proxy_influencerpy_dashboard_ws(websocket: WebSocket, agent: str, rest: str = "") -> None:
    target = influencerpy_dashboard_upstream(agent)
    if not target.get("ok"):
        await websocket.close(code=4404)
        return
    suffix = f"/{rest or ''}".replace("//", "/")
    query = f"?{websocket.url.query}" if websocket.url.query else ""
    if suffix == "/api" or suffix.startswith("/api/"):
        api_suffix = suffix[4:] or "/"
        upstream_base = str(target.get("api_base") or "").replace("http://", "ws://").replace("https://", "wss://")
        if not upstream_base:
            await websocket.close(code=1011, reason="InfluencerPy API upstream is unavailable")
            return
        upstream_url = f"{upstream_base}{api_suffix}{query}"
    else:
        mount_path = str(target.get("mount_path") or "").rstrip("/")
        normalized_suffix = "" if suffix == "/" else suffix
        upstream_url = f"{target['ws_base']}{mount_path}{normalized_suffix}{query}"
    subprotocols = list(websocket.scope.get("subprotocols") or [])
    upstream_origin = _header_origin(websocket.headers, fallback_scheme="http")
    await websocket.accept()
    try:
        async with websockets.connect(
            upstream_url,
            subprotocols=subprotocols or None,
            origin=upstream_origin or None,
        ) as upstream:
            async def client_to_upstream() -> None:
                while True:
                    message = await websocket.receive()
                    if message["type"] == "websocket.disconnect":
                        break
                    if message.get("text") is not None:
                        await upstream.send(message["text"])
                    elif message.get("bytes") is not None:
                        await upstream.send(message["bytes"])

            async def upstream_to_client() -> None:
                async for message in upstream:
                    if isinstance(message, bytes):
                        await websocket.send_bytes(message)
                    else:
                        await websocket.send_text(message)

            await asyncio.gather(client_to_upstream(), upstream_to_client())
    except WebSocketDisconnect:
        return
    except ConnectionClosed:
        return
    except Exception:
        await websocket.close(code=1011)


@app.get("/api/status")
def get_status() -> dict:
    return status_summary()


@app.get("/api/alerts")
def get_alerts(
    limit: int = Query(default=50, ge=1, le=200),
    sort: str = Query(default="criticality"),
    status: str = Query(default=""),
    agent: str = Query(default=""),
) -> dict:
    return alerts_summary(limit=limit, sort=sort, status=status, agent=agent)


@app.get("/api/runtime-events")
def get_runtime_events(
    limit: int = Query(default=100, ge=1, le=500),
    agent: str = Query(default=""),
    event_type: str = Query(default=""),
) -> dict:
    return runtime_events_summary(limit=limit, agent=agent, event_type=event_type)


@app.get("/api/settings")
def get_settings() -> dict:
    return settings_summary()


@app.post("/api/settings", dependencies=[Depends(require_ui_token)])
def post_settings(req: SettingsUpdateRequest) -> dict:
    return update_settings(req.model_dump(exclude_none=True))


@app.get("/api/settings/secret", dependencies=[Depends(require_ui_token)])
def get_settings_secret(key: str = Query(min_length=1)) -> dict:
    return reveal_secret_value(key)


@app.post("/api/settings/telegram/sync", dependencies=[Depends(require_ui_token)])
def post_settings_telegram_sync() -> dict:
    payload = sync_telegram_setup()
    if payload.get("ok") is False:
        raise HTTPException(status_code=400, detail=payload.get("message", "Failed to sync Telegram setup."))
    return payload


@app.post("/api/settings/telegram/commands/sync", dependencies=[Depends(require_ui_token)])
def post_settings_telegram_commands_sync() -> dict:
    payload = sync_telegram_shortcuts()
    if payload.get("ok") is False:
        raise HTTPException(status_code=400, detail=payload.get("message", "Failed to sync Telegram command shortcuts."))
    return payload


@app.get("/api/agents/supported")
def get_supported_agents() -> dict:
    return supported_agents_summary()


@app.get("/api/agents/installed")
def get_installed_agents() -> dict:
    return installed_agents_summary()


@app.get("/api/openclaw/setup", dependencies=[Depends(require_ui_token)])
def get_openclaw_setup(agent: str = Query(min_length=1)) -> dict:
    payload = openclaw_setup_summary(agent)
    if not payload.get("ok"):
        raise HTTPException(status_code=404, detail=payload.get("message", "OpenClaw setup context not found"))
    return payload


@app.post("/api/openclaw/setup", dependencies=[Depends(require_ui_token)])
def post_openclaw_setup(req: OpenClawSetupApplyRequest, agent: str = Query(min_length=1)) -> dict:
    payload = apply_openclaw_setup(agent, req.model_dump(exclude_none=True))
    if payload.get("ok") is False:
        raise HTTPException(status_code=400, detail=payload.get("message", "Failed to apply OpenClaw setup"))
    return payload


@app.post("/api/openclaw/native-ui/approve", dependencies=[Depends(require_ui_token)])
def post_openclaw_native_ui_approve(
    request: Request,
    agent: str = Query(min_length=1),
    openedAtMs: int = Query(default=0, ge=0),
) -> dict:
    payload = approve_openclaw_native_ui_browser(
        agent,
        opened_at_ms=openedAtMs,
        ui_origin=_request_origin(request),
    )
    if payload.get("ok") is False:
        raise HTTPException(status_code=400, detail=payload.get("message", "Failed to approve OpenClaw browser access"))
    return payload


def _openclaw_proxy_headers(source_headers) -> dict[str, str]:
    headers: dict[str, str] = {}
    for key, value in source_headers.items():
        lower = key.lower()
        if lower in _HOP_BY_HOP_HEADERS:
            continue
        headers[key] = value
    return headers


async def _proxy_openclaw_http(request: Request, agent: str, rest: str) -> Response:
    target = openclaw_native_ui_upstream(agent)
    if not target.get("ok"):
        raise HTTPException(status_code=404, detail=target.get("message", "OpenClaw native UI is unavailable"))
    suffix = f"/{rest or ''}".replace("//", "/")
    query = f"?{request.url.query}" if request.url.query else ""
    upstream_url = f"{target['http_base']}{suffix}{query}"
    headers = _openclaw_proxy_headers(request.headers)
    body = await request.body()
    timeout = httpx.Timeout(30.0, connect=10.0)
    async with httpx.AsyncClient(follow_redirects=False, timeout=timeout) as client:
        upstream = await client.request(
            request.method,
            upstream_url,
            headers=headers,
            content=body,
        )
    response_headers = {
        key: value
        for key, value in upstream.headers.items()
        if key.lower() not in _HOP_BY_HOP_HEADERS
    }
    return Response(content=upstream.content, status_code=upstream.status_code, headers=response_headers)


@app.api_route("/api/openclaw/native-ui/{agent}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
@app.api_route("/api/openclaw/native-ui/{agent}/{rest:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"])
async def proxy_openclaw_native_ui_http(request: Request, agent: str, rest: str = "") -> Response:
    return await _proxy_openclaw_http(request, agent, rest)


@app.websocket("/api/openclaw/native-ui/{agent}")
@app.websocket("/api/openclaw/native-ui/{agent}/{rest:path}")
async def proxy_openclaw_native_ui_ws(websocket: WebSocket, agent: str, rest: str = "") -> None:
    target = openclaw_native_ui_upstream(agent)
    if not target.get("ok"):
        await websocket.close(code=4404)
        return
    suffix = f"/{rest or ''}".replace("//", "/")
    query = f"?{websocket.url.query}" if websocket.url.query else ""
    upstream_url = f"{target['ws_base']}{suffix}{query}"
    subprotocols = list(websocket.scope.get("subprotocols") or [])
    upstream_origin = _header_origin(websocket.headers, fallback_scheme="http")
    await websocket.accept()
    try:
        async with websockets.connect(
            upstream_url,
            subprotocols=subprotocols or None,
            origin=upstream_origin or None,
        ) as upstream:
            async def client_to_upstream() -> None:
                while True:
                    message = await websocket.receive()
                    if message["type"] == "websocket.disconnect":
                        break
                    if message.get("text") is not None:
                        await upstream.send(message["text"])
                    elif message.get("bytes") is not None:
                        await upstream.send(message["bytes"])

            async def upstream_to_client() -> None:
                async for message in upstream:
                    if isinstance(message, bytes):
                        await websocket.send_bytes(message)
                    else:
                        await websocket.send_text(message)

            await asyncio.gather(client_to_upstream(), upstream_to_client())
    except WebSocketDisconnect:
        return
    except ConnectionClosed:
        await websocket.close()
    except Exception:
        await websocket.close(code=1011)


@app.get("/api/jobs")
def get_jobs(limit: int = 30) -> dict:
    return {"ok": True, "jobs": read_recent_jobs(limit=limit)}


@app.get("/api/jobs/{job_id}")
def get_job(job_id: str) -> dict:
    payload = orchestrator.get_job_status(job_id)
    if not payload.get("ok"):
        raise HTTPException(status_code=404, detail=payload.get("message", "Job not found"))
    return sanitize_job_payload(payload)


@app.post("/api/actions/install", dependencies=[Depends(require_ui_token)])
def post_install(req: InstallActionRequest) -> dict:
    return orchestrator.start_install_agent_job(
        agent=req.agent,
        alias=req.alias,
        skip_scan=bool(req.skipScan),
    )


@app.post("/api/actions/run", dependencies=[Depends(require_ui_token)])
def post_run(req: RunActionRequest) -> dict:
    return orchestrator.start_run_agent_job(
        agent=req.agent,
        mode=req.mode,
        command=req.command,
    )


@app.post("/api/actions/uninstall", dependencies=[Depends(require_ui_token)])
def post_uninstall(req: UninstallActionRequest) -> dict:
    return orchestrator.start_uninstall_agent_job(
        agent=req.agent,
        force=bool(req.force),
    )


@app.post("/api/chat", dependencies=[Depends(require_ui_token)])
def post_chat(req: ChatRequest) -> dict:
    ensure_armorer_core()
    config_dir = resolve_armorer_config_dir()
    cm = ConfigManager()
    relay = InboxOutboxRelay(
        config_dir=config_dir,
        signing_key=cm.get_key("ARMORER_TASK_SIGNING_KEY"),
    )
    metadata = build_local_cli_task_metadata()
    session_id = str(req.sessionId or "").strip()
    if session_id:
        metadata["channel_id"] = f"web:{session_id}"
    task_id = relay.submit_task(prompt=req.prompt, source="cli", metadata=metadata)
    result = relay.wait_for_response(task_id, timeout=300, poll_interval=0.5, include_ui_hints=False)
    raw_payload = result.get("raw", {}).get("response") if isinstance(result.get("raw"), dict) else {"message": result.get("message", "")}
    payload = _normalize_core_response(raw_payload)
    payload["ok"] = True
    payload["task_id"] = task_id
    if result.get("timed_out"):
        payload["timed_out"] = True
    return payload


@app.get("/api/openclaw/chat/history", dependencies=[Depends(require_ui_token)])
def get_openclaw_chat_history(
    session_id: str = Query(alias="sessionId", min_length=1),
    limit: int = Query(default=60, ge=1, le=200),
) -> dict:
    return openclaw_chat_history(session_id=session_id, limit=limit)


@app.post("/api/openclaw/chat", dependencies=[Depends(require_ui_token)])
def post_openclaw_chat(req: OpenClawChatRequest) -> dict:
    return run_openclaw_chat_turn(prompt=req.prompt, session_id=req.sessionId)
