"""SaaS API routes — auth, chatbot agents, knowledge feed, chat, API keys."""

from __future__ import annotations

import asyncio
import json
import re
import secrets
import shutil
from pathlib import Path
from typing import Annotated, Literal

from fastapi import APIRouter, BackgroundTasks, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel, Field

from app.models.schemas import ChatResponse, FeedbackResponse
from app.saas import db as saas_db
from app.saas.agent import agent_chat
from app import database as core_db
from app.config import get_settings
from app.saas.kb import (
    avatar_storage_dir,
    delete_kb_vectors,
    drop_agent_store,
    ingest_kb_file,
    org_storage_dir,
)
from app.saas.security import (
    AuthContext,
    generate_api_key,
    get_auth_context,
    hash_password,
    issue_user_token,
    verify_password,
)

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


class RegisterRequest(BaseModel):
    email: str = Field(min_length=5, max_length=200)
    password: str = Field(min_length=8, max_length=128)
    full_name: str = Field(min_length=1, max_length=120)
    org_name: str = Field(min_length=1, max_length=120)


class LoginRequest(BaseModel):
    email: str = Field(min_length=5, max_length=200)
    password: str


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"
    user: dict
    org: dict
    agents: list[dict] = Field(default_factory=list)


class MeResponse(BaseModel):
    user: dict
    org: dict
    agents: list[dict]


class AgentCreateRequest(BaseModel):
    name: str = Field(min_length=1, max_length=80)
    description: str | None = Field(default=None, max_length=400)
    kb_mode: Literal["platform", "tenant", "combined"] = "tenant"
    welcome_message: str | None = Field(default=None, max_length=500)


class AgentUpdateRequest(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=80)
    description: str | None = Field(default=None, max_length=400)
    kb_mode: Literal["platform", "tenant", "combined"] | None = None
    welcome_message: str | None = Field(default=None, max_length=500)


class AgentChatRequest(BaseModel):
    question: str = Field(min_length=1, max_length=4000)
    session_id: str | None = None
    kb_mode: Literal["platform", "tenant", "combined"] | None = None


class AgentChatResetRequest(BaseModel):
    session_id: str | None = None


class ApiKeyCreateRequest(BaseModel):
    name: str = Field(min_length=1, max_length=80)


class ApiKeyCreateResponse(BaseModel):
    id: str
    name: str
    key_prefix: str
    api_key: str
    message: str


def _agent_public(agent: dict) -> dict:
    out = dict(agent)
    if out.get("avatar_path"):
        out["avatar_url"] = f"/saas/agents/{out['id']}/avatar"
    else:
        out["avatar_url"] = None
    out["is_main"] = bool(out.get("is_main"))
    out.pop("avatar_path", None)
    return out


def _require_agent(auth: AuthContext, agent_id: str) -> dict:
    agent = saas_db.get_agent(agent_id)
    if not agent or agent["org_id"] != auth.org_id:
        raise HTTPException(status_code=404, detail="Chatbot not found")
    return agent


def _purge_extra_agents(org_id: str, *, created_by: str | None = None) -> dict:
    """Workspace policy: only Alex exists. Remove any legacy extra chatbots."""
    import shutil

    alex = saas_db.ensure_main_agent(org_id, created_by=created_by)
    extras = saas_db.list_extra_agents(org_id)
    if not extras:
        return alex
    for extra in extras:
        aid = extra["id"]
        try:
            drop_agent_store(org_id, aid)
        except Exception:
            pass
        if extra.get("avatar_path"):
            try:
                Path(extra["avatar_path"]).unlink(missing_ok=True)
            except OSError:
                pass
        folder = org_storage_dir(org_id, aid)
        if folder.exists():
            shutil.rmtree(folder, ignore_errors=True)
        saas_db.delete_agent(aid)
    return alex


def _agents_for_org(org_id: str, *, created_by: str | None = None) -> list[dict]:
    """List agents without running purge/chroma work on every request."""
    agents = saas_db.list_agents(org_id)
    if agents:
        return agents
    alex = saas_db.ensure_main_agent(org_id, created_by=created_by)
    return [alex]


