"""Knowledge Base API — create, ingest, compile, research, lint."""

from __future__ import annotations

import logging

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel

from agntspace.api.routes.wiki import invalidate_article_cache

log = logging.getLogger(__name__)

router = APIRouter(prefix="/kb", tags=["knowledge-base"])


class CreateKBRequest(BaseModel):
    topic: str
    description: str = ""
    focus_questions: list[str] = []
    source_types: list[str] = []
    notes: str = ""


class ScrapeRequest(BaseModel):
    url: str
    filename: str = ""


class ResearchScrapeRequest(BaseModel):
    topic: str
    max_pages: int = 5


class ResearchRequest(BaseModel):
    query: str
    save_output: bool = False
    file_to_wiki: bool = False
    output_format: str = "markdown"  # markdown | comparison | slides | briefing


class AddSourceRequest(BaseModel):
    title: str = ""
    content: str
    filename: str = ""


class LibraryLayoutRequest(BaseModel):
    layout: str  # flat | type-date | thematic


class ReorganizeLibraryRequest(BaseModel):
    dry_run: bool = True


@router.get("")
async def list_kbs(request: Request) -> list[dict]:
    """List all knowledge bases."""
    return request.app.state.kb_service.list_kbs()


@router.post("")
async def create_kb(request: Request, body: CreateKBRequest) -> dict:
    """Create a new knowledge base."""
    return request.app.state.kb_service.create_kb(
        body.topic,
        description=body.description,
        focus_questions=body.focus_questions,
        source_types=body.source_types,
        notes=body.notes,
    )


class RenameKBRequest(BaseModel):
    title: str


# ── Global inbox/ drop zone (MUST be before /{slug} routes) ──


@router.get("/inbox/pending")
async def global_raw_pending(request: Request) -> dict:
    """List files in the global inbox/ drop zone (vault root)."""
    vault = request.app.state.vault
    try:
        files = vault.list_files("inbox", "**/*")
        names = [f.name for f in files if f.name != ".gitkeep"]
        return {"files": names, "count": len(names)}
    except Exception:
        return {"files": [], "count": 0}


@router.post("/inbox/ingest")
async def global_raw_ingest(request: Request, target_kb: str | None = None) -> dict:
    """Ingest files from the global inbox/ drop zone into a specific KB.

    target_kb is required when multiple KBs exist. If only one KB exists,
    files are auto-routed there. No catch-all KB is created.
    """
    import shutil

    vault = request.app.state.vault
    kb = request.app.state.kb_service
    inbox_dir = vault.paths.resolve("inbox")
    if not inbox_dir.is_dir():
        return {"error": "No inbox/ directory found"}

    files = [f for f in inbox_dir.iterdir() if f.is_file() and f.name != ".gitkeep"]
    if not files:
        return {"message": "inbox/ is empty", "count": 0}

    kbs = kb.list_kbs()
    if target_kb:
        slug = target_kb
    elif len(kbs) == 1:
        slug = kbs[0]["slug"]
    elif len(kbs) == 0:
        raise HTTPException(status_code=400, detail="No knowledge bases exist. Create one first.")
    else:
        raise HTTPException(status_code=400, detail="Multiple KBs exist. Specify target_kb parameter.")

    if not any(k["slug"] == slug for k in kbs):
        raise HTTPException(status_code=404, detail=f"KB not found: {slug}")

    kb_inbox = vault.paths.resolve(f"knowledge/{slug}/inbox")
    kb_inbox.mkdir(parents=True, exist_ok=True)
    moved = []
    for f in files:
        shutil.move(str(f), str(kb_inbox / f.name))
        moved.append(f.name)

    result = await kb.ingest(slug)
    return {
        "target_kb": slug,
        "moved": moved,
        "ingested": result.get("count", 0),
        "wiki_updates": result.get("wiki_updates", []),
    }


# ── Per-KB routes ──


@router.delete("/{slug}")
async def delete_kb(request: Request, slug: str) -> dict:
    """Delete a knowledge base and clean up all references."""
    try:
        result = request.app.state.kb_service.delete_kb(slug)
        invalidate_article_cache()
        return result
    except FileNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc


@router.put("/{slug}")
async def rename_kb(request: Request, slug: str, body: RenameKBRequest) -> dict:
    """Rename a knowledge base."""
    try:
        result = request.app.state.kb_service.rename_kb(slug, body.title)
        invalidate_article_cache()
        return result
    except FileNotFoundError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc


