# -*- coding: utf-8 -*-
"""园区子应用（app/park）—— 大屏控制 / 数据 / 设置 / 园区企业（骨架版）。

迁自 park-desktop/backend/app/routers.py，本骨架仅含不依赖重模块（llm/rag/asr/
s2s/vision）的大屏控制端点；AI/知识库/语音/视觉在后续全量迁移补齐。
全部端点经 dispatcher `/park` 前缀暴露（include_router 挂 `/park`）。
"""
from __future__ import annotations

import json
import logging
import time

from fastapi import APIRouter, File, Header, HTTPException, Request, UploadFile
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel, Field

from . import park_config, tenants
from .auth import create_token, parse_token, require_tenant
from .config import settings
from .event_bus import bus
from .mqtt import hub
from .sim_engine import engine_data, get_engine, refresh_engine, sim_engine
from .storage import storage

logger = logging.getLogger("dpm.api")


def _resolve_tenant(authorization: str | None, tenant_id: str | None) -> str:
    """解析目标园区：?tenant_id=（admin 亮传）> 大屏 park-token > 默认园区。
    admin 携带平台 token，不应被当作 park-token 解析失败；故 tenant_id 优先。"""
    if tenant_id:
        return tenant_id
    if authorization and authorization.startswith("Bearer "):
        try:
            return parse_token(authorization.split(" ", 1)[1])
        except HTTPException:
            pass  # 平台 token → 回退默认
    return tenants.ensure_default_tenant()

log = logging.getLogger("dpm.api")

router = APIRouter()


# ==================== 园区认证 + 租户管理 ====================

class LoginBody(BaseModel):
    username: str
    password: str


class TenantBody(BaseModel):
    name: str = "新园区"
    intro: list[str] = ["", ""]
    username: str = "admin"
    password: str = "123456"


class TenantPatch(BaseModel):
    name: str | None = None
    intro: list[str] | None = None
    username: str | None = None
    password: str | None = None


class BindBody(BaseModel):
    username: str


@router.post("/auth/login", summary="园区端登录（绑定为园区管理员的平台账号）")
async def park_login(body: LoginBody):
    # 登录 = 校验平台账号密码 + 该账号已绑定为某园区管理员；成功 → 签发该园区 tenant token
    from app.infrastructure.repositories import Database
    db = Database()
    try:
        user = await db.users.get_by_username(body.username.strip())
        if not user or not await db.users.verify_password(user, body.password):
            return JSONResponse({"ok": False, "error": "账号或密码不正确"}, status_code=401)
        t = tenants.find_by_admin(body.username.strip())
        if not t:
            return JSONResponse({"ok": False, "error": "该账号未绑定任何园区管理员"}, status_code=401)
        if t.get("status") == "disabled":
            return JSONResponse({"ok": False, "error": "该园区已禁用，禁止登录使用"}, status_code=403)
    finally:
        await db.close()
    return {"ok": True, "tenant_id": t["id"], "name": t["name"], "intro": t["intro"], "token": create_token(t["id"])}


@router.get("/auth/me", summary="当前园区资料（页眉）")
async def park_me(authorization: str | None = Header(default=None)):
    tid = require_tenant(authorization)
    t = tenants.get_tenant(tid)
    if t is None:
        raise HTTPException(status_code=404, detail="园区不存在")
    return {"ok": True, "tenant_id": tid, "name": t["name"], "intro": t.get("intro", [])}


# ---- 租户管理（运营端）----
@router.get("/tenants", summary="园区列表")
async def tenant_list():
    return {"ok": True, "tenants": tenants.list_tenants()}


@router.post("/tenants", summary="创建园区（名称/两段式简介/账密，seed 默认数据）")
async def tenant_create(body: TenantBody):
    t = tenants.create_tenant(body.name, body.intro, body.username, body.password)
    return {"ok": True, "tenant": t}


@router.put("/tenants/{tid}", summary="更新园区（名称/简介/管理员账号/密码）")
async def tenant_update(tid: str, body: TenantPatch):
    patch = {k: v for k, v in body.model_dump(exclude_none=True).items() if v is not None}
    t = tenants.update_tenant(tid, patch)
    if t is None:
        return JSONResponse({"ok": False, "error": "园区不存在"}, status_code=404)
    refresh_engine(tid)
    return {"ok": True, "tenant": t}


