"""Journal entry routes""" from fastapi import APIRouter, HTTPException, Query from db import get_database from models import JournalEntryCreate, JournalEntryUpdate, JournalEntry, EntriesListResponse, PaginationMeta from datetime import datetime, timedelta from typing import List, Optional from bson import ObjectId from utils import format_ist_timestamp router = APIRouter() def _format_entry(entry: dict) -> dict: """Helper to format entry document for API response.""" return { "id": str(entry["_id"]), "userId": str(entry["userId"]), "title": entry.get("title", ""), "content": entry.get("content", ""), "mood": entry.get("mood"), "tags": entry.get("tags", []), "isPublic": entry.get("isPublic", False), "entryDate": entry.get("entryDate", entry.get("createdAt")).isoformat() if entry.get("entryDate") or entry.get("createdAt") else None, "createdAt": entry["createdAt"].isoformat(), "updatedAt": entry["updatedAt"].isoformat(), "encryption": entry.get("encryption", { "encrypted": False, "iv": None, "algorithm": None }) } @router.post("/{user_id}", response_model=dict) async def create_entry(user_id: str, entry_data: JournalEntryCreate): """ Create a new journal entry. entryDate: The logical journal date for this entry (defaults to today UTC). createdAt: Database write timestamp. """ db = get_database() try: user_oid = ObjectId(user_id) # Verify user exists user = db.users.find_one({"_id": user_oid}) if not user: raise HTTPException(status_code=404, detail="User not found") now = datetime.utcnow() entry_date = entry_data.entryDate or now.replace(hour=0, minute=0, second=0, microsecond=0) entry_doc = { "userId": user_oid, "title": entry_data.title, "content": entry_data.content, "mood": entry_data.mood, "tags": entry_data.tags or [], "isPublic": entry_data.isPublic or False, "entryDate": entry_date, # Logical journal date "createdAt": now, "updatedAt": now, "encryption": entry_data.encryption.model_dump() if entry_data.encryption else { "encrypted": False, "iv": None, "algorithm": None } } result = db.entries.insert_one(entry_doc) return { "id": str(result.inserted_id), "userId": user_id, "message": "Entry created successfully" } except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid user ID format") raise HTTPException(status_code=500, detail=f"Failed to create entry: {str(e)}") @router.get("/{user_id}") async def get_user_entries( user_id: str, limit: int = Query(50, ge=1, le=100), skip: int = Query(0, ge=0) ): """ Get paginated entries for a user (most recent first). Supports pagination via skip and limit. """ db = get_database() try: user_oid = ObjectId(user_id) # Verify user exists user = db.users.find_one({"_id": user_oid}) if not user: raise HTTPException(status_code=404, detail="User not found") # Get entries entries = list( db.entries.find( {"userId": user_oid} ).sort("createdAt", -1).skip(skip).limit(limit) ) # Format entries formatted_entries = [_format_entry(entry) for entry in entries] # Get total count total = db.entries.count_documents({"userId": user_oid}) has_more = (skip + limit) < total return { "entries": formatted_entries, "pagination": { "total": total, "limit": limit, "skip": skip, "hasMore": has_more } } except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid user ID format") raise HTTPException(status_code=500, detail=f"Failed to fetch entries: {str(e)}") @router.get("/{user_id}/{entry_id}") async def get_entry(user_id: str, entry_id: str): """Get a specific entry by ID.""" db = get_database() try: user_oid = ObjectId(user_id) entry_oid = ObjectId(entry_id) entry = db.entries.find_one({ "_id": entry_oid, "userId": user_oid }) if not entry: raise HTTPException(status_code=404, detail="Entry not found") return _format_entry(entry) except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid ID format") raise HTTPException(status_code=500, detail=f"Failed to fetch entry: {str(e)}") @router.put("/{user_id}/{entry_id}") async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpdate): """Update a journal entry.""" db = get_database() try: user_oid = ObjectId(user_id) entry_oid = ObjectId(entry_id) update_data = entry_data.model_dump(exclude_unset=True) update_data["updatedAt"] = datetime.utcnow() # If entryDate provided in update data, ensure it's a datetime if "entryDate" in update_data and isinstance(update_data["entryDate"], str): update_data["entryDate"] = datetime.fromisoformat(update_data["entryDate"].replace("Z", "+00:00")) result = db.entries.update_one( { "_id": entry_oid, "userId": user_oid }, {"$set": update_data} ) if result.matched_count == 0: raise HTTPException(status_code=404, detail="Entry not found") # Fetch and return updated entry entry = db.entries.find_one({"_id": entry_oid}) return _format_entry(entry) except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid ID format") raise HTTPException(status_code=500, detail=f"Failed to update entry: {str(e)}") @router.delete("/{user_id}/{entry_id}") async def delete_entry(user_id: str, entry_id: str): """Delete a journal entry.""" db = get_database() try: user_oid = ObjectId(user_id) entry_oid = ObjectId(entry_id) result = db.entries.delete_one({ "_id": entry_oid, "userId": user_oid }) if result.deleted_count == 0: raise HTTPException(status_code=404, detail="Entry not found") return {"message": "Entry deleted successfully"} except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid ID format") raise HTTPException(status_code=500, detail=f"Failed to delete entry: {str(e)}") @router.get("/{user_id}/by-date/{date_str}") async def get_entries_by_date(user_id: str, date_str: str): """ Get entries for a specific date (format: YYYY-MM-DD). Matches entries by entryDate field. """ db = get_database() try: user_oid = ObjectId(user_id) # Parse date target_date = datetime.strptime(date_str, "%Y-%m-%d") next_date = target_date + timedelta(days=1) entries = list( db.entries.find({ "userId": user_oid, "entryDate": { "$gte": target_date, "$lt": next_date } }).sort("createdAt", -1) ) formatted_entries = [_format_entry(entry) for entry in entries] return { "entries": formatted_entries, "date": date_str, "count": len(formatted_entries) } except ValueError: raise HTTPException( status_code=400, detail="Invalid date format. Use YYYY-MM-DD") except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid user ID format") raise HTTPException(status_code=500, detail=f"Failed to fetch entries: {str(e)}") @router.get("/{user_id}/by-month/{year}/{month}") async def get_entries_by_month(user_id: str, year: int, month: int, limit: int = Query(100, ge=1, le=500)): """ Get entries for a specific month (for calendar view). Query format: GET /api/entries/{user_id}/by-month/{year}/{month}?limit=100 """ db = get_database() try: user_oid = ObjectId(user_id) if not (1 <= month <= 12): raise HTTPException(status_code=400, detail="Month must be between 1 and 12") # Calculate date range start_date = datetime(year, month, 1) if month == 12: end_date = datetime(year + 1, 1, 1) else: end_date = datetime(year, month + 1, 1) entries = list( db.entries.find({ "userId": user_oid, "entryDate": { "$gte": start_date, "$lt": end_date } }).sort("entryDate", -1).limit(limit) ) formatted_entries = [_format_entry(entry) for entry in entries] return { "entries": formatted_entries, "year": year, "month": month, "count": len(formatted_entries) } except ValueError: raise HTTPException(status_code=400, detail="Invalid year or month") except Exception as e: if "invalid ObjectId" in str(e).lower(): raise HTTPException(status_code=400, detail="Invalid user ID format") raise HTTPException(status_code=500, detail=f"Failed to fetch entries: {str(e)}") @router.post("/convert-timestamp/utc-to-ist") async def convert_utc_to_ist(data: dict): """Convert UTC ISO timestamp to IST (Indian Standard Time).""" try: utc_timestamp = data.get("timestamp") if not utc_timestamp: raise HTTPException( status_code=400, detail="Missing 'timestamp' field") ist_timestamp = format_ist_timestamp(utc_timestamp) return { "utc": utc_timestamp, "ist": ist_timestamp } except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Conversion failed: {str(e)}")