@router.get("/{slug}")
async def get_kb_status(request: Request, slug: str) -> dict:
    """Get detailed status of a knowledge base."""
    result = request.app.state.kb_service.get_kb_status(slug)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.get("/language-drift")
async def language_drift(request: Request, slug: str | None = None) -> dict:
    """List source pages whose recorded language policy differs from the
    currently effective policy (spec §19 item 3). Dry-run only.
    """
    kb = request.app.state.kb_service
    return await kb.find_drifted_sources(slug)


class ReextractRequest(BaseModel):
    extractor_id: str = ""   # empty → ExtractionSelector picks highest-fidelity binding


@router.post("/extraction/upgrade/{kb_slug}/{source_slug}")
async def upgrade_extraction(
    request: Request,
    kb_slug: str,
    source_slug: str,
    body: ReextractRequest | None = None,
) -> dict:
    """Re-extract one source with a higher-fidelity tier.

    Phase 4.75 of decisions.md §2026-04-18. Invokes
    ``KnowledgeBaseService.reextract_source`` which resolves the original
    file from ``library_path`` frontmatter, runs the target extractor,
    and rewrites the source page (updates provenance + appends a new
    ``## Re-extracted Content`` section). The ingest pipeline's LLM
    classify + entity/concept extraction is NOT re-run — users who want
    a full re-ingest trigger that via ``POST /api/kb/{slug}/reingest``.

    404 when the source page doesn't exist. 400 when the extractor_id
    is unknown or the library file is missing on disk.
    """
    extractor_id = (body.extractor_id if body else "") or ""
    svc = request.app.state.kb_service
    result = await svc.reextract_source(
        kb_slug=kb_slug, source_slug=source_slug, extractor_id=extractor_id,
    )
    if not result.get("ok"):
        err = str(result.get("error") or "Re-extraction failed")
        # Missing source → 404, everything else → 400 (the result dict
        # carries the detailed failure string for the UI toast).
        if "not found" in err.lower() and "source page" in err.lower():
            raise HTTPException(status_code=404, detail=err)
        raise HTTPException(status_code=400, detail=err)
    invalidate_article_cache()
    return result


@router.get("/extraction/upgrades")
async def list_extraction_upgrades(request: Request, kb: str | None = None) -> dict:
    """List upgrade proposals — source pages whose recorded extractor is
    now superseded by a higher-fidelity tier available in the registry
    (Phase 4.5 of decisions.md §2026-04-18 revised).

    Walks ``wiki/sources/*.md`` across KBs (or a single KB when ``kb=slug``
    is passed), reads each page's ``extractor_id`` + ``type_name``
    frontmatter, and runs ``propose_extractor_upgrades`` against the live
    registry. Read-only — does NOT re-ingest anything. The UI uses the
    returned proposals to offer explicit "re-ingest with {tier}" buttons;
    actual re-ingest lands in Phase 4.75 as an explicit POST endpoint.

    Response shape::

        {
          "scanned": <int>,       # source pages walked
          "proposals": [
            {"source_path": "...", "type_name": "paper_pdf",
             "current_extractor": "pdf_pdfplumber",
             "proposed_extractor": "pdf_pdfplumber_ocr",
             "proposed_tier": "capable", "proposed_fidelity": "high",
             "reason": "..."},
            ...
          ]
        }
    """
    from dataclasses import asdict as _asdict

    from agntspace.kb.file_types import propose_extractor_upgrades
    svc = request.app.state.kb_service
    records = svc.list_source_records(slug=kb)
    proposals = propose_extractor_upgrades(records, svc._file_types)
    return {
        "scanned": len(records),
        "proposals": [_asdict(p) for p in proposals],
    }


@router.post("/language-drift/reingest")
async def reingest_drifted(request: Request, slug: str | None = None) -> dict:
    """Re-ingest every source page that drifted under the current policy
    (spec §19 item 3). Optionally scoped to a single KB slug.
    """
    kb = request.app.state.kb_service
    result = await kb.reingest_drifted_sources(slug)
    invalidate_article_cache()
    return result


@router.post("/{slug}/ingest")
async def ingest(request: Request, slug: str) -> dict:
    """Ingest new/changed files from raw/ — summarize, index, then compile wiki articles."""
    kb = request.app.state.kb_service
    result = await kb.ingest(slug)
    invalidate_article_cache()
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    # Auto-compile after ingest so source summaries become wiki articles
    if result.get("ingested"):
        try:
            compile_result = await kb.compile_wiki(slug)
            result["compiled"] = compile_result.get("count", 0)
            result["compiled_articles"] = compile_result.get("articles", [])
            invalidate_article_cache()
        except Exception:
            log.exception("Post-ingest compile failed for KB %s", slug)
    return result


