"""
RAG Permanence v2 - Cloudflare R2 cold storage via REST API.
Hot (Redis) → Warm (local 7-day cache) → Cold (R2 permanent).

R2 free tier: 10GB storage, 10M Class A ops/month, zero egress.
Uses CF REST API with Bearer token - no S3 credentials needed.
"""

import json
import logging
import os
from datetime import UTC, datetime, timedelta
from pathlib import Path

import httpx

logger = logging.getLogger(__name__)

# ═══════════════════════════════════════════════════════════════
# CONFIG
# ═══════════════════════════════════════════════════════════════
R2_ACCOUNT_ID = os.getenv("R2_ACCOUNT_ID", "8f9bd9165c1250b426c66dc1967deefd")
R2_BUCKET = os.getenv("R2_BUCKET", "rag-backup")
R2_API_TOKEN = os.getenv("R2_API_TOKEN", "")
R2_BASE = f"https://api.cloudflare.com/client/v4/accounts/{R2_ACCOUNT_ID}/r2/buckets/{R2_BUCKET}"

LOCAL_CACHE = Path(os.getenv("RAG_LOCAL_CACHE", "/data/rag-storage"))
LOCAL_RETENTION_DAYS = 7
EMBEDDING_VERSION = os.getenv("EMBEDDING_MODEL_VERSION", "v1")

LOCAL_CACHE.mkdir(parents=True, exist_ok=True)


def _headers():
    return {"Authorization": f"Bearer {R2_API_TOKEN}", "Content-Type": "application/json"}


# ═══════════════════════════════════════════════════════════════
# R2 REST API OPERATIONS
# ═══════════════════════════════════════════════════════════════
async def _r2_put(key: str, data: bytes, content_type: str = "application/json") -> bool:
    """Upload an object to R2 via REST API."""
    if not R2_API_TOKEN:
        logger.error("R2_API_TOKEN not set")
        return False
    url = f"{R2_BASE}/objects/{key}"
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            resp = await client.put(
                url,
                content=data,
                headers={"Authorization": f"Bearer {R2_API_TOKEN}", "Content-Type": content_type},
            )
            if resp.status_code == 200:
                return True
            logger.error(f"R2 PUT {key}: {resp.status_code} {resp.text[:200]}")
            return False
        except Exception as e:
            logger.error(f"R2 PUT {key}: {e}")
            return False


async def _r2_get(key: str) -> bytes | None:
    """Download an object from R2 via REST API."""
    if not R2_API_TOKEN:
        return None
    url = f"{R2_BASE}/objects/{key}"
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            resp = await client.get(url, headers=_headers())
            if resp.status_code == 200:
                return resp.content
            return None
        except Exception as e:
            logger.error(f"R2 GET {key}: {e}")
            return None


async def _r2_list(prefix: str = "") -> list:
    """List objects in R2 bucket."""
    if not R2_API_TOKEN:
        return []
    url = f"{R2_BASE}/objects"
    if prefix:
        url += f"?prefix={prefix}"
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            resp = await client.get(url, headers=_headers())
            if resp.status_code == 200:
                data = resp.json()
                return data.get("result", [])
            return []
        except Exception as e:
            logger.error(f"R2 LIST: {e}")
            return []


async def _r2_delete(key: str) -> bool:
    """Delete an object from R2."""
    if not R2_API_TOKEN:
        return False
    url = f"{R2_BASE}/objects/{key}"
    async with httpx.AsyncClient(timeout=30) as client:
        try:
            resp = await client.delete(url, headers=_headers())
            return resp.status_code in (200, 204, 404)
        except Exception as e:
            logger.error(f"R2 DELETE {key}: {e}")
            return False