def _token_response(user: dict, org: dict, role: str) -> TokenResponse:
    agents = [_agent_public(a) for a in _agents_for_org(org["id"], created_by=user.get("id"))]
    return TokenResponse(
        access_token=issue_user_token(user, org, role),
        user={"id": user["id"], "email": user["email"], "full_name": user.get("full_name")},
        org={
            "id": org["id"],
            "name": org["name"],
            "slug": org["slug"],
            "plan": org["plan"],
            "kb_mode": org["kb_mode"],
        },
        agents=agents,
    )


# ── Auth ─────────────────────────────────────────────────────────────────────

@router.post("/auth/guest", response_model=TokenResponse)
async def guest_session() -> TokenResponse:
    """Skip login for now — create or reuse a local guest workspace."""
    email = "guest@local.dev"
    user = saas_db.get_user_by_email(email)
    if not user:
        created = saas_db.create_user_with_org(
            email=email,
            password_hash=hash_password(secrets.token_urlsafe(24)),
            full_name="Guest",
            org_name="My Workspace",
        )
        user = saas_db.get_user_by_id(created["user_id"])
        org = saas_db.get_org(created["org_id"])
        role = "owner"
    else:
        orgs = saas_db.list_user_orgs(user["id"])
        if not orgs:
            raise HTTPException(status_code=500, detail="Guest workspace missing")
        org = orgs[0]
        role = org["role"]
        agents = saas_db.list_agents(org["id"])
        if not agents:
            saas_db.create_agent(
                org_id=org["id"],
                name="Alex",
                description="Your main accounting and finance assistant",
                created_by=user["id"],
                kb_mode=org.get("kb_mode") or "tenant",
                is_main=True,
            )
        else:
            # Ensure Alex exists; keep any extra chatbots the user created.
            saas_db.ensure_main_agent(org["id"], created_by=user["id"])
    assert user and org
    return _token_response(user, org, role)


@router.post("/auth/register", response_model=TokenResponse)
async def register(body: RegisterRequest) -> TokenResponse:
    email = body.email.lower().strip()
    if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email):
        raise HTTPException(status_code=400, detail="Invalid email")
    if saas_db.get_user_by_email(email):
        raise HTTPException(status_code=400, detail="Email already registered")

    created = saas_db.create_user_with_org(
        email=email,
        password_hash=hash_password(body.password),
        full_name=body.full_name,
        org_name=body.org_name,
    )
    user = saas_db.get_user_by_id(created["user_id"])
    org = saas_db.get_org(created["org_id"])
    assert user and org
    return _token_response(user, org, "owner")


@router.post("/auth/login", response_model=TokenResponse)
async def login(body: LoginRequest) -> TokenResponse:
    user = saas_db.get_user_by_email(body.email.lower().strip())
    if not user or not verify_password(body.password, user["password_hash"]):
        raise HTTPException(status_code=401, detail="Invalid email or password")
    orgs = saas_db.list_user_orgs(user["id"])
    if not orgs:
        raise HTTPException(status_code=400, detail="No organization found for user")
    org = orgs[0]
    # Ensure Alex exists; keep any extra chatbots the user created.
    saas_db.ensure_main_agent(org["id"], created_by=user["id"])
    return _token_response(user, org, org["role"])


@router.get("/me", response_model=MeResponse)
async def me(auth: Annotated[AuthContext, Depends(get_auth_context)]) -> MeResponse:
    # Org fields come from JWT (or API-key auth) — avoid an extra remote DB round-trip.
    agents = [
        _agent_public(a)
        for a in _agents_for_org(
            auth.org_id,
            created_by=auth.user_id if auth.auth_via == "jwt" else None,
        )
    ]
    return MeResponse(
        user={
            "id": auth.user_id,
            "email": auth.email,
            "full_name": auth.full_name,
            "role": auth.role,
        },
        org={
            "id": auth.org_id,
            "name": auth.org_name,
            "slug": auth.org_slug,
            "plan": auth.plan,
            "kb_mode": auth.kb_mode,
        },
        agents=agents,
    )


@router.get("/stats")
async def workspace_stats(auth: Annotated[AuthContext, Depends(get_auth_context)]) -> dict:
    """Org-wide chatbot dashboard: documents, chunks, and feedback per agent."""
    return saas_db.build_org_dashboard(auth.org_id)


