"""
api.v1.agentpacks — AgentPack install / list / uninstall (M2 Phase D).

Spec: docs/AGENTPACK_SPEC.md. Kernel manifest logic: lambdagent.agentpack.
Filesystem ops: agentpaas.engine.agentpack_store.

  GET    /agentpacks            list installed packs
  GET    /agentpacks/{id}       detail (manifest + permission summary)
  POST   /agentpacks/install    install from a local .zip path
  DELETE /agentpacks/{id}       uninstall (all versions, or ?version=)

Install is loopback-only in desktop mode (the zip is a local-filesystem
concern, same posture as /setup/data-dir); auth-gated in lab/paas.
"""
from __future__ import annotations

from typing import Any, Dict, Optional

import yaml
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from pydantic import BaseModel, Field

from agentpaas.api.deps import get_tenant
from agentpaas.api.middleware.auth import TenantContext
from agentpaas.config import settings
from agentpaas.engine.agentpack_store import (
    install_from_zip,
    list_installed,
    get_installed,
    peek_manifest_from_zip,
    uninstall,
    AgentPackInstallError,
)
from lambdagent.agentpack import AgentPackError

router = APIRouter(prefix="/agentpacks", tags=["agentpacks"])


def _pack_json(p, include_config: bool = False) -> dict:
    m = p.manifest
    result: dict = {
        "id": p.id,
        "version": p.version,
        "name": p.name,
        "domain": p.domain,
        "path": p.path,
        "description": m.description,
        "audience": m.audience,
        "permissions": m.permissions.to_dict(),
        "permission_summary": m.permissions.summary_zh(),
        "model_recommended": m.model_recommended,
        "entrypoint": m.entrypoint,
    }
    if include_config:
        try:
            with open(m.entrypoint_path(), encoding="utf-8") as _f:
                result["entrypoint_config"] = yaml.safe_load(_f) or {}
        except Exception:
            result["entrypoint_config"] = {}
    return result


class InstallRequest(BaseModel):
    zip_path: str = Field(..., description="Local filesystem path to the .zip")
    allow_shell: bool = Field(
        default=False,
        description="Permit a pack declaring shell:true (third-party packs "
                    "are refused shell otherwise)",
    )


@router.get("")
async def list_agentpacks(tenant: TenantContext = Depends(get_tenant)):
    """List installed AgentPacks."""
    packs = list_installed(settings.agentpacks_dir)
    return {"agentpacks": [_pack_json(p) for p in packs], "count": len(packs)}


@router.get("/{pack_id}")
async def get_agentpack(
    pack_id: str,
    version: Optional[str] = None,
    tenant: TenantContext = Depends(get_tenant),
):
    """Detail for one installed pack (newest version unless ?version=).

    Includes ``entrypoint_config`` — the parsed entrypoint YAML — so the
    webui can pre-fill the advanced-customisation form without a second
    round-trip.
    """
    p = get_installed(settings.agentpacks_dir, pack_id, version)
    if not p:
        raise HTTPException(status_code=404, detail="AgentPack not found")
    return _pack_json(p, include_config=True)


def _loopback_only(request: Request) -> None:
    """Raise 404 for non-loopback callers (oracle defense, same as /setup/bootstrap)."""
    client_host = (request.client.host if request.client else "") or ""
    if client_host not in ("127.0.0.1", "::1", "localhost"):
        raise HTTPException(status_code=404, detail="Not Found")


@router.post("/install")
async def install_agentpack(req: InstallRequest, request: Request):
    """Install an AgentPack from a local .zip path.

    Loopback-only — installing from a local file path is a local-machine
    operation; a remote caller has no business pointing the server at the
    host filesystem. (lab/paas operators install via the host CLI / a
    trusted deploy pipeline, not this endpoint.)
    """
    _loopback_only(request)
    try:
        pack = install_from_zip(
            req.zip_path,
            settings.agentpacks_dir,
            allow_shell=req.allow_shell,
        )
    except (AgentPackInstallError, AgentPackError) as e:
        raise HTTPException(status_code=400, detail=str(e))

    return {"ok": True, "installed": _pack_json(pack)}


