"""FastAPI router — mount this in any FastAPI app to add Muleline sync endpoints."""
from __future__ import annotations

import re
import time as _time
from typing import List, Optional

import numpy as np
from fastapi import APIRouter, File, Form, Query, Request, UploadFile
from fastapi.responses import FileResponse, JSONResponse, Response
from pydantic import BaseModel

from muleline.config import FACE_CROP_DIR
from muleline.db import get_db
from muleline.engine import SyncEngine
from muleline.face_engine import (
    cluster_unknown_faces,
    delete_item_faces,
    process_item_faces,
)
from muleline import clip_engine

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

# The engine instance must be set by the host app before mounting the router.
# Call muleline.router.init(engine) after creating your SyncEngine.
_engine: SyncEngine | None = None


def init(engine: SyncEngine):
    global _engine
    _engine = engine


def _get_engine() -> SyncEngine:
    if _engine is None:
        raise RuntimeError("Muleline router not initialized. Call muleline.router.init(engine) first.")
    return _engine


def _owner_id(request: Request) -> str:
    """Extract owner_id from request. Supports common auth patterns."""
    if hasattr(request.state, "user"):
        user = request.state.user
        if isinstance(user, dict):
            return str(user.get("id", user.get("sub", "")))
        return str(getattr(user, "id", getattr(user, "sub", "")))
    if hasattr(request.state, "owner_id"):
        return str(request.state.owner_id)
    raise ValueError("No user/owner_id on request.state. Add auth middleware.")


def _error(msg: str, status: int = 400) -> JSONResponse:
    return JSONResponse({"error": msg}, status_code=status)


# --- Upload endpoints ---

@router.post("/upload/init")
async def init_upload(request: Request):
    engine = _get_engine()
    body = await request.json()
    result = engine.init_upload(
        owner_id=_owner_id(request),
        original_name=body.get("original_name", ""),
        total_size=body.get("total_size", 0),
        file_hash=body.get("file_hash", ""),
        mime_type=body.get("mime_type", ""),
        metadata=body.get("metadata", "{}"),
    )
    if "error" in result:
        return _error(result["error"])
    return result


@router.post("/upload/{upload_id}/chunk")
async def upload_chunk(upload_id: str, request: Request, chunk_index: int = Form(...), chunk: UploadFile = File(...)):
    engine = _get_engine()
    chunk_data = await chunk.read()
    result = engine.upload_chunk(upload_id, _owner_id(request), chunk_index, chunk_data)
    if "error" in result:
        return _error(result["error"], 404 if "not found" in result["error"].lower() else 400)
    return result