@router.post("/{slug}/add-source")
async def add_source(request: Request, slug: str, body: AddSourceRequest) -> dict:
    """Add source content directly to KB inbox, then ingest + compile."""
    from datetime import date

    vault = request.app.state.vault
    kb = request.app.state.kb_service

    inbox_dir = vault.paths.resolve(f"knowledge/{slug}/inbox")
    if not inbox_dir.parent.exists():
        raise HTTPException(status_code=404, detail=f"Knowledge base '{slug}' does not exist.")
    inbox_dir.mkdir(parents=True, exist_ok=True)

    title = body.title.strip() or f"Source {date.today().isoformat()}"
    filename = body.filename.strip() if body.filename else ""
    if not filename:
        import re
        filename = re.sub(r"[^a-zA-Z0-9]+", "-", title).strip("-")[:80] or "source"

    # Write to inbox with frontmatter
    out_path = inbox_dir / f"{filename}.md"
    frontmatter = f'---\ntitle: "{title}"\nauthor: user\ndate: {date.today().isoformat()}\n---\n\n'
    out_path.write_text(frontmatter + body.content, encoding="utf-8")

    result: dict = {"title": title, "filename": f"{filename}.md", "kb": slug}

    # Ingest + compile
    try:
        ingest_result = await kb.ingest(slug)
        result["ingested"] = ingest_result.get("ingested", 0)
        if ingest_result.get("ingested"):
            compile_result = await kb.compile_wiki(slug)
            result["compiled"] = compile_result.get("articles_written", 0)
            invalidate_article_cache()
    except Exception:
        log.exception("Post-add-source ingest/compile failed for KB %s", slug)

    return result


@router.post("/{slug}/reingest")
async def reingest(request: Request, slug: str) -> dict:
    """Re-ingest all library files back through the pipeline.

    Re-copies files from library/ back to inbox/, clears processed hashes,
    and runs a full ingest.
    """
    import shutil

    kb = request.app.state.kb_service
    vault = request.app.state.vault

    base = vault.paths.resolve(f"knowledge/{slug}")
    if not (base / "wiki" / ".index.md").exists():
        raise HTTPException(status_code=404, detail=f"KB not found: {slug}")

    inbox = base / "inbox"
    inbox.mkdir(parents=True, exist_ok=True)
    library = base / "library"

    copied = 0
    if library.is_dir():
        for f in library.rglob("*"):
            if not f.is_file() or f.name == ".gitkeep":
                continue
            dest = inbox / f.name
            if not dest.exists():
                shutil.copy2(str(f), str(dest))
                copied += 1

    # Clear processed hashes so ingest treats everything as new
    # Preserve compiled_articles so compile dedup still works
    old_state = kb._read_compile_state(slug)
    kb._write_compile_state(slug, {
        "last_ingest": None,
        "last_compile": None,
        "processed_hashes": {},
        "sources": {},
        "compiled_articles": old_state.get("compiled_articles", {}),
    })

    # Run ingest
    result = await kb.ingest(slug)
    invalidate_article_cache()
    result["recopied"] = copied
    return result


@router.post("/{slug}/compile")
async def compile_wiki(request: Request, slug: str, full: bool = False) -> dict:
    """Compile sources into wiki articles. Incremental by default, full=true for complete recompile."""
    result = await request.app.state.kb_service.compile_wiki(slug, full=full)
    invalidate_article_cache()
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


def _scrape_url_sync(url: str, inbox_dir, filename: str) -> dict:
    """Blocking scrape — runs in thread pool to avoid blocking the event loop."""
    from pathlib import Path

    from agntspace.integrations.browser import scrape_page_sync

    content = scrape_page_sync(url)

    if not content:
        return {"error": "Could not extract content from page. It may require JavaScript rendering or login."}

    out_path = Path(inbox_dir) / f"{filename}.md"
    frontmatter = f"---\ntitle: {filename}\nsource_url: {url}\nauthor: agent\nagent: researcher\n---\n\n"
    out_path.write_text(frontmatter + content, encoding="utf-8")

    return {"scraped": True, "url": url, "filename": f"{filename}.md", "size": len(content)}


