# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Memory, files, pins, downloads, macros routes."""

from __future__ import annotations

import json
import logging
import os
import threading
from datetime import datetime
from pathlib import Path

from flask import Blueprint, Response, jsonify, request, send_file

from ..auth import require_auth
from ..helpers import get_agent, get_service_manager, DATA_DIR, safe_error

logger = logging.getLogger(__name__)

bp = Blueprint("memory", __name__)

# Local file-store singleton — mirrors mesh.py pattern, avoids circular import.
_file_store_instance = None
_file_store_init_lock = threading.Lock()


def _get_file_store():
    global _file_store_instance
    if _file_store_instance is None:
        with _file_store_init_lock:
            if _file_store_instance is None:
                from familiar.core.files import get_file_store
                _file_store_instance = get_file_store()
    return _file_store_instance


# ── Working Memory API ──


@bp.route("/api/memory/working")
@require_auth
def api_memory_working():
    """Get the agent's working memory buffer."""
    agent = get_agent()
    items = []
    try:
        mm = agent._memory_manager
        # Try to get the default user's episodic memory system
        for uid, ms in mm._episodic_stores.items():
            wm = ms.working_memory
            for m in wm.get_all():
                items.append(
                    {
                        "id": m.id,
                        "content": m.content,
                        "type": m.memory_type.value
                        if hasattr(m.memory_type, "value")
                        else str(m.memory_type),
                        "importance": round(m.importance, 2),
                        "created_at": m.created_at.isoformat()
                        if hasattr(m.created_at, "isoformat")
                        else str(m.created_at),
                        "source": m.source or "",
                        "tags": list(m.tags) if m.tags else [],
                        "status": m.status.value if hasattr(m.status, "value") else str(m.status),
                    }
                )
            break  # First user only (dashboard is single-user)
    except Exception:
        pass
    return jsonify({"items": items, "count": len(items)})


# ── Downloads Shelf API ──

_downloads_shelf: list = []  # In-memory list; persists per process lifetime
_downloads_lock = threading.Lock()


@bp.route("/api/downloads")
@require_auth
def api_downloads_list():
    """List generated downloads."""
    with _downloads_lock:
        return jsonify({"downloads": list(_downloads_shelf)})


@bp.route("/api/downloads/track", methods=["POST"])
@require_auth
def api_downloads_track():
    """Track a newly generated download."""
    import uuid
    from datetime import datetime, timezone

    data = request.get_json(silent=True) or {}
    entry = {
        "id": str(uuid.uuid4())[:8],
        "filename": data.get("filename", "download"),
        "url": data.get("url", ""),
        "type": data.get("type", "file"),  # file, csv, pdf, chart, zip
        "source": data.get("source", ""),  # which endpoint generated it
        "created_at": datetime.now(timezone.utc).isoformat(),
        "size_bytes": data.get("size_bytes", 0),
    }
    with _downloads_lock:
        _downloads_shelf.insert(0, entry)
        # Keep last 50
        if len(_downloads_shelf) > 50:
            _downloads_shelf[:] = _downloads_shelf[:50]
    return jsonify({"download": entry}), 201


@bp.route("/api/downloads/<dl_id>", methods=["DELETE"])
@require_auth
def api_downloads_delete(dl_id):
    """Remove a download from the shelf."""
    with _downloads_lock:
        before = len(_downloads_shelf)
        _downloads_shelf[:] = [d for d in _downloads_shelf if d["id"] != dl_id]
        found = len(_downloads_shelf) < before
    if found:
        return jsonify({"status": "ok"})
    return jsonify({"error": "Not found"}), 404


# ── Pins API (cross-tab favorites) ──

_pins_db_path = None


def _get_pins_db():
    """Get the pins SQLite database path, creating if needed."""
    global _pins_db_path
    if _pins_db_path is None:
        from familiar.core.paths import DATA_DIR

        _pins_db_path = DATA_DIR / "pins.db"
    import sqlite3

    conn = sqlite3.connect(str(_pins_db_path))
    conn.execute("""
        CREATE TABLE IF NOT EXISTS pins (
            id TEXT PRIMARY KEY,
            pin_type TEXT NOT NULL,
            title TEXT NOT NULL,
            reference TEXT NOT NULL DEFAULT '',
            namespace TEXT NOT NULL DEFAULT '',
            created_at TEXT NOT NULL,
            UNIQUE(pin_type, reference)
        )
    """)
    conn.commit()
    return conn