@router.delete("/tenants/{tid}", summary="删除园区")
async def tenant_delete(tid: str):
    ok = tenants.delete_tenant(tid)
    return {"ok": ok, "id": tid}


@router.post("/tenants/{tid}/bind", summary="绑定园区管理员（已有平台账号）")
async def tenant_bind(tid: str, body: BindBody):
    from app.infrastructure.repositories import Database
    db = Database()
    try:
        user = await db.users.get_by_username(body.username.strip())
    finally:
        await db.close()
    if user is None:
        return JSONResponse({"ok": False, "error": "账号不存在"}, status_code=404)
    ok = tenants.bind_admin(tid, body.username.strip())
    return {"ok": ok, "tenant_id": tid, "admin_username": body.username.strip()}


@router.post("/tenants/{tid}/unbind", summary="解除园区管理员")
async def tenant_unbind(tid: str):
    ok = tenants.unbind_admin(tid)
    return {"ok": ok, "tenant_id": tid}


class StatusBody(BaseModel):
    status: str  # active | disabled


@router.post("/tenants/{tid}/status", summary="禁用/启用园区（不删，禁用禁止登录）")
async def tenant_status(tid: str, body: StatusBody):
    ok = tenants.set_status(tid, body.status)
    if not ok:
        return JSONResponse({"ok": False, "error": "无效状态或园区不存在"}, status_code=400)
    return {"ok": True, "tenant_id": tid, "status": body.status}


# ==================== 数据模型 ====================

class SettingsBody(BaseModel):
    volume: int | None = None
    sfx_volume: int | None = None
    play_mode: str | None = None
    image_duration: int | None = None
    fullscreen: bool | None = None
    autostart: bool | None = None
    username: str | None = None
    password: str | None = None


class DisplayCommandBody(BaseModel):
    action: str
    params: dict = Field(default_factory=dict)
    screen_id: str = ""
    screen_role: str = ""


class RegisterBody(BaseModel):
    device_id: str = ""
    role: str = ""
    parent_id: str = ""


class CompanyBody(BaseModel):
    name: str = ""
    zone: str = ""
    room: str = ""
    industry: str = ""
    bio: str = ""
    founder: str = ""
    status: str = "applying"
    employees: int | None = None


class AgentBody(BaseModel):
    system_prompt: str | None = None
    model: str | None = None
    enabled_tools: list[str] | None = None
    preset_questions: list[str] | None = None
    opening: str | None = None


class KbDocBody(BaseModel):
    group: str = "general"
    title: str = ""
    content_md: str = ""


class ScreenBody(BaseModel):
    name: str | None = None
    region: str | None = None
    capacity: int | None = None
    invested: int | None = None
    jobs: int | None = None
    area: int | None = None
    founded: int | None = None
    address: str | None = None
    phone: str | None = None
    email: str | None = None
    revenue_total: float | None = None
    revenue_tax: float | None = None
    intro: list[str] | None = None
    feed: list[dict] | None = None
    zones: list[dict] | None = None
    industryMix: list[dict] | None = None


# ==================== 健康 / 设置 / 引导 ====================

@router.get("/api/health")
async def health():
    return {
        "ok": True,
        "service": "opc-park",
        "mqtt_connected": hub.connected,
        "screens_online": hub.screens_online(),
        "version": "1.0.0",
    }


@router.get("/api/settings")
async def get_settings():
    return storage.get_settings()


@router.post("/api/settings")
async def update_settings(body: SettingsBody):
    patch = body.model_dump(exclude_none=True)
    storage.update_settings(**patch)
    hub.publish_command("settings_changed", patch)
    return storage.get_settings()