@router.post("/{slug}/scrape")
async def scrape_url(request: Request, slug: str, body: ScrapeRequest) -> dict:
    """Scrape a URL and save to KB inbox (agent-browser → httpx fallback)."""
    import asyncio
    import re
    from urllib.parse import urlparse

    vault = request.app.state.vault

    inbox_dir = vault.paths.resolve(f"knowledge/{slug}/inbox")
    if not inbox_dir.parent.exists():
        raise HTTPException(status_code=404, detail=f"Knowledge base '{slug}' does not exist.")
    inbox_dir.mkdir(parents=True, exist_ok=True)

    filename = body.filename
    if not filename:
        parsed = urlparse(body.url)
        fname_slug = parsed.netloc.replace("www.", "") + parsed.path
        filename = re.sub(r"[^a-zA-Z0-9]+", "-", fname_slug).strip("-")[:80] or "scraped-page"

    try:
        result = await asyncio.to_thread(_scrape_url_sync, body.url, str(inbox_dir), filename)
        if "error" in result:
            raise HTTPException(status_code=422, detail=result["error"])
        result["kb"] = slug

        # Auto-ingest + compile so scraped source becomes a wiki article immediately
        kb = request.app.state.kb_service
        try:
            ingest_result = await kb.ingest(slug)
            result["ingested"] = ingest_result.get("ingested", 0)
            if ingest_result.get("ingested"):
                compile_result = await kb.compile_wiki(slug)
                result["compiled"] = compile_result.get("count", 0)
                result["compiled_articles"] = compile_result.get("articles", [])
                invalidate_article_cache()
        except Exception:
            log.exception("Post-scrape ingest/compile failed for KB %s", slug)

        return result
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Scrape failed: {e}") from e


def _research_scrape_sync(topic: str, max_pages: int, inbox_dir: str, tool_executor: object) -> dict:
    """Search + scrape — delegates to the agent-tool executor so the API route
    and the chat-tool path share ONE implementation (and thus ONE quality
    gate, ONE relevance gate, ONE image policy).

    Previously this route had its own copy of the search-filter-scrape-save
    loop with no quality/relevance gates. After the 2026-04-19 redesign that
    introduced both gates in `_exec_research_scrape`, the API route would
    have silently kept the old behaviour — saving every result regardless of
    word count or topic match. Routing through the executor is the
    architectural fix: there is now exactly one place where scraping
    decisions are made.

    The `inbox_dir` arg is preserved for backward compatibility with the
    route signature but the executor resolves the path itself from `kb`,
    so the two paths must match (the route already validates KB existence
    before calling).
    """
    # Re-derive the kb slug from the inbox_dir path so we can pass it
    # through. The route gives us .../knowledge/<slug>/inbox.
    from pathlib import Path
    inbox_path = Path(inbox_dir)
    kb_slug = inbox_path.parent.name

    return tool_executor._exec_research_scrape({  # noqa: SLF001
        "topic": topic, "kb": kb_slug, "max_pages": max_pages,
    })


@router.post("/{slug}/research-scrape")
async def research_scrape(request: Request, slug: str, body: ResearchScrapeRequest) -> dict:
    """Search the web for a topic and scrape top results into KB inbox."""
    import asyncio

    vault = request.app.state.vault

    inbox_dir = vault.paths.resolve(f"knowledge/{slug}/inbox")
    if not inbox_dir.parent.exists():
        raise HTTPException(status_code=404, detail=f"Knowledge base '{slug}' does not exist.")
    inbox_dir.mkdir(parents=True, exist_ok=True)

    try:
        # Delegate to the agent-tool executor so quality + relevance gates
        # apply to UI-initiated scrapes too (single source of truth, see
        # _research_scrape_sync docstring for the architectural rationale).
        tool_executor = request.app.state.tool_executor
        result = await asyncio.to_thread(
            _research_scrape_sync, body.topic, body.max_pages, str(inbox_dir), tool_executor,
        )
        if "error" in result:
            raise HTTPException(status_code=422, detail=result["error"])
        result["kb"] = slug

        # Auto-ingest + compile so scraped sources become wiki articles immediately
        if result.get("scraped", 0) > 0:
            kb = request.app.state.kb_service
            try:
                ingest_result = await kb.ingest(slug)
                result["ingested"] = ingest_result.get("ingested", 0)
                if ingest_result.get("ingested"):
                    compile_result = await kb.compile_wiki(slug)
                    result["compiled"] = compile_result.get("articles_written", 0)
                    invalidate_article_cache()
            except Exception:
                log.exception("Post-scrape ingest/compile failed for KB %s", slug)

        return result
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Research scrape failed: {e}") from e