@bp.route("/api/pins")
@require_auth
def api_pins_list():
    """List all pinned items."""
    conn = _get_pins_db()
    rows = conn.execute(
        "SELECT id, pin_type, title, reference, namespace, created_at FROM pins ORDER BY created_at DESC"
    ).fetchall()
    conn.close()
    return jsonify(
        {
            "pins": [
                {
                    "id": r[0],
                    "pin_type": r[1],
                    "title": r[2],
                    "reference": r[3],
                    "namespace": r[4],
                    "created_at": r[5],
                }
                for r in rows
            ]
        }
    )


@bp.route("/api/pins", methods=["POST"])
@require_auth
def api_pins_add():
    """Pin an item."""
    import uuid
    from datetime import datetime, timezone

    data = request.get_json(silent=True) or {}
    pin_type = data.get("pin_type", "")  # episodic, markdown, file, note
    title = data.get("title", "")
    reference = data.get("reference", "")  # key, slug, file_id
    namespace = data.get("namespace", "")

    if not pin_type or not title:
        return jsonify({"error": "pin_type and title required"}), 400

    conn = _get_pins_db()
    pin_id = str(uuid.uuid4())[:8]
    try:
        conn.execute(
            "INSERT INTO pins (id, pin_type, title, reference, namespace, created_at) VALUES (?, ?, ?, ?, ?, ?)",
            (pin_id, pin_type, title, reference, namespace, datetime.now(timezone.utc).isoformat()),
        )
        conn.commit()
    except Exception:
        conn.close()
        return jsonify({"error": "Already pinned"}), 409
    conn.close()
    return jsonify({"id": pin_id}), 201


@bp.route("/api/pins/<pin_id>", methods=["DELETE"])
@require_auth
def api_pins_delete(pin_id):
    """Unpin an item."""
    conn = _get_pins_db()
    cur = conn.execute("DELETE FROM pins WHERE id = ?", (pin_id,))
    conn.commit()
    conn.close()
    if cur.rowcount:
        return jsonify({"status": "ok"})
    return jsonify({"error": "Not found"}), 404


@bp.route("/api/pins/check")
@require_auth
def api_pins_check():
    """Check if an item is pinned. Pass pin_type and reference as query params."""
    pin_type = request.args.get("pin_type", "")
    reference = request.args.get("reference", "")
    conn = _get_pins_db()
    row = conn.execute(
        "SELECT id FROM pins WHERE pin_type = ? AND reference = ?",
        (pin_type, reference),
    ).fetchone()
    conn.close()
    return jsonify({"pinned": row is not None, "pin_id": row[0] if row else None})


# ── Markdown Memory (Knowledge + Job-Class Notes) API ──

_md_store_cache: dict = {}
_md_store_lock = threading.Lock()


def _get_md_store(namespace: str = ""):
    """Get a MarkdownMemoryStore, optionally namespaced for job-class notes."""
    cache_key = namespace or "__root__"
    if cache_key not in _md_store_cache:
        with _md_store_lock:
            if cache_key not in _md_store_cache:
                from familiar.core.markdown_memory import MarkdownMemoryStore
                from familiar.core.paths import MEMORIES_DIR

                user_id = "dashboard"
                if namespace:
                    # Job-class notes live under _namespace/ subdir
                    store_dir = MEMORIES_DIR / user_id / f"_{namespace}"
                    store_dir.mkdir(parents=True, exist_ok=True)
                    # Create store with the subdir as its base
                    store = MarkdownMemoryStore.__new__(MarkdownMemoryStore)
                    store._base = store_dir
                    store._lock = threading.RLock()
                    store._alias_index = {}
                    store._rebuild_alias_index()
                else:
                    store = MarkdownMemoryStore(MEMORIES_DIR, user_id)
                _md_store_cache[cache_key] = store
    return _md_store_cache[cache_key]


@bp.route("/api/memories/markdown")
@require_auth
def api_md_memories_list():
    """List markdown memory topics."""
    namespace = request.args.get("namespace", "")
    store = _get_md_store(namespace)
    limit = int(request.args.get("limit", 50))
    topics = store.list_topics(limit=limit)
    return jsonify({"topics": topics, "namespace": namespace})


