from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import and_, desc, asc, func from pydantic import BaseModel, Field from typing import Optional import uuid from datetime import datetime import httpx from urllib.parse import urlparse, urljoin import re import json import os from ..database import get_db from ..models import ( User, MarketApp, AppPurchase, AppReview, AppInstallation, Order, AdBanner, MCPServer, ModelProvider, gen_uuid ) from ..auth import get_current_user, get_current_user_id, get_current_user_id_optional from ..middleware.log_service import LogService from ..capability_registry import resolve_auto_model as _resolve_auto_model from app.api.admin.mcp_admin import _test_mcp_server, parse_mcp_config router = APIRouter() def _extract_url_from_text(text: Optional[str]) -> Optional[str]: """Extract first http/https URL from text.""" if not text: return None match = re.search(r"https?://[^\s\"'<>\)\]\}]+", text) return match.group(0) if match else None def _skill_http_proxy(): """容器内走智能代理;显式返回供 httpx 使用(trust_env=False 避免误用系统代理导致 502)。""" return os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy") or None def _sanitize_skill_content(text: str) -> str: """仅当内容疑似有效 SKILL.md 时才接受,避免把无关网页/乱码写进 skill_content。""" if not text: return "" t = text.strip() if len(t) < 50: return "" low = t.lower() # 拒绝整页 HTML(避免把网页源码当技能内容写库) if low.startswith(" 3: return "" # 接受:YAML front matter 或 含 markdown 标题(# / ##) has_front = t.startswith("---") and ("name:" in t or "description:" in t) has_heading = t.startswith("#") or "\n#" in t or "##" in t if not (has_front or has_heading): return "" return t def _extract_skill_from_html(html: str, base_url: str) -> str: """从 HTML 页面尽力抽取 SKILL.md 正文(代码块优先,其次跳转到 raw 链接)。""" proxy = _skill_http_proxy() # 1) fenced code block fences = re.findall(r"```(?:markdown|md|yaml|json)?\s*\n(.*?)```", html, re.DOTALL | re.IGNORECASE) for f in fences: if ("name:" in f.lower() or "description:" in f.lower() or f.count("#") >= 2) and len(f) > 80: return f.strip() # 2)
/ blocks
    for tag in (r"pre", r"code"):
        for b in re.findall(r"<%s[^>]*>(.*?)" % (tag, tag), html, re.DOTALL | re.IGNORECASE):
            txt = re.sub(r"<[^>]+>", "", b)
            if ("name:" in txt.lower() or txt.count("#") >= 2) and len(txt) > 80:
                return txt.strip()
    # 3) 跳转到 raw SKILL.md 链接(最多试 3 个)
    links = re.findall(r'href=["\']([^"\']*(?:SKILL\.md|skill\.md|/raw/|/raw\.)[^"\']*)["\']', html, re.IGNORECASE)
    for link in links[:3]:
        try:
            abs_url = urljoin(base_url, link)
            with httpx.Client(timeout=15, follow_redirects=True, trust_env=False, proxy=proxy) as c:
                rr = c.get(abs_url, headers={"User-Agent": "Mozilla/5.0 (compatible; SkillFetcher/1.0)"})
                if rr.status_code == 200 and len(rr.text) > 80:
                    return rr.text.strip()
        except Exception:
            continue
    return ""


def _fetch_skill_content(url: str) -> str:
    """抓取目录 URL 上的 SKILL.md 内容回填;失败或无效则返回空字符串。"""
    if not url or not url.startswith("http"):
        return ""
    proxy = _skill_http_proxy()
    try:
        with httpx.Client(timeout=20, follow_redirects=True, trust_env=False, proxy=proxy) as client:
            r = client.get(url, headers={"User-Agent": "Mozilla/5.0 (compatible; SkillFetcher/1.0)"})
            if r.status_code != 200:
                return ""
            raw = r.text
        ctype = (r.headers.get("content-type") or "").lower()
        u = url.rstrip("/")
        if u.endswith(".md") or "text/plain" in ctype or "markdown" in ctype:
            return _sanitize_skill_content(raw)
        return _sanitize_skill_content(_extract_skill_from_html(raw, url))
    except Exception:
        return ""


# ===== Agent 接入前连通性 / 鉴权校验 =====
def _normalize_dify_url(endpoint: str) -> str:
    ep = endpoint.rstrip("/")
    if ep.endswith("/chat-messages"):
        return ep
    if ep.endswith("/v1/chat-messages"):
        return ep
    if ep.endswith("/v1"):
        return ep + "/chat-messages"
    return ep + "/v1/chat-messages"


def _short_err(e: Exception) -> str:
    return str(e)[:160]


def _post_follow(url: str, headers: dict, json_body: dict, timeout: float = 15.0):
    """POST 并手动跟随 301/302/307/308 重定向(**保持 POST 方法与请求体**)。

    背景:Dify 若填写 http:// 地址会 301 永久重定向到 https://;httpx 内建
    follow_redirects 对 301/302 会把 POST 降级为 GET(浏览器行为),导致对
    /v1/chat-messages 变成 GET → 405 method_not_allowed。故手动跟随并保持 POST。
    容器内 env 有坏代理(502),必须 trust_env=False 直连。
    """
    cur = url
    last = None
    for _ in range(4):
        last = httpx.post(cur, headers=headers, json=json_body, timeout=timeout,
                          trust_env=False, follow_redirects=False)
        if last.status_code in (301, 302, 307, 308):
            loc = last.headers.get("location")
            if not loc:
                return last
            cur = loc if loc.startswith("http") else str(httpx.URL(cur).join(loc))
            continue
        return last
    return last


def verify_agent_connectivity(platform: str, endpoint: str, api_key: str) -> dict:
    """
    接入 Agent 前的连通性 + 鉴权校验。
    返回 {ok, verified, detail}
      ok:       是否通过(dify=鉴权通过;n8n/coze/其他=地址可达)
      verified: 是否做了真正的鉴权验证(dify 为 True,其余为 False)
    """
    ep = (endpoint or "").strip()
    if not ep or not ep.startswith(("http://", "https://")):
        return {"ok": False, "verified": False, "detail": "Endpoint 必须以 http:// 或 https:// 开头"}
    platform = (platform or "dify").lower()

    # 关键:容器内 env 有坏代理(502),必须 trust_env=False 直连
    if platform == "dify":
        url = _normalize_dify_url(ep)
        payload = {
            "inputs": {},
            "query": "连通性测试",
            "user": "__verify__",
            "response_mode": "blocking",
        }
        headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
        try:
            r = _post_follow(url, headers, payload, timeout=15.0)
        except Exception as e:
            return {"ok": False, "verified": False, "detail": f"无法连接 Dify 地址:{_short_err(e)}"}
        if r.status_code == 200:
            return {"ok": True, "verified": True, "detail": "连通且 API Key 有效"}
        if r.status_code in (401, 403):
            return {"ok": False, "verified": False, "detail": "API Key 无效(401/403)"}
        if r.status_code == 404:
            return {"ok": False, "verified": False,
                    "detail": "地址路径不正确(404),应为 .../v1/chat-messages"}
        if r.status_code in (301, 302, 307, 308):
            loc = r.headers.get("location") or ""
            return {"ok": False, "verified": False,
                    "detail": f"地址被重定向({r.status_code})到 {loc or '未知'};"
                              f"请直接填写重定向后的地址(通常把 http:// 改为 https://)"}
        if r.status_code == 400:
            # 400 需区分两种情况:
            #  (a) 到达应用且鉴权通过,但 Dify 应用/工作流内部执行报错(invalid_param + Run failed)
            #  (b) 纯参数错误
            body = (r.text or "")
            low = body.lower()
            if ("run failed" in low or "error code" in low or "workflow" in low
                    or "node" in low or "-500" in body):
                return {"ok": True, "verified": True,
                        "detail": "连通且 API Key 有效;但你的 Dify 应用内部执行报错:"
                                  f"{body[:180]}。这是你自建 Dify 应用/工作流本身的问题"
                                  "(例如代码节点沙箱缺少 python3),请在 Dify 侧修复。"}
            return {"ok": False, "verified": False, "detail": f"参数错误(400):{body[:180]}"}
        return {"ok": False, "verified": False, "detail": f"Dify 返回状态码 {r.status_code}"}

    # n8n / coze / 其他:仅做可达性探测(HEAD,避免触发 webhook 副作用)
    try:
        r = httpx.head(ep, timeout=12.0, trust_env=False, follow_redirects=True)
        if r.status_code < 500:
            return {"ok": True, "verified": False,
                    "detail": f"地址可连通(HTTP {r.status_code}),运行时再做协议级鉴权"}
        return {"ok": False, "verified": False, "detail": f"服务返回 {r.status_code},地址可能不可用"}
    except Exception as e:
        return {"ok": False, "verified": False, "detail": f"地址不可达:{_short_err(e)}"}


# ===== 业务场景分类(面向用户的技能/MCP 分类,与后端模型路由的 capability_registry 解耦) =====
# 注意:MarketApp.capability 列在此存放「业务场景 id」(见 MARKET_SCENES),不再是 text/vision 等模型能力。
MARKET_SCENES: list = [
    # 元宝生成的「岗位 / 职能」分类(面向使用者视角,与后端模型路由能力解耦)
    {"id": "design",    "label": "🎨 做设计",     "color": "#EC4899", "desc": "页面、UI、视觉"},
    {"id": "frontend",  "label": "💻 写前端",     "color": "#3B82F6", "desc": "Web 页面、React、Next.js"},
    {"id": "mobile",    "label": "📱 做移动端",   "color": "#06B6D4", "desc": "React Native、Expo"},
    {"id": "backend",   "label": "🧪 后端/全栈",  "color": "#8B5CF6", "desc": "API、数据库、TypeScript"},
    {"id": "qa",        "label": "🧪 测试质量",   "color": "#6366F1", "desc": "TDD、Playwright、验证"},
    {"id": "search",    "label": "🔍 搜索信息",   "color": "#0EA5E9", "desc": "网页抓取、质疑、SEO"},
    {"id": "docs",      "label": "✍️ 写文档",     "color": "#F59E0B", "desc": "Word/PPT/PDF、文案、PRD"},
    {"id": "marketing", "label": "📢 推广获客",   "color": "#F43F5E", "desc": "营销、社媒、邮件、定价"},
    {"id": "workflow",  "label": "🔄 工作流",     "color": "#14B8A6", "desc": "计划、调试、分级、原型"},
    {"id": "secops",    "label": "🛡️ 安全运维",  "color": "#EF4444", "desc": "安全审计、Azure、K8s"},
    {"id": "learning",  "label": "🎓 学习教学",   "color": "#10B981", "desc": "教学、提问、架构改进"},
    {"id": "video",     "label": "🎬 视频多媒体", "color": "#D946EF", "desc": "AI视频、数字人"},
    {"id": "collab",    "label": "🏢 办公协作",   "color": "#64748B", "desc": "飞书全家桶"},
    {"id": "ai",        "label": "🛠️ AI能力",     "color": "#A855F7", "desc": "创建Skill、代码审查、统计"},
]
_VALID_SCENE_IDS = {s["id"] for s in MARKET_SCENES}


def _scene_label(scene_id):
    return next((s["label"] for s in MARKET_SCENES if s["id"] == scene_id), None)


def _classify_scene(name: str, description: Optional[str], content: Optional[str], db: Session) -> Optional[str]:
    """用平台文本模型把应用归类到面向用户的业务场景之一;失败返回 None(不阻断导入)。"""
    try:
        text_model = _resolve_auto_model("text", db)
        if not text_model:
            return None
        provider = None
        if getattr(text_model, "provider_id", None):
            provider = db.query(ModelProvider).filter(ModelProvider.id == text_model.provider_id).first()
        api_key = text_model.api_key or (provider.api_key if provider else None)
        if not api_key:
            return None
        api_url = text_model.endpoint or (
            f"{provider.api_base_url}/v1/chat/completions" if provider and provider.api_base_url else None
        )
        if not api_url:
            return None
        options = "、".join(f"{s['id']}({s['label']}:{s.get('desc','')})" for s in MARKET_SCENES)
        sys_prompt = (
            "你是 AI 应用市场的『技能场景分类器』。请站在使用者视角,根据应用名称、描述与内容,"
            "判断它最贴合下列哪一类【岗位 / 职能 / 业务场景】,并归类到对应 id:"
            f"{options}。\n只返回一个 JSON 对象:{{\"capability\": \"\"}},不要任何额外解释或 Markdown。"
        )
        user_text = f"名称:{name}\n描述:{description or ''}\n内容摘要:{(content or '')[:2000]}"
        messages = [
            {"role": "system", "content": sys_prompt},
            {"role": "user", "content": user_text},
        ]
        proxy = _skill_http_proxy()
        with httpx.Client(timeout=25, follow_redirects=True, trust_env=False, proxy=proxy) as c:
            r = c.post(
                api_url,
                headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                json={"model": text_model.name, "messages": messages, "max_tokens": 64, "temperature": 0, "enable_thinking": False},
            )
        if r.status_code != 200:
            return None
        data = r.json()
        text_out = data.get("choices", [{}])[0].get("message", {}).get("content", "")
        m = re.search(r'"capability"\s*:\s*"([a-z_]+)"', text_out)
        if m and m.group(1) in _VALID_SCENE_IDS:
            return m.group(1)
        return None
    except Exception:
        return None


class VerifyAgentRequest(BaseModel):
    platform: Optional[str] = None
    endpoint: str
    api_key: str


@router.post("/apps/verify-agent")
def verify_agent(
    req: VerifyAgentRequest,
    current_user: User = Depends(get_current_user),
):
    """接入 Agent 前手动测试连通性 / 鉴权,供前台「测试连接」按钮调用。"""
    result = verify_agent_connectivity(req.platform or "dify", req.endpoint, req.api_key)
    return {"code": 0, "data": result, "message": "ok"}


class DetectCapabilityRequest(BaseModel):
    name: str
    description: Optional[str] = None
    skill_content: Optional[str] = None
    type: Optional[str] = None


@router.post("/apps/detect-capability")
def detect_capability(
    req: DetectCapabilityRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """导入前 AI 验证阶段:根据名称/描述/内容识别业务场景分类,供前台预填并允许用户修改。"""
    cap = _classify_scene(req.name, req.description, req.skill_content, db)
    label = _scene_label(cap)
    return {"code": 0, "data": {"capability": cap, "label": label}, "message": "ok"}


@router.get("/ads")
def list_public_ads(db: Session = Depends(get_db)):
    """
    前台首页「精选推荐」广告位:返回已上架(active)广告,按排序升序,无需登录。
    供 LandingPage 轮播调用,无鉴权依赖。
    """
    ads = (
        db.query(AdBanner)
        .filter(AdBanner.status == "active")
        .order_by(AdBanner.sort_order.asc())
        .all()
    )
    return [
        {
            "id": a.id,
            "title": a.title,
            "image_url": a.image_url,
            "link_app_id": a.link_app_id,
            "link_app_name": a.link_app_name,
            "sort_order": a.sort_order,
            "click_count": a.click_count,
            "status": a.status,
            "created_at": a.created_at.isoformat() if a.created_at else None,
        }
        for a in ads
    ]


class UploadAppRequest(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    description: Optional[str] = None
    icon: Optional[str] = None
    type: Optional[str] = None  # skill, agent, mcp
    source: Optional[str] = None
    price: float = Field(default=0, ge=0)
    download_url: Optional[str] = None
    skill_content: Optional[str] = None
    category: Optional[str] = None
    capability: Optional[str] = None
    tags: Optional[str] = None
    is_member_free: Optional[bool] = False
    join_promotion: Optional[bool] = False
    agent_endpoint: Optional[str] = None
    api_key: Optional[str] = None
    platform: Optional[str] = None


class PurchaseAppRequest(BaseModel):
    amount: Optional[float] = None


class ReviewAppRequest(BaseModel):
    rating: int = Field(ge=1, le=5)
    content: Optional[str] = None


@router.get("/apps")
def list_apps(
    type: Optional[str] = Query(None),
    source: Optional[str] = Query(None),
    capability: Optional[str] = Query(None),
    search: Optional[str] = Query(None),
    sort: str = Query(default="latest"),
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=20, ge=1, le=100),
    db: Session = Depends(get_db),
    user_id: Optional[str] = Depends(get_current_user_id_optional),
):
    query = db.query(MarketApp).filter(MarketApp.status == "published")

    if type:
        query = query.filter(MarketApp.type == type)
    if source:
        query = query.filter(MarketApp.source == source)
    if capability:
        query = query.filter(MarketApp.capability == capability)
    if search:
        query = query.filter(
            and_(
                MarketApp.name.ilike(f"%{search}%")
                | MarketApp.description.ilike(f"%{search}%")
            )
        )

    if sort == "popular":
        query = query.order_by(desc(MarketApp.install_count))
    elif sort == "rating":
        query = query.order_by(desc(MarketApp.rating_avg))
    else:
        query = query.order_by(desc(MarketApp.created_at))

    total = query.count()
    apps = query.offset((page - 1) * page_size).limit(page_size).all()

    # 如果用户已登录,获取该用户已安装的应用ID集合(确保按用户隔离)
    installed_app_ids = set()
    if user_id:
        installations = db.query(AppInstallation.app_id).filter(
            AppInstallation.user_id == user_id
        ).all()
        installed_app_ids = {inst[0] for inst in installations}

    # 查询已安装状态时始终带 user_id 过滤

    result = []
    for app in apps:
        # 用户自己创建/导入的应用应始终视为已安装(避免安装记录缺失导致显示未安装)
        is_user_owned = bool(user_id) and app.source == "user" and app.publisher_id == user_id
        item = {
            "id": app.id,
            "name": app.name,
            "description": app.description,
            "icon": app.icon,
            "type": app.type,
            "source": app.source,
            "publisher_id": app.publisher_id,
            "publisher_name": app.publisher_name,
            "is_member_free": app.is_member_free if app.is_member_free else False,
            "join_promotion": app.join_promotion if app.join_promotion else False,
            "status": app.status,
            "price": float(app.price) if app.price else 0,
            "install_count": app.install_count or 0,
            "rating_avg": float(app.rating_avg) if app.rating_avg else 0,
            "review_count": app.review_count or 0,
            "created_at": str(app.created_at) if app.created_at else None,
            "capability": app.capability or None,
            "installed": (app.id in installed_app_ids or is_user_owned) if user_id else False,
        }
        result.append(item)

    return {
        "code": 0,
        "data": {
            "total": total,
            "page": page,
            "page_size": page_size,
            "items": result,
        },
        "message": "ok",
    }


@router.get("/apps/{app_id}")
def get_app(
    app_id: str,
    db: Session = Depends(get_db),
    current_user: Optional[User] = Depends(get_current_user),
):
    app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
    if not app:
        raise HTTPException(status_code=404, detail="应用不存在")

    # 检查当前用户是否已购买(按用户隔离)
    purchased = False
    installed = False
    if current_user:
        purchase = db.query(AppPurchase).filter(
            and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
        ).first()
        if purchase:
            purchased = True
            installed = purchase.status == "installed"
        # 通过 AppInstallation 表检查是否已安装(按用户隔离)
        installation = db.query(AppInstallation).filter(
            and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
        ).first()
        if installation:
            installed = True

    return {
        "code": 0,
        "data": {
            "id": app.id, "name": app.name, "description": app.description,
            "icon": app.icon, "type": app.type, "source": app.source,
            "type": app.type, 
            "price": float(app.price) if app.price else 0,
            "install_count": app.install_count or 0,
            "rating_avg": float(app.rating_avg) if app.rating_avg else 0,
            "review_count": app.review_count or 0,
            "publisher_id": app.publisher_id,
            "download_url": getattr(app, "download_url", None),
            "detail": getattr(app, "detail", None),
            "capability": app.capability or None,
            "created_at": str(app.created_at),
            "purchased": purchased,
            "installed": installed,
        },
        "message": "ok",
    }


@router.post("/apps")
def upload_app(
    req: UploadAppRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    # 接入 Agent 时先做连通性 / 鉴权校验,避免把无效连接存进库
    if (req.type or "") == "agent" and req.agent_endpoint and req.api_key:
        v = verify_agent_connectivity(req.platform or "dify", req.agent_endpoint, req.api_key)
        if not v["ok"]:
            raise HTTPException(
                status_code=400,
                detail=f"Agent 连接校验未通过:{v['detail']}",
            )

    # source 判定:管理员(admin 角色)通过前台市场导入/开发的 Agent/Skill/MCP
    # 统一标记为官方(official),其余用户按请求或默认为 user。
    if current_user.role == "admin":
        app_source = "official"
    elif req.source and req.source in ("official", "user"):
        app_source = req.source
    else:
        app_source = "user"

    app = MarketApp(
        id=gen_uuid(),
        name=req.name,
        description=req.description,
        icon=req.icon,
        type=req.type if req.type in ("skill","agent","mcp") else "skill",
        source=app_source,
        platform=req.platform,
        price=req.price,
        install_count=0,
        rating_avg=0,
        review_count=0,
        publisher_id=current_user.id,
        publisher_name=current_user.username,
        is_member_free=req.is_member_free if req.is_member_free is not None else False,
        join_promotion=req.join_promotion if req.join_promotion is not None else False,
        agent_endpoint=req.agent_endpoint,
        agent_api_key=req.api_key,
        status="published",  # 用户创建的应用直接发布
        created_at=datetime.now(),
        updated_at=datetime.now(),
    )
    # 能力分类:前端显式指定优先;否则导入阶段由 AI 自动归类;都不行则留空(不阻断导入)
    _classify_content = req.skill_content or req.download_url or ""
    app.capability = req.capability or _classify_scene(req.name, req.description, _classify_content, db) or None
    db.add(app)
    db.commit()
    db.refresh(app)

    # MCP 应用同步注册到后台 MCP Client 管理(mcp_servers 表)
    if app.type == "mcp":
        code = req.download_url or ""
        if code:
            app.skill_content = code
        parsed = parse_mcp_config(code)
        transport = parsed["transport_type"]
        app.mcp_transport = transport
        db.commit()
        db.refresh(app)

        existing_server = db.query(MCPServer).filter(MCPServer.id == app.id).first()
        if not existing_server:
            url = parsed["url"] or _extract_url_from_text(req.description or "") or ""
            headers_json = json.dumps(parsed["headers"], ensure_ascii=False) if parsed["headers"] else "{}"
            server = MCPServer(
                id=app.id,
                name=app.name,
                description=app.description or "",
                transport_type=transport,
                command="",
                args="{}",
                env_vars="{}",
                url=url,
                headers=headers_json,
                is_enabled=True,
                status="disconnected",
                tools_count=0,
                resources_count=0,
                created_at=app.created_at or datetime.now(),
                updated_at=app.updated_at or datetime.now(),
            )
            db.add(server)
            db.commit()

            # 导入后自动做一次连通性测试,状态会同步为 connected / error
            _test_mcp_server(server, db)

    # Skill / Agent:保存真实技能定义内容,否则对话里读不到 skill_content → 导入成功但失效
    if app.type == "skill":
        content = req.skill_content or ""
        if not content and req.download_url:
            # Hub 导入:按目录 URL 抓取真实 SKILL.md 内容回填
            content = _fetch_skill_content(req.download_url) or ""
        if content:
            app.skill_content = content
            db.commit()
    elif app.type == "agent":
        if req.skill_content:
            app.skill_content = req.skill_content
            db.commit()

    # 用户创建/导入的应用自动标记为已安装
    existing_install = db.query(AppInstallation).filter(
        AppInstallation.user_id == current_user.id,
        AppInstallation.app_id == app.id,
    ).first()
    if not existing_install:
        installation = AppInstallation(
            user_id=current_user.id,
            app_id=app.id,
            installed_at=datetime.now(),
        )
        db.add(installation)
        db.commit()

    LogService.log_operation(
        user_id=current_user.id, username=current_user.username, action="upload_app",
        target="market_app", target_id=app.id,
        detail=f"上传应用 {app.name}",
    )

    return {
        "code": 0,
        "data": {
            "id": app.id, "name": app.name, "status": app.status,
            "capability": app.capability,
            "created_at": str(app.created_at),
        },
        "message": "提交成功,等待审核",
    }


@router.post("/apps/{app_id}/purchase")
def purchase_app(
    app_id: str,
    req: PurchaseAppRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    app = db.query(MarketApp).filter(
        and_(MarketApp.id == app_id, MarketApp.status == "published")
    ).first()
    if not app:
        raise HTTPException(status_code=404, detail="应用不存在或未上架")

    # 检查是否已购买
    existing = db.query(AppPurchase).filter(
        and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
    ).first()
    if existing and existing.status in ("purchased", "installed"):
        raise HTTPException(status_code=400, detail="您已购买该应用")

    amount = req.amount or app.price

    # 创建订单
    order = Order(
        id=gen_uuid(),
        user_id=current_user.id,
        order_type="app",
        item_id=app_id,
        item_name=app.name,
        amount=amount,
        status="pending",
        created_at=datetime.now(),
    )
    db.add(order)
    db.flush()

    # 创建购买记录
    purchase = AppPurchase(
        id=gen_uuid(),
        user_id=current_user.id,
        app_id=app_id,
        order_id=order.id,
        status="purchased",
        purchased_at=datetime.now(),
    )
    db.add(purchase)

    # 更新应用下载量
    app.install_count = (app.install_count or 0) + 1
    app.updated_at = datetime.now()

    db.commit()

    return {
        "code": 0,
        "data": {
            "order_id": order.id,
            "purchase_id": purchase.id,
            "amount": amount,
            "status": "purchased",
        },
        "message": "购买成功",
    }


@router.post("/apps/{app_id}/install")
def install_app(
    app_id: str,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
    if not app:
        raise HTTPException(status_code=404, detail="应用不存在")

    # 检查是否已安装(通过 AppInstallation 表)
    existing_install = db.query(AppInstallation).filter(
        and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
    ).first()
    if existing_install:
        raise HTTPException(status_code=400, detail="应用已安装")

    # 检查是否已购买(付费应用需要先购买)
    purchase = db.query(AppPurchase).filter(
        and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
    ).first()

    # 对于付费应用,必须已购买才能安装
    if float(app.price or 0) > 0:
        if not purchase:
            raise HTTPException(status_code=403, detail="请先购买应用")
        if purchase.status == "installed":
            raise HTTPException(status_code=400, detail="应用已安装")

    # 创建安装记录
    installation = AppInstallation(
        id=gen_uuid(),
        user_id=current_user.id,
        app_id=app_id,
        installed_at=datetime.now(),
    )
    db.add(installation)

    # 如果有购买记录,更新状态为已安装
    if purchase:
        purchase.status = "installed"
        purchase.installed_at = datetime.now()

    # 更新应用安装量
    app.install_count = (app.install_count or 0) + 1
    app.updated_at = datetime.now()

    db.commit()

    return {
        "code": 0,
        "data": {"installation_id": installation.id, "status": "installed"},
        "message": "安装成功",
    }


@router.delete("/apps/{app_id}/uninstall")
def uninstall_app(
    app_id: str,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """卸载应用 - 删除当前用户的安装记录(按用户隔离)"""
    app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
    if not app:
        raise HTTPException(status_code=404, detail="应用不存在")

    # 查找当前用户的安装记录(按用户隔离)
    installation = db.query(AppInstallation).filter(
        and_(AppInstallation.app_id == app_id, AppInstallation.user_id == current_user.id)
    ).first()
    if not installation:
        raise HTTPException(status_code=400, detail="应用未安装")

    # 删除安装记录
    db.delete(installation)

    # 问题4修复: 将AppPurchase状态从installed重置为purchased
    purchase = db.query(AppPurchase).filter(
        and_(AppPurchase.app_id == app_id, AppPurchase.user_id == current_user.id)
    ).first()
    if purchase and purchase.status == "installed":
        purchase.status = "purchased"

    # 更新应用安装量(减1,最小为0)
    if app.install_count and app.install_count > 0:
        app.install_count = app.install_count - 1
    app.updated_at = datetime.now()

    db.commit()

    LogService.log_operation(
        user_id=current_user.id, username=current_user.username, action="uninstall_app",
        target="market_app", target_id=app_id,
        detail=f"卸载应用 {app.name}",
    )

    return {
        "code": 0,
        "message": "卸载成功",
    }


@router.post("/apps/{app_id}/review")
def review_app(
    app_id: str,
    req: ReviewAppRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    app = db.query(MarketApp).filter(MarketApp.id == app_id).first()
    if not app:
        raise HTTPException(status_code=404, detail="应用不存在")

    # 检查是否已评价
    existing = db.query(AppReview).filter(
        and_(AppReview.app_id == app_id, AppReview.user_id == current_user.id)
    ).first()
    if existing:
        # 更新已有评价
        old_rating = existing.rating
        existing.rating = req.rating
        existing.content = req.content
        existing.updated_at = datetime.now()

        # 重新计算平均分 (总数不变,减去旧评分加新评分)
        if app.review_count and app.review_count > 0:
            total_rating = float(app.rating_avg or 0) * app.review_count
            total_rating = total_rating - old_rating + req.rating
            app.rating_avg = total_rating / app.review_count
        else:
            app.rating_avg = req.rating
            app.review_count = 1
    else:
        # 创建新评价
        review = AppReview(
            id=gen_uuid(),
            app_id=app_id,
            user_id=current_user.id,
            rating=req.rating,
            content=req.content,
            created_at=datetime.now(),
            updated_at=datetime.now(),
        )
        db.add(review)

        # 重新计算平均分
        app.review_count = (app.review_count or 0) + 1
        if app.review_count == 1:
            app.rating_avg = req.rating
        else:
            total_rating = float(app.rating_avg or 0) * (app.review_count - 1) + req.rating
            app.rating_avg = round(total_rating / app.review_count, 1)

    app.updated_at = datetime.now()
    db.commit()

    return {
        "code": 0,
        "data": {
            "rating_avg": float(app.rating_avg or 0),
            "review_count": app.review_count or 0,
        },
        "message": "评价成功",
    }


@router.get("/my-apps")
def get_my_apps(
    status: Optional[str] = Query(None),
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    query = db.query(AppPurchase).filter(AppPurchase.user_id == current_user.id)
    purchases = query.order_by(desc(AppPurchase.created_at)).all()

    result = []
    for p in purchases:
        app = db.query(MarketApp).filter(MarketApp.id == p.app_id).first()
        if app:
            result.append({
                "id": p.id,
                "app_id": app.id,
                "name": app.name,
                "icon": app.icon,
                "type": app.type,
                "price": float(app.price or 0),
                "purchased_at": str(p.created_at) if p.created_at else None,
            })

    return {"code": 0, "data": result, "message": "ok"}


@router.get("/reviews/{app_id}")
def get_reviews(
    app_id: str,
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=20, ge=1, le=100),
    db: Session = Depends(get_db),
):
    query = db.query(AppReview).filter(AppReview.app_id == app_id)
    total = query.count()

    reviews = (
        query.order_by(desc(AppReview.created_at))
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all()
    )

    result = []
    for r in reviews:
        user = db.query(User).filter(User.id == r.user_id).first()
        result.append({
            "id": r.id,
            "user_id": r.user_id,
            "username": user.username if user else "unknown",
            "avatar": user.avatar if user else None,
            "rating": r.rating,
            "content": r.content,
            "created_at": str(r.created_at) if r.created_at else None,
        })

    return {
        "code": 0,
        "data": {"total": total, "page": page, "page_size": page_size, "items": result},
        "message": "ok",
    }


# ============================================================
# Agent部署资源查询接口
# ============================================================

@router.get("/agent-resources")
def get_agent_resources(
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Agent部署时需要的资源列表:可用的应用、知识库、文档"""
    # 用户已发布的应用
    my_apps = db.query(MarketApp).filter(
        MarketApp.publisher_id == current_user.id,
        MarketApp.status == "published",
    ).order_by(desc(MarketApp.updated_at)).all()

    apps_result = []
    for app in my_apps:
        apps_result.append({
            "id": app.id,
            "name": app.name,
            "type": app.type,
            "description": app.description,
            "install_count": app.install_count or 0,
        })

    # 用户知识库
    from ..models import KnowledgeBase, KnowledgeDocument
    kb_list = db.query(KnowledgeBase).filter(
        KnowledgeBase.user_id == current_user.id
    ).order_by(desc(KnowledgeBase.updated_at)).all()

    kb_result = []
    for kb in kb_list:
        doc_count = db.query(KnowledgeDocument).filter(
            KnowledgeDocument.knowledge_base_id == kb.id
        ).count()
        kb_result.append({
            "id": kb.id,
            "name": kb.name,
            "description": kb.description,
            "document_count": doc_count,
        })

    # 用户已安装的应用(从AppPurchase)
    installed = db.query(AppPurchase).filter(
        AppPurchase.user_id == current_user.id
    ).all()
    installed_ids = [p.app_id for p in installed]
    installed_apps = []
    if installed_ids:
        iapps = db.query(MarketApp).filter(MarketApp.id.in_(installed_ids)).all()
        for ia in iapps:
            installed_apps.append({
                "id": ia.id,
                "name": ia.name,
                "type": ia.type,
                "description": ia.description,
            })

    return {
        "code": 0,
        "data": {
            "my_apps": apps_result,
            "knowledge_bases": kb_result,
            "installed_apps": installed_apps,
        },
        "message": "ok",
    }