@router.get("/api/config")
async def runtime_config(request: Request):
    """大屏启动引导：下发 MQTT/语音接入地址与 tick 间隔。"""
    return {
        "mqtt": {
            "host": settings.MQTT_HOST,
            "port": settings.MQTT_PORT,
            "ws_url": settings.MQTT_WS_URL,
            "tick_interval": settings.MQTT_TICK_INTERVAL,
        },
        "voice_ws": settings.S2S_WS_URL if settings.S2S_ENABLED else "",
        # 大屏前端 request() 以 `${api_base}/api/...`、`${api_base}/file/...` 拼接，
        # 故 api_base 返回 `/park` 前缀（不含 /api），避免 /api 双写。
        "media_base": f"{request.base_url}park/file",
        "api_base": f"{request.base_url}park",
    }


# ==================== 大屏数据（sim_engine） ====================

@router.get("/api/dashboard/snapshot")
async def dashboard_snapshot(authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return get_engine(tid).snapshot()


@router.get("/api/dashboard/overview")
async def dashboard_overview(authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return get_engine(tid).snapshot()


@router.get("/api/park/zones")
async def park_zones(authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return get_engine(tid).snapshot().get("zones", [])


# ==================== 屏幕管理（园区端创建/管理大屏设备） ====================

class ScreenBody2(BaseModel):
    device_id: str = ""
    name: str = ""
    role: str = "main"
    location: str = ""


@router.get("/api/screens")
async def screens_list(authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return tenants.list_screens(tid)


@router.post("/api/screens")
async def screens_create(body: ScreenBody2, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return tenants.create_screen(tid, body.model_dump())


@router.delete("/api/screens/{sid}")
async def screens_delete(sid: str, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    ok = tenants.delete_screen(tid, sid)
    return {"ok": ok, "id": sid}


# ==================== 入驻企业（租户 data.companies 主数据源） ====================

@router.get("/api/park/companies")
async def park_companies(authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return tenants.list_companies(tid)


@router.post("/api/park/companies")
async def park_company_create(body: CompanyBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return tenants.create_company(tid, body.model_dump())


@router.put("/api/park/companies/{cid}")
async def park_company_update(cid: str, body: CompanyBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    row = tenants.update_company(tid, cid, body.model_dump(exclude_unset=True))
    if row is None:
        return JSONResponse({"ok": False, "error": "企业不存在"}, status_code=404)
    return row


@router.delete("/api/park/companies/{cid}")
async def park_company_delete(cid: str, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    ok = tenants.delete_company(tid, cid)
    return {"ok": ok, "id": cid}


# ==================== 园区智能体 / 知识库 / 大屏数据 ====================

@router.get("/api/agent/config")
async def agent_config_get(authorization: str | None = Header(None), tenant_id: str | None = None):
    return tenants.get_agent(_resolve_tenant(authorization, tenant_id))


@router.put("/api/agent/config")
async def agent_config_put(body: AgentBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return tenants.update_agent(tid, body.model_dump(exclude_none=True))


@router.get("/api/kb/docs")
async def kb_docs_list(authorization: str | None = Header(None), tenant_id: str | None = None):
    return tenants.kb_docs(_resolve_tenant(authorization, tenant_id))


@router.post("/api/kb/docs")
async def kb_docs_create(body: KbDocBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    return tenants.create_kb_doc(_resolve_tenant(authorization, tenant_id), body.model_dump())


@router.put("/api/kb/docs/{did}")
async def kb_docs_update(did: str, body: KbDocBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    row = tenants.update_kb_doc(tid, did, body.model_dump(exclude_unset=True))
    if row is None:
        return JSONResponse({"ok": False, "error": "文档不存在"}, status_code=404)
    return row


@router.delete("/api/kb/docs/{did}")
async def kb_docs_delete(did: str, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    ok = tenants.delete_kb_doc(tid, did)
    return {"ok": ok, "id": did}


@router.post("/api/kb/reindex")
async def kb_reindex(authorization: str | None = Header(None), tenant_id: str | None = None):
    # 知识库索引重建（重模块 rag 后续接入）；当前仅返回 ok 占位
    return {"ok": True, "reindexed": False}


@router.get("/api/screen/data")
async def screen_data_get(authorization: str | None = Header(None), tenant_id: str | None = None):
    return tenants.screen_view(_resolve_tenant(authorization, tenant_id))


@router.put("/api/screen/data")
async def screen_data_put(body: ScreenBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    return tenants.update_screen(tid, body.model_dump(exclude_none=True))


# ==================== AI / 知识库 / 语音识别 / 视觉（重模块已迁入） ====================

class ChatMessage(BaseModel):
    role: str
    content: str


class ChatBody(BaseModel):
    messages: list[ChatMessage]
    screen_id: str = ""


class ToolsExecBody(BaseModel):
    name: str = ""
    args: dict = {}
    screen_id: str = ""


class VisionEventBody(BaseModel):
    event: str
    faces: int = 0
    dwell_ms: int = 0
    detail: str = ""


class VisionFrameBody(BaseModel):
    image: str = ""
    conf: float = 0.0


class VisionLlmBody(BaseModel):
    image: str = ""
    prompt: str = ""


AI_GROUPED_QUESTIONS = [
    {"title": "入驻与流程", "questions": ["如何申请入驻园区？", "园区入驻的条件有哪些？", "入驻需要准备哪些材料？", "入驻流程是怎样的？", "入驻评审如何打分？", "入驻需要多长时间？"]},
    {"title": "政策与扶持", "questions": ["园区有哪些创业政策扶持？", "如何申请创业补贴？", "如何申请创业担保贷款？", "对高校毕业生有什么优惠？", "科技成果转化有哪些支持？"]},
    {"title": "OPC 概念", "questions": ["什么是 OPC？", "OPC 创业有哪些模式？", "OPC 适合哪些人？", "OPC 创业者从哪里开始？", "OPC 常用的人工智能工具有哪些？"]},
    {"title": "场地与服务", "questions": ["园区提供哪些免费办公空间？", "园区有哪些孵化服务？", "园区有哪些创业辅导？", "园区有哪些基础配套？", "园区可以免费使用哪些资源？"]},
    {"title": "园区企业介绍", "questions": ["介绍一下园区入驻企业", "园区有哪些 AI 科技企业？", "园区有哪些跨境电商企业？", "园区有哪些生物医药企业？", "介绍一下云南派音人工智能科技"]},
]
AI_OPC_TOOLS = ["DeepSeek", "通义千问", "ChatGPT", "豆包", "Midjourney", "Stable Diffusion", "剪映", "Notion AI", "WPS AI", "GitHub Copilot"]


@router.get("/api/ai/questions")
async def ai_questions():
    return {"groups": AI_GROUPED_QUESTIONS, "opcTools": AI_OPC_TOOLS}


@router.post("/api/ai/chat")
async def ai_chat(body: ChatBody):
    from .llm import run_chat as llm_run_chat
    messages = [m.model_dump() for m in body.messages]
    return llm_run_chat(messages, body.screen_id or "")


@router.post("/api/ai/asr")
async def ai_asr(file: UploadFile = File(...), format: str = "m4a"):
    """语音识别：上传录音（表单 file）→ 阿里云 paraformer 转写为文本。"""
    from .asr import transcribe as asr_transcribe
    data = await file.read()
    if not data:
        return JSONResponse({"ok": False, "error": "空音频"}, status_code=400)
    try:
        return {"ok": True, "text": asr_transcribe(data, fmt=format)}
    except Exception as e:  # noqa: BLE001
        return JSONResponse({"ok": False, "error": str(e)}, status_code=502)


@router.post("/api/tools/exec")
def tools_exec(body: ToolsExecBody):
    from .tools import exec_tool
    name = (body.name or "").strip()
    if not name:
        return {"ok": False, "error": "missing tool name"}
    result = exec_tool(name, body.args or {}, body.screen_id or "")
    return {"ok": True, "result": result}


@router.get("/api/kb/brief")
def kb_brief(max_chars: int = 1200):
    try:
        from .rag import brief
        return {"ok": True, "brief": brief(max_chars=max_chars)}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


@router.get("/api/kb/retrieve")
def kb_retrieve(q: str = "", top_k: int = 3):
    if not q.strip():
        return {"ok": False, "error": "missing q"}
    try:
        from .rag import retrieve
        return {"ok": True, "chunks": retrieve(q, top_k=top_k)}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


@router.get("/api/s2s/instructions")
def s2s_instructions():
    try:
        from .rag import build_instructions
        return {"ok": True, "instructions": build_instructions()}
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


@router.post("/api/vision/event")
async def vision_event(body: VisionEventBody):
    log.info("vision event=%s faces=%d dwell_ms=%d detail=%s", body.event, body.faces, body.dwell_ms, body.detail)
    return {"ok": True}


@router.post("/api/vision/frame")
def vision_frame(body: VisionFrameBody):
    img_b64 = (body.image or "").strip()
    if not img_b64:
        return {"ok": False, "error": "missing image"}
    from .vision_yolo import predict_base64
    return predict_base64(img_b64)


@router.post("/api/vision/llm")
def vision_llm(body: VisionLlmBody):
    img = (body.image or "").strip()
    if not img:
        return {"ok": False, "error": "missing image"}
    try:
        from .vision_llm import analyze_scene
        return analyze_scene(img, body.prompt)
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": str(e)}


# ==================== 展示控制（管理端 → MQTT） ====================
# 全部 MQTT 前端控制命令白名单（与 park-desktop docs/mqtt-commands.md 一致）
_VALID_DISPLAY_ACTIONS = {
    "navigate", "navigate_rel",
    "vision_set",
    "ai_input", "ai_preset", "ai_company", "ai_zone",
    "voice_start", "voice_stop", "voice_refresh",
    "play", "pause", "next", "prev", "set_mode", "play_target",
    "dual_screen",
    "alert", "show_card", "minimize", "settings_changed", "playlist_changed",
}


@router.post("/api/display/register")
async def display_register(body: RegisterBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    hub.register_device(body.device_id, body.role, body.parent_id, tid)
    return {"ok": True, "device_id": body.device_id, "role": body.role, "parent_id": body.parent_id, "tenant_id": tid}


@router.post("/api/display/command")
async def display_command_publish(body: DisplayCommandBody, authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = tenant_id or require_tenant(authorization)
    if body.action not in _VALID_DISPLAY_ACTIONS:
        return JSONResponse({"ok": False, "error": f"不支持的指令: {body.action}"}, status_code=400)
    cmd = hub.publish_command(body.action, body.params, body.screen_id or None, body.screen_role or None, tid)
    return {"ok": cmd.get("published", False), "cmd_id": cmd["cmd_id"], "action": body.action,
            "screen_id": body.screen_id or "", "screen_role": body.screen_role or "both",
            "tenant_id": tid, "mqtt_connected": hub.connected}


@router.get("/api/display/state")
async def display_state(authorization: str | None = Header(None), tenant_id: str | None = None):
    tid = _resolve_tenant(authorization, tenant_id)
    st = hub.status()
    # 只返回该租户在线设备
    st["devices"] = [d for d in st.get("devices", []) if _device_tenant(d) == tid]
    st["screens_online"] = len(st["devices"])
    st["tenant_id"] = tid
    return st


def _device_tenant(device: dict) -> str:
    # status() 未带 tenant_id，从 hub 设备表补齐
    for cid, v in hub.devices.items():
        if cid == device.get("device_id"):
            return v.get("tenant_id", "")
    return ""


# ==================== SSE 兼容通道（MQTT 不可用时前端回退） ====================

@router.get("/api/events")
async def sse_events(request: Request):
    async def gen():
        q = bus.subscribe()
        try:
            while True:
                if await request.is_disconnected():
                    break
                try:
                    data = await asyncio_timeout(q.get(), 15)
                    yield f"data: {data}\n\n"
                except Exception:  # noqa: BLE001
                    yield ": keepalive\n\n"
        finally:
            bus.unsubscribe(q)

    return StreamingResponse(gen(), media_type="text/event-stream")


def asyncio_timeout(awaitable, seconds):
    """极简超时包装：避免依赖 asyncio.timeout（3.11+ 的上下文管理器）。"""
    import asyncio
    return asyncio.wait_for(awaitable, timeout=seconds)