# ═══════════════════════════════════════════════════════════════
# RAG PERSISTENCE OPERATIONS
# ═══════════════════════════════════════════════════════════════
async def snapshot_all():
    """Snapshot all RAG collections to R2. Returns per-collection status."""
    results = {}
    ts = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")

    # Get collections from rag_service (Redis-backed)
    try:
        from app.rag_service import COLLECTIONS, _get_redis

        redis = await _get_redis()
        has_redis = True
    except Exception:
        COLLECTIONS = []
        has_redis = False

    for name in COLLECTIONS:
        try:
            # Get document IDs from Redis
            if not has_redis:
                results[name] = {"collection": name, "status": "skipped", "count": 0}
                continue

            doc_ids = await redis.smembers(f"rag:idx:{name}")
            count = len(doc_ids) if doc_ids else 0
            if count == 0:
                results[name] = {"collection": name, "status": "empty", "count": 0}
                continue

            # Collect all documents for this collection
            # Redis stores docs as strings at rag:{collection}:{id}, NOT as hashes
            items = []
            for doc_id in doc_ids:
                try:
                    raw = await redis.get(f"rag:{name}:{doc_id}")
                    if raw:
                        doc = json.loads(raw) if isinstance(raw, str) else raw
                        items.append(doc)
                except Exception:
                    pass

            # Upload in chunks if payload is large (R2 REST limit ~5GB but we chunk at 5MB)
            MAX_CHUNK_SIZE = 5 * 1024 * 1024  # 5MB per chunk
            all_items = items
            chunk_idx = 0
            total_uploaded = 0

            while all_items:
                # Build chunk - add items until we exceed size limit
                chunk_items = []
                chunk_size = 0
                while all_items and chunk_size < MAX_CHUNK_SIZE:
                    item = all_items.pop(0)
                    item_json = json.dumps(item, default=str)
                    if chunk_size + len(item_json.encode()) > MAX_CHUNK_SIZE and chunk_items:
                        # Put it back and upload this chunk
                        all_items.insert(0, item)
                        break
                    chunk_items.append(item)
                    chunk_size += len(item_json.encode())

                payload = json.dumps(
                    {
                        "collection": name,
                        "version": EMBEDDING_VERSION,
                        "timestamp": ts,
                        "chunk": chunk_idx,
                        "count": len(chunk_items),
                        "items": chunk_items,
                    },
                    default=str,
                ).encode()

                key = f"rag_snapshots/{name}/{ts}_chunk{chunk_idx}.json"
                ok = await _r2_put(key, payload)
                total_uploaded += len(chunk_items)

                if not ok and chunk_idx == 0:
                    # First chunk failed - report failure
                    results[name] = {
                        "collection": name,
                        "status": "failed",
                        "count": count,
                        "key": key,
                        "size_bytes": len(payload),
                        "chunks_uploaded": chunk_idx,
                    }
                    break
                chunk_idx += 1

            # Save latest pointer (only if at least one chunk succeeded)
            if chunk_idx > 0 or count == 0:
                await _r2_put(
                    f"rag_snapshots/{name}/latest.json",
                    json.dumps(
                        {
                            "key": f"rag_snapshots/{name}/{ts}",
                            "timestamp": ts,
                            "count": count,
                            "version": EMBEDDING_VERSION,
                            "chunks": chunk_idx,
                        }
                    ).encode(),
                )

                results[name] = {
                    "collection": name,
                    "status": "uploaded" if count > 0 else "empty",
                    "count": count,
                    "key": f"rag_snapshots/{name}/{ts}",
                    "size_bytes": sum(len(json.dumps(i, default=str).encode()) for i in items),
                    "chunks": chunk_idx,
                }
        except Exception as e:
            results[name] = {"collection": name, "error": str(e)[:200]}

    # Save local metadata
    meta = {
        "last_snapshot": ts,
        "results": {k: v.get("status", "error") for k, v in results.items()},
    }
    (LOCAL_CACHE / "snapshot_meta.json").write_text(json.dumps(meta, indent=2))

    return results


