"""Recipe Crave — FastAPI backend.

Dynamic recipe platform with:
- JWT auth (httpOnly cookies) + admin seed account
- Recipes, categories, collections, authors, tags CRUD
- Object storage image uploads
- XML sitemap, robots.txt
- Search & filtering
"""
from dotenv import load_dotenv
from pathlib import Path

ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / ".env")

import os
import re
import uuid
import logging
import secrets
from datetime import datetime, timezone, timedelta
from typing import List, Optional, Any

import bcrypt
import jwt
import requests
from bson import ObjectId
from fastapi import (
    FastAPI,
    APIRouter,
    HTTPException,
    Depends,
    Request,
    Response,
    Query,
    UploadFile,
    File,
    Form,
)
from fastapi.responses import PlainTextResponse
from starlette.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel, Field, EmailStr

# ---------- Setup ----------
mongo_url = os.environ["MONGO_URL"]
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ["DB_NAME"]]

APP_NAME = os.environ.get("APP_NAME", "recipe-crave")
JWT_ALGORITHM = "HS256"
JWT_SECRET = os.environ["JWT_SECRET"]
FRONTEND_URL = os.environ.get("FRONTEND_URL", "http://localhost:3000")

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="Recipe Crave API")
api_router = APIRouter(prefix="/api")

# ---------- Storage ----------
STORAGE_BASE = (os.environ.get("INTEGRATION_PROXY_URL") or "").strip() or "https://integrations.emergentagent.com"
STORAGE_URL = STORAGE_BASE.rstrip("/") + "/objstore/api/v1/storage"
EMERGENT_KEY = os.environ.get("EMERGENT_LLM_KEY")
_storage_key = None


def init_storage(force: bool = False):
    global _storage_key
    if _storage_key and not force:
        return _storage_key
    resp = requests.post(f"{STORAGE_URL}/init", json={"emergent_key": EMERGENT_KEY}, timeout=30)
    resp.raise_for_status()
    _storage_key = resp.json()["storage_key"]
    return _storage_key


def put_object(path: str, data: bytes, content_type: str) -> dict:
    key = init_storage()
    resp = requests.put(
        f"{STORAGE_URL}/objects/{path}",
        headers={"X-Storage-Key": key, "Content-Type": content_type},
        data=data,
        timeout=120,
    )
    if resp.status_code == 404:
        key = init_storage(force=True)
        resp = requests.put(
            f"{STORAGE_URL}/objects/{path}",
            headers={"X-Storage-Key": key, "Content-Type": content_type},
            data=data,
            timeout=120,
        )
    resp.raise_for_status()
    return resp.json()


def get_object(path: str):
    key = init_storage()
    resp = requests.get(f"{STORAGE_URL}/objects/{path}", headers={"X-Storage-Key": key}, timeout=60)
    if resp.status_code == 404:
        key = init_storage(force=True)
        resp = requests.get(f"{STORAGE_URL}/objects/{path}", headers={"X-Storage-Key": key}, timeout=60)
    resp.raise_for_status()
    return resp.content, resp.headers.get("Content-Type", "application/octet-stream")


# ---------- Auth utils ----------
def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")


def verify_password(plain: str, hashed: str) -> bool:
    try:
        return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
    except Exception:
        return False