@router.post("/{slug}/synthesize")
async def synthesize(request: Request, slug: str) -> dict:
    """Generate cross-source synthesis pages — insights no single source contains alone."""
    result = await request.app.state.kb_service.synthesize(slug)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/cross-link")
async def cross_link(request: Request, slug: str) -> dict:
    """Scan wiki for unlinked mentions of existing articles and add [[wikilinks]]."""
    result = request.app.state.kb_service.cross_link(slug)
    invalidate_article_cache()
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/research")
async def research(request: Request, slug: str, body: ResearchRequest) -> dict:
    """Research a question against the knowledge base."""
    return await request.app.state.kb_service.research(
        slug, body.query, body.save_output, body.file_to_wiki,
        output_format=body.output_format,
    )


@router.post("/{slug}/lint")
async def lint(request: Request, slug: str) -> dict:
    """Run health check — find inconsistencies, missing links, suggest improvements."""
    result = await request.app.state.kb_service.lint(slug)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.get("/{slug}/schema")
async def get_schema(request: Request, slug: str) -> dict:
    """Get the KB schema file."""
    vault = request.app.state.vault
    path = f"knowledge/{slug}/SCHEMA.md"
    if not vault.exists(path):
        raise HTTPException(status_code=404, detail=f"Schema not found for {slug}")
    doc = vault.read(path)
    return {"slug": slug, "content": doc.content, "metadata": doc.metadata}


@router.get("/{slug}/log")
async def get_log(request: Request, slug: str) -> dict:
    """Get the KB operation log."""
    vault = request.app.state.vault
    path = f"knowledge/{slug}/log.md"
    if not vault.exists(path):
        return {"slug": slug, "content": "", "entries": 0}
    doc = vault.read(path)
    entries = doc.content.count("## [")
    return {"slug": slug, "content": doc.content, "entries": entries}


@router.post("/{slug}/build")
async def full_build(request: Request, slug: str) -> dict:
    """Full pipeline: ingest + compile in one call."""
    kb = request.app.state.kb_service
    ingest_result = await kb.ingest(slug)
    if "error" in ingest_result:
        raise HTTPException(status_code=404, detail=ingest_result["error"])
    compile_result = await kb.compile_wiki(slug)
    return {
        "ingested": ingest_result.get("count", 0),
        "compiled": compile_result.get("count", 0),
        "articles": compile_result.get("articles", []),
    }


@router.post("/{slug}/reflect")
async def reflect(request: Request, slug: str) -> dict:
    """Run structural reflection on recent wiki changes."""
    # Get recent compile state to know what changed
    state = request.app.state.kb_service._read_compile_state(slug)
    compiled = list(state.get("compiled_articles", {}).keys())
    result = await request.app.state.kb_service.reflect(
        slug, articles_created=compiled[-10:],
    )
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/repair")
async def repair_wiki(request: Request, slug: str) -> dict:
    """Auto-fix wiki issues: delete orphans, duplicates, empty articles, clean history."""
    result = await request.app.state.kb_service.repair(slug)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/repair-citations")
async def repair_citations_all(request: Request, dry_run: bool = False) -> dict:
    """Backfill numberified `[N]` references on wiki articles that still have
    raw `[Source: filename]` markers across every KB. Idempotent.

    Use `?dry_run=true` to preview candidate articles without mutating the vault.
    """
    return await request.app.state.kb_service.repair_citations(None, dry_run=dry_run)


@router.post("/{slug}/repair-citations")
async def repair_citations_one(request: Request, slug: str, dry_run: bool = False) -> dict:
    """Backfill numberified `[N]` references for a single KB's wiki articles.

    Use `?dry_run=true` to preview candidate articles without mutating the vault.
    """
    result = await request.app.state.kb_service.repair_citations(slug, dry_run=dry_run)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/sync-projects")
async def sync_projects(request: Request, slug: str) -> dict:
    """Sync vault projects into this KB's wiki."""
    result = await request.app.state.kb_service.sync_projects_to_wiki(slug)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.get("/{slug}/library/layout")