async def restore_all():
    """Restore all RAG collections from latest R2 snapshots."""
    # Get collections and Redis connection
    try:
        from app.rag_service import COLLECTIONS, _get_redis

        redis = await _get_redis()
    except Exception:
        return {"error": "Cannot connect to RAG service"}

    results = {}

    for name in COLLECTIONS:
        try:
            # Get latest pointer
            latest_raw = await _r2_get(f"rag_snapshots/{name}/latest.json")
            if not latest_raw:
                results[name] = {"collection": name, "status": "no_snapshot"}
                continue

            latest = json.loads(latest_raw)
            key = latest["key"]

            # Download the snapshot
            data_raw = await _r2_get(key)
            if not data_raw:
                results[name] = {"collection": name, "status": "download_failed"}
                continue

            snapshot = json.loads(data_raw)
            items = snapshot.get("items", [])
            snapshot_ts = latest.get("timestamp", "")

            # Handle chunked snapshots - download all chunks
            total_chunks = latest.get("chunks", 1)
            if total_chunks > 1:
                for ci in range(1, total_chunks):
                    chunk_key = f"rag_snapshots/{name}/{snapshot_ts}_chunk{ci}.json"
                    chunk_raw = await _r2_get(chunk_key)
                    if chunk_raw:
                        chunk_data = json.loads(chunk_raw)
                        items.extend(chunk_data.get("items", []))

            # Restore to Redis: set each doc as string and add to index set
            restored = 0
            for item in items:
                try:
                    doc_id = item.get("id", "")
                    if not doc_id:
                        continue
                    # Store as JSON string (matches rag_service format)
                    await redis.set(f"rag:{name}:{doc_id}", json.dumps(item, default=str))
                    await redis.sadd(f"rag:idx:{name}", doc_id)
                    restored += 1
                except Exception:
                    pass

            results[name] = {
                "collection": name,
                "status": "restored",
                "count": restored,
                "total_in_snapshot": len(items),
                "snapshot_key": key,
            }
        except Exception as e:
            results[name] = {"collection": name, "error": str(e)[:200]}

    return results


async def nightly_cycle():
    """Full nightly cycle: snapshot to R2, clean local cache."""
    snap = await snapshot_all()
    ok = sum(1 for v in snap.values() if v.get("status") in ("uploaded", "empty"))
    total = len(snap)

    # Clean local cache older than retention period
    cleaned = 0
    cutoff = datetime.now(UTC) - timedelta(days=LOCAL_RETENTION_DAYS)
    for f in LOCAL_CACHE.glob("*.json"):
        try:
            if datetime.fromtimestamp(f.stat().st_mtime, tz=UTC) < cutoff:
                f.unlink()
                cleaned += 1
        except Exception:
            pass

    return {
        "snapshot_results": {k: v.get("status", "error") for k, v in snap.items()},
        "collections_ok": ok,
        "collections_total": total,
        "local_cache_cleaned": cleaned,
    }


async def r2_stats():
    """Get R2 usage + local cache stats."""
    # Get collections and Redis connection
    try:
        from app.rag_service import COLLECTIONS, _get_redis

        redis = await _get_redis()
        has_redis = True
    except Exception:
        COLLECTIONS = []
        has_redis = False

    # R2 bucket stats
    objects = await _r2_list("rag_snapshots/")
    total_size = 0
    snapshot_count = 0
    for obj in objects:
        size = int(obj.get("size", 0))
        total_size += size
        snapshot_count += 1

    # Local cache stats
    local_files = list(LOCAL_CACHE.glob("*.json"))
    local_size = sum(f.stat().st_size for f in local_files) if local_files else 0

    # Collection stats (from Redis)
    coll_info = {}
    if has_redis:
        for name in COLLECTIONS:
            try:
                count = await redis.scard(f"rag:idx:{name}")
                coll_info[name] = count
            except Exception:
                coll_info[name] = 0

    return {
        "r2": {
            "bucket": R2_BUCKET,
            "endpoint": R2_BASE,
            "snapshot_count": snapshot_count,
            "total_size_bytes": total_size,
            "total_size_mb": round(total_size / 1024 / 1024, 2),
            "has_token": bool(R2_API_TOKEN),
        },
        "local": {
            "path": str(LOCAL_CACHE),
            "file_count": len(local_files),
            "size_bytes": local_size,
            "retention_days": LOCAL_RETENTION_DAYS,
        },
        "collections": coll_info,
        "model_version": EMBEDDING_VERSION,
    }
