"""Core sync engine. Orchestrates chunked uploads, dedup, manifest diff, and storage."""
from __future__ import annotations

import hashlib
import hmac
import io
import json
import mimetypes
import re
import shutil
import time
import uuid
import zipfile
from math import ceil
from pathlib import Path

from muleline.config import CHUNK_DIR, CHUNK_SIZE, MAX_UPLOAD_SIZE, PENDING_UPLOAD_TTL, MAX_PENDING_UPLOADS, THUMB_DIR
from muleline.db import get_db
from muleline.exif import extract_metadata
from muleline.hasher import sha256
from muleline.storage.base import StorageBackend


class SyncEngine:
    def __init__(self, storage: StorageBackend):
        self.storage = storage

    # --- Chunked Upload ---

    def init_upload(
        self,
        owner_id: str,
        original_name: str,
        total_size: int,
        file_hash: str,
        mime_type: str = "",
        metadata: str = "{}",
    ) -> dict:
        if total_size <= 0:
            return {"error": "Invalid file size"}
        if total_size > MAX_UPLOAD_SIZE:
            return {"error": f"File too large (max {MAX_UPLOAD_SIZE // (1024*1024)}MB)"}

        conn = get_db()

        active = conn.execute(
            "SELECT COUNT(*) FROM pending_uploads WHERE owner_id = ? AND status = 'uploading'",
            (owner_id,),
        ).fetchone()[0]
        if active >= MAX_PENDING_UPLOADS:
            conn.close()
            return {"error": "Too many concurrent uploads"}

        dup = self.check_duplicate(file_hash, owner_id, conn)
        if dup:
            conn.close()
            return {"duplicate": True, "existing_file": dup}

        upload_id = str(uuid.uuid4())
        total_chunks = ceil(total_size / CHUNK_SIZE)
        now = time.time()

        conn.execute(
            """INSERT INTO pending_uploads
               (id, owner_id, original_name, mime_type, total_size, total_chunks,
                received_chunks, file_hash, metadata, status, created_at, updated_at, expires_at)
               VALUES (?,?,?,?,?,?,0,?,?,?,?,?,?)""",
            (upload_id, owner_id, original_name, mime_type, total_size, total_chunks,
             file_hash, metadata, "uploading", now, now, now + PENDING_UPLOAD_TTL),
        )
        conn.commit()
        conn.close()

        self._chunk_dir(upload_id).mkdir(parents=True, exist_ok=True)

        return {
            "upload_id": upload_id,
            "chunk_size": CHUNK_SIZE,
            "total_chunks": total_chunks,
        }

    def upload_chunk(self, upload_id: str, owner_id: str, chunk_index: int, chunk_data: bytes) -> dict:
        conn = get_db()

        pending = conn.execute(
            "SELECT * FROM pending_uploads WHERE id = ? AND owner_id = ? AND status = 'uploading'",
            (upload_id, owner_id),
        ).fetchone()
        if not pending:
            conn.close()
            return {"error": "Upload session not found"}

        if chunk_index < 0 or chunk_index >= pending["total_chunks"]:
            conn.close()
            return {"error": f"Invalid chunk index (must be 0-{pending['total_chunks'] - 1})"}

        max_chunk = CHUNK_SIZE + 1024  # small tolerance for encoding overhead
        if len(chunk_data) > max_chunk:
            conn.close()
            return {"error": f"Chunk too large ({len(chunk_data)} > {max_chunk})"}

        chunk_path = self._chunk_dir(upload_id) / f"{chunk_index:06d}"
        already_exists = chunk_path.exists()
        chunk_path.write_bytes(chunk_data)

        if not already_exists:
            conn.execute(
                "UPDATE pending_uploads SET received_chunks = received_chunks + 1, updated_at = ? WHERE id = ?",
                (time.time(), upload_id),
            )
        else:
            conn.execute(
                "UPDATE pending_uploads SET updated_at = ? WHERE id = ?",
                (time.time(), upload_id),
            )
        conn.commit()

        updated = conn.execute(
            "SELECT received_chunks, total_chunks FROM pending_uploads WHERE id = ?",
            (upload_id,),
        ).fetchone()
        conn.close()

        return {
            "received": chunk_index,
            "total_received": updated["received_chunks"],
            "remaining": updated["total_chunks"] - updated["received_chunks"],
        }

    def complete_upload(self, upload_id: str, owner_id: str) -> dict:
        conn = get_db()

        pending = conn.execute(
            "SELECT * FROM pending_uploads WHERE id = ? AND owner_id = ? AND status = 'uploading'",
            (upload_id, owner_id),
        ).fetchone()
        if not pending:
            conn.close()
            return {"error": "Upload session not found"}

        if pending["received_chunks"] != pending["total_chunks"]:
            conn.close()
            return {"error": f"Incomplete: {pending['received_chunks']}/{pending['total_chunks']} chunks"}

        chunk_d = self._chunk_dir(upload_id)
        indexes = self._received_indexes(upload_id)
        if len(indexes) != pending["total_chunks"]:
            conn.close()
            return {"error": "Chunk files missing on disk"}

        content = b"".join(
            (chunk_d / f"{idx:06d}").read_bytes() for idx in indexes
        )

        actual_hash = sha256(content)
        if actual_hash != pending["file_hash"]:
            conn.execute(
                "UPDATE pending_uploads SET status = 'failed', updated_at = ? WHERE id = ?",
                (time.time(), upload_id),
            )
            conn.commit()
            conn.close()
            shutil.rmtree(chunk_d, ignore_errors=True)
            return {"error": "Hash mismatch — assembled file is corrupt"}

        ext = ""
        if "." in pending["original_name"]:
            ext = "." + pending["original_name"].rsplit(".", 1)[1].lower()

        filename = f"{uuid.uuid4()}{ext}"
        mime = pending["mime_type"] or mimetypes.guess_type(pending["original_name"])[0] or "application/octet-stream"

        storage_key = f"{owner_id}/{filename}"
        self.storage.save(storage_key, content, mime)

        exif = extract_metadata(content, mime)

        base_meta: dict = {}
        try:
            base_meta = json.loads(pending["metadata"] or "{}")
        except (ValueError, TypeError):
            pass
        base_meta.update(exif)
        merged_metadata = json.dumps(base_meta)

        now = time.time()
        conn.execute(
            """INSERT INTO files
               (owner_id, filename, original_name, mime_type, file_size, file_hash,
                storage_backend, storage_key, metadata,
                date_taken, gps_lat, gps_lon, width, height,
                created_at, updated_at)
               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            (owner_id, filename, pending["original_name"], mime, len(content),
             pending["file_hash"], "default", storage_key, merged_metadata,
             exif.get("date_taken"), exif.get("gps_lat"), exif.get("gps_lon"),
             exif.get("width"), exif.get("height"),
             now, now),
        )
        file_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]

        conn.execute(
            "UPDATE pending_uploads SET status = 'complete', updated_at = ? WHERE id = ?",
            (now, upload_id),
        )
        conn.commit()
        conn.close()

        shutil.rmtree(chunk_d, ignore_errors=True)

        if mime.startswith("image/"):
            try:
                from muleline.face_engine import process_item_faces
                from muleline.storage.local import LocalStorage
                import tempfile
                import os as _os

                if isinstance(self.storage, LocalStorage):
                    image_path = self.storage._path(storage_key)
                    face_conn = get_db()
                    try:
                        process_item_faces(file_id, image_path, face_conn, owner_id)
                    finally:
                        face_conn.close()
                else:
                    ext = Path(filename).suffix or ".jpg"
                    with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
                        tmp.write(content)
                        tmp_path = Path(tmp.name)
                    try:
                        face_conn = get_db()
                        try:
                            process_item_faces(file_id, tmp_path, face_conn, owner_id)
                        finally:
                            face_conn.close()
                    finally:
                        tmp_path.unlink(missing_ok=True)
            except Exception as e:
                import logging as _logging
                _logging.getLogger(__name__).warning("Face scan failed for file %s: %s", file_id, e)

        return {"ok": True, "file_id": file_id, "filename": filename, "storage_key": storage_key}

    def abort_upload(self, upload_id: str, owner_id: str) -> dict:
        conn = get_db()
        pending = conn.execute(
            "SELECT id FROM pending_uploads WHERE id = ? AND owner_id = ?",
            (upload_id, owner_id),
        ).fetchone()
        if not pending:
            conn.close()
            return {"error": "Upload session not found"}

        shutil.rmtree(self._chunk_dir(upload_id), ignore_errors=True)
        conn.execute("DELETE FROM pending_uploads WHERE id = ?", (upload_id,))
        conn.commit()
        conn.close()
        return {"ok": True}

    def upload_status(self, upload_id: str, owner_id: str) -> dict:
        conn = get_db()
        pending = conn.execute(
            "SELECT * FROM pending_uploads WHERE id = ? AND owner_id = ?",
            (upload_id, owner_id),
        ).fetchone()
        if not pending:
            conn.close()
            return {"error": "Upload session not found"}
        conn.close()
        return {
            "upload_id": upload_id,
            "received_chunks": pending["received_chunks"],
            "total_chunks": pending["total_chunks"],
            "status": pending["status"],
            "received_indexes": self._received_indexes(upload_id),
        }

    # --- Deduplication ---

    def check_duplicate(self, file_hash: str, owner_id: str, conn=None) -> dict | None:
        close = False
        if conn is None:
            conn = get_db()
            close = True
        row = conn.execute(
            "SELECT id, filename, original_name FROM files WHERE file_hash = ? AND owner_id = ? LIMIT 1",
            (file_hash, owner_id),
        ).fetchone()
        if close:
            conn.close()
        return dict(row) if row else None

    # --- File Browsing ---

    def list_files(self, owner_id: str, sort: str = "newest", mime_filter: str = "", limit: int = 200, offset: int = 0) -> dict:
        limit = min(limit, 1000)
        conn = get_db()
        where = "WHERE owner_id = ? AND deleted_at IS NULL"
        params: list = [owner_id]
        if mime_filter:
            where += " AND mime_type LIKE ?"
            params.append(f"{mime_filter}%")
        order = "created_at DESC" if sort == "newest" else "created_at ASC" if sort == "oldest" else "original_name ASC"
        total = conn.execute(f"SELECT COUNT(*) FROM files {where}", params).fetchone()[0]
        rows = conn.execute(
            f"SELECT id, filename, original_name, mime_type, file_size, file_hash, storage_key, date_taken, gps_lat, gps_lon, width, height, created_at FROM files {where} ORDER BY {order} LIMIT ? OFFSET ?",
            params + [limit, offset],
        ).fetchall()
        conn.close()
        return {"files": [dict(r) for r in rows], "total": total}

    def search_files(
        self,
        owner_id: str,
        query: str = "",
        mime_filter: str = "",
        date_from: float | None = None,
        date_to: float | None = None,
        sort: str = "newest",
        limit: int = 200,
    ) -> dict:
        limit = min(limit, 1000)
        conn = get_db()
        where = "WHERE owner_id = ? AND deleted_at IS NULL"
        params: list = [owner_id]

        if query:
            where += " AND original_name LIKE ?"
            params.append(f"%{query}%")

        if mime_filter:
            where += " AND mime_type LIKE ?"
            params.append(f"{mime_filter}%")

        if date_from is not None:
            where += " AND COALESCE(date_taken, created_at) >= ?"
            params.append(date_from)

        if date_to is not None:
            where += " AND COALESCE(date_taken, created_at) <= ?"
            params.append(date_to)

        order_map = {
            "newest": "created_at DESC",
            "oldest": "created_at ASC",
            "largest": "file_size DESC",
            "smallest": "file_size ASC",
        }
        order = order_map.get(sort, "created_at DESC")

        total = conn.execute(f"SELECT COUNT(*) FROM files {where}", params).fetchone()[0]
        rows = conn.execute(
            f"SELECT id, filename, original_name, mime_type, file_size, file_hash, storage_key, date_taken, gps_lat, gps_lon, width, height, created_at FROM files {where} ORDER BY {order} LIMIT ?",
            params + [limit],
        ).fetchall()
        conn.close()
        return {"files": [dict(r) for r in rows], "total": total}

    def bulk_delete(self, owner_id: str, file_ids: list) -> dict:
        if not file_ids:
            return {"deleted": 0}
        conn = get_db()
        placeholders = ",".join("?" * len(file_ids))
        now = time.time()
        rows = conn.execute(
            f"SELECT id FROM files WHERE id IN ({placeholders}) AND owner_id = ? AND deleted_at IS NULL",
            file_ids + [owner_id],
        ).fetchall()
        deleted = 0
        for row in rows:
            conn.execute("UPDATE files SET deleted_at = ? WHERE id = ?", (now, row["id"]))
            deleted += 1
        conn.commit()
        conn.close()
        return {"deleted": deleted}

    def bulk_download_zip(self, owner_id: str, file_ids: list) -> bytes:
        if not file_ids:
            return b""
        conn = get_db()
        placeholders = ",".join("?" * len(file_ids))
        rows = conn.execute(
            f"SELECT id, original_name, storage_key FROM files WHERE id IN ({placeholders}) AND owner_id = ?",
            file_ids + [owner_id],
        ).fetchall()
        conn.close()

        buf = io.BytesIO()
        seen_names: dict = {}
        with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
            for row in rows:
                content = self.storage.read(row["storage_key"])
                if not content:
                    continue
                name = row["original_name"]
                if name in seen_names:
                    seen_names[name] += 1
                    base, _, ext = name.rpartition(".")
                    name = f"{base}_{seen_names[name]}.{ext}" if ext else f"{name}_{seen_names[name]}"
                else:
                    seen_names[name] = 0
                zf.writestr(name, content)
        return buf.getvalue()

    def get_file(self, file_id: int, owner_id: str) -> dict | None:
        conn = get_db()
        row = conn.execute("SELECT * FROM files WHERE id = ? AND owner_id = ?", (file_id, owner_id)).fetchone()
        conn.close()
        return dict(row) if row else None

    def serve_file(self, file_id: int, owner_id: str) -> tuple[bytes | None, str]:
        meta = self.get_file(file_id, owner_id)
        if not meta:
            return None, ""
        content = self.storage.read(meta["storage_key"])
        return content, meta["mime_type"]

    def serve_thumbnail(self, file_id: int, owner_id: str) -> bytes | None:
        meta = self.get_file(file_id, owner_id)
        if not meta:
            return None
        thumb_path = THUMB_DIR / f"{file_id}.jpg"
        if thumb_path.exists():
            return thumb_path.read_bytes()
        content = self.storage.read(meta["storage_key"])
        if not content or not meta["mime_type"].startswith("image/"):
            return None
        thumb = self._generate_thumbnail(content)
        if thumb:
            THUMB_DIR.mkdir(parents=True, exist_ok=True)
            thumb_path.write_bytes(thumb)
        return thumb

    def delete_file(self, file_id: int, owner_id: str) -> dict:
        conn = get_db()
        row = conn.execute("SELECT * FROM files WHERE id = ? AND owner_id = ? AND deleted_at IS NULL", (file_id, owner_id)).fetchone()
        if not row:
            conn.close()
            return {"error": "File not found"}
        conn.execute("UPDATE files SET deleted_at = ? WHERE id = ?", (time.time(), file_id))
        conn.commit()
        conn.close()
        return {"ok": True}

    def restore_file(self, file_id: int, owner_id: str) -> dict:
        conn = get_db()
        row = conn.execute("SELECT * FROM files WHERE id = ? AND owner_id = ? AND deleted_at IS NOT NULL", (file_id, owner_id)).fetchone()
        if not row:
            conn.close()
            return {"error": "File not found in trash"}
        conn.execute("UPDATE files SET deleted_at = NULL WHERE id = ?", (file_id,))
        conn.commit()
        conn.close()
        return {"ok": True}

    def purge_file(self, file_id: int, owner_id: str) -> dict:
        conn = get_db()
        row = conn.execute("SELECT * FROM files WHERE id = ? AND owner_id = ? AND deleted_at IS NOT NULL", (file_id, owner_id)).fetchone()
        if not row:
            conn.close()
            return {"error": "File not found in trash"}
        self.storage.delete(row["storage_key"])
        conn.execute("DELETE FROM files WHERE id = ?", (file_id,))
        conn.commit()
        conn.close()
        thumb_path = THUMB_DIR / f"{file_id}.jpg"
        if thumb_path.exists():
            thumb_path.unlink()
        return {"ok": True}

    def list_trash(self, owner_id: str, limit: int = 200) -> dict:
        conn = get_db()
        rows = conn.execute(
            "SELECT id, filename, original_name, mime_type, file_size, deleted_at, created_at FROM files WHERE owner_id = ? AND deleted_at IS NOT NULL ORDER BY deleted_at DESC LIMIT ?",
            (owner_id, limit),
        ).fetchall()
        total = conn.execute("SELECT COUNT(*) FROM files WHERE owner_id = ? AND deleted_at IS NOT NULL", (owner_id,)).fetchone()[0]
        conn.close()
        return {"files": [dict(r) for r in rows], "total": total}

    def empty_trash(self, owner_id: str) -> dict:
        conn = get_db()
        rows = conn.execute("SELECT id, storage_key FROM files WHERE owner_id = ? AND deleted_at IS NOT NULL", (owner_id,)).fetchall()
        count = 0
        for row in rows:
            self.storage.delete(row["storage_key"])
            conn.execute("DELETE FROM files WHERE id = ?", (row["id"],))
            thumb_path = THUMB_DIR / f"{row['id']}.jpg"
            if thumb_path.exists():
                thumb_path.unlink()
            count += 1
        conn.commit()
        conn.close()
        return {"purged": count}

    def auto_purge_trash(self, owner_id: str, days: int = 30) -> dict:
        cutoff = time.time() - (days * 86400)
        conn = get_db()
        rows = conn.execute("SELECT id, storage_key FROM files WHERE owner_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?", (owner_id, cutoff)).fetchall()
        count = 0
        for row in rows:
            self.storage.delete(row["storage_key"])
            conn.execute("DELETE FROM files WHERE id = ?", (row["id"],))
            thumb_path = THUMB_DIR / f"{row['id']}.jpg"
            if thumb_path.exists():
                thumb_path.unlink()
            count += 1
        conn.commit()
        conn.close()
        return {"purged": count}

    # --- Favorites ---

    def toggle_favorite(self, owner_id: str, file_id: int) -> dict:
        conn = get_db()
        existing = conn.execute("SELECT 1 FROM favorites WHERE user_id = ? AND file_id = ?", (owner_id, file_id)).fetchone()
        if existing:
            conn.execute("DELETE FROM favorites WHERE user_id = ? AND file_id = ?", (owner_id, file_id))
            conn.commit()
            conn.close()
            return {"ok": True, "favorited": False}
        conn.execute("INSERT INTO favorites (user_id, file_id, created_at) VALUES (?, ?, ?)", (owner_id, file_id, time.time()))
        conn.commit()
        conn.close()
        return {"ok": True, "favorited": True}

    def list_favorites(self, owner_id: str, limit: int = 200) -> dict:
        conn = get_db()
        rows = conn.execute(
            """SELECT f.id, f.filename, f.original_name, f.mime_type, f.file_size, f.file_hash,
                      f.storage_key, f.date_taken, f.gps_lat, f.gps_lon, f.width, f.height, f.created_at,
                      fav.created_at as favorited_at
               FROM favorites fav JOIN files f ON f.id = fav.file_id
               WHERE fav.user_id = ? AND f.deleted_at IS NULL
               ORDER BY fav.created_at DESC LIMIT ?""",
            (owner_id, limit),
        ).fetchall()
        conn.close()
        return {"files": [dict(r) for r in rows], "total": len(rows)}

    def is_favorited(self, owner_id: str, file_id: int) -> bool:
        conn = get_db()
        row = conn.execute("SELECT 1 FROM favorites WHERE user_id = ? AND file_id = ?", (owner_id, file_id)).fetchone()
        conn.close()
        return row is not None

    # --- Timeline ---

    def timeline(self, owner_id: str, group_by: str = "day") -> dict:
        conn = get_db()
        if group_by == "month":
            fmt = "%Y-%m"
        elif group_by == "year":
            fmt = "%Y"
        else:
            fmt = "%Y-%m-%d"

        rows = conn.execute(
            """SELECT id, original_name, mime_type, file_size,
                      COALESCE(date_taken, created_at) as ts
               FROM files
               WHERE owner_id = ? AND deleted_at IS NULL
               ORDER BY ts DESC""",
            (owner_id,),
        ).fetchall()
        conn.close()

        from datetime import datetime
        groups = []
        current_key = None
        current_group = None
        for row in rows:
            d = datetime.fromtimestamp(row["ts"])
            key = d.strftime(fmt)
            if key != current_key:
                if current_group:
                    groups.append(current_group)
                current_key = key
                current_group = {
                    "date": key,
                    "count": 0,
                    "total_size": 0,
                    "cover_id": row["id"],
                    "items": [],
                }
            current_group["count"] += 1
            current_group["total_size"] += row["file_size"] or 0
            current_group["items"].append({
                "id": row["id"],
                "original_name": row["original_name"],
                "mime_type": row["mime_type"],
            })
        if current_group:
            groups.append(current_group)

        return {"groups": groups, "total_groups": len(groups)}

    def _generate_thumbnail(self, content: bytes, size: int = 400) -> bytes | None:
        try:
            from PIL import Image
            import io
            img = Image.open(io.BytesIO(content))
            img.thumbnail((size, size), Image.LANCZOS)
            if img.mode in ("RGBA", "P"):
                img = img.convert("RGB")
            buf = io.BytesIO()
            img.save(buf, format="JPEG", quality=80)
            return buf.getvalue()
        except Exception:
            return None

    # --- Device Management ---

    def register_device(self, owner_id: str, device_id: str, device_name: str = "", platform: str = "") -> dict:
        now = time.time()
        conn = get_db()
        conn.execute(
            """INSERT INTO devices (owner_id, device_id, device_name, platform, created_at, updated_at)
               VALUES (?,?,?,?,?,?)
               ON CONFLICT(device_id) DO UPDATE SET
                   device_name = excluded.device_name,
                   platform = excluded.platform,
                   updated_at = excluded.updated_at
               WHERE owner_id = excluded.owner_id""",
            (owner_id, device_id, device_name, platform, now, now),
        )
        conn.commit()
        conn.close()
        return {"device_id": device_id, "registered": True}

    def list_devices(self, owner_id: str) -> list[dict]:
        conn = get_db()
        rows = conn.execute(
            "SELECT * FROM devices WHERE owner_id = ? ORDER BY last_sync_at DESC, created_at DESC",
            (owner_id,),
        ).fetchall()
        conn.close()
        return [dict(r) for r in rows]

    # --- Manifest Sync ---

    def manifest_diff(self, owner_id: str, device_id: str, client_hashes: set[str]) -> dict:
        conn = get_db()

        server_rows = conn.execute(
            "SELECT id, filename, file_hash, file_size, original_name, updated_at FROM files WHERE owner_id = ?",
            (owner_id,),
        ).fetchall()

        server_hashes = {r["file_hash"] for r in server_rows}

        to_upload = list(client_hashes - server_hashes)
        to_download = [dict(r) for r in server_rows if r["file_hash"] not in client_hashes]
        already_synced = len(client_hashes & server_hashes)

        now = time.time()
        conn.execute(
            "UPDATE devices SET last_sync_at = ?, updated_at = ? WHERE device_id = ? AND owner_id = ?",
            (now, now, device_id, owner_id),
        )
        conn.commit()
        conn.close()

        return {
            "to_upload": to_upload,
            "to_download": to_download,
            "already_synced": already_synced,
            "server_total": len(server_rows),
        }

    # --- Sync Logging ---

    def log_sync(self, owner_id: str, device_id: str, action: str,
                 file_id: int | None = None, file_hash: str = "",
                 bytes_transferred: int = 0, status: str = "ok", error_detail: str = "") -> dict:
        conn = get_db()
        conn.execute(
            """INSERT INTO sync_log
               (device_id, owner_id, action, file_id, file_hash, bytes_transferred,
                status, error_detail, created_at)
               VALUES (?,?,?,?,?,?,?,?,?)""",
            (device_id, owner_id, action, file_id, file_hash, bytes_transferred,
             status, error_detail, time.time()),
        )
        conn.commit()
        conn.close()
        return {"ok": True}

    def sync_status(self, owner_id: str) -> dict:
        conn = get_db()
        devices = conn.execute(
            "SELECT * FROM devices WHERE owner_id = ? ORDER BY last_sync_at DESC",
            (owner_id,),
        ).fetchall()
        recent = conn.execute(
            "SELECT * FROM sync_log WHERE owner_id = ? ORDER BY created_at DESC LIMIT 20",
            (owner_id,),
        ).fetchall()
        total_files = conn.execute("SELECT COUNT(*) FROM files WHERE owner_id = ?", (owner_id,)).fetchone()[0]
        total_bytes = conn.execute(
            "SELECT COALESCE(SUM(bytes_transferred), 0) FROM sync_log WHERE owner_id = ?",
            (owner_id,),
        ).fetchone()[0]
        conn.close()
        return {
            "devices": [dict(d) for d in devices],
            "recent_activity": [dict(r) for r in recent],
            "stats": {"total_files": total_files, "total_synced_bytes": total_bytes},
        }

    # --- Cleanup ---

    def cleanup_stale(self) -> int:
        now = time.time()
        conn = get_db()
        stale = conn.execute(
            "SELECT id FROM pending_uploads WHERE expires_at < ? AND status = 'uploading'", (now,)
        ).fetchall()
        cleaned = 0
        for row in stale:
            shutil.rmtree(self._chunk_dir(row["id"]), ignore_errors=True)
            conn.execute("DELETE FROM pending_uploads WHERE id = ?", (row["id"],))
            cleaned += 1
        conn.commit()
        conn.close()
        return cleaned

    # --- Albums ---

    def create_album(self, owner_id: str, name: str, description: str = "") -> dict:
        now = time.time()
        conn = get_db()
        conn.execute(
            "INSERT INTO albums (owner_id, name, description, created_at, updated_at) VALUES (?,?,?,?,?)",
            (owner_id, name, description, now, now),
        )
        album_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
        conn.commit()
        conn.close()
        return {"ok": True, "album_id": album_id, "name": name, "description": description}

    def list_albums(self, owner_id: str) -> list[dict]:
        conn = get_db()
        rows = conn.execute(
            "SELECT * FROM albums WHERE owner_id = ? ORDER BY updated_at DESC",
            (owner_id,),
        ).fetchall()
        result = []
        for row in rows:
            album = dict(row)
            album["file_count"] = conn.execute(
                "SELECT COUNT(*) FROM album_files WHERE album_id = ?", (album["id"],)
            ).fetchone()[0]
            if album["cover_file_id"]:
                album["cover_thumb_url"] = f"/sync/files/{album['cover_file_id']}/thumb"
            else:
                first = conn.execute(
                    "SELECT file_id FROM album_files WHERE album_id = ? LIMIT 1", (album["id"],)
                ).fetchone()
                album["cover_thumb_url"] = f"/sync/files/{first['file_id']}/thumb" if first else None
            result.append(album)
        conn.close()
        return result

    def get_album(self, album_id: int, owner_id: str) -> dict | None:
        conn = get_db()
        row = conn.execute(
            "SELECT * FROM albums WHERE id = ? AND owner_id = ?", (album_id, owner_id)
        ).fetchone()
        if not row:
            conn.close()
            return None
        album = dict(row)
        file_rows = conn.execute(
            """SELECT f.id, f.filename, f.original_name, f.mime_type, f.file_size, f.created_at
               FROM files f
               JOIN album_files af ON af.file_id = f.id
               WHERE af.album_id = ?
               ORDER BY af.added_at DESC""",
            (album_id,),
        ).fetchall()
        album["files"] = [dict(r) for r in file_rows]
        conn.close()
        return album

    def update_album(self, album_id: int, owner_id: str, name: str | None = None,
                     description: str | None = None, cover_file_id: int | None = None) -> dict:
        conn = get_db()
        row = conn.execute(
            "SELECT * FROM albums WHERE id = ? AND owner_id = ?", (album_id, owner_id)
        ).fetchone()
        if not row:
            conn.close()
            return {"error": "Album not found"}
        new_name = name if name is not None else row["name"]
        new_desc = description if description is not None else row["description"]
        new_cover = cover_file_id if cover_file_id is not None else row["cover_file_id"]
        now = time.time()
        conn.execute(
            "UPDATE albums SET name=?, description=?, cover_file_id=?, updated_at=? WHERE id=?",
            (new_name, new_desc, new_cover, now, album_id),
        )
        conn.commit()
        conn.close()
        return {"ok": True, "album_id": album_id, "name": new_name}

    def delete_album(self, album_id: int, owner_id: str) -> dict:
        conn = get_db()
        row = conn.execute(
            "SELECT id FROM albums WHERE id = ? AND owner_id = ?", (album_id, owner_id)
        ).fetchone()
        if not row:
            conn.close()
            return {"error": "Album not found"}
        conn.execute("DELETE FROM album_files WHERE album_id = ?", (album_id,))
        conn.execute("DELETE FROM albums WHERE id = ?", (album_id,))
        conn.commit()
        conn.close()
        return {"ok": True}

    def add_files_to_album(self, album_id: int, owner_id: str, file_ids: list[int]) -> dict:
        conn = get_db()
        row = conn.execute(
            "SELECT id FROM albums WHERE id = ? AND owner_id = ?", (album_id, owner_id)
        ).fetchone()
        if not row:
            conn.close()
            return {"error": "Album not found"}
        now = time.time()
        added = 0
        for fid in file_ids:
            exists = conn.execute(
                "SELECT id FROM files WHERE id = ? AND owner_id = ?", (fid, owner_id)
            ).fetchone()
            if exists:
                conn.execute(
                    "INSERT OR IGNORE INTO album_files (album_id, file_id, added_at) VALUES (?,?,?)",
                    (album_id, fid, now),
                )
                added += 1
        conn.execute("UPDATE albums SET updated_at = ? WHERE id = ?", (now, album_id))
        conn.commit()
        conn.close()
        return {"ok": True, "added": added}

    def remove_files_from_album(self, album_id: int, owner_id: str, file_ids: list[int]) -> dict:
        conn = get_db()
        row = conn.execute(
            "SELECT id FROM albums WHERE id = ? AND owner_id = ?", (album_id, owner_id)
        ).fetchone()
        if not row:
            conn.close()
            return {"error": "Album not found"}
        for fid in file_ids:
            conn.execute(
                "DELETE FROM album_files WHERE album_id = ? AND file_id = ?", (album_id, fid)
            )
        conn.execute("UPDATE albums SET updated_at = ? WHERE id = ?", (time.time(), album_id))
        conn.commit()
        conn.close()
        return {"ok": True, "removed": len(file_ids)}

    # --- Sharing ---

    def create_shared_link(self, owner_id: str, link_type: str, target_id: int,
                           password: str = "", expires_hours: int = 0,
                           allow_download: bool = True) -> dict:
        if link_type not in ("file", "album"):
            return {"error": "link_type must be 'file' or 'album'"}
        conn = get_db()
        if link_type == "file":
            exists = conn.execute(
                "SELECT id FROM files WHERE id = ? AND owner_id = ?", (target_id, owner_id)
            ).fetchone()
        else:
            exists = conn.execute(
                "SELECT id FROM albums WHERE id = ? AND owner_id = ?", (target_id, owner_id)
            ).fetchone()
        if not exists:
            conn.close()
            return {"error": f"{link_type.capitalize()} not found"}
        link_id = str(uuid.uuid4())
        password_hash = ""
        if password:
            import hashlib
            password_hash = hashlib.sha256(password.encode()).hexdigest()
        now = time.time()
        expires_at = now + expires_hours * 3600 if expires_hours > 0 else None
        conn.execute(
            """INSERT INTO shared_links
               (id, owner_id, link_type, target_id, password_hash, expires_at,
                allow_download, view_count, created_at)
               VALUES (?,?,?,?,?,?,?,0,?)""",
            (link_id, owner_id, link_type, target_id, password_hash,
             expires_at, 1 if allow_download else 0, now),
        )
        conn.commit()
        conn.close()
        return {"ok": True, "link_id": link_id, "link_type": link_type, "target_id": target_id}

    def get_shared_content(self, link_id: str, password: str = "") -> dict:
        conn = get_db()
        row = conn.execute("SELECT * FROM shared_links WHERE id = ?", (link_id,)).fetchone()
        if not row:
            conn.close()
            return {"error": "Link not found"}
        link = dict(row)
        if link["expires_at"] and time.time() > link["expires_at"]:
            conn.close()
            return {"error": "Link expired"}
        if link["password_hash"]:
            pw_hash = hashlib.sha256(password.encode()).hexdigest()
            if not hmac.compare_digest(pw_hash, link["password_hash"]):
                conn.close()
                return {"error": "Invalid password"}
        conn.execute(
            "UPDATE shared_links SET view_count = view_count + 1 WHERE id = ?", (link_id,)
        )
        conn.commit()
        if link["link_type"] == "file":
            file_row = conn.execute(
                "SELECT id, filename, original_name, mime_type, file_size, created_at FROM files WHERE id = ?",
                (link["target_id"],),
            ).fetchone()
            conn.close()
            if not file_row:
                return {"error": "File not found"}
            return {
                "link_type": "file",
                "allow_download": bool(link["allow_download"]),
                "file": dict(file_row),
            }
        album_row = conn.execute(
            "SELECT * FROM albums WHERE id = ?", (link["target_id"],)
        ).fetchone()
        if not album_row:
            conn.close()
            return {"error": "Album not found"}
        album = dict(album_row)
        file_rows = conn.execute(
            """SELECT f.id, f.filename, f.original_name, f.mime_type, f.file_size, f.created_at
               FROM files f
               JOIN album_files af ON af.file_id = f.id
               WHERE af.album_id = ?
               ORDER BY af.added_at DESC""",
            (album["id"],),
        ).fetchall()
        album["files"] = [dict(r) for r in file_rows]
        conn.close()
        return {
            "link_type": "album",
            "allow_download": bool(link["allow_download"]),
            "album": album,
        }

    def delete_shared_link(self, link_id: str, owner_id: str) -> dict:
        conn = get_db()
        row = conn.execute(
            "SELECT id FROM shared_links WHERE id = ? AND owner_id = ?", (link_id, owner_id)
        ).fetchone()
        if not row:
            conn.close()
            return {"error": "Link not found"}
        conn.execute("DELETE FROM shared_links WHERE id = ?", (link_id,))
        conn.commit()
        conn.close()
        return {"ok": True}

    def list_shared_links(self, owner_id: str) -> list[dict]:
        conn = get_db()
        rows = conn.execute(
            "SELECT * FROM shared_links WHERE owner_id = ? ORDER BY created_at DESC",
            (owner_id,),
        ).fetchall()
        conn.close()
        return [dict(r) for r in rows]

    # --- Internal ---

    @staticmethod
    def _validate_uuid(value: str) -> bool:
        return bool(re.match(r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', value))

    def _chunk_dir(self, upload_id: str) -> Path:
        if not self._validate_uuid(upload_id):
            raise ValueError(f"Invalid upload_id format: {upload_id}")
        CHUNK_DIR.mkdir(parents=True, exist_ok=True)
        return CHUNK_DIR / upload_id

    def _received_indexes(self, upload_id: str) -> list[int]:
        d = self._chunk_dir(upload_id)
        if not d.exists():
            return []
        return sorted(int(f.name) for f in d.iterdir() if f.is_file() and f.name.isdigit())