async def get_library_layout(request: Request, slug: str) -> dict:
    """Return the current library_layout for a KB (flat | type-date | thematic)."""
    svc = request.app.state.kb_service
    from pathlib import Path as _P  # noqa: F401 — defensive, not strictly needed
    if not svc._reader.exists(f"knowledge/{slug}/SCHEMA.md"):
        raise HTTPException(status_code=404, detail=f"KB {slug} not found")
    return {"slug": slug, "layout": svc._kb_library_layout(slug)}


@router.put("/{slug}/library/layout")
async def set_library_layout_route(request: Request, slug: str, body: LibraryLayoutRequest) -> dict:
    """Change a KB's library_layout. Does NOT move existing files —
    call POST /library/reorganize to rearrange the library to match."""
    result = request.app.state.kb_service.set_library_layout(slug, body.layout)
    if "error" in result:
        raise HTTPException(status_code=400, detail=result["error"])
    return result


@router.post("/{slug}/library/reorganize")
async def reorganize_library_route(
    request: Request, slug: str, body: ReorganizeLibraryRequest | None = None,
) -> dict:
    """Reorganize library/ to match the KB's current layout.

    Defaults to dry_run=True — returns the proposed moves without
    touching disk. POST with {"dry_run": false} to execute.
    """
    dry_run = True if body is None else bool(body.dry_run)
    result = await request.app.state.kb_service.reorganize_library(slug, dry_run=dry_run)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/consolidate")
async def consolidate(request: Request, slug: str, min_updates: int = 3) -> dict:
    """Consolidate append-only articles into coherent single articles."""
    result = await request.app.state.kb_service.consolidate_articles(slug, min_updates=min_updates)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/evolve-schema")
async def evolve_schema(request: Request, slug: str) -> dict:
    """Analyze KB patterns and propose SCHEMA.md updates."""
    result = await request.app.state.kb_service.evolve_schema(slug)
    if "error" in result and "not found" in str(result.get("error", "")):
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/{slug}/job")
async def run_wiki_job(
    request: Request, slug: str,
    skip_lint: bool = False, skip_reflect: bool = False,
) -> dict:
    """Full wiki pipeline: snapshot → ingest → compile → reflect → lint.

    The complete wiki processing cycle. Safe to run anytime — snapshots first.
    """
    result = await request.app.state.kb_service.run_wiki_job(
        slug, skip_lint=skip_lint, skip_reflect=skip_reflect,
    )
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.post("/job/all")
async def run_all_wiki_jobs(
    request: Request, skip_lint: bool = False, skip_reflect: bool = False,
) -> dict:
    """Run the full wiki pipeline across ALL knowledge bases."""
    kb = request.app.state.kb_service
    kbs = kb.list_kbs()
    results = []
    for kb_info in kbs:
        result = await kb.run_wiki_job(
            kb_info["slug"], skip_lint=skip_lint, skip_reflect=skip_reflect,
        )
        results.append(result)
    total_ingested = sum(r.get("ingested", 0) for r in results)
    total_compiled = sum(r.get("compiled", 0) for r in results)
    total_issues = sum(r.get("issues", 0) for r in results)
    return {
        "kbs_processed": len(results),
        "total_ingested": total_ingested,
        "total_compiled": total_compiled,
        "total_issues": total_issues,
        "details": results,
    }


@router.post("/{slug}/snapshot")
async def snapshot(request: Request, slug: str, reason: str = "manual") -> dict:
    """Create a timestamped archive of the wiki before destructive operations."""
    result = request.app.state.kb_service.snapshot(slug, reason=reason)
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


@router.get("/{slug}/snapshots")
async def list_snapshots(request: Request, slug: str) -> list[dict]:
    """List available wiki snapshots."""
    return request.app.state.kb_service.list_snapshots(slug)


@router.post("/{slug}/restore/{archive_id}")
async def restore_snapshot(request: Request, slug: str, archive_id: str) -> dict:
    """Restore wiki from a previous snapshot. Auto-snapshots current state first."""
    result = request.app.state.kb_service.restore_snapshot(slug, archive_id)
    invalidate_article_cache()
    if "error" in result:
        raise HTTPException(status_code=404, detail=result["error"])
    return result


class ProjectWikiRequest(BaseModel):
    project_slug: str
    project_title: str


@router.post("/{slug}/project")
async def create_project_wiki(request: Request, slug: str, body: ProjectWikiRequest) -> dict:
    """Create a project namespace within a KB wiki."""
    path = request.app.state.kb_service.create_project_wiki(
        slug, body.project_slug, body.project_title,
    )
    return {"path": path, "project_slug": body.project_slug}