@router.post("/install-upload")
async def install_agentpack_upload(
    request: Request,
    file: UploadFile = File(..., description="AgentPack .zip 文件"),
    allow_shell: bool = False,
):
    """Install an AgentPack from a browser file-upload (multipart/form-data).

    Loopback-only — same security posture as /install. Desktop webui uses this
    so users can pick a .zip via a file-chooser dialog instead of typing a path.
    The uploaded bytes are written to a server-side temp file, then handed to
    install_from_zip(), and the temp file is removed afterwards.
    """
    import os
    import tempfile

    _loopback_only(request)

    if not (file.filename or "").lower().endswith(".zip"):
        raise HTTPException(status_code=400, detail="只接受 .zip 文件")

    # Stream upload to a temp file so install_from_zip() can read it.
    try:
        suffix = f"_{file.filename}" if file.filename else ".zip"
        with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
            tmp_path = tmp.name
            contents = await file.read()
            tmp.write(contents)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"上传文件写入失败: {e}")

    try:
        pack = install_from_zip(
            tmp_path,
            settings.agentpacks_dir,
            allow_shell=allow_shell,
        )
    except (AgentPackInstallError, AgentPackError) as e:
        raise HTTPException(status_code=400, detail=str(e))
    finally:
        try:
            os.unlink(tmp_path)
        except Exception:
            pass

    return {"ok": True, "installed": _pack_json(pack)}


def _refresh_agents_from_pack(pack) -> list:
    """把更新后的包 entrypoint 配置传播到所有由该包创建的智能体。

    对每个 ``agents.agent_template == pack.id`` 的智能体：
    - 用新 entrypoint 配置生成一个**新 agent_version**（保留版本历史，
      可随时通过 /rollback 回到更新前的配置）；
    - ``agent_dir`` 指向新安装路径（子智能体 ./agents/x.yml 相对引用
      跟着解析到新版本目录）。

    Returns: [{agent_id, name, version}] 被刷新的智能体列表。
    """
    import hashlib
    import json
    import yaml
    from agentpaas.db.session import get_db
    from agentpaas.db.models import now_utc

    try:
        with open(pack.manifest.entrypoint_path(), encoding="utf-8") as f:
            config = yaml.safe_load(f) or {}
    except (OSError, yaml.YAMLError) as e:
        raise HTTPException(status_code=500,
                            detail=f"failed to read pack entrypoint: {e}")
    # 子智能体相对路径在新包根下解析（同 create-agent 的 _config_dir 语义）
    config.setdefault("_config_dir", pack.path)

    config_json = json.dumps(config, ensure_ascii=False)
    config_hash = hashlib.sha256(config_json.encode()).hexdigest()

    db = get_db()
    agents = db.fetchall(
        "SELECT id, name, current_version FROM agents "
        "WHERE agent_template = ? AND status = 'active'",
        (pack.id,),
    )
    refreshed = []
    now = now_utc()
    for a in agents:
        new_version = int(a["current_version"] or 0) + 1
        db.execute(
            "INSERT INTO agent_versions "
            "(agent_id, version, config, config_hash, changelog, created_by, created_at) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            (a["id"], new_version, config_json, config_hash,
             f"AgentPack updated: {pack.id} v{pack.version}", "system", now),
        )
        db.execute(
            "UPDATE agents SET current_version = ?, agent_dir = ?, updated_at = ? "
            "WHERE id = ?",
            (new_version, pack.path, now, a["id"]),
        )
        refreshed.append({"agent_id": a["id"], "name": a["name"],
                          "version": new_version})
    db.commit()
    return refreshed