@bp.route("/api/memories/markdown", methods=["POST"])
@require_auth
def api_md_memories_save():
    """Create or append to a markdown memory topic."""
    data = request.get_json(silent=True) or {}
    topic = data.get("topic", "").strip()
    content = data.get("content", "").strip()
    namespace = data.get("namespace", "")

    if not topic or not content:
        return jsonify({"error": "topic and content are required"}), 400

    store = _get_md_store(namespace)
    tags = data.get("tags", [])
    heading = data.get("heading", "")
    slug, rev = store.save_memory(
        topic=topic,
        content=content,
        tags=tags,
        heading=heading,
    )
    return jsonify({"slug": slug, "revision": rev}), 201


@bp.route("/api/memories/markdown/<slug>")
@require_auth
def api_md_memories_read(slug):
    """Read a markdown memory topic."""
    namespace = request.args.get("namespace", "")
    store = _get_md_store(namespace)
    result = store.read_memory(slug)
    if result is None:
        return jsonify({"error": "Topic not found"}), 404
    meta, body = result
    return jsonify(
        {
            "slug": slug,
            "topic": meta.topic if meta else slug,
            "body": body,
            "tags": meta.tags if meta else [],
            "revision": meta.revision if meta else 1,
            "created": meta.created if meta else "",
            "updated": meta.updated if meta else "",
        }
    )


@bp.route("/api/memories/markdown/<slug>", methods=["DELETE"])
@require_auth
def api_md_memories_delete(slug):
    """Delete a markdown memory topic."""
    namespace = request.args.get("namespace", "")
    store = _get_md_store(namespace)
    if store.delete_memory(slug):
        return jsonify({"status": "ok"})
    return jsonify({"error": "Topic not found"}), 404


@bp.route("/api/memories/markdown/search")
@require_auth
def api_md_memories_search():
    """Search markdown memories."""
    q = request.args.get("q", "")
    namespace = request.args.get("namespace", "")
    store = _get_md_store(namespace)
    results = store.search(q, limit=20)
    return jsonify(
        {
            "results": [
                {"slug": slug, "score": score, "preview": preview}
                for slug, score, preview in results
            ]
        }
    )


# ── Memory Tree API ──


def _get_memory_tree():
    """Get the MemoryTree singleton."""
    from familiar.core.memory_tree import get_memory_tree

    return get_memory_tree()


@bp.route("/api/memory/tree")
@require_auth
def api_memory_tree_list():
    """List memories from the MemoryTree, filtered by tier/tag."""
    tier = request.args.get("tier", "working")
    tag = request.args.get("tag", "")
    limit = min(int(request.args.get("limit", 50)), 200)

    tree = _get_memory_tree()

    if tag:
        entries = tree.list_by_tag(tag, tier=tier if tier != "all" else None, limit=limit)
    elif tier == "all":
        entries = []
        for t in ["working", "short", "long", "archival"]:
            entries.extend(tree.list_by_tier(t, limit=limit))
    else:
        entries = tree.list_by_tier(tier, limit=limit)

    return jsonify(
        {
            "entries": [e.to_dict() for e in entries],
            "tier": tier,
            "stats": tree.stats(),
        }
    )


@bp.route("/api/memory/tree/<memory_id>")
@require_auth
def api_memory_tree_get(memory_id):
    """Get a single memory by ID."""
    tree = _get_memory_tree()
    entry = tree.get(memory_id)
    if not entry:
        return jsonify({"error": "Memory not found"}), 404
    return jsonify({"entry": entry.to_dict()})


@bp.route("/api/memory/tree", methods=["POST"])
@require_auth
def api_memory_tree_store():
    """Store a new memory in the MemoryTree."""
    data = request.json or {}
    title = data.get("title", "").strip()
    body = data.get("body", "").strip()
    tier = data.get("tier", "working")
    tags = data.get("tags", [])
    importance = float(data.get("importance", 0.5))

    if not title:
        return jsonify({"error": "Title required"}), 400

    tree = _get_memory_tree()
    entry = tree.store(
        title=title,
        body=body,
        tier=tier,
        tags=tags,
        importance=importance,
    )
    return jsonify({"entry": entry.to_dict()}), 201