@router.get("/upload/{upload_id}/status")
async def upload_status(upload_id: str, request: Request):
    engine = _get_engine()
    result = engine.upload_status(upload_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.post("/upload/{upload_id}/complete")
async def complete_upload(upload_id: str, request: Request):
    engine = _get_engine()
    result = engine.complete_upload(upload_id, _owner_id(request))
    if "error" in result:
        status = 404 if "not found" in result["error"].lower() else 400
        return _error(result["error"], status)
    return result


@router.delete("/upload/{upload_id}")
async def abort_upload(upload_id: str, request: Request):
    engine = _get_engine()
    result = engine.abort_upload(upload_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


# --- File browsing endpoints ---

@router.get("/files")
async def list_files(request: Request, sort: str = "newest", mime: str = "", limit: int = 200, offset: int = 0):
    engine = _get_engine()
    return engine.list_files(_owner_id(request), sort=sort, mime_filter=mime, limit=limit, offset=offset)


@router.get("/files/{file_id}")
async def get_file(file_id: int, request: Request):
    engine = _get_engine()
    result = engine.get_file(file_id, _owner_id(request))
    if not result:
        return _error("File not found", 404)
    return result


@router.get("/files/{file_id}/serve")
async def serve_file(file_id: int, request: Request):
    engine = _get_engine()
    content, mime = engine.serve_file(file_id, _owner_id(request))
    if content is None:
        return _error("File not found", 404)
    headers = {"X-Content-Type-Options": "nosniff"}
    if not mime.startswith(("image/", "video/", "audio/")):
        headers["Content-Disposition"] = "attachment"
    return Response(content=content, media_type=mime, headers=headers)


@router.get("/files/{file_id}/thumb")
async def serve_thumbnail(file_id: int, request: Request):
    engine = _get_engine()
    thumb = engine.serve_thumbnail(file_id, _owner_id(request))
    if thumb is None:
        return _error("No thumbnail available", 404)
    return Response(content=thumb, media_type="image/jpeg")


@router.delete("/files/{file_id}")
async def delete_file(file_id: int, request: Request):
    engine = _get_engine()
    result = engine.delete_file(file_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


# --- Search ---

@router.get("/search")
async def search_files(
    request: Request,
    q: str = "",
    type: str = "",
    date_from: Optional[float] = None,
    date_to: Optional[float] = None,
    sort: str = "newest",
    limit: int = 200,
    semantic: bool = False,
):
    owner_id = _owner_id(request)
    # Try CLIP semantic search if requested or if query looks natural-language
    if semantic and q:
        try:
            results = clip_engine.search(owner_id, q, limit=limit)
            return {"files": results, "total": len(results), "query": q, "semantic": True}
        except (ImportError, Exception):
            pass
    engine = _get_engine()
    return engine.search_files(
        owner_id=owner_id,
        query=q,
        mime_filter=type,
        date_from=date_from,
        date_to=date_to,
        sort=sort,
        limit=limit,
    )


# --- Timeline ---

@router.get("/timeline")
async def timeline(request: Request, group_by: str = "day"):
    if group_by not in ("day", "month", "year"):
        return _error("group_by must be day, month, or year", 400)
    engine = _get_engine()
    return engine.timeline(_owner_id(request), group_by=group_by)


# --- Bulk operations ---

class BulkFileIds(BaseModel):
    file_ids: List[int]


@router.post("/files/bulk-delete")
async def bulk_delete(body: BulkFileIds, request: Request):
    engine = _get_engine()
    return engine.bulk_delete(_owner_id(request), body.file_ids)


@router.post("/files/bulk-download")
async def bulk_download(body: BulkFileIds, request: Request):
    engine = _get_engine()
    zip_bytes = engine.bulk_download_zip(_owner_id(request), body.file_ids)
    if not zip_bytes:
        return _error("No files found", 404)
    return Response(
        content=zip_bytes,
        media_type="application/zip",
        headers={"Content-Disposition": "attachment; filename=muleline-export.zip"},
    )


# --- Device endpoints ---

@router.post("/devices/register")
async def register_device(request: Request):
    engine = _get_engine()
    body = await request.json()
    return engine.register_device(
        owner_id=_owner_id(request),
        device_id=body.get("device_id", ""),
        device_name=body.get("device_name", ""),
        platform=body.get("platform", ""),
    )


@router.get("/devices")
async def list_devices(request: Request):
    engine = _get_engine()
    return {"devices": engine.list_devices(_owner_id(request))}


# --- Manifest sync ---

@router.post("/manifest")
async def manifest_diff(request: Request):
    engine = _get_engine()
    body = await request.json()
    client_items = body.get("items", [])
    client_hashes = {item["file_hash"] for item in client_items if item.get("file_hash")}
    return engine.manifest_diff(
        owner_id=_owner_id(request),
        device_id=body.get("device_id", ""),
        client_hashes=client_hashes,
    )


# --- Sync status & logging ---

@router.post("/log")
async def log_sync(request: Request):
    engine = _get_engine()
    body = await request.json()
    return engine.log_sync(
        owner_id=_owner_id(request),
        device_id=body.get("device_id", ""),
        action=body.get("action", ""),
        file_id=body.get("file_id"),
        file_hash=body.get("file_hash", ""),
        bytes_transferred=int(body.get("bytes_transferred", 0)),
        status=body.get("status", "ok"),
        error_detail=body.get("error_detail", ""),
    )


@router.get("/status")
async def sync_status(request: Request):
    engine = _get_engine()
    return engine.sync_status(_owner_id(request))


# --- Cleanup ---

@router.post("/cleanup")
async def cleanup(request: Request):
    engine = _get_engine()
    cleaned = engine.cleanup_stale()
    return {"cleaned": cleaned}


# --- Server info & settings ---

_server_start = _time.time()


@router.get("/server/info")
async def server_info(request: Request):
    engine = _get_engine()
    status = engine.sync_status(_owner_id(request))
    uptime_s = int(_time.time() - _server_start)
    h, rem = divmod(uptime_s, 3600)
    m, s = divmod(rem, 60)
    uptime_str = f"{h}h {m}m {s}s" if h else f"{m}m {s}s"
    return {
        "version": "1.0.0",
        "uptime": uptime_str,
        "uptime_seconds": uptime_s,
        "total_files": status.get("stats", {}).get("total_files", 0),
        "storage_used_bytes": status.get("stats", {}).get("total_synced_bytes", 0),
        "backend_type": type(engine.storage).__name__.replace("Storage", "").lower(),
    }


@router.post("/server/settings")
async def save_settings(request: Request):
    return {"ok": True}


# --- Album endpoints ---

class AlbumCreate(BaseModel):
    name: str
    description: str = ""


class AlbumUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    cover_file_id: Optional[int] = None


class AlbumFileIds(BaseModel):
    file_ids: List[int]


@router.post("/albums")
async def create_album(body: AlbumCreate, request: Request):
    engine = _get_engine()
    return engine.create_album(_owner_id(request), body.name, body.description)


@router.get("/albums")
async def list_albums(request: Request):
    engine = _get_engine()
    return {"albums": engine.list_albums(_owner_id(request))}


@router.get("/albums/{album_id}")
async def get_album(album_id: int, request: Request):
    engine = _get_engine()
    result = engine.get_album(album_id, _owner_id(request))
    if not result:
        return _error("Album not found", 404)
    return result


@router.put("/albums/{album_id}")
async def update_album(album_id: int, body: AlbumUpdate, request: Request):
    engine = _get_engine()
    result = engine.update_album(
        album_id, _owner_id(request),
        name=body.name, description=body.description, cover_file_id=body.cover_file_id,
    )
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.delete("/albums/{album_id}")
async def delete_album(album_id: int, request: Request):
    engine = _get_engine()
    result = engine.delete_album(album_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.post("/albums/{album_id}/files")
async def add_files_to_album(album_id: int, body: AlbumFileIds, request: Request):
    engine = _get_engine()
    result = engine.add_files_to_album(album_id, _owner_id(request), body.file_ids)
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.delete("/albums/{album_id}/files")
async def remove_files_from_album(album_id: int, body: AlbumFileIds, request: Request):
    engine = _get_engine()
    result = engine.remove_files_from_album(album_id, _owner_id(request), body.file_ids)
    if "error" in result:
        return _error(result["error"], 404)
    return result


# --- Sharing endpoints ---

class ShareCreate(BaseModel):
    link_type: str
    target_id: int
    password: str = ""
    expires_hours: int = 0
    allow_download: bool = True


@router.post("/share")
async def create_shared_link(body: ShareCreate, request: Request):
    engine = _get_engine()
    result = engine.create_shared_link(
        owner_id=_owner_id(request),
        link_type=body.link_type,
        target_id=body.target_id,
        password=body.password,
        expires_hours=body.expires_hours,
        allow_download=body.allow_download,
    )
    if "error" in result:
        return _error(result["error"], 400)
    return result


@router.get("/share/{link_id}")
async def get_shared_content(link_id: str, request: Request, password: str = Query(default="")):
    engine = _get_engine()
    result = engine.get_shared_content(link_id, password=password)
    if "error" in result:
        status = 404 if "not found" in result["error"].lower() else 403
        return _error(result["error"], status)
    return result


@router.delete("/share/{link_id}")
async def delete_shared_link(link_id: str, request: Request):
    engine = _get_engine()
    result = engine.delete_shared_link(link_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.get("/shares")
async def list_shared_links(request: Request):
    engine = _get_engine()
    return {"links": engine.list_shared_links(_owner_id(request))}


# --- Face recognition endpoints ---

_CROP_RE = re.compile(r"^face_\d+_\d+\.webp$")


class PersonCreate(BaseModel):
    name: str


class PersonUpdate(BaseModel):
    name: str


@router.post("/faces/scan/{file_id}")
async def scan_file_faces(file_id: int, request: Request):
    engine = _get_engine()
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        row = conn.execute(
            "SELECT id, storage_key, mime_type FROM files WHERE id = ? AND owner_id = ?",
            (file_id, owner_id),
        ).fetchone()
        if not row:
            return _error("File not found", 404)

        mime = row["mime_type"] or ""
        if not mime.startswith("image/"):
            return _error("Not an image", 400)

        import tempfile, os
        from muleline.storage.local import LocalStorage
        from pathlib import Path

        if isinstance(engine.storage, LocalStorage):
            image_path = engine.storage._path(row["storage_key"])
            delete_item_faces(file_id, conn)
            result = process_item_faces(file_id, image_path, conn, owner_id)
        else:
            content = engine.storage.read(row["storage_key"])
            if not content:
                return _error("File not found on storage", 404)
            ext = os.path.splitext(row["storage_key"])[1] or ".jpg"
            with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
                tmp.write(content)
                tmp_path = Path(tmp.name)
            try:
                delete_item_faces(file_id, conn)
                result = process_item_faces(file_id, tmp_path, conn, owner_id)
            finally:
                tmp_path.unlink(missing_ok=True)

        return result
    finally:
        conn.close()


@router.post("/faces/scan-all")
async def scan_all_faces(request: Request, limit: int = 20):
    engine = _get_engine()
    owner_id = _owner_id(request)
    limit = min(limit, 100)
    conn = get_db()
    try:
        rows = conn.execute(
            """SELECT id, storage_key, mime_type FROM files
               WHERE owner_id = ?
               AND id NOT IN (SELECT DISTINCT item_id FROM face_embeddings)
               AND (
                   mime_type LIKE 'image/%'
               )
               LIMIT ?""",
            (owner_id, limit),
        ).fetchall()

        scanned = 0
        faces_found = 0
        faces_matched = 0

        import tempfile, os
        from muleline.storage.local import LocalStorage
        from pathlib import Path

        for row in rows:
            if isinstance(engine.storage, LocalStorage):
                image_path = engine.storage._path(row["storage_key"])
                if not image_path.exists():
                    continue
                result = process_item_faces(row["id"], image_path, conn, owner_id)
            else:
                content = engine.storage.read(row["storage_key"])
                if not content:
                    continue
                ext = os.path.splitext(row["storage_key"])[1] or ".jpg"
                with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
                    tmp.write(content)
                    tmp_path = Path(tmp.name)
                try:
                    result = process_item_faces(row["id"], tmp_path, conn, owner_id)
                finally:
                    tmp_path.unlink(missing_ok=True)

            scanned += 1
            faces_found += result.get("faces_found", 0)
            faces_matched += result.get("faces_matched", 0)

        return {"scanned": scanned, "faces_found": faces_found, "faces_matched": faces_matched}
    finally:
        conn.close()


@router.get("/faces/unassigned")
async def list_unassigned_faces(request: Request, cluster_id: Optional[int] = None):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        query = """
            SELECT fe.id, fe.item_id, fe.crop_filename, fe.cluster_id,
                   fe.bbox_x, fe.bbox_y, fe.bbox_w, fe.bbox_h,
                   f.filename as item_filename
            FROM face_embeddings fe
            JOIN files f ON fe.item_id = f.id
            WHERE fe.person_id IS NULL AND f.owner_id = ?
        """
        params: list = [owner_id]

        if cluster_id is not None:
            query += " AND fe.cluster_id = ?"
            params.append(cluster_id)

        query += " ORDER BY fe.cluster_id, fe.id LIMIT 500"

        rows = conn.execute(query, params).fetchall()
        faces = [{
            "id": r["id"],
            "item_id": r["item_id"],
            "crop_filename": r["crop_filename"],
            "cluster_id": r["cluster_id"],
            "bbox": {"x": r["bbox_x"], "y": r["bbox_y"], "w": r["bbox_w"], "h": r["bbox_h"]},
            "item_filename": r["item_filename"],
        } for r in rows]
        return {"faces": faces}
    finally:
        conn.close()


@router.post("/faces/assign/{face_id}/{person_id}")
async def assign_face(face_id: int, person_id: int, request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        face = conn.execute(
            """SELECT fe.* FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE fe.id = ? AND f.owner_id = ?""",
            (face_id, owner_id),
        ).fetchone()
        if not face:
            return _error("Face not found", 404)

        person = conn.execute(
            "SELECT id FROM known_people WHERE id = ? AND owner_id = ?",
            (person_id, owner_id),
        ).fetchone()
        if not person:
            return _error("Person not found", 404)

        now = _time.time()
        conn.execute(
            "UPDATE face_embeddings SET person_id = ?, cluster_id = NULL WHERE id = ?",
            (person_id, face_id),
        )

        existing_tag = conn.execute(
            "SELECT id FROM item_people WHERE item_id = ? AND person_id = ?",
            (face["item_id"], person_id),
        ).fetchone()
        if not existing_tag:
            conn.execute(
                "INSERT INTO item_people (item_id, person_id, source, created_at) VALUES (?,?,?,?)",
                (face["item_id"], person_id, "face_recognition", now),
            )

        conn.execute(
            "UPDATE known_people SET reference_item_id = ? WHERE id = ? AND reference_item_id IS NULL",
            (face["item_id"], person_id),
        )

        conn.commit()
        return {"ok": True}
    finally:
        conn.close()


@router.post("/faces/cluster")
async def cluster_faces(request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        rows = conn.execute(
            """SELECT fe.id, fe.embedding FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE fe.person_id IS NULL AND f.owner_id = ?
               LIMIT 2000""",
            (owner_id,),
        ).fetchall()

        if not rows:
            return {"clustered": 0, "num_clusters": 0}

        pairs = [(r["id"], np.frombuffer(r["embedding"], dtype=np.float64)) for r in rows]
        label_map = cluster_unknown_faces(pairs)

        for face_id, label in label_map.items():
            cluster_val = label if label >= 0 else None
            conn.execute(
                "UPDATE face_embeddings SET cluster_id = ? WHERE id = ?",
                (cluster_val, face_id),
            )

        num_clusters = len(set(v for v in label_map.values() if v >= 0))
        conn.commit()
        return {"clustered": len(pairs), "num_clusters": num_clusters}
    finally:
        conn.close()


@router.get("/faces/item/{file_id}")
async def get_file_faces(file_id: int, request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        rows = conn.execute(
            """SELECT fe.id, fe.person_id, fe.crop_filename, fe.confidence,
                      kp.name as person_name
               FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               LEFT JOIN known_people kp ON fe.person_id = kp.id
               WHERE fe.item_id = ? AND f.owner_id = ?
               ORDER BY fe.id""",
            (file_id, owner_id),
        ).fetchall()

        faces = [{
            "id": r["id"],
            "person_id": r["person_id"],
            "person_name": r["person_name"] or "",
            "crop_filename": r["crop_filename"],
            "confidence": r["confidence"],
        } for r in rows]
        return {"faces": faces}
    finally:
        conn.close()


@router.get("/faces/person/{person_id}")
async def get_person_faces(person_id: int, request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        person = conn.execute(
            "SELECT id FROM known_people WHERE id = ? AND owner_id = ?",
            (person_id, owner_id),
        ).fetchone()
        if not person:
            return _error("Person not found", 404)

        rows = conn.execute(
            """SELECT fe.id, fe.item_id, fe.crop_filename, fe.confidence
               FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE fe.person_id = ? AND f.owner_id = ?
               ORDER BY fe.confidence DESC""",
            (person_id, owner_id),
        ).fetchall()

        faces = [{"id": r["id"], "item_id": r["item_id"], "crop_filename": r["crop_filename"], "confidence": r["confidence"]} for r in rows]
        return {"faces": faces, "total": len(faces)}
    finally:
        conn.close()


@router.get("/faces/crop/{filename}")
async def serve_face_crop(filename: str, request: Request):
    if not _CROP_RE.match(filename):
        return _error("Invalid filename", 400)

    owner_id = _owner_id(request)
    conn = get_db()
    try:
        face = conn.execute(
            """SELECT fe.id FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE fe.crop_filename = ? AND f.owner_id = ?""",
            (filename, owner_id),
        ).fetchone()
        if not face:
            return _error("Not found", 404)
    finally:
        conn.close()

    crop_path = FACE_CROP_DIR / filename
    if not crop_path.exists():
        return _error("Not found", 404)

    return FileResponse(str(crop_path), media_type="image/webp")


@router.get("/faces/stats")
async def face_stats(request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        total = conn.execute(
            """SELECT COUNT(*) FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE f.owner_id = ?""",
            (owner_id,),
        ).fetchone()[0]

        assigned = conn.execute(
            """SELECT COUNT(*) FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE f.owner_id = ? AND fe.person_id IS NOT NULL""",
            (owner_id,),
        ).fetchone()[0]

        unique_people = conn.execute(
            """SELECT COUNT(DISTINCT fe.person_id) FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE f.owner_id = ? AND fe.person_id IS NOT NULL""",
            (owner_id,),
        ).fetchone()[0]

        return {
            "total_faces": total,
            "assigned": assigned,
            "unassigned": total - assigned,
            "unique_people": unique_people,
        }
    finally:
        conn.close()


@router.delete("/faces/{face_id}")
async def delete_face(face_id: int, request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    face = None
    try:
        face = conn.execute(
            """SELECT fe.* FROM face_embeddings fe
               JOIN files f ON fe.item_id = f.id
               WHERE fe.id = ? AND f.owner_id = ?""",
            (face_id, owner_id),
        ).fetchone()
        if not face:
            return _error("Face not found", 404)

        conn.execute("DELETE FROM face_embeddings WHERE id = ?", (face_id,))
        conn.commit()
    finally:
        conn.close()

    if face:
        crop = face["crop_filename"]
        if crop and _CROP_RE.match(crop):
            crop_path = FACE_CROP_DIR / crop
            if crop_path.resolve().is_relative_to(FACE_CROP_DIR.resolve()) and crop_path.exists():
                crop_path.unlink()

    return {"ok": True}


# --- People endpoints ---

@router.post("/people")
async def create_person(body: PersonCreate, request: Request):
    owner_id = _owner_id(request)
    now = _time.time()
    conn = get_db()
    try:
        conn.execute(
            "INSERT INTO known_people (owner_id, name, created_at) VALUES (?,?,?)",
            (owner_id, body.name.strip(), now),
        )
        person_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
        conn.commit()
        return {"ok": True, "person_id": person_id, "name": body.name.strip()}
    finally:
        conn.close()


@router.get("/people")
async def list_people(request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        rows = conn.execute(
            """SELECT kp.id, kp.name, kp.reference_item_id, kp.created_at,
                      COUNT(DISTINCT ip.item_id) as photo_count
               FROM known_people kp
               LEFT JOIN item_people ip ON ip.person_id = kp.id
               WHERE kp.owner_id = ?
               GROUP BY kp.id
               ORDER BY kp.name""",
            (owner_id,),
        ).fetchall()
        return {"people": [dict(r) for r in rows]}
    finally:
        conn.close()


@router.put("/people/{person_id}")
async def rename_person(person_id: int, body: PersonUpdate, request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        row = conn.execute(
            "SELECT id FROM known_people WHERE id = ? AND owner_id = ?",
            (person_id, owner_id),
        ).fetchone()
        if not row:
            return _error("Person not found", 404)
        conn.execute(
            "UPDATE known_people SET name = ? WHERE id = ?",
            (body.name.strip(), person_id),
        )
        conn.commit()
        return {"ok": True, "person_id": person_id, "name": body.name.strip()}
    finally:
        conn.close()


@router.delete("/people/{person_id}")
async def delete_person(person_id: int, request: Request):
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        row = conn.execute(
            "SELECT id FROM known_people WHERE id = ? AND owner_id = ?",
            (person_id, owner_id),
        ).fetchone()
        if not row:
            return _error("Person not found", 404)

        conn.execute(
            "UPDATE face_embeddings SET person_id = NULL WHERE person_id = ?",
            (person_id,),
        )
        conn.execute("DELETE FROM item_people WHERE person_id = ?", (person_id,))
        conn.execute("DELETE FROM known_people WHERE id = ?", (person_id,))
        conn.commit()
        return {"ok": True}
    finally:
        conn.close()


# ── Import ────────────────────────────────────────────────────────────


@router.post("/import/google-photos")
async def import_google_photos(request: Request, file: UploadFile = File(...)):
    engine = _get_engine()
    owner_id = _owner_id(request)

    import tempfile
    from pathlib import Path
    from muleline.importer import import_takeout_zip

    with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
        content = await file.read()
        tmp.write(content)
        tmp_path = Path(tmp.name)

    try:
        result = import_takeout_zip(tmp_path, owner_id, engine.storage)
        if "error" in result:
            return _error(result["error"], 400)
        return result
    finally:
        tmp_path.unlink(missing_ok=True)


# ── CLIP Semantic Search ──────────────────────────────────────────────


@router.post("/search/index-all")
async def search_index_all(request: Request):
    owner_id = _owner_id(request)
    engine = _get_engine()
    upload_dir = engine.storage.base_path if hasattr(engine.storage, "base_path") else None
    if not upload_dir:
        return _error("Index only supported with local storage", 400)
    try:
        result = clip_engine.index_all(owner_id, upload_dir)
        return result
    except ImportError as e:
        return _error(str(e), 503)


@router.get("/search/stats")
async def search_stats(request: Request):
    owner_id = _owner_id(request)
    return clip_engine.stats(owner_id)


# ── Trash ─────────────────────────────────────────────────────────────


@router.get("/trash")
async def list_trash(request: Request, limit: int = 200):
    engine = _get_engine()
    return engine.list_trash(_owner_id(request), limit=limit)


@router.post("/files/{file_id}/restore")
async def restore_file(file_id: int, request: Request):
    engine = _get_engine()
    result = engine.restore_file(file_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.delete("/files/{file_id}/purge")
async def purge_file(file_id: int, request: Request):
    engine = _get_engine()
    result = engine.purge_file(file_id, _owner_id(request))
    if "error" in result:
        return _error(result["error"], 404)
    return result


@router.post("/trash/empty")
async def empty_trash(request: Request):
    engine = _get_engine()
    return engine.empty_trash(_owner_id(request))


@router.post("/trash/auto-purge")
async def auto_purge_trash(request: Request, days: int = 30):
    engine = _get_engine()
    return engine.auto_purge_trash(_owner_id(request), days=days)


# ── Favorites ─────────────────────────────────────────────────────────


@router.post("/files/{file_id}/favorite")
async def toggle_favorite(file_id: int, request: Request):
    engine = _get_engine()
    return engine.toggle_favorite(_owner_id(request), file_id)


@router.get("/favorites")
async def list_favorites(request: Request, limit: int = 200):
    engine = _get_engine()
    return engine.list_favorites(_owner_id(request), limit=limit)


@router.get("/files/{file_id}/favorited")
async def is_favorited(file_id: int, request: Request):
    engine = _get_engine()
    return {"favorited": engine.is_favorited(_owner_id(request), file_id)}


@router.get("/connectors/gmail/status")
async def gmail_status(request: Request):
    """Check Gmail connector authentication status."""
    try:
        from muleline.connectors.gmail import get_credentials, get_service
        creds = get_credentials()
        if not creds or creds.expired:
            return {"authenticated": False}
        service = get_service()
        profile = service.users().getProfile(userId="me").execute()
        return {"authenticated": True, "email": profile["emailAddress"], "total_messages": profile.get("messagesTotal")}
    except Exception as e:
        return {"authenticated": False, "error": str(e)}


@router.post("/connectors/gmail/import")
async def gmail_import(request: Request, query: str = "has:attachment", max_results: int = 100):
    """Import photo/video attachments from Gmail."""
    engine = _get_engine()
    owner_id = _owner_id(request)
    try:
        from muleline.connectors.gmail import import_from_gmail
        result = import_from_gmail(owner_id, engine.storage, query=query, max_results=max_results)
        if "error" in result:
            return _error(result["error"], 400)
        return result
    except ImportError:
        return _error("Gmail connector dependencies not installed", 400)
    except Exception as e:
        return _error(str(e), 500)


# ── Receipts ─────────────────────────────────────────────────────────


@router.post("/receipts/scan")
async def scan_receipts(request: Request, max_results: int = 200):
    """Scan Gmail for receipts and invoices."""
    owner_id = _owner_id(request)
    try:
        from muleline.connectors.receipts import scan_receipts as _scan
        result = _scan(owner_id, max_results=max_results)
        if "error" in result:
            return _error(result["error"], 400)
        return result
    except ImportError:
        return _error("Gmail connector dependencies not installed", 400)
    except Exception as e:
        return _error(str(e), 500)


@router.get("/receipts")
async def list_receipts_endpoint(
    request: Request, vendor: str = "", since: str = "", limit: int = 100
):
    """List receipts with optional filters."""
    owner_id = _owner_id(request)
    try:
        from muleline.connectors.receipts import list_receipts
        rows = list_receipts(owner_id, vendor=vendor, since=since, limit=limit)
        return {"receipts": rows, "total": len(rows)}
    except Exception as e:
        return _error(str(e), 500)


@router.get("/receipts/summary")
async def receipts_summary(request: Request):
    """Get receipt spending summary by vendor, category, and month."""
    owner_id = _owner_id(request)
    try:
        from muleline.connectors.receipts import receipt_summary
        return receipt_summary(owner_id)
    except Exception as e:
        return _error(str(e), 500)


@router.delete("/receipts/{receipt_id}")
async def delete_receipt(receipt_id: int, request: Request):
    """Delete a receipt."""
    owner_id = _owner_id(request)
    conn = get_db()
    try:
        row = conn.execute(
            "SELECT id FROM receipts WHERE id = ? AND owner_id = ?",
            (receipt_id, owner_id),
        ).fetchone()
        if not row:
            return _error("Receipt not found", 404)
        conn.execute("DELETE FROM receipts WHERE id = ?", (receipt_id,))
        conn.commit()
        return {"ok": True}
    finally:
        conn.close()