@router.post("/{pack_id}/update-upload")
async def update_agentpack_upload(
    pack_id: str,
    request: Request,
    file: UploadFile = File(..., description="新版 AgentPack .zip 文件"),
    allow_shell: bool = False,
    refresh_agents: bool = True,
):
    """上传 .zip 更新一个已安装的 AgentPack，并让基于它创建的智能体
    跑在最新配置上。

    流程：
    1. 校验 zip 内 manifest.id 与 URL pack_id 一致（防误传别的包）；
    2. 安装（同版本号 → 覆盖重装；新版本号 → 并存，detail 取最新）；
    3. refresh_agents=true（默认）时，所有 agent_template == pack_id 的
       智能体生成新 agent_version 指向新配置 —— 旧配置保留在版本历史，
       可随时回滚。

    Loopback-only —— 与 /install 相同的安全态势。
    """
    import os
    import tempfile

    _loopback_only(request)

    existing = get_installed(settings.agentpacks_dir, pack_id)
    if not existing:
        raise HTTPException(status_code=404,
                            detail=f"AgentPack {pack_id!r} 未安装，请先用「安装」上传")

    if not (file.filename or "").lower().endswith(".zip"):
        raise HTTPException(status_code=400, detail="只接受 .zip 文件")

    try:
        suffix = f"_{file.filename}" if file.filename else ".zip"
        with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
            tmp_path = tmp.name
            tmp.write(await file.read())
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"上传文件写入失败: {e}")

    try:
        # 先窥探 manifest：包 id 必须匹配，否则拒绝（不落盘）
        try:
            incoming = peek_manifest_from_zip(tmp_path)
        except (AgentPackInstallError, AgentPackError) as e:
            raise HTTPException(status_code=400, detail=f"zip 校验失败: {e}")
        if incoming.id != pack_id:
            raise HTTPException(
                status_code=400,
                detail=f"包 id 不匹配：zip 内是 {incoming.id!r}，"
                       f"要更新的是 {pack_id!r}。如要安装新包请走「安装」入口。")

        try:
            pack = install_from_zip(
                tmp_path,
                settings.agentpacks_dir,
                allow_shell=allow_shell,
            )
        except (AgentPackInstallError, AgentPackError) as e:
            raise HTTPException(status_code=400, detail=str(e))

        refreshed = _refresh_agents_from_pack(pack) if refresh_agents else []
    finally:
        try:
            os.unlink(tmp_path)
        except Exception:
            pass

    return {
        "ok": True,
        "installed": _pack_json(pack),
        "previous_version": existing.version,
        "refreshed_agents": refreshed,
    }


@router.delete("/{pack_id}", status_code=200)
async def uninstall_agentpack(
    pack_id: str,
    version: Optional[str] = None,
    tenant: TenantContext = Depends(get_tenant),
):
    """Uninstall a pack (all versions, or a specific ?version=)."""
    removed = uninstall(settings.agentpacks_dir, pack_id, version)
    if removed == 0:
        raise HTTPException(status_code=404, detail="AgentPack not found")
    return {"ok": True, "removed_versions": removed}


# ─────────────────────────────────────────────────────────────────────
# Phase G: create an agent from an installed pack
# ─────────────────────────────────────────────────────────────────────

class CreateAgentFromPackRequest(BaseModel):
    name: str = Field(..., max_length=256,
                      description="Display name for the new agent")
    description: str = Field(default="", max_length=4096)
    version: Optional[str] = Field(default=None,
        description="Specific pack version; newest installed if omitted")
    kb_ids: list = Field(default_factory=list,
        description="Knowledge bases to link to this agent")
    kb_search_mode: str = Field(default="bm25")
    tags: list = Field(default_factory=list)
    # ── Advanced customisation ─────────────────────────────────────────
    # These fields let the caller override specific parts of the pack's
    # entrypoint config without forking the whole pack.
    model_override: Optional[Dict[str, Any]] = Field(
        default=None,
        description='Override model fields, e.g. {"provider":"anthropic","name":"claude-3-5-sonnet"}',
    )
    system_prompt_override: Optional[str] = Field(
        default=None,
        max_length=32768,
        description="Replace the pack systemPrompt entirely. None = use pack default.",
    )
    # 3-directory config (same semantics as CreateAgentRequest)
    work_dir: str = Field(default="", description="工作产物目录: Bash tool CWD")
    source_dir: str = Field(default="", description="数据来源目录: injected into system prompt preamble")
    run_dir: str = Field(default="", description="运行日志目录: run workspace base")