# ── Chatbot agents ───────────────────────────────────────────────────────────

@router.get("/agents")
async def list_agents(auth: Annotated[AuthContext, Depends(get_auth_context)]) -> list[dict]:
    return [
        _agent_public(a)
        for a in _agents_for_org(
            auth.org_id,
            created_by=auth.user_id if auth.auth_via == "jwt" else None,
        )
    ]


@router.post("/agents")
async def create_agent(
    body: AgentCreateRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    """Create an additional chatbot alongside Alex (Alex stays the main assistant)."""
    name = (body.name or "").strip()
    if not name:
        raise HTTPException(status_code=400, detail="Name is required")
    if name.lower() == "alex":
        raise HTTPException(
            status_code=400,
            detail="Alex is reserved as the main assistant — pick another name.",
        )
    existing = saas_db.list_agents(auth.org_id)
    if len(existing) >= 20:
        raise HTTPException(status_code=400, detail="You can have at most 20 chatbots.")
    saas_db.ensure_main_agent(auth.org_id, created_by=auth.user_id)
    agent = saas_db.create_agent(
        org_id=auth.org_id,
        name=name,
        description=body.description,
        created_by=auth.user_id,
        kb_mode=body.kb_mode if body.kb_mode in {"tenant", "combined"} else "tenant",
        welcome_message=body.welcome_message,
        is_main=False,
    )
    return _agent_public(agent)


@router.get("/agents/{agent_id}")
async def get_agent(
    agent_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    agent = _require_agent(auth, agent_id)
    docs = saas_db.list_kb_documents(auth.org_id, agent_id)
    out = _agent_public(agent)
    out["documents"] = docs
    return out


@router.patch("/agents/{agent_id}")
async def update_agent(
    agent_id: str,
    body: AgentUpdateRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    agent = _require_agent(auth, agent_id)
    # Alex stays named Alex; other chatbots can be renamed.
    if agent.get("is_main"):
        name = "Alex"
    else:
        name = (body.name or agent.get("name") or "Assistant").strip()
        if name.lower() == "alex":
            raise HTTPException(
                status_code=400,
                detail="Alex is reserved as the main assistant — pick another name.",
            )
    kb_mode = body.kb_mode
    if kb_mode not in {None, "tenant", "combined"}:
        kb_mode = "tenant"
    updated = saas_db.update_agent(
        agent_id,
        name=name,
        description=body.description,
        kb_mode=kb_mode,
        welcome_message=body.welcome_message,
    )
    return _agent_public(updated or {})


@router.delete("/agents/{agent_id}")
async def delete_agent(
    agent_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    agent = _require_agent(auth, agent_id)
    if agent.get("is_main"):
        raise HTTPException(
            status_code=403,
            detail="Alex is the main assistant and cannot be deleted.",
        )
    # Tear down storage / chroma for this agent only
    try:
        drop_agent_store(auth.org_id, agent_id)
    except Exception:
        pass
    if agent.get("avatar_path"):
        try:
            Path(agent["avatar_path"]).unlink(missing_ok=True)
        except OSError:
            pass
    folder = org_storage_dir(auth.org_id, agent_id)
    if folder.exists():
        shutil.rmtree(folder, ignore_errors=True)
    saas_db.delete_agent(agent_id)
    return {"ok": True, "deleted": agent_id}


@router.post("/agents/{agent_id}/avatar")
async def upload_avatar(
    agent_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
    file: UploadFile = File(...),
) -> dict:
    _require_agent(auth, agent_id)
    if not file.filename:
        raise HTTPException(status_code=400, detail="Filename required")
    suffix = Path(file.filename).suffix.lower()
    if suffix not in {".png", ".jpg", ".jpeg", ".webp", ".gif"}:
        raise HTTPException(status_code=400, detail="Use PNG, JPG, WEBP, or GIF")
    raw = await file.read()
    if not raw or len(raw) > 5 * 1024 * 1024:
        raise HTTPException(status_code=400, detail="Avatar must be under 5 MB")
    dest = avatar_storage_dir(auth.org_id) / f"{agent_id}{suffix}"
    dest.write_bytes(raw)
    updated = saas_db.update_agent(agent_id, avatar_path=str(dest))
    return _agent_public(updated or {})


@router.get("/agents/{agent_id}/avatar")
async def get_avatar(agent_id: str):
    agent = saas_db.get_agent(agent_id)
    if not agent or not agent.get("avatar_path"):
        raise HTTPException(status_code=404, detail="No avatar")
    path = Path(agent["avatar_path"])
    if not path.exists():
        raise HTTPException(status_code=404, detail="Avatar file missing")
    return FileResponse(path)


# ── Knowledge feed (per chatbot) ─────────────────────────────────────────────

@router.get("/agents/{agent_id}/documents")
async def list_agent_documents(
    agent_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> list[dict]:
    _require_agent(auth, agent_id)
    return saas_db.list_kb_documents(auth.org_id, agent_id)


@router.post("/agents/{agent_id}/documents")
async def upload_agent_document(
    agent_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
    file: UploadFile = File(...),
) -> dict:
    _require_agent(auth, agent_id)
    if not file.filename:
        raise HTTPException(status_code=400, detail="Filename required")
    suffix = Path(file.filename).suffix.lower()
    if suffix not in {".pdf", ".txt", ".md", ".markdown", ".csv"}:
        raise HTTPException(status_code=400, detail="Supported types: PDF, TXT, MD, CSV")

    raw = await file.read()
    if not raw:
        raise HTTPException(status_code=400, detail="Empty file")
    if len(raw) > 25 * 1024 * 1024:
        raise HTTPException(status_code=400, detail="File too large (max 25 MB)")

    safe_name = re.sub(r"[^\w.\- ]+", "_", file.filename)[:180]
    doc = saas_db.create_kb_document(
        org_id=auth.org_id,
        filename=safe_name,
        content_type=file.content_type,
        uploaded_by=auth.user_id if auth.auth_via == "jwt" else None,
        agent_id=agent_id,
    )
    dest = org_storage_dir(auth.org_id, agent_id) / f"{doc['id']}_{safe_name}"
    dest.write_bytes(raw)

    saas_db.update_kb_document(
        doc["id"],
        status="processing",
        progress_stage="Queued…",
        progress_pct=1,
        error="",
    )

    org_id = auth.org_id
    doc_id = doc["id"]

    async def _run_ingest() -> None:
        try:
            await asyncio.to_thread(
                ingest_kb_file,
                org_id,
                doc_id,
                dest,
                safe_name,
                agent_id=agent_id,
            )
        except Exception:
            # ingest_kb_file already marks the document failed
            pass

    asyncio.create_task(_run_ingest())

    updated = saas_db.get_kb_document(doc_id)
    agent = saas_db.get_agent(agent_id)
    return {
        "document": updated,
        "chunks_indexed": 0,
        "async": True,
        "agent": _agent_public(agent) if agent else None,
        "message": "Processing started — watch progress in Knowledge.",
    }


@router.post("/agents/{agent_id}/documents/{doc_id}/retry")
async def retry_agent_document(
    agent_id: str,
    doc_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    _require_agent(auth, agent_id)
    doc = saas_db.get_kb_document(doc_id)
    if not doc or doc["org_id"] != auth.org_id or doc.get("agent_id") != agent_id:
        raise HTTPException(status_code=404, detail="Document not found")

    folder = org_storage_dir(auth.org_id, agent_id)
    matches = sorted(folder.glob(f"{doc_id}_*"))
    if not matches:
        raise HTTPException(status_code=404, detail="Original file missing — please re-upload")
    dest = matches[0]
    safe_name = doc.get("filename") or dest.name

    await asyncio.to_thread(delete_kb_vectors, auth.org_id, doc_id, agent_id)
    saas_db.update_kb_document(
        doc_id,
        status="processing",
        progress_stage="Retrying…",
        progress_pct=1,
        error="",
        chunk_count=0,
        embeddings_done=0,
        vectors_stored=0,
        chunks_created=0,
    )

    org_id = auth.org_id

    async def _run_ingest() -> None:
        try:
            await asyncio.to_thread(
                ingest_kb_file,
                org_id,
                doc_id,
                dest,
                safe_name,
                agent_id=agent_id,
            )
        except Exception:
            pass

    asyncio.create_task(_run_ingest())
    updated = saas_db.get_kb_document(doc_id)
    return {
        "document": updated,
        "async": True,
        "message": "Retry started — watch progress in Knowledge.",
    }


@router.delete("/agents/{agent_id}/documents/{doc_id}")
async def delete_agent_document(
    agent_id: str,
    doc_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
    background_tasks: BackgroundTasks,
) -> dict:
    _require_agent(auth, agent_id)
    doc = saas_db.get_kb_document(doc_id)
    if not doc or doc["org_id"] != auth.org_id or doc.get("agent_id") != agent_id:
        raise HTTPException(status_code=404, detail="Document not found")
    # Remove from DB first so list endpoints stop returning it immediately.
    saas_db.delete_kb_document(doc_id)
    folder = org_storage_dir(auth.org_id, agent_id)
    for path in folder.glob(f"{doc_id}_*"):
        try:
            path.unlink()
        except OSError:
            pass
    # Chroma cleanup can be slow — finish after the response.
    background_tasks.add_task(delete_kb_vectors, auth.org_id, doc_id, agent_id)
    return {"ok": True, "doc_id": doc_id}


# ── Agent chat ───────────────────────────────────────────────────────────────

@router.post("/agents/{agent_id}/chat/reset")
async def reset_agent_chat(
    agent_id: str,
    body: AgentChatResetRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    """Clear in-memory follow-up turns so the next message starts a fresh thread."""
    from app.saas.conversation import clear_turns

    _require_agent(auth, agent_id)
    sid = (body.session_id or "").strip() or None
    if sid:
        clear_turns(sid)
    return {"ok": True, "session_id": sid}


@router.post("/agents/{agent_id}/chat", response_model=ChatResponse)
async def chat_with_agent(
    agent_id: str,
    body: AgentChatRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> ChatResponse:
    _require_agent(auth, agent_id)
    try:
        return await asyncio.to_thread(
            agent_chat,
            question=body.question,
            auth=auth,
            agent_id=agent_id,
            session_id=body.session_id,
            kb_mode=body.kb_mode,
        )
    except RuntimeError as exc:
        raise HTTPException(status_code=503, detail=str(exc)) from exc


@router.post("/agents/{agent_id}/chat/stream")
async def chat_with_agent_stream(
    agent_id: str,
    body: AgentChatRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> StreamingResponse:
    """Stream assistant tokens via Server-Sent Events, then a final done payload."""
    _require_agent(auth, agent_id)
    loop = asyncio.get_running_loop()
    queue: asyncio.Queue[tuple[str, object]] = asyncio.Queue()

    def on_token(chunk: str) -> None:
        loop.call_soon_threadsafe(queue.put_nowait, ("token", chunk))

    def on_status(status: str) -> None:
        loop.call_soon_threadsafe(queue.put_nowait, ("status", status))

    def run_chat() -> None:
        try:
            response = agent_chat(
                question=body.question,
                auth=auth,
                agent_id=agent_id,
                session_id=body.session_id,
                kb_mode=body.kb_mode,
                on_token=on_token,
                on_status=on_status,
            )
            loop.call_soon_threadsafe(queue.put_nowait, ("done", response))
        except Exception as exc:
            loop.call_soon_threadsafe(queue.put_nowait, ("error", str(exc)))

    asyncio.create_task(asyncio.to_thread(run_chat))

    async def event_stream():
        while True:
            kind, payload = await queue.get()
            if kind == "token":
                yield f"event: token\ndata: {json.dumps({'content': payload})}\n\n"
            elif kind == "status":
                yield f"event: status\ndata: {json.dumps({'phase': payload})}\n\n"
            elif kind == "done":
                assert isinstance(payload, ChatResponse)
                yield f"event: done\ndata: {payload.model_dump_json()}\n\n"
                break
            elif kind == "error":
                yield f"event: error\ndata: {json.dumps({'detail': payload})}\n\n"
                break

    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )


# Legacy alias
@router.post("/agent/chat", response_model=ChatResponse)
async def saas_agent_chat_legacy(
    body: AgentChatRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
    agent_id: str | None = None,
) -> ChatResponse:
    agents = saas_db.list_agents(auth.org_id)
    if not agents:
        raise HTTPException(status_code=400, detail="Create a chatbot first")
    target = agent_id or agents[0]["id"]
    return await chat_with_agent(target, body, auth)


class AgentFeedbackRequest(BaseModel):
    question: str = Field(..., min_length=1, max_length=4000)
    answer: str = Field(..., min_length=1)
    rating: Literal["up", "down"]
    correction: str | None = Field(default=None, max_length=4000)
    session_id: str | None = None
    log_id: int | None = None
    message_id: str | None = Field(default=None, max_length=80)
    mode: str | None = Field(default=None, max_length=40)


@router.post("/agents/{agent_id}/feedback", response_model=FeedbackResponse)
async def submit_agent_feedback(
    agent_id: str,
    body: AgentFeedbackRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> FeedbackResponse:
    """Save thumbs up/down for this agent into SQLite (used for future model training)."""
    _require_agent(auth, agent_id)
    feedback_id = core_db.save_feedback(
        log_id=body.log_id,
        session_id=body.session_id,
        question=body.question,
        answer=body.answer,
        rating=body.rating,
        correction=body.correction,
        org_id=auth.org_id,
        agent_id=agent_id,
        user_id=auth.user_id,
        message_id=body.message_id,
        mode=body.mode,
        model_name=get_settings().ollama_model,
    )
    return FeedbackResponse(
        feedback_id=feedback_id,
        message="Thanks — your rating was saved for model improvement.",
        rating=body.rating,
    )


@router.get("/agents/{agent_id}/feedback")
async def list_agent_feedback(
    agent_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
    rating: Literal["up", "down"] | None = None,
    limit: int = Query(default=50, ge=1, le=500),
) -> dict:
    """List feedback for this agent (workspace scoped)."""
    _require_agent(auth, agent_id)
    rows = core_db.get_feedback(
        limit=limit,
        offset=0,
        rating=rating,
        org_id=auth.org_id,
        agent_id=agent_id,
    )
    return {
        "count": len(rows),
        "items": [
            {
                "id": r["id"],
                "rating": r["rating"],
                "question": r["question"],
                "answer": r["answer"],
                "correction": r.get("correction"),
                "mode": r.get("mode"),
                "model_name": r.get("model_name"),
                "message_id": r.get("message_id"),
                "created_at": r.get("created_at"),
            }
            for r in rows
        ],
    }


# ── API keys ─────────────────────────────────────────────────────────────────

@router.get("/api-keys")
async def list_keys(auth: Annotated[AuthContext, Depends(get_auth_context)]) -> list[dict]:
    if auth.role not in {"owner", "admin"}:
        raise HTTPException(status_code=403, detail="Owner/admin only")
    return saas_db.list_api_keys(auth.org_id)


@router.post("/api-keys", response_model=ApiKeyCreateResponse)
async def create_key(
    body: ApiKeyCreateRequest,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> ApiKeyCreateResponse:
    if auth.role not in {"owner", "admin"}:
        raise HTTPException(status_code=403, detail="Owner/admin only")
    raw, prefix, key_hash = generate_api_key()
    row = saas_db.create_api_key(
        org_id=auth.org_id,
        name=body.name,
        key_prefix=prefix,
        key_hash=key_hash,
        created_by=auth.user_id,
    )
    return ApiKeyCreateResponse(
        id=row["id"],
        name=row["name"],
        key_prefix=prefix,
        api_key=raw,
        message="Copy this key now — it will not be shown again.",
    )


@router.delete("/api-keys/{key_id}")
async def revoke_key(
    key_id: str,
    auth: Annotated[AuthContext, Depends(get_auth_context)],
) -> dict:
    if auth.role not in {"owner", "admin"}:
        raise HTTPException(status_code=403, detail="Owner/admin only")
    ok = saas_db.revoke_api_key(key_id, auth.org_id)
    if not ok:
        raise HTTPException(status_code=404, detail="API key not found")
    return {"ok": True, "key_id": key_id}