@bp.route("/api/memory/tree/<memory_id>", methods=["PUT"])
@require_auth
def api_memory_tree_update(memory_id):
    """Update an existing memory."""
    data = request.json or {}
    tree = _get_memory_tree()

    entry = tree.update(
        memory_id,
        title=data.get("title"),
        body=data.get("body"),
        importance=data.get("importance"),
        tier=data.get("tier"),
        tags=data.get("tags"),
    )
    if not entry:
        return jsonify({"error": "Memory not found"}), 404
    return jsonify({"entry": entry.to_dict()})


@bp.route("/api/memory/tree/<memory_id>", methods=["DELETE"])
@require_auth
def api_memory_tree_delete(memory_id):
    """Soft-delete (invalidate) a memory."""
    data = request.json or {}
    reason = data.get("reason", "")
    tree = _get_memory_tree()

    success = tree.invalidate(memory_id, reason=reason)
    if success:
        return jsonify({"status": "ok"})
    return jsonify({"error": "Memory not found or already invalidated"}), 404


@bp.route("/api/memory/tree/search")
@require_auth
def api_memory_tree_search():
    """Search the MemoryTree via FTS5."""
    q = request.args.get("q", "").strip()
    tier = request.args.get("tier")
    limit = min(int(request.args.get("limit", 20)), 100)

    if not q:
        return jsonify({"error": "Query parameter 'q' required"}), 400

    tree = _get_memory_tree()
    results = tree.search(q, tier=tier, limit=limit)
    return jsonify(
        {
            "query": q,
            "results": [e.to_dict() for e in results],
            "total": len(results),
        }
    )