def create_access_token(user_id: str, email: str) -> str:
    payload = {
        "sub": user_id,
        "email": email,
        "exp": datetime.now(timezone.utc) + timedelta(minutes=60 * 24),
        "type": "access",
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def create_refresh_token(user_id: str) -> str:
    payload = {
        "sub": user_id,
        "exp": datetime.now(timezone.utc) + timedelta(days=30),
        "type": "refresh",
    }
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def set_auth_cookies(response: Response, access: str, refresh: str):
    response.set_cookie("access_token", access, httponly=True, secure=True, samesite="none", max_age=60 * 60 * 24, path="/")
    response.set_cookie("refresh_token", refresh, httponly=True, secure=True, samesite="none", max_age=60 * 60 * 24 * 30, path="/")


async def get_current_user(request: Request) -> dict:
    token = request.cookies.get("access_token")
    if not token:
        auth = request.headers.get("Authorization", "")
        if auth.startswith("Bearer "):
            token = auth[7:]
    if not token:
        raise HTTPException(status_code=401, detail="Not authenticated")
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        if payload.get("type") != "access":
            raise HTTPException(status_code=401, detail="Invalid token type")
        user = await db.users.find_one({"_id": ObjectId(payload["sub"])})
        if not user:
            raise HTTPException(status_code=401, detail="User not found")
        user["_id"] = str(user["_id"])
        user.pop("password_hash", None)
        return user
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")


async def require_admin(user: dict = Depends(get_current_user)) -> dict:
    if user.get("role") != "admin":
        raise HTTPException(status_code=403, detail="Admin only")
    return user


# ---------- Helpers ----------
def slugify(text: str) -> str:
    text = (text or "").lower().strip()
    text = re.sub(r"[^a-z0-9\s-]", "", text)
    text = re.sub(r"[\s_-]+", "-", text)
    return text.strip("-")


def _sd(doc):
    """Serialize Mongo doc -> plain JSON."""
    if doc is None:
        return None
    out = {}
    for k, v in doc.items():
        if k == "_id":
            out["id"] = str(v)
        elif isinstance(v, datetime):
            out[k] = v.isoformat()
        elif isinstance(v, ObjectId):
            out[k] = str(v)
        else:
            out[k] = v
    return out


# ---------- Models ----------
class LoginBody(BaseModel):
    email: EmailStr
    password: str


class Ingredient(BaseModel):
    name: str
    quantity: str = ""
    unit: str = ""
    notes: str = ""


class InstructionStep(BaseModel):
    step: int
    title: str = ""
    description: str
    image: str = ""


class Faq(BaseModel):
    question: str
    answer: str


class Nutrition(BaseModel):
    calories: Optional[str] = ""
    protein: Optional[str] = ""
    carbs: Optional[str] = ""
    fat: Optional[str] = ""
    fiber: Optional[str] = ""
    sodium: Optional[str] = ""


class RecipeIn(BaseModel):
    title: str
    slug: Optional[str] = None
    shortDescription: str = ""
    introduction: str = ""
    whyYoullLove: str = ""
    authorId: Optional[str] = None
    featuredImage: str = ""
    galleryImages: List[str] = []
    prepTime: str = ""
    cookTime: str = ""
    totalTime: str = ""
    servings: str = ""
    recipeCategory: str = ""
    recipeCuisine: str = ""
    ingredients: List[Ingredient] = []
    instructions: List[InstructionStep] = []
    ingredientNotes: str = ""
    recipeTips: str = ""
    substitutions: str = ""
    variations: str = ""
    storageInstructions: str = ""
    reheatingInstructions: str = ""
    nutrition: Nutrition = Field(default_factory=Nutrition)
    faqs: List[Faq] = []
    tags: List[str] = []
    categoryIds: List[str] = []
    collectionIds: List[str] = []
    videoUrl: str = ""
    seoTitle: str = ""
    seoDescription: str = ""
    canonicalUrl: str = ""
    status: str = "draft"  # draft | published | scheduled
    scheduledFor: Optional[str] = None
    featured: bool = False


class CategoryIn(BaseModel):
    name: str
    slug: Optional[str] = None
    description: str = ""
    heroImage: str = ""


class CollectionIn(BaseModel):
    name: str
    slug: Optional[str] = None
    description: str = ""
    heroImage: str = ""
    recipeIds: List[str] = []


class AuthorIn(BaseModel):
    name: str
    slug: Optional[str] = None
    bio: str = ""
    experience: str = ""
    expertise: str = ""
    photo: str = ""
    email: str = ""


class ContactIn(BaseModel):
    name: str
    email: EmailStr
    reason: str
    message: str


# ---------- Auth Endpoints ----------
@api_router.post("/auth/login")
async def login(body: LoginBody, response: Response):
    email = body.email.lower().strip()
    user = await db.users.find_one({"email": email})
    if not user or not verify_password(body.password, user.get("password_hash", "")):
        raise HTTPException(status_code=401, detail="Invalid email or password")
    uid = str(user["_id"])
    access = create_access_token(uid, email)
    refresh = create_refresh_token(uid)
    set_auth_cookies(response, access, refresh)
    return {"id": uid, "email": user["email"], "name": user.get("name"), "role": user.get("role")}


@api_router.post("/auth/logout")
async def logout(response: Response, _user: dict = Depends(get_current_user)):
    response.delete_cookie("access_token", path="/")
    response.delete_cookie("refresh_token", path="/")
    return {"ok": True}


@api_router.get("/auth/me")
async def me(user: dict = Depends(get_current_user)):
    return {"id": user["_id"], "email": user["email"], "name": user.get("name"), "role": user.get("role")}


@api_router.post("/auth/refresh")
async def refresh_token(request: Request, response: Response):
    token = request.cookies.get("refresh_token")
    if not token:
        raise HTTPException(status_code=401, detail="No refresh token")
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        if payload.get("type") != "refresh":
            raise HTTPException(status_code=401, detail="Invalid token type")
        user = await db.users.find_one({"_id": ObjectId(payload["sub"])})
        if not user:
            raise HTTPException(status_code=401, detail="User not found")
        access = create_access_token(str(user["_id"]), user["email"])
        response.set_cookie("access_token", access, httponly=True, secure=True, samesite="none", max_age=60 * 60 * 24, path="/")
        return {"ok": True}
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid refresh token")


# ---------- Upload ----------
@api_router.post("/upload")
async def upload(file: UploadFile = File(...), _admin=Depends(require_admin)):
    ext = file.filename.split(".")[-1].lower() if "." in file.filename else "bin"
    if ext not in {"jpg", "jpeg", "png", "webp", "gif"}:
        raise HTTPException(status_code=400, detail="Unsupported file type")
    data = await file.read()
    if len(data) > 10 * 1024 * 1024:
        raise HTTPException(status_code=400, detail="File too large (max 10 MB)")
    path = f"{APP_NAME}/uploads/{uuid.uuid4()}.{ext}"
    result = put_object(path, data, file.content_type or "image/jpeg")
    await db.files.insert_one({
        "storage_path": result["path"],
        "original_filename": file.filename,
        "content_type": file.content_type,
        "size": result.get("size"),
        "is_deleted": False,
        "created_at": datetime.now(timezone.utc).isoformat(),
    })
    return {"path": result["path"], "url": f"/api/files/{result['path']}"}


@api_router.get("/files/{path:path}")
async def download(path: str):
    record = await db.files.find_one({"storage_path": path, "is_deleted": False})
    if not record:
        raise HTTPException(status_code=404, detail="File not found")
    data, content_type = get_object(path)
    return Response(content=data, media_type=record.get("content_type") or content_type)


# ---------- Recipes ----------
async def _unique_slug(collection, base_slug: str, exclude_id: Optional[str] = None) -> str:
    slug = base_slug or "recipe"
    i = 1
    while True:
        q = {"slug": slug}
        if exclude_id:
            q["_id"] = {"$ne": ObjectId(exclude_id)}
        existing = await collection.find_one(q)
        if not existing:
            return slug
        i += 1
        slug = f"{base_slug}-{i}"


@api_router.get("/recipes")
async def list_recipes(
    status: Optional[str] = "published",
    category: Optional[str] = None,
    collection: Optional[str] = None,
    author: Optional[str] = None,
    tag: Optional[str] = None,
    q: Optional[str] = None,
    featured: Optional[bool] = None,
    limit: int = 24,
    skip: int = 0,
    sort: str = "recent",
):
    query: dict = {}
    if status and status != "all":
        query["status"] = status
    if category:
        cat = await db.categories.find_one({"slug": category})
        if cat:
            query["categoryIds"] = str(cat["_id"])
    if collection:
        col = await db.collections.find_one({"slug": collection})
        if col:
            query["collectionIds"] = str(col["_id"])
    if author:
        au = await db.authors.find_one({"slug": author})
        if au:
            query["authorId"] = str(au["_id"])
    if tag:
        query["tags"] = tag
    if featured is not None:
        query["featured"] = featured
    if q:
        query["$or"] = [
            {"title": {"$regex": q, "$options": "i"}},
            {"shortDescription": {"$regex": q, "$options": "i"}},
            {"tags": {"$regex": q, "$options": "i"}},
            {"ingredients.name": {"$regex": q, "$options": "i"}},
            {"recipeCategory": {"$regex": q, "$options": "i"}},
        ]
    sort_field = "datePublished" if sort == "recent" else "views"
    cursor = db.recipes.find(query).sort(sort_field, -1).skip(skip).limit(limit)
    docs = [_sd(d) async for d in cursor]
    total = await db.recipes.count_documents(query)
    return {"items": docs, "total": total, "skip": skip, "limit": limit}


@api_router.get("/recipes/slug/{slug}")
async def get_recipe_by_slug(slug: str):
    doc = await db.recipes.find_one({"slug": slug})
    if not doc:
        raise HTTPException(status_code=404, detail="Recipe not found")
    # Increment view count silently
    await db.recipes.update_one({"_id": doc["_id"]}, {"$inc": {"views": 1}})
    recipe = _sd(doc)
    # Related recipes: same category or shared tags, exclude self, only published
    related_q = {
        "_id": {"$ne": doc["_id"]},
        "status": "published",
        "$or": [
            {"categoryIds": {"$in": doc.get("categoryIds", [])}} if doc.get("categoryIds") else {},
            {"tags": {"$in": doc.get("tags", [])}} if doc.get("tags") else {},
        ],
    }
    related_q["$or"] = [c for c in related_q["$or"] if c]
    if not related_q["$or"]:
        related_q.pop("$or")
    related_cursor = db.recipes.find(related_q).limit(4)
    recipe["related"] = [_sd(r) async for r in related_cursor]
    # Author
    if doc.get("authorId"):
        try:
            au = await db.authors.find_one({"_id": ObjectId(doc["authorId"])})
            recipe["author"] = _sd(au)
        except Exception:
            recipe["author"] = None
    # Categories & collections
    cat_ids = [ObjectId(c) for c in doc.get("categoryIds", []) if c]
    col_ids = [ObjectId(c) for c in doc.get("collectionIds", []) if c]
    recipe["categories"] = [_sd(c) async for c in db.categories.find({"_id": {"$in": cat_ids}})] if cat_ids else []
    recipe["collections"] = [_sd(c) async for c in db.collections.find({"_id": {"$in": col_ids}})] if col_ids else []
    return recipe


@api_router.get("/recipes/{recipe_id}")
async def get_recipe(recipe_id: str, _admin=Depends(require_admin)):
    doc = await db.recipes.find_one({"_id": ObjectId(recipe_id)})
    if not doc:
        raise HTTPException(status_code=404, detail="Recipe not found")
    return _sd(doc)


def _build_recipe_doc(body: RecipeIn, existing_slug: Optional[str] = None) -> dict:
    now = datetime.now(timezone.utc).isoformat()
    doc = body.model_dump()
    if not doc.get("slug"):
        doc["slug"] = slugify(body.title)
    else:
        doc["slug"] = slugify(doc["slug"])
    if not doc.get("seoTitle"):
        doc["seoTitle"] = f"{body.title} Recipe"
    if not doc.get("seoDescription"):
        doc["seoDescription"] = (body.shortDescription or "")[:160]
    return doc


@api_router.post("/recipes")
async def create_recipe(body: RecipeIn, _admin=Depends(require_admin)):
    doc = _build_recipe_doc(body)
    doc["slug"] = await _unique_slug(db.recipes, doc["slug"])
    now = datetime.now(timezone.utc).isoformat()
    doc["createdAt"] = now
    doc["updatedAt"] = now
    doc["datePublished"] = now if body.status == "published" else None
    doc["dateModified"] = now
    doc["views"] = 0
    result = await db.recipes.insert_one(doc)
    return {"id": str(result.inserted_id), "slug": doc["slug"]}


@api_router.put("/recipes/{recipe_id}")
async def update_recipe(recipe_id: str, body: RecipeIn, _admin=Depends(require_admin)):
    existing = await db.recipes.find_one({"_id": ObjectId(recipe_id)})
    if not existing:
        raise HTTPException(status_code=404, detail="Recipe not found")
    doc = _build_recipe_doc(body)
    if doc["slug"] != existing.get("slug"):
        doc["slug"] = await _unique_slug(db.recipes, doc["slug"], exclude_id=recipe_id)
    now = datetime.now(timezone.utc).isoformat()
    doc["updatedAt"] = now
    doc["dateModified"] = now
    if body.status == "published" and not existing.get("datePublished"):
        doc["datePublished"] = now
    else:
        doc["datePublished"] = existing.get("datePublished")
    await db.recipes.update_one({"_id": ObjectId(recipe_id)}, {"$set": doc})
    return {"id": recipe_id, "slug": doc["slug"]}


@api_router.delete("/recipes/{recipe_id}")
async def delete_recipe(recipe_id: str, _admin=Depends(require_admin)):
    await db.recipes.delete_one({"_id": ObjectId(recipe_id)})
    return {"ok": True}


# ---------- Categories ----------
@api_router.get("/categories")
async def list_categories():
    cats = [_sd(c) async for c in db.categories.find().sort("name", 1)]
    # counts
    for c in cats:
        c["recipeCount"] = await db.recipes.count_documents({"categoryIds": c["id"], "status": "published"})
    return cats


@api_router.get("/categories/{slug}")
async def get_category(slug: str):
    cat = await db.categories.find_one({"slug": slug})
    if not cat:
        raise HTTPException(status_code=404, detail="Category not found")
    return _sd(cat)


@api_router.post("/categories")
async def create_category(body: CategoryIn, _admin=Depends(require_admin)):
    doc = body.model_dump()
    doc["slug"] = slugify(doc.get("slug") or body.name)
    doc["slug"] = await _unique_slug(db.categories, doc["slug"])
    doc["createdAt"] = datetime.now(timezone.utc).isoformat()
    result = await db.categories.insert_one(doc)
    return {"id": str(result.inserted_id), "slug": doc["slug"]}


@api_router.put("/categories/{cat_id}")
async def update_category(cat_id: str, body: CategoryIn, _admin=Depends(require_admin)):
    doc = body.model_dump()
    doc["slug"] = slugify(doc.get("slug") or body.name)
    doc["slug"] = await _unique_slug(db.categories, doc["slug"], exclude_id=cat_id)
    await db.categories.update_one({"_id": ObjectId(cat_id)}, {"$set": doc})
    return {"id": cat_id, "slug": doc["slug"]}


@api_router.delete("/categories/{cat_id}")
async def delete_category(cat_id: str, _admin=Depends(require_admin)):
    await db.categories.delete_one({"_id": ObjectId(cat_id)})
    return {"ok": True}


# ---------- Collections ----------
@api_router.get("/collections")
async def list_collections():
    cols = [_sd(c) async for c in db.collections.find().sort("name", 1)]
    for c in cols:
        c["recipeCount"] = await db.recipes.count_documents({"collectionIds": c["id"], "status": "published"})
    return cols


@api_router.get("/collections/{slug}")
async def get_collection(slug: str):
    col = await db.collections.find_one({"slug": slug})
    if not col:
        raise HTTPException(status_code=404, detail="Collection not found")
    return _sd(col)


@api_router.post("/collections")
async def create_collection(body: CollectionIn, _admin=Depends(require_admin)):
    doc = body.model_dump()
    doc["slug"] = slugify(doc.get("slug") or body.name)
    doc["slug"] = await _unique_slug(db.collections, doc["slug"])
    doc["createdAt"] = datetime.now(timezone.utc).isoformat()
    result = await db.collections.insert_one(doc)
    return {"id": str(result.inserted_id), "slug": doc["slug"]}


@api_router.put("/collections/{col_id}")
async def update_collection(col_id: str, body: CollectionIn, _admin=Depends(require_admin)):
    doc = body.model_dump()
    doc["slug"] = slugify(doc.get("slug") or body.name)
    doc["slug"] = await _unique_slug(db.collections, doc["slug"], exclude_id=col_id)
    await db.collections.update_one({"_id": ObjectId(col_id)}, {"$set": doc})
    return {"id": col_id, "slug": doc["slug"]}


@api_router.delete("/collections/{col_id}")
async def delete_collection(col_id: str, _admin=Depends(require_admin)):
    await db.collections.delete_one({"_id": ObjectId(col_id)})
    return {"ok": True}


# ---------- Authors ----------
@api_router.get("/authors")
async def list_authors():
    authors = [_sd(a) async for a in db.authors.find().sort("name", 1)]
    for a in authors:
        a["recipeCount"] = await db.recipes.count_documents({"authorId": a["id"], "status": "published"})
    return authors


@api_router.get("/authors/{slug}")
async def get_author(slug: str):
    au = await db.authors.find_one({"slug": slug})
    if not au:
        raise HTTPException(status_code=404, detail="Author not found")
    author = _sd(au)
    author["recipes"] = [_sd(r) async for r in db.recipes.find({"authorId": author["id"], "status": "published"}).sort("datePublished", -1).limit(24)]
    return author


@api_router.post("/authors")
async def create_author(body: AuthorIn, _admin=Depends(require_admin)):
    doc = body.model_dump()
    doc["slug"] = slugify(doc.get("slug") or body.name)
    doc["slug"] = await _unique_slug(db.authors, doc["slug"])
    doc["createdAt"] = datetime.now(timezone.utc).isoformat()
    result = await db.authors.insert_one(doc)
    return {"id": str(result.inserted_id), "slug": doc["slug"]}


@api_router.put("/authors/{author_id}")
async def update_author(author_id: str, body: AuthorIn, _admin=Depends(require_admin)):
    doc = body.model_dump()
    doc["slug"] = slugify(doc.get("slug") or body.name)
    doc["slug"] = await _unique_slug(db.authors, doc["slug"], exclude_id=author_id)
    await db.authors.update_one({"_id": ObjectId(author_id)}, {"$set": doc})
    return {"id": author_id, "slug": doc["slug"]}


@api_router.delete("/authors/{author_id}")
async def delete_author(author_id: str, _admin=Depends(require_admin)):
    await db.authors.delete_one({"_id": ObjectId(author_id)})
    return {"ok": True}


# ---------- Contact ----------
@api_router.post("/contact")
async def submit_contact(body: ContactIn):
    doc = body.model_dump()
    doc["createdAt"] = datetime.now(timezone.utc).isoformat()
    doc["ip"] = "hidden"
    await db.contact_messages.insert_one(doc)
    return {"ok": True}


# ---------- Admin Stats ----------
@api_router.get("/admin/stats")
async def admin_stats(_admin=Depends(require_admin)):
    total_recipes = await db.recipes.count_documents({})
    published = await db.recipes.count_documents({"status": "published"})
    drafts = await db.recipes.count_documents({"status": "draft"})
    scheduled = await db.recipes.count_documents({"status": "scheduled"})
    categories = await db.categories.count_documents({})
    collections = await db.collections.count_documents({})
    authors = await db.authors.count_documents({})
    contact_messages = await db.contact_messages.count_documents({})
    return {
        "recipes": total_recipes,
        "published": published,
        "drafts": drafts,
        "scheduled": scheduled,
        "categories": categories,
        "collections": collections,
        "authors": authors,
        "contactMessages": contact_messages,
    }


@api_router.get("/admin/recipes")
async def admin_list_recipes(_admin=Depends(require_admin), status: Optional[str] = None, q: Optional[str] = None):
    query = {}
    if status and status != "all":
        query["status"] = status
    if q:
        query["title"] = {"$regex": q, "$options": "i"}
    cursor = db.recipes.find(query).sort("updatedAt", -1).limit(200)
    return [_sd(d) async for d in cursor]


@api_router.get("/admin/contact-messages")
async def admin_contact(_admin=Depends(require_admin)):
    cursor = db.contact_messages.find().sort("createdAt", -1).limit(200)
    return [_sd(d) async for d in cursor]


# ---------- Sitemap & Robots (top-level, not under /api) ----------
@app.get("/sitemap.xml", response_class=Response)
async def sitemap():
    base = FRONTEND_URL.rstrip("/")
    urls = [
        f"{base}/",
        f"{base}/recipes",
        f"{base}/categories",
        f"{base}/collections",
        f"{base}/about",
        f"{base}/contact",
        f"{base}/editorial-policy",
        f"{base}/recipe-testing-policy",
        f"{base}/privacy-policy",
        f"{base}/terms-of-use",
        f"{base}/disclaimer",
        f"{base}/accessibility",
    ]
    lastmod_default = datetime.now(timezone.utc).date().isoformat()
    entries = "".join([f"<url><loc>{u}</loc><lastmod>{lastmod_default}</lastmod></url>" for u in urls])

    async for r in db.recipes.find({"status": "published"}):
        lm = r.get("dateModified") or r.get("datePublished") or lastmod_default
        if isinstance(lm, str):
            lm = lm.split("T")[0]
        entries += f"<url><loc>{base}/recipes/{r['slug']}</loc><lastmod>{lm}</lastmod></url>"
    async for c in db.categories.find():
        entries += f"<url><loc>{base}/recipes/{c['slug']}</loc></url>"
    async for c in db.collections.find():
        entries += f"<url><loc>{base}/collections/{c['slug']}</loc></url>"
    async for a in db.authors.find():
        entries += f"<url><loc>{base}/author/{a['slug']}</loc></url>"

    xml = f'<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">{entries}</urlset>'
    return Response(content=xml, media_type="application/xml")


@app.get("/robots.txt", response_class=PlainTextResponse)
async def robots():
    base = FRONTEND_URL.rstrip("/")
    return f"""User-agent: *
Allow: /
Disallow: /admin
Disallow: /admin/*
Disallow: /api/admin
Disallow: /search?
Disallow: /*?*

Sitemap: {base}/sitemap.xml
"""


@api_router.get("/sitemap.xml")
async def sitemap_api():
    return await sitemap()


@api_router.get("/robots.txt")
async def robots_api():
    return await robots()


# ---------- Health ----------
@api_router.get("/")
async def root():
    return {"service": "recipe-crave", "status": "ok"}


# ---------- Register router ----------
app.include_router(api_router)

app.add_middleware(
    CORSMiddleware,
    allow_origins=[FRONTEND_URL, "http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ---------- Seed ----------
async def _seed_admin():
    admin_email = os.environ.get("ADMIN_EMAIL", "admin@example.com").lower()
    admin_password = os.environ.get("ADMIN_PASSWORD", "admin123")
    existing = await db.users.find_one({"email": admin_email})
    if not existing:
        await db.users.insert_one({
            "email": admin_email,
            "password_hash": hash_password(admin_password),
            "name": "Jesse Thomas",
            "role": "admin",
            "created_at": datetime.now(timezone.utc).isoformat(),
        })
        logger.info(f"Seeded admin: {admin_email}")
    elif not verify_password(admin_password, existing.get("password_hash", "")):
        await db.users.update_one(
            {"email": admin_email},
            {"$set": {"password_hash": hash_password(admin_password)}},
        )
        logger.info(f"Updated admin password for {admin_email}")


async def _seed_content():
    """Seed initial categories, collections, author, and sample recipes if empty."""
    if await db.recipes.count_documents({}) > 0:
        return
    from seed_data import CATEGORIES, COLLECTIONS, AUTHOR, RECIPES

    # Author
    au_slug = slugify(AUTHOR["name"])
    author_id = None
    existing = await db.authors.find_one({"slug": au_slug})
    if existing:
        author_id = str(existing["_id"])
    else:
        doc = {**AUTHOR, "slug": au_slug, "createdAt": datetime.now(timezone.utc).isoformat()}
        r = await db.authors.insert_one(doc)
        author_id = str(r.inserted_id)

    # Categories
    cat_map = {}
    for c in CATEGORIES:
        slug = slugify(c["name"])
        existing = await db.categories.find_one({"slug": slug})
        if existing:
            cat_map[c["name"]] = str(existing["_id"])
        else:
            doc = {**c, "slug": slug, "createdAt": datetime.now(timezone.utc).isoformat()}
            r = await db.categories.insert_one(doc)
            cat_map[c["name"]] = str(r.inserted_id)

    # Collections
    col_map = {}
    for c in COLLECTIONS:
        slug = slugify(c["name"])
        existing = await db.collections.find_one({"slug": slug})
        if existing:
            col_map[c["name"]] = str(existing["_id"])
        else:
            doc = {**c, "slug": slug, "recipeIds": [], "createdAt": datetime.now(timezone.utc).isoformat()}
            r = await db.collections.insert_one(doc)
            col_map[c["name"]] = str(r.inserted_id)

    # Recipes
    for rec in RECIPES:
        slug = slugify(rec["title"])
        if await db.recipes.find_one({"slug": slug}):
            continue
        now = datetime.now(timezone.utc).isoformat()
        doc = {
            **rec,
            "slug": slug,
            "authorId": author_id,
            "categoryIds": [cat_map[name] for name in rec.pop("_categories", []) if name in cat_map],
            "collectionIds": [col_map[name] for name in rec.pop("_collections", []) if name in col_map],
            "status": "published",
            "featured": rec.get("featured", False),
            "views": 0,
            "createdAt": now,
            "updatedAt": now,
            "datePublished": now,
            "dateModified": now,
            "seoTitle": rec.get("seoTitle") or f"{rec['title']} Recipe",
            "seoDescription": rec.get("seoDescription") or rec.get("shortDescription", "")[:160],
        }
        await db.recipes.insert_one(doc)
    logger.info("Seeded sample content")


@app.on_event("startup")
async def on_startup():
    try:
        await db.users.create_index("email", unique=True)
        await db.recipes.create_index("slug", unique=True)
        await db.recipes.create_index("status")
        await db.recipes.create_index("categoryIds")
        await db.recipes.create_index("collectionIds")
        await db.recipes.create_index("authorId")
        await db.recipes.create_index("tags")
        await db.categories.create_index("slug", unique=True)
        await db.collections.create_index("slug", unique=True)
        await db.authors.create_index("slug", unique=True)
    except Exception as e:
        logger.warning(f"Index setup: {e}")
    await _seed_admin()
    try:
        init_storage()
        logger.info("Storage initialized")
    except Exception as e:
        logger.error(f"Storage init failed: {e}")
    try:
        await _seed_content()
    except Exception as e:
        logger.exception(f"Seed content failed: {e}")


@app.on_event("shutdown")
async def on_shutdown():
    client.close()
