"""Plugin endpoints: listing/status, enable/disable, install-from-zip, the
frontend UI payload, and plugin asset serving."""
import io
import json
import re
import zipfile
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse

from clipshelf import config
from clipshelf.plugin_host import loader, registry

router = APIRouter(tags=["plugins"])


@router.get("/api/plugins")
def plugins_list():
    return {"plugins": [
        {k: p.get(k) for k in ("id", "name", "version", "capabilities",
                               "provides", "status", "error", "author",
                               "homepage")}
        for p in loader.PLUGINS.values()
    ]}


def _set_disabled(pid: str, disabled: bool):
    cur = set(config.CFG.get("plugins_disabled") or [])
    if disabled:
        cur.add(pid)
    else:
        cur.discard(pid)
    config.CFG["plugins_disabled"] = sorted(cur)
    config.save(config.CFG)


@router.post("/api/plugins/{pid}/disable")
def plugin_disable(pid: str):
    if pid not in loader.PLUGINS:
        raise HTTPException(404, "no such plugin")
    _set_disabled(pid, True)
    return {"ok": True, "restart_required": True}


@router.post("/api/plugins/{pid}/enable")
def plugin_enable(pid: str):
    if pid not in loader.PLUGINS:
        raise HTTPException(404, "no such plugin")
    _set_disabled(pid, False)
    return {"ok": True, "restart_required": True}


@router.post("/api/plugins/install")
async def plugin_install(file: UploadFile):
    """Unzip a plugin release into the plugins folder. Nothing executes until
    the next boot — install is pure file placement."""
    raw = await file.read()
    if len(raw) > 200_000_000:
        raise HTTPException(413, "plugin zip too large")
    try:
        z = zipfile.ZipFile(io.BytesIO(raw))
        names = z.namelist()
    except zipfile.BadZipFile:
        raise HTTPException(400, "not a zip file")
    # manifest at the zip root, or inside a single top-level folder
    prefix = ""
    if "plugin.json" not in names:
        tops = {n.split("/", 1)[0] for n in names if "/" in n}
        cand = [t for t in sorted(tops) if f"{t}/plugin.json" in names]
        if len(cand) != 1:
            raise HTTPException(400, "zip must contain plugin.json (at the root or in one folder)")
        prefix = cand[0] + "/"
    try:
        manifest = json.loads(z.read(prefix + "plugin.json"))
        pid = str(manifest.get("id") or "")
    except Exception:
        raise HTTPException(400, "unreadable plugin.json")
    if not re.match(r"^[a-z0-9][a-z0-9_-]{0,63}$", pid):
        raise HTTPException(400, f"bad plugin id {pid!r}")
    dest = config.PLUGINS_DIR / pid
    if dest.exists():
        raise HTTPException(409, f"plugin '{pid}' is already installed — remove its folder first")
    dest_r = dest.resolve()
    for n in names:
        if not n.startswith(prefix) or n.endswith("/"):
            continue
        rel = n[len(prefix):]
        out = (dest / rel).resolve()
        if not str(out).startswith(str(dest_r)):   # zip-slip guard
            raise HTTPException(400, f"zip entry escapes the plugin dir: {rel}")
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_bytes(z.read(n))
    return {"ok": True, "id": pid, "restart_required": True}


@router.get("/api/plugins/ui")
def plugins_ui():
    return registry.ui_payload()


# ------------------------------------------------------- media providers
# One generic surface for every search/import source (core keyed APIs and
# plugin scrapers alike). Contract: search(q, params) -> {"results": [...]},
# import_item(item) -> {"url": local_url, ...}; optional
# preview_stream(item, range_header) -> (status, headers, iterator).
@router.get("/api/providers")
def providers_list():
    return {"providers": [
        {"id": pid, "kind": p.KIND, "label": getattr(p, "LABEL", pid),
         "ready": registry.provider_ready(p), "hints": getattr(p, "HINTS", {})}
        for pid, p in sorted(registry.media_providers.items())
    ]}


def _provider(pid: str):
    p = registry.media_providers.get(pid)
    if not p:
        raise HTTPException(404, "no such provider")
    return p


@router.get("/api/providers/{pid}/search")
async def provider_search(pid: str, request: Request, q: str = ""):
    import asyncio
    p = _provider(pid)
    params = {k: v for k, v in request.query_params.items() if k != "q"}
    try:
        res = await asyncio.to_thread(p.search, q, params)
    except Exception as e:
        raise HTTPException(502, f"search failed: {type(e).__name__}: {e}")
    return res if isinstance(res, dict) else {"results": list(res or [])}


@router.post("/api/providers/{pid}/import")
async def provider_import(pid: str, item: dict):
    import asyncio
    p = _provider(pid)
    try:
        return await asyncio.to_thread(p.import_item, item or {})
    except Exception as e:
        raise HTTPException(502, f"import failed: {type(e).__name__}: {e}")


@router.get("/api/providers/{pid}/preview")
def provider_preview(pid: str, request: Request, item: str = "{}"):
    from fastapi.responses import StreamingResponse
    p = _provider(pid)
    if not callable(getattr(p, "preview_stream", None)):
        raise HTTPException(404, "provider has no preview stream")
    try:
        parsed = json.loads(item)
    except Exception:
        raise HTTPException(400, "item must be JSON")
    try:
        status, headers, it = p.preview_stream(parsed, request.headers.get("range"))
    except Exception as e:
        raise HTTPException(502, f"preview failed: {type(e).__name__}: {e}")
    return StreamingResponse(it, status_code=status, headers=headers)


@router.get("/theme-css/{tid}.css")
def theme_css(tid: str):
    """One uniform URL for every theme stylesheet — core and plugin alike."""
    t = registry.themes.get(re.sub(r"[^a-z0-9_-]", "", tid.lower()))
    if not t:
        raise HTTPException(404, "no such theme")
    return FileResponse(t["css"], media_type="text/css")


@router.get("/plugin-assets/{pid}/{path:path}")
def plugin_asset(pid: str, path: str):
    info = loader.PLUGINS.get(pid)
    if not info or info.get("status") != "ok":
        raise HTTPException(404, "no such plugin")
    for root in info.get("asset_dirs") or []:
        p = (Path(root) / path).resolve()
        if str(p).startswith(str(Path(root).resolve())) and p.is_file():
            return FileResponse(p)
    raise HTTPException(404, "no such asset")