@bp.route("/api/memory/consolidation/status")
@require_auth
def api_memory_consolidation_status():
    """Get memory consolidation bridge status."""
    try:
        from familiar.core.consolidation_bridge import ConsolidationBridge

        bridge = ConsolidationBridge(tree=_get_memory_tree())
        status = bridge.get_status()
        phi_stats = bridge.get_phi_stats()
        return jsonify(
            {
                "last_run": status.last_run,
                "encryption_available": status.encryption_available,
                "phi_scrubbing_available": status.phi_scrubbing_available,
                "phi_stats": phi_stats,
                "results": {
                    source: {
                        "added": r.added,
                        "updated": r.updated,
                        "noops": r.noops,
                        "errors": r.errors,
                        "duration_ms": round(r.duration_ms, 1),
                    }
                    for source, r in status.results.items()
                },
            }
        )
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/memory/consolidation/log")
@require_auth
def api_memory_consolidation_log():
    """Get recent memory consolidation log entries."""
    try:
        from familiar.core.consolidation_bridge import ConsolidationBridge

        limit = min(int(request.args.get("limit", 50)), 200)
        source = request.args.get("source")
        bridge = ConsolidationBridge(tree=_get_memory_tree())
        log = bridge.get_log(limit=limit, source=source)
        return jsonify({"entries": log, "total": len(log)})
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/memory/consolidation/run", methods=["POST"])
@require_auth
def api_memory_consolidation_run():
    """Trigger a manual consolidation run."""
    try:
        from familiar.core.consolidation_bridge import ConsolidationBridge

        bridge = ConsolidationBridge(tree=_get_memory_tree())
        results = bridge.consolidate_all()
        return jsonify(
            {
                "status": "completed",
                "results": {
                    source: {
                        "added": r.added,
                        "updated": r.updated,
                        "noops": r.noops,
                        "errors": r.errors,
                    }
                    for source, r in results.items()
                },
            }
        )
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/memory/consolidation/hipaa-audit")
@require_auth
def api_memory_consolidation_hipaa_audit():
    """Generate a HIPAA compliance audit report for the consolidation pipeline."""
    try:
        from familiar.core.consolidation_bridge import ConsolidationBridge

        bridge = ConsolidationBridge(tree=_get_memory_tree())
        report = bridge.generate_hipaa_audit_report()
        return jsonify(report)
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/memory/tree/full-graph")
@require_auth
def api_memory_tree_full_graph():
    """Return the full memory graph for force-directed visualization.

    Returns nodes with tier/type/importance and typed edges with relations.
    Used by the MemoryGraph canvas component.
    """
    tree = _get_memory_tree()
    try:
        limit = min(int(request.args.get("limit", 500)), 2000)

        # Get memories from all tiers (MemoryTree has no list(), use list_by_tier)
        all_entries = []
        for tier in ("working", "short", "long", "archival"):
            try:
                entries = tree.list_by_tier(tier, limit=limit)
                all_entries.extend(entries)
            except Exception:
                pass
        # Cap at limit
        all_entries = all_entries[:limit]
        node_ids = {e.memory_id for e in all_entries}

        # Get all edges between these nodes
        edges = []
        try:
            conn = tree._connect()
            rows = conn.execute(
                "SELECT from_id, to_id, relation_type, weight FROM memory_edges"
            ).fetchall()
            for row in rows:
                if row["from_id"] in node_ids and row["to_id"] in node_ids:
                    edges.append({
                        "source": row["from_id"],
                        "target": row["to_id"],
                        "relation": row["relation_type"],
                        "weight": row["weight"],
                    })
        except Exception:
            pass

        nodes = []
        for e in all_entries:
            d = e.to_dict()
            d["importance"] = getattr(e, "importance", 0.5)
            # Derive memory_type from tags or source
            tags = d.get("tags", [])
            meta = d.get("meta", {})
            if isinstance(meta, dict):
                mt = meta.get("category", "")
            else:
                mt = ""
            if not mt:
                if "fact" in tags:
                    mt = "fact"
                elif "goal" in tags or "task" in tags:
                    mt = "goal"
                elif "decision" in tags:
                    mt = "decision"
                elif "question" in tags:
                    mt = "question"
                elif "observation" in tags or "brain" in tags:
                    mt = "observation"
                else:
                    source = d.get("source", "")
                    if "episodic" in source:
                        mt = "context"
                    elif "extracted" in source:
                        mt = "observation"
                    else:
                        mt = "fact"
            d["memory_type"] = mt
            nodes.append(d)

        # Compute stats from processed nodes
        tier_counts = {}
        type_counts = {}
        for n in nodes:
            tier_counts[n.get("tier", "?")] = tier_counts.get(n.get("tier", "?"), 0) + 1
            type_counts[n.get("memory_type", "?")] = type_counts.get(n.get("memory_type", "?"), 0) + 1

        return jsonify({
            "nodes": nodes,
            "edges": edges,
            "stats": {
                "total_nodes": len(nodes),
                "total_edges": len(edges),
                "tiers": tier_counts,
                "types": type_counts,
            },
        })
    except Exception as ex:
        return jsonify({"nodes": [], "edges": [], "stats": {}, "error": str(ex)})


@bp.route("/api/memory/tree/<memory_id>/graph")
@require_auth
def api_memory_tree_graph(memory_id):
    """Get the subgraph around a memory (for graph visualization)."""
    depth = min(int(request.args.get("depth", 2)), 5)
    tree = _get_memory_tree()

    entry = tree.get(memory_id)
    if not entry:
        return jsonify({"error": "Memory not found"}), 404

    subgraph = tree.get_subgraph(memory_id, depth=depth)
    return jsonify(
        {
            "nodes": [n.to_dict() for n in subgraph["nodes"]],
            "edges": subgraph["edges"],
        }
    )


@bp.route("/api/memory/tree/<memory_id>/history")
@require_auth
def api_memory_tree_history(memory_id):
    """Get the version history for a memory (SUPERSEDES chain)."""
    tree = _get_memory_tree()

    entry = tree.get(memory_id)
    if not entry:
        return jsonify({"error": "Memory not found"}), 404

    history = tree.get_history(memory_id)
    return jsonify(
        {
            "history": [e.to_dict() for e in history],
            "current_id": memory_id,
        }
    )


@bp.route("/api/files/tree")
@require_auth
def api_files_tree():
    """Get files grouped by job_class then record_type."""
    store = _get_file_store()
    job_class = request.args.get("job_class")

    if job_class:
        files = store.list_for_job_class(job_class)
    else:
        files = store.search(limit=500)

    tree: dict = {}
    for f in files:
        jc = f.job_class
        rt = f.record_type
        if jc not in tree:
            tree[jc] = {}
        if rt not in tree[jc]:
            tree[jc][rt] = []
        tree[jc][rt].append(f.to_dict())

    return jsonify({"tree": tree})


