from dotenv import load_dotenv
from pathlib import Path

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

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

import bcrypt
import jwt
import resend
import requests
from bson import ObjectId
from fastapi import FastAPI, APIRouter, HTTPException, Request, Response, Depends, status, UploadFile, File
from starlette.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel, EmailStr, Field

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

JWT_SECRET = os.environ['JWT_SECRET']
JWT_ALGORITHM = "HS256"
ADMIN_EMAIL = os.environ['ADMIN_EMAIL']
ADMIN_PASSWORD = os.environ['ADMIN_PASSWORD']
RESEND_API_KEY = os.environ.get('RESEND_API_KEY', '')
SENDER_EMAIL = os.environ.get('SENDER_EMAIL', 'onboarding@resend.dev')
ADMIN_NOTIFY_EMAIL = os.environ.get('ADMIN_NOTIFY_EMAIL', ADMIN_EMAIL)
resend.api_key = RESEND_API_KEY

# ---------------- Object Storage (Emergent) ----------------
STORAGE_URL = "https://integrations.emergentagent.com/objstore/api/v1/storage"
EMERGENT_KEY = os.environ.get("EMERGENT_LLM_KEY", "")
APP_NAME = "ayaba-ventures"
_storage_key = None

MIME_TYPES = {
    "jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif",
    "webp": "image/webp", "svg": "image/svg+xml", "pdf": "application/pdf",
    "mp4": "video/mp4", "mov": "video/quicktime", "webm": "video/webm", "avi": "video/x-msvideo",
}


def init_storage():
    global _storage_key
    if _storage_key:
        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:
    global _storage_key
    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 == 403:
        _storage_key = None
        key = init_storage()
        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):
    global _storage_key
    key = init_storage()
    resp = requests.get(f"{STORAGE_URL}/objects/{path}", headers={"X-Storage-Key": key}, timeout=60)
    if resp.status_code == 403:
        _storage_key = None
        key = init_storage()
        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")

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("ayaba")

# Hold references to fire-and-forget email tasks so they aren't garbage-collected mid-run.
_bg_tasks: set = set()


def fire_email(to_email: str, subject: str, html: str):
    task = asyncio.create_task(send_email_async(to_email, subject, html))
    _bg_tasks.add(task)
    task.add_done_callback(_bg_tasks.discard)

app = FastAPI(title="Ayaba Ventures API")
api = APIRouter(prefix="/api")


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


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


def create_access_token(user_id: str, email: str, role: str) -> str:
    payload = {"sub": user_id, "email": email, "role": role,
               "exp": datetime.now(timezone.utc) + timedelta(hours=12), "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=7), "type": "refresh"}
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)


def serialize_user(u: dict) -> dict:
    return {"id": str(u["_id"]), "email": u["email"], "name": u.get("name", ""), "role": u.get("role", "client"),
            "created_at": u.get("created_at").isoformat() if isinstance(u.get("created_at"), datetime) else u.get("created_at")}