@router.post("/{pack_id}/create-agent", status_code=201)
async def create_agent_from_pack(
    pack_id: str,
    req: CreateAgentFromPackRequest,
    tenant: TenantContext = Depends(get_tenant),
):
    """Create an agent backed by an installed AgentPack (Phase G).

    Reads the pack's entrypoint config + sets the new agent's
    ``agent_dir`` = pack install path, ``agent_template`` = pack.id.
    Sub-agent ``config: ./agents/x.yml`` references resolve inside the
    pack root via the existing _config_dir compiler precedence (M1
    work). KB linkage threads through as on a normal agent.
    """
    import hashlib
    import json
    import yaml
    from agentpaas.db.session import get_db
    from agentpaas.db.models import gen_id, now_utc

    pack = get_installed(settings.agentpacks_dir, pack_id, req.version)
    if not pack:
        raise HTTPException(status_code=404, detail="AgentPack not found")

    # Load the entrypoint config — this is the agent's authoritative config.
    try:
        with open(pack.manifest.entrypoint_path(), encoding="utf-8") as f:
            config = yaml.safe_load(f) or {}
    except (OSError, yaml.YAMLError) as e:
        raise HTTPException(status_code=500,
                            detail=f"failed to read pack entrypoint: {e}")

    # Sub-agent yaml references inside the pack get resolved against the
    # pack root by the lambdagent compiler (see M1 _config_dir precedence
    # fix at compiler.py:219; explicit cfg._config_dir trumps inference).
    config.setdefault("_config_dir", pack.path)

    # ── Apply caller-supplied overrides (advanced customisation) ───────
    if req.model_override:
        # Merge into existing model dict so fields not in the override
        # (e.g. temperature, maxTokens) are preserved from the pack.
        existing_model = config.get("model") or {}
        existing_model.update(req.model_override)
        config["model"] = existing_model
    if req.system_prompt_override is not None:
        config["systemPrompt"] = req.system_prompt_override

    config_json = json.dumps(config, ensure_ascii=False)
    config_hash = hashlib.sha256(config_json.encode()).hexdigest()

    # 自动开辟工作目录（同 POST /agents 语义，参照 Workspace/
    # multi-reviewer67 布局）：三目录全空才自动建，显式输入优先。
    work_dir, source_dir, run_dir = req.work_dir, req.source_dir, req.run_dir
    if not (work_dir or source_dir or run_dir):
        from agentpaas.engine.workspace_provision import provision_agent_workspace
        dirs = provision_agent_workspace(req.name)
        work_dir, source_dir, run_dir = (
            dirs["work_dir"], dirs["source_dir"], dirs["run_dir"])

    agent_id = gen_id("ag_")
    now = now_utc()
    db = get_db()
    db.execute(
        "INSERT INTO agents (id, tenant_id, name, description, current_version, "
        "tags, environment, agent_dir, agent_template, instance_dir, kb_ids, "
        "kb_search_mode, work_dir, source_dir, run_dir, status, created_at, updated_at) "
        "VALUES (?, ?, ?, ?, 1, ?, 'production', ?, ?, '', ?, ?, ?, ?, ?, 'active', ?, ?)",
        (agent_id, tenant.tenant_id, req.name,
         req.description or pack.manifest.description,
         json.dumps(req.tags),
         pack.path, pack.id,
         json.dumps(req.kb_ids), req.kb_search_mode,
         work_dir, source_dir, run_dir, now, now),
    )
    db.execute(
        "INSERT INTO agent_versions "
        "(agent_id, version, config, config_hash, changelog, created_by, created_at) "
        "VALUES (?, 1, ?, ?, ?, ?, ?)",
        (agent_id, config_json, config_hash,
         f"Created from agentpack {pack.id} v{pack.version}",
         tenant.user_id, now),
    )
    db.commit()

    return {
        "ok": True,
        "agent_id": agent_id,
        "name": req.name,
        "pack": {"id": pack.id, "version": pack.version, "name": pack.name},
        "work_dir": work_dir,
        "source_dir": source_dir,
        "run_dir": run_dir,
    }