@bp.route("/api/memory/search")
@require_auth
def api_memory_unified_search():
    """Unified search across episodic memories, markdown topics, and files."""
    q = request.args.get("q", "").strip()
    if not q:
        return jsonify({"error": "Query parameter 'q' is required"}), 400

    results = []

    # 1. Episodic memories
    try:
        agent = get_agent()
        if hasattr(agent, "memory") and agent.memory:
            memories = agent.memory
            for m in memories:
                key = m.get("key", "")
                value = m.get("value", "")
                if q.lower() in key.lower() or q.lower() in value.lower():
                    results.append(
                        {
                            "type": "episodic",
                            "title": key,
                            "preview": value[:200],
                            "category": m.get("category", ""),
                            "source": "brain",
                        }
                    )
    except Exception:
        pass

    # 2. Markdown knowledge
    try:
        md_store = _get_md_store("")
        md_results = md_store.search(q, limit=10)
        for slug, score, preview in md_results:
            results.append(
                {
                    "type": "markdown",
                    "title": slug,
                    "preview": preview[:200] if preview else "",
                    "score": score,
                    "source": "knowledge",
                }
            )
    except Exception:
        pass

    # 3. MemoryTree (FTS5)
    try:
        tree = _get_memory_tree()
        tree_results = tree.search(q, limit=10)
        for entry in tree_results:
            results.append(
                {
                    "type": "memory_tree",
                    "title": entry.title,
                    "preview": entry.body[:200] if entry.body else "",
                    "tier": entry.tier,
                    "importance": entry.importance,
                    "memory_id": entry.memory_id,
                    "source": "memory_tree",
                }
            )
    except Exception:
        pass

    # 4. Files
    try:
        file_store = _get_file_store()
        files = file_store.search(query=q, limit=10)
        for f in files:
            results.append(
                {
                    "type": "file",
                    "title": f.filename,
                    "preview": f"{f.job_class}/{f.record_type} · {f.mime_type}",
                    "file_id": f.file_id,
                    "source": "files",
                }
            )
    except Exception:
        pass

    return jsonify({"query": q, "results": results, "total": len(results)})


@bp.route("/api/downloads/tome")
@require_auth
def api_downloads_tome():
    """Download the user guide as PDF (or HTML fallback)."""
    try:
        from familiar.dashboard.guide import generate_guide_pdf

        pdf_path = generate_guide_pdf()
        return send_file(
            str(pdf_path),
            as_attachment=True,
            download_name="familiar_user_guide.pdf",
            mimetype="application/pdf",
        )
    except ImportError:
        # WeasyPrint not installed — serve HTML fallback
        from familiar.dashboard.guide import get_guide_html

        return Response(get_guide_html(), mimetype="text/html")
    except Exception:
        logger.exception("Failed to generate guide PDF")
        # Fallback to HTML
        try:
            from familiar.dashboard.guide import get_guide_html

            return Response(get_guide_html(), mimetype="text/html")
        except Exception:
            return jsonify({"error": "Guide generation failed"}), 500


@bp.route("/api/downloads/bundle")
@require_auth
def api_downloads_bundle():
    """Download a config-only backup archive."""
    import shutil
    import tempfile

    try:
        from familiar.core.backup import BackupManager, BackupType

        manager = BackupManager()
        manifest = manager.create_backup(backup_type=BackupType.CONFIG_ONLY)

        # The backup file is in the manager's backup dir
        backup_path = manager.config.path / f"{manifest.backup_id}.tar.gz"
        if not backup_path.exists():
            return jsonify({"error": "Backup file not found"}), 500

        # Copy to temp file for serving (so we don't hold a lock)
        fd, tmp_path = tempfile.mkstemp(suffix=".tar.gz")
        os.close(fd)
        shutil.copy2(str(backup_path), tmp_path)

        return send_file(
            tmp_path,
            as_attachment=True,
            download_name=f"familiar_config_{manifest.backup_id}.tar.gz",
            mimetype="application/gzip",
        )
    except Exception:
        logger.exception("Failed to create config backup")
        return jsonify({"error": "Backup creation failed"}), 500


# ============================================================
# Marketplace
# ============================================================