async def get_current_user(request: Request) -> dict:
    token = request.cookies.get("access_token")
    if not token:
        auth_header = request.headers.get("Authorization", "")
        if auth_header.startswith("Bearer "):
            token = auth_header[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")
        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 access required")
    return user


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


async def send_email_async(to_email: str, subject: str, html: str):
    if not RESEND_API_KEY:
        logger.warning("RESEND_API_KEY not set; skipping email")
        return
    try:
        await asyncio.to_thread(resend.Emails.send, {
            "from": SENDER_EMAIL, "to": [to_email], "subject": subject, "html": html
        })
    except Exception as e:
        logger.error(f"Email send failed: {e}")


async def send_email_strict(recipients: List[str], subject: str, html: str):
    """Like send_email_async but RAISES on failure so callers can block the action.
    If RESEND_API_KEY is not configured, the send is skipped (no failure)."""
    recipients = [r for r in (recipients or []) if r]
    if not recipients:
        return
    if not RESEND_API_KEY:
        logger.warning("RESEND_API_KEY not set; skipping notification (not blocking)")
        return
    try:
        await asyncio.to_thread(resend.Emails.send, {
            "from": SENDER_EMAIL, "to": recipients, "subject": subject, "html": html
        })
    except Exception as e:
        logger.error(f"Strict email send failed: {e}")
        raise HTTPException(status_code=502, detail="Notification email could not be sent; reply not posted.")


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


class CreateUserIn(BaseModel):
    email: EmailStr
    password: str
    name: str
    role: str = "client"


class InquiryIn(BaseModel):
    name: str
    email: EmailStr
    phone: str
    country: Optional[str] = ""
    message: str
    project_type: Optional[str] = ""


class RescueIn(BaseModel):
    name: str
    email: EmailStr
    phone: str
    country: Optional[str] = ""
    project_location: str
    budget_spent: str
    current_status: str
    contractor_issue: str
    desired_outcome: str


class ProjectIn(BaseModel):
    client_id: str
    title: str
    location: str
    description: str
    budget: float
    progress: int = 0
    status: str = "active"
    start_date: Optional[str] = ""
    expected_completion: Optional[str] = ""
    staff_ids: List[str] = []


class WorkLogIn(BaseModel):
    project_id: str
    log_date: str
    activity: str
    notes: str
    hours: Optional[float] = 0
    photos: List[str] = []


class CalendarEventIn(BaseModel):
    project_id: str
    event_date: str
    title: str
    notes: Optional[str] = ""
    assignee_id: Optional[str] = ""


class ReportUpdateIn(BaseModel):
    body: str
    parent_id: Optional[str] = None


class ShowcaseIn(BaseModel):
    code: str
    title: str
    location: str
    duration: str
    progress: str
    summary: str
    image_url: str
    outcome: List[str] = []
    quote: Optional[str] = ""
    quote_attr: Optional[str] = ""
    order: int = 0


class ChangePasswordIn(BaseModel):
    current_password: str
    new_password: str


class ForgotPasswordIn(BaseModel):
    email: EmailStr


class ResetPasswordIn(BaseModel):
    token: str
    new_password: str


class ReportIn(BaseModel):
    title: str
    summary: str
    week_of: Optional[str] = ""
    photos: List[str] = []
    drone_url: Optional[str] = ""
    budget_used: Optional[float] = 0


# ---------------- Auth Endpoints ----------------
@api.post("/auth/login")
async def login(body: LoginIn, 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["password_hash"]):
        raise HTTPException(status_code=401, detail="Invalid email or password")
    uid = str(user["_id"])
    access = create_access_token(uid, email, user.get("role", "client"))
    refresh = create_refresh_token(uid)
    set_auth_cookies(response, access, refresh)
    return {"user": serialize_user(user), "access_token": access}


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


@api.get("/auth/me")
async def me(user: dict = Depends(get_current_user)):
    return {"user": serialize_user(user)}


@api.post("/auth/change-password")
async def change_password(body: ChangePasswordIn, user: dict = Depends(get_current_user)):
    if len(body.new_password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
    if not verify_password(body.current_password, user["password_hash"]):
        raise HTTPException(status_code=400, detail="Current password is incorrect")
    await db.users.update_one({"_id": user["_id"]}, {"$set": {"password_hash": hash_password(body.new_password)}})
    return {"ok": True}


@api.post("/auth/forgot-password")
async def forgot_password(body: ForgotPasswordIn):
    email = body.email.lower().strip()
    user = await db.users.find_one({"email": email})
    # Always return ok to prevent email enumeration
    if user:
        token = secrets.token_urlsafe(32)
        await db.password_reset_tokens.insert_one({
            "token": token,
            "user_id": str(user["_id"]),
            "email": email,
            "expires_at": datetime.now(timezone.utc) + timedelta(hours=1),
            "used": False,
            "created_at": datetime.now(timezone.utc),
        })
        frontend = os.environ.get("FRONTEND_URL", "").rstrip("/")
        reset_link = f"{frontend}/reset-password?token={token}"
        html = f"""
        <h2>Reset your Ayaba portal password</h2>
        <p>Hello {user.get('name', '')},</p>
        <p>We received a request to reset your password. Click the link below — it expires in 1 hour.</p>
        <p><a href="{reset_link}" style="background:#682860;color:#fff;padding:12px 24px;text-decoration:none;display:inline-block;">Reset Password</a></p>
        <p style="font-size:12px;color:#666;">Or copy this link: {reset_link}</p>
        <p>If you didn't request this, you can safely ignore this email.</p>
        <p>— Ayaba Ventures Ltd</p>
        """
        logger.info(f"Password reset link for {email}: {reset_link}")
        asyncio.create_task(send_email_async(email, "Reset your Ayaba password", html))
    return {"ok": True, "message": "If that email exists, a reset link has been sent."}


@api.post("/auth/reset-password")
async def reset_password(body: ResetPasswordIn):
    if len(body.new_password) < 8:
        raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
    record = await db.password_reset_tokens.find_one({"token": body.token})
    if not record or record.get("used"):
        raise HTTPException(status_code=400, detail="Invalid or already-used token")
    expires = record["expires_at"]
    if isinstance(expires, str):
        expires = datetime.fromisoformat(expires)
    if expires.tzinfo is None:
        expires = expires.replace(tzinfo=timezone.utc)
    if expires < datetime.now(timezone.utc):
        raise HTTPException(status_code=400, detail="Token expired")
    try:
        oid = ObjectId(record["user_id"])
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid token")
    await db.users.update_one({"_id": oid}, {"$set": {"password_hash": hash_password(body.new_password)}})
    await db.password_reset_tokens.update_one({"_id": record["_id"]}, {"$set": {"used": True}})
    return {"ok": True}


# ---------------- Public Inquiries ----------------
@api.post("/inquiries")
async def submit_inquiry(body: InquiryIn):
    doc = body.model_dump()
    doc["type"] = "general"
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    doc["status"] = "new"
    result = await db.inquiries.insert_one(doc)
    html = f"""
    <h2>New Inquiry — Ayaba Ventures</h2>
    <p><b>Name:</b> {body.name}<br><b>Email:</b> {body.email}<br>
    <b>Phone:</b> {body.phone}<br><b>Country:</b> {body.country}<br>
    <b>Project Type:</b> {body.project_type}</p>
    <p><b>Message:</b><br>{body.message}</p>
    """
    asyncio.create_task(send_email_async(ADMIN_NOTIFY_EMAIL, "New Ayaba Inquiry", html))
    return {"id": str(result.inserted_id), "ok": True}


@api.post("/inquiries/rescue")
async def submit_rescue(body: RescueIn):
    doc = body.model_dump()
    doc["type"] = "rescue"
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    doc["status"] = "new"
    result = await db.inquiries.insert_one(doc)
    html = f"""
    <h2>Project Rescue Request — Ayaba Ventures</h2>
    <p><b>Name:</b> {body.name} ({body.email})<br><b>Phone:</b> {body.phone}<br>
    <b>Country:</b> {body.country}<br><b>Project Location:</b> {body.project_location}<br>
    <b>Budget Spent So Far:</b> {body.budget_spent}<br><b>Current Status:</b> {body.current_status}</p>
    <p><b>Contractor Issue:</b><br>{body.contractor_issue}</p>
    <p><b>Desired Outcome:</b><br>{body.desired_outcome}</p>
    """
    asyncio.create_task(send_email_async(ADMIN_NOTIFY_EMAIL, "Project Rescue Request", html))
    return {"id": str(result.inserted_id), "ok": True}


# ---------------- Admin ----------------
@api.post("/admin/users")
async def admin_create_user(body: CreateUserIn, _: dict = Depends(require_admin)):
    email = body.email.lower().strip()
    if await db.users.find_one({"email": email}):
        raise HTTPException(status_code=400, detail="Email already registered")
    doc = {"email": email, "password_hash": hash_password(body.password), "name": body.name,
           "role": body.role, "created_at": datetime.now(timezone.utc)}
    result = await db.users.insert_one(doc)
    welcome = f"""
    <h2>Welcome to Ayaba Ventures</h2>
    <p>Hello {body.name},</p>
    <p>Your client portal account has been created. You can now sign in to view your project dashboard, drone footage and weekly reports.</p>
    <p><b>Login email:</b> {email}<br><b>Temporary password:</b> {body.password}</p>
    <p>Please change your password after first login.</p>
    <p>— Ayaba Ventures Ltd</p>
    """
    asyncio.create_task(send_email_async(email, "Your Ayaba Portal Access", welcome))
    return {"id": str(result.inserted_id), "email": email}


@api.get("/admin/users")
async def admin_list_users(role: Optional[str] = None, _: dict = Depends(require_admin)):
    q = {"role": role} if role else {"role": {"$in": ["client", "staff"]}}
    users = await db.users.find(q).sort("created_at", -1).to_list(500)
    return [serialize_user(u) for u in users]


@api.get("/admin/inquiries")
async def admin_list_inquiries(_: dict = Depends(require_admin)):
    items = await db.inquiries.find().sort("created_at", -1).to_list(500)
    for i in items:
        i["id"] = str(i.pop("_id"))
    return items


@api.post("/admin/projects")
async def admin_create_project(body: ProjectIn, _: dict = Depends(require_admin)):
    doc = body.model_dump()
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    result = await db.projects.insert_one(doc)
    return {"id": str(result.inserted_id), "ok": True}


@api.get("/admin/projects")
async def admin_list_projects(_: dict = Depends(require_admin)):
    items = await db.projects.find().sort("created_at", -1).to_list(500)
    for i in items:
        i["id"] = str(i.pop("_id"))
    return items


@api.post("/admin/projects/{project_id}/reports")
async def admin_add_report(project_id: str, body: ReportIn, _: dict = Depends(require_admin)):
    try:
        oid = ObjectId(project_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid project id")
    if not await db.projects.find_one({"_id": oid}):
        raise HTTPException(status_code=404, detail="Project not found")
    doc = body.model_dump()
    doc["project_id"] = project_id
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    result = await db.reports.insert_one(doc)
    return {"id": str(result.inserted_id), "ok": True}


# ---------------- Client ----------------
@api.get("/client/projects")
async def client_projects(user: dict = Depends(get_current_user)):
    uid = str(user["_id"])
    items = await db.projects.find({"client_id": uid}).sort("created_at", -1).to_list(100)
    for i in items:
        i["id"] = str(i.pop("_id"))
    return items


@api.get("/client/projects/{project_id}")
async def client_project_detail(project_id: str, user: dict = Depends(get_current_user)):
    try:
        oid = ObjectId(project_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid project id")
    p = await db.projects.find_one({"_id": oid})
    if not p:
        raise HTTPException(status_code=404, detail="Project not found")
    if user.get("role") != "admin" and p.get("client_id") != str(user["_id"]):
        raise HTTPException(status_code=403, detail="Not your project")
    p["id"] = str(p.pop("_id"))
    reports = await db.reports.find({"project_id": project_id}).sort("created_at", -1).to_list(200)
    for r in reports:
        r["id"] = str(r.pop("_id"))
        updates = await db.report_updates.find({"report_id": r["id"]}).sort("created_at", 1).to_list(100)
        for u in updates:
            u["id"] = str(u.pop("_id"))
        r["updates"] = updates
    return {"project": p, "reports": reports}


# ---------------- Staff ----------------
def _project_for_staff(p: dict, uid: str, role: str) -> bool:
    return role == "admin" or uid in (p.get("staff_ids") or [])


@api.get("/staff/projects")
async def staff_projects(user: dict = Depends(get_current_user)):
    uid = str(user["_id"])
    q = {} if user.get("role") == "admin" else {"staff_ids": uid}
    items = await db.projects.find(q).sort("created_at", -1).to_list(200)
    for i in items:
        i["id"] = str(i.pop("_id"))
    return items


@api.post("/staff/worklogs")
async def staff_create_worklog(body: WorkLogIn, user: dict = Depends(get_current_user)):
    if user.get("role") not in ("staff", "admin"):
        raise HTTPException(status_code=403, detail="Staff or admin only")
    try:
        oid = ObjectId(body.project_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid project id")
    p = await db.projects.find_one({"_id": oid})
    if not p:
        raise HTTPException(status_code=404, detail="Project not found")
    if not _project_for_staff(p, str(user["_id"]), user.get("role")):
        raise HTTPException(status_code=403, detail="Not assigned to this project")
    doc = body.model_dump()
    doc["staff_id"] = str(user["_id"])
    doc["staff_name"] = user.get("name", "")
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    res = await db.worklogs.insert_one(doc)
    return {"id": str(res.inserted_id), "ok": True}


@api.get("/staff/worklogs")
async def staff_list_worklogs(project_id: Optional[str] = None, user: dict = Depends(get_current_user)):
    if user.get("role") not in ("staff", "admin"):
        raise HTTPException(status_code=403, detail="Staff or admin only")
    q = {}
    if project_id:
        q["project_id"] = project_id
    elif user.get("role") == "staff":
        q["staff_id"] = str(user["_id"])
    items = await db.worklogs.find(q).sort("created_at", -1).to_list(500)
    for i in items:
        i["id"] = str(i.pop("_id"))
    return items


@api.get("/staff/calendar")
async def staff_calendar(user: dict = Depends(get_current_user)):
    if user.get("role") not in ("staff", "admin"):
        raise HTTPException(status_code=403, detail="Staff or admin only")
    uid = str(user["_id"])
    q = {} if user.get("role") == "admin" else {"$or": [{"assignee_id": uid}, {"assignee_id": ""}]}
    items = await db.calendar_events.find(q).sort("event_date", 1).to_list(500)
    for i in items:
        i["id"] = str(i.pop("_id"))
    return items


@api.post("/admin/calendar")
async def admin_create_event(body: CalendarEventIn, _: dict = Depends(require_admin)):
    doc = body.model_dump()
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    res = await db.calendar_events.insert_one(doc)
    return {"id": str(res.inserted_id), "ok": True}


# ---------------- Reports & Updates ----------------
async def _get_report_with_project(report_id: str):
    try:
        oid = ObjectId(report_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid report id")
    report = await db.reports.find_one({"_id": oid})
    if not report:
        raise HTTPException(status_code=404, detail="Report not found")
    project = None
    try:
        project = await db.projects.find_one({"_id": ObjectId(report["project_id"])})
    except Exception:
        project = None
    return report, project


def _can_access_project(project: dict, user: dict) -> bool:
    if not project:
        return user.get("role") == "admin"
    role = user.get("role")
    uid = str(user["_id"])
    if role == "admin":
        return True
    if role == "staff":
        return uid in (project.get("staff_ids") or [])
    return project.get("client_id") == uid


async def _updates_for_report(report_id: str) -> List[dict]:
    updates = await db.report_updates.find({"report_id": report_id}).sort("created_at", 1).to_list(300)
    for u in updates:
        u["id"] = str(u.pop("_id"))
        u.setdefault("parent_id", None)
    return updates


@api.get("/projects/{project_id}/reports")
async def list_project_reports(project_id: str, user: dict = Depends(get_current_user)):
    try:
        oid = ObjectId(project_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid project id")
    project = await db.projects.find_one({"_id": oid})
    if not project:
        raise HTTPException(status_code=404, detail="Project not found")
    if not _can_access_project(project, user):
        raise HTTPException(status_code=403, detail="Not allowed")
    reports = await db.reports.find({"project_id": project_id}).sort("created_at", -1).to_list(200)
    for r in reports:
        r["id"] = str(r.pop("_id"))
        r["updates"] = await _updates_for_report(r["id"])
    return reports


@api.get("/reports/{report_id}/updates")
async def list_report_updates(report_id: str, user: dict = Depends(get_current_user)):
    report, project = await _get_report_with_project(report_id)
    if not _can_access_project(project, user):
        raise HTTPException(status_code=403, detail="Not allowed")
    return await _updates_for_report(report_id)


async def _validate_parent(parent_id_raw, report_id):
    """Normalize and validate an optional parent_id; returns the cleaned id or None."""
    parent_id = (parent_id_raw or "").strip() or None
    if not parent_id:
        return None
    parent = await db.report_updates.find_one({"_id": ObjectId(parent_id)}) if ObjectId.is_valid(parent_id) else None
    if not parent or parent.get("report_id") != report_id:
        raise HTTPException(status_code=400, detail="Invalid parent update")
    return parent_id


async def _email_client_reply(report, project, user, body_text):
    """Best-effort: notify admin + every staff who posted on this report (non-blocking)."""
    recipients = set()
    if ADMIN_NOTIFY_EMAIL:
        recipients.add(ADMIN_NOTIFY_EMAIL)
    staff_ids = await db.report_updates.distinct("author_id", {"report_id": str(report["_id"]), "author_role": "staff"})
    for sid in staff_ids:
        try:
            su = await db.users.find_one({"_id": ObjectId(sid)})
        except Exception:
            su = None
        if su and su.get("email"):
            recipients.add(su["email"])
    project_title = project.get("title", "") if project else ""
    report_title = report.get("title", "")
    html = f"""
    <h2>New client reply on a weekly report</h2>
    <p><b>From:</b> {user.get('name', 'Client')} ({user.get('email', '')})</p>
    <p><b>Project:</b> {project_title}<br><b>Report:</b> {report_title}</p>
    <p><b>Reply:</b><br>{body_text}</p>
    <p>— Ayaba Ventures portal</p>
    """
    for to_email in recipients:
        fire_email(to_email, f"New client reply — {report_title}", html)


@api.post("/reports/{report_id}/updates")
async def add_report_update(report_id: str, body: ReportUpdateIn, user: dict = Depends(get_current_user)):
    role = user.get("role")
    if role not in ("staff", "admin", "client"):
        raise HTTPException(status_code=403, detail="Not allowed")
    report, project = await _get_report_with_project(report_id)
    if not _can_access_project(project, user):
        raise HTTPException(status_code=403, detail="Not allowed for this report")

    parent_id = await _validate_parent(body.parent_id, report_id)

    doc = {
        "report_id": report_id,
        "body": body.body,
        "author_id": str(user["_id"]),
        "author_name": user.get("name", ""),
        "author_role": role,
        "parent_id": parent_id,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    res = await db.report_updates.insert_one(doc)

    if role == "client":
        await _email_client_reply(report, project, user, body.body)

    # In-app notifications (email-independent) for the relevant recipients.
    await _notify_report_activity(report, project, user, role, parent_id, body.body, str(res.inserted_id))

    return {"id": str(res.inserted_id), "ok": True}


# ---------------- File Upload / Serve ----------------
async def _notify_report_activity(report, project, actor, role, parent_id, body_text, update_id):
    """Create in-app notifications for a new report update/reply."""
    report_id = report["id"] if "id" in report else str(report["_id"])
    project_id = report.get("project_id", "")
    report_title = report.get("title", "")
    actor_id = str(actor["_id"])
    actor_name = actor.get("name", "")
    recipient_ids = set()

    if role == "client":
        # notify all admins + staff who posted on this report
        admins = await db.users.find({"role": "admin"}).to_list(50)
        for a in admins:
            recipient_ids.add(str(a["_id"]))
        staff_ids = await db.report_updates.distinct("author_id", {"report_id": report_id, "author_role": "staff"})
        recipient_ids.update(staff_ids)
    else:
        # staff/admin posted -> notify the project's client
        if project and project.get("client_id"):
            recipient_ids.add(project["client_id"])

    recipient_ids.discard(actor_id)
    if not recipient_ids:
        return

    is_reply = bool(parent_id)
    verb = "replied to a report" if is_reply else "posted an update"
    message = f"{actor_name} {verb} on \u201c{report_title}\u201d"
    now = datetime.now(timezone.utc).isoformat()
    docs = [{
        "user_id": rid,
        "type": "reply" if is_reply else "update",
        "report_id": report_id,
        "project_id": project_id,
        "update_id": update_id,
        "actor_name": actor_name,
        "actor_role": role,
        "message": message,
        "preview": (body_text or "")[:140],
        "read": False,
        "created_at": now,
    } for rid in recipient_ids]
    if docs:
        await db.notifications.insert_many(docs)


@api.get("/notifications")
async def list_notifications(user: dict = Depends(get_current_user)):
    uid = str(user["_id"])
    items = await db.notifications.find({"user_id": uid}).sort("created_at", -1).to_list(50)
    for i in items:
        i["id"] = str(i.pop("_id"))
    unread = await db.notifications.count_documents({"user_id": uid, "read": False})
    return {"items": items, "unread": unread}


@api.post("/notifications/mark-read")
async def mark_notifications_read(user: dict = Depends(get_current_user)):
    await db.notifications.update_many(
        {"user_id": str(user["_id"]), "read": False}, {"$set": {"read": True}}
    )
    return {"ok": True}


@api.post("/notifications/clear")
async def clear_notifications(user: dict = Depends(get_current_user)):
    await db.notifications.delete_many({"user_id": str(user["_id"])})
    return {"ok": True}


@api.post("/upload")
async def upload_file(file: UploadFile = File(...), user: dict = Depends(get_current_user)):
    data = await file.read()
    filename = file.filename or "file"
    ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "bin"
    content_type = file.content_type or MIME_TYPES.get(ext, "application/octet-stream")
    path = f"{APP_NAME}/uploads/{str(user['_id'])}/{uuid.uuid4()}.{ext}"
    try:
        result = await asyncio.to_thread(put_object, path, data, content_type)
    except Exception as e:
        logger.error(f"Upload failed: {e}")
        raise HTTPException(status_code=502, detail="File upload failed")
    stored_path = result.get("path", path)
    await db.files.insert_one({
        "storage_path": stored_path,
        "original_filename": filename,
        "content_type": content_type,
        "size": result.get("size", len(data)),
        "uploaded_by": str(user["_id"]),
        "is_deleted": False,
        "created_at": datetime.now(timezone.utc).isoformat(),
    })
    return {"path": stored_path, "url": f"/api/files/{stored_path}", "content_type": content_type}


@api.get("/files/{path:path}")
async def serve_file(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: bytes = b""
    content_type: str = "application/octet-stream"
    try:
        data, content_type = await asyncio.to_thread(get_object, path)
    except Exception as e:
        logger.error(f"File fetch failed: {e}")
        raise HTTPException(status_code=502, detail="File fetch failed")
    return Response(content=data, media_type=record.get("content_type") or content_type)


# ---------------- Showcase (public + admin) ----------------
def _serialize_showcase(c: dict) -> dict:
    c["id"] = str(c.pop("_id"))
    return c


@api.get("/showcase")
async def list_showcase():
    items = await db.showcase.find().sort([("order", 1), ("created_at", -1)]).to_list(100)
    return [_serialize_showcase(c) for c in items]


@api.post("/admin/showcase")
async def admin_create_showcase(body: ShowcaseIn, _: dict = Depends(require_admin)):
    doc = body.model_dump()
    doc["created_at"] = datetime.now(timezone.utc).isoformat()
    res = await db.showcase.insert_one(doc)
    return {"id": str(res.inserted_id), "ok": True}


@api.put("/admin/showcase/{case_id}")
async def admin_update_showcase(case_id: str, body: ShowcaseIn, _: dict = Depends(require_admin)):
    try:
        oid = ObjectId(case_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid id")
    res = await db.showcase.update_one({"_id": oid}, {"$set": body.model_dump()})
    if res.matched_count == 0:
        raise HTTPException(status_code=404, detail="Case not found")
    return {"ok": True}


@api.delete("/admin/showcase/{case_id}")
async def admin_delete_showcase(case_id: str, _: dict = Depends(require_admin)):
    try:
        oid = ObjectId(case_id)
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid id")
    await db.showcase.delete_one({"_id": oid})
    return {"ok": True}


@api.get("/")
async def root():
    return {"message": "Ayaba Ventures API", "status": "ok"}


app.include_router(api)

# ---------------- CORS ----------------
app.add_middleware(
    CORSMiddleware,
    allow_origins=os.environ.get("CORS_ORIGINS", "*").split(","),
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ---------------- Startup ----------------
@app.on_event("startup")
async def startup():
    await db.users.create_index("email", unique=True)
    await db.projects.create_index("client_id")
    await db.reports.create_index("project_id")
    await db.report_updates.create_index("report_id")
    await db.files.create_index("storage_path")
    await db.notifications.create_index("user_id")
    try:
        await asyncio.to_thread(init_storage)
        logger.info("Object storage initialized")
    except Exception as e:
        logger.error(f"Object storage init failed: {e}")
    # Seed admin
    existing = await db.users.find_one({"email": ADMIN_EMAIL.lower()})
    if not existing:
        await db.users.insert_one({
            "email": ADMIN_EMAIL.lower(), "password_hash": hash_password(ADMIN_PASSWORD),
            "name": "Ayaba Admin", "role": "admin", "created_at": datetime.now(timezone.utc)
        })
        logger.info("Seeded admin user")
    elif not verify_password(ADMIN_PASSWORD, existing["password_hash"]):
        await db.users.update_one({"email": ADMIN_EMAIL.lower()},
                                  {"$set": {"password_hash": hash_password(ADMIN_PASSWORD)}})
    # Seed test client
    if not await db.users.find_one({"email": "client@example.com"}):
        await db.users.insert_one({
            "email": "client@example.com", "password_hash": hash_password("Client@2026"),
            "name": "Adaeze Okonkwo", "role": "client", "created_at": datetime.now(timezone.utc)
        })
        logger.info("Seeded test client")
    # Seed test staff
    if not await db.users.find_one({"email": "staff@ayabaventures.org"}):
        await db.users.insert_one({
            "email": "staff@ayabaventures.org", "password_hash": hash_password("Staff@2026"),
            "name": "Tunde Adebayo", "role": "staff", "created_at": datetime.now(timezone.utc)
        })
        logger.info("Seeded staff user")
    # Seed default showcase entries
    if await db.showcase.count_documents({}) == 0:
        defaults = [
            {"code": "AYV-LAG-21", "title": "Five-bedroom duplex completed for a UK-based family", "location": "Magodo, Lagos", "duration": "16 months", "progress": "100% delivered", "summary": "Client based in Manchester engaged Ayaba after losing trust in their previous contractor. We ran a full audit, re-tendered the remaining scope, and managed the build to handover with weekly drone updates.", "image_url": "https://images.pexels.com/photos/8134821/pexels-photo-8134821.jpeg", "outcome": ["Saved approximately ₦14M against the previous contractor's revised quote", "Build completed two weeks ahead of revised milestone", "Client visited only twice during a 16-month construction window"], "quote": "I went from waking up at 3am worrying about my project to scrolling drone footage during lunch breaks. Worth every kobo.", "quote_attr": "Diaspora client, Manchester", "order": 1},
            {"code": "AYV-ABJ-RES-07", "title": "Stalled six-flat investment property rescued", "location": "Lokogoma, Abuja", "duration": "9 months (rescue)", "progress": "Defects resolved · Fully tenanted", "summary": "Six-flat block had stalled at second-floor slab for nineteen months. Contractor had abandoned site after consuming over ₦42M with no records. We audited, secured the site, redesigned the BOQ and completed the build.", "image_url": "https://images.unsplash.com/photo-1757377968583-0fed8ea67708", "outcome": ["Independent quantity survey recovered ₦8.7M in over-billed materials", "New contractor appointed with milestone-tied payments", "All six units rented out within 60 days of handover"], "quote": "I had given up. Ayaba turned a graveyard into a passive income property in under a year.", "quote_attr": "Investor, Houston TX", "order": 2},
            {"code": "AYV-LAG-MON-12", "title": "Remote monitoring of an existing contract — no surprises at handover", "location": "Lekki Phase 1, Lagos", "duration": "11 months (monitoring only)", "progress": "Handed over · No defect snags", "summary": "Client preferred to keep their family-recommended contractor but engaged Ayaba purely as an independent inspector. Weekly drone reports, material delivery verification and BOQ reconciliation gave them peace of mind.", "image_url": "https://images.pexels.com/photos/6165166/pexels-photo-6165166.jpeg", "outcome": ["Three material delivery discrepancies caught and refunded", "Final invoice reconciled to within 1.2% of original BOQ", "Zero post-handover defect calls in first six months"], "quote": "Ayaba kept my cousin honest. Best money I spent on the project.", "quote_attr": "Diaspora client, London", "order": 3},
        ]
        for d in defaults:
            d["created_at"] = datetime.now(timezone.utc).isoformat()
        await db.showcase.insert_many(defaults)
        logger.info("Seeded default showcase entries")


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