"""Local sync queue for future server mode.

The app is still local-first. This queue only prepares safe metadata and future
sync jobs so later backend integration does not block the UI or overwrite data.
"""

import json
import time
import uuid
from pathlib import Path

from .constants import DATA_DIR
from .storage import load_json, save_json

SYNC_QUEUE_PATH = DATA_DIR / "sync_queue.json"
SYNC_QUEUE_BACKUP_DIR = DATA_DIR / "sync_queue_backups"
SYNC_QUEUE_BACKUP_LIMIT = 8
SYNC_QUEUE_MAX_ITEMS = 1000
SYNC_QUEUE_MAX_PAYLOAD_CHARS = 12000
SYNC_QUEUE_SECRET_KEYS = {
    "password",
    "passcode",
    "token",
    "api_token",
    "access_token",
    "refresh_token",
    "authorization",
    "secret",
    "client_secret",
    "cookie",
    "set_cookie",
}


def _redact_payload(value, depth=0):
    """Recursively remove secrets before data enters the future sync queue."""
    if depth > 6:
        return str(value)[:240]
    if isinstance(value, dict):
        result = {}
        for key, item in value.items():
            key_text = str(key)
            if key_text.lower() in SYNC_QUEUE_SECRET_KEYS or any(marker in key_text.lower() for marker in ("token", "password", "secret", "authorization")):
                result[key_text] = "***"
            else:
                result[key_text] = _redact_payload(item, depth + 1)
        return result
    if isinstance(value, list):
        return [_redact_payload(item, depth + 1) for item in value[:200]]
    if isinstance(value, tuple):
        return [_redact_payload(item, depth + 1) for item in value[:200]]
    return value


def _compact_payload(payload):
    """Keep offline sync metadata bounded so future online mode stays smooth."""
    if payload is None:
        return {}
    payload = _redact_payload(payload)
    try:
        raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
    except Exception:
        return {"value": str(payload)[:SYNC_QUEUE_MAX_PAYLOAD_CHARS]}
    if len(raw) <= SYNC_QUEUE_MAX_PAYLOAD_CHARS:
        return payload
    if isinstance(payload, dict):
        result = {}
        for key, value in payload.items():
            if key.lower() in {"content", "body", "text", "description"}:
                result[key] = str(value)[:4000]
                result[key + "_truncated"] = True
            elif key.lower() in {"password", "token", "api_token", "secret"}:
                result[key] = "***"
            else:
                result[key] = value
            try:
                if len(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) > SYNC_QUEUE_MAX_PAYLOAD_CHARS:
                    result["_truncated"] = True
                    break
            except Exception:
                break
        return result
    return {"value": str(payload)[:SYNC_QUEUE_MAX_PAYLOAD_CHARS], "_truncated": True}

def default_sync_queue():
    return {"schema": 1, "items": [], "last_attempt": "", "last_status": "local_ready", "last_compacted_at": 0}


def load_sync_queue():
    data = load_json(SYNC_QUEUE_PATH, default_sync_queue())
    if not isinstance(data, dict):
        data = default_sync_queue()
    data.setdefault("schema", 1)
    items = data.get("items", [])
    if not isinstance(items, list):
        items = []
    data["items"] = items[-SYNC_QUEUE_MAX_ITEMS:]
    data.setdefault("last_attempt", "")
    data.setdefault("last_status", "local_ready")
    data.setdefault("last_compacted_at", 0)
    return data


def save_sync_queue(data):
    if not isinstance(data, dict):
        data = default_sync_queue()
    items = data.get("items", [])
    if not isinstance(items, list):
        items = []
    data["items"] = items[-SYNC_QUEUE_MAX_ITEMS:]
    save_json(SYNC_QUEUE_PATH, data)


def enqueue_sync_event(queue, entity_type, action, payload=None, user="", local_id=""):
    if not isinstance(queue, dict):
        queue = default_sync_queue()
    items = queue.setdefault("items", [])
    if not isinstance(items, list):
        items = []
        queue["items"] = items
    now = int(time.time())
    entity_type = str(entity_type or "unknown")
    action = str(action or "update")
    local_id = str(local_id or "")
    user = str(user or "")

    # Future online mode can generate many local edits quickly. Compact pending
    # updates for the same entity instead of growing the queue and causing slow
    # startup/sync checks after long offline sessions. Create/delete actions are
    # still appended because their order can matter.
    compacted_payload = _compact_payload(payload)
    if local_id and action in {"update", "save", "edit"}:
        for existing in reversed(items):
            if (
                existing.get("status") == "pending"
                and existing.get("entity_type") == entity_type
                and existing.get("action") == action
                and existing.get("local_id") == local_id
                and existing.get("user", "") == user
            ):
                # v2026.06.20.17: autosave can enqueue the same object many
                # times.  If the payload is unchanged, skip the disk write
                # entirely.  If it changed, merge into the existing pending job
                # instead of growing the queue.
                if existing.get("payload") == compacted_payload:
                    return queue, existing
                existing["payload"] = compacted_payload
                existing["updated_at"] = now
                queue["last_compacted_at"] = now
                save_sync_queue(queue)
                return queue, existing

    item = {
        "id": str(uuid.uuid4()),
        "created_at": now,
        "entity_type": entity_type,
        "action": action,
        "local_id": local_id,
        "user": user,
        "payload": compacted_payload,
        "status": "pending",
        "attempts": 0,
    }
    items.append(item)
    queue["items"] = items[-SYNC_QUEUE_MAX_ITEMS:]
    save_sync_queue(queue)
    return queue, item



def backup_sync_queue(queue=None, reason="preflight"):
    """Create a local, redacted backup before sync-queue maintenance.

    The backup is kept offline in data/sync_queue_backups and is bounded so it
    cannot grow forever.  It never contains raw secrets because payloads are
    compacted/redacted before writing.
    """
    if not isinstance(queue, dict):
        queue = load_sync_queue()
    if not isinstance(queue, dict):
        queue = default_sync_queue()
    try:
        SYNC_QUEUE_BACKUP_DIR.mkdir(parents=True, exist_ok=True)
        safe_items = []
        for raw in (queue.get("items", []) if isinstance(queue.get("items", []), list) else []):
            if not isinstance(raw, dict):
                continue
            item = dict(raw)
            item["payload"] = _compact_payload(raw.get("payload", {}))
            safe_items.append(item)
        payload = {
            "schema": queue.get("schema", 1),
            "reason": str(reason or "preflight"),
            "created_at": int(time.time()),
            "items": safe_items[-SYNC_QUEUE_MAX_ITEMS:],
        }
        path = SYNC_QUEUE_BACKUP_DIR / f"sync_queue_{int(time.time())}_{uuid.uuid4().hex[:6]}.json"
        save_json(path, payload)
        backups = sorted(SYNC_QUEUE_BACKUP_DIR.glob("sync_queue_*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
        for old in backups[SYNC_QUEUE_BACKUP_LIMIT:]:
            try:
                old.unlink()
            except Exception:
                pass
        return str(path)
    except Exception:
        return ""


def repair_sync_queue(queue=None, make_backup=False):
    """Normalize and redact the local offline queue before future online mode.

    This is a local-only maintenance step.  It never sends data to the network.
    """
    if not isinstance(queue, dict):
        queue = load_sync_queue()
    if not isinstance(queue, dict):
        queue = default_sync_queue()
    items = queue.get("items", [])
    if not isinstance(items, list):
        items = []
    if make_backup:
        backup_sync_queue(queue, reason="repair")
    cleaned_reversed = []
    seen_pending = set()
    now = int(time.time())
    # Iterate newest-to-oldest so duplicate pending edits keep the newest item,
    # then reverse back to chronological order before saving.
    for raw in reversed(items[-SYNC_QUEUE_MAX_ITEMS:]):
        if not isinstance(raw, dict):
            continue
        entity_type = str(raw.get("entity_type") or "unknown")
        action = str(raw.get("action") or "update")
        local_id = str(raw.get("local_id") or "")
        user = str(raw.get("user") or "")
        status = str(raw.get("status") or "pending")
        if status not in {"pending", "failed", "done"}:
            status = "pending"
        pending_key = (entity_type, action, local_id, user)
        if status == "pending" and action in {"update", "save", "edit"} and local_id:
            if pending_key in seen_pending:
                continue
            seen_pending.add(pending_key)
        item = {
            "id": str(raw.get("id") or uuid.uuid4()),
            "created_at": int(raw.get("created_at") or now),
            "updated_at": int(raw.get("updated_at") or raw.get("created_at") or now),
            "entity_type": entity_type,
            "action": action,
            "local_id": local_id,
            "user": user,
            "payload": _compact_payload(raw.get("payload", {})),
            "status": status,
            "attempts": max(0, min(9, int(raw.get("attempts") or 0))),
        }
        cleaned_reversed.append(item)
    cleaned = list(reversed(cleaned_reversed))
    queue["schema"] = 1
    queue["items"] = cleaned[-SYNC_QUEUE_MAX_ITEMS:]
    queue["last_repaired_at"] = now
    queue.setdefault("last_attempt", "")
    queue.setdefault("last_status", "local_ready")
    save_sync_queue(queue)
    return queue


def queue_summary(queue):
    if not isinstance(queue, dict):
        queue = default_sync_queue()
    items = queue.get("items", []) if isinstance(queue.get("items", []), list) else []
    pending = sum(1 for item in items if item.get("status") == "pending")
    failed = sum(1 for item in items if item.get("status") == "failed")
    done = sum(1 for item in items if item.get("status") == "done")
    return {"total": len(items), "pending": pending, "failed": failed, "done": done}


def _has_unredacted_secret(value, key_hint=""):
    key_lower = str(key_hint or "").lower()
    is_secret_key = key_lower in SYNC_QUEUE_SECRET_KEYS or any(marker in key_lower for marker in ("token", "password", "secret", "authorization"))
    if is_secret_key:
        return str(value or "") not in {"", "***", "none", "None", "null"}
    if isinstance(value, dict):
        return any(_has_unredacted_secret(item, key) for key, item in value.items())
    if isinstance(value, (list, tuple)):
        return any(_has_unredacted_secret(item, "") for item in value)
    return False


def sync_queue_integrity_report(queue=None):
    """Return a local-only health report for future online sync.

    This does not send data. It helps confirm that the offline queue is bounded,
    secret-redacted, and safe to replay once a real server is configured.
    """
    if not isinstance(queue, dict):
        queue = load_sync_queue()
    if not isinstance(queue, dict):
        queue = default_sync_queue()
    items = queue.get("items", []) if isinstance(queue.get("items", []), list) else []
    oversized = 0
    secret_hits = 0
    invalid = 0
    duplicated_pending = 0
    pending_seen = set()
    entity_counts = {}
    for item in items:
        if not isinstance(item, dict):
            invalid += 1
            continue
        entity = str(item.get("entity_type") or "unknown")
        entity_counts[entity] = entity_counts.get(entity, 0) + 1
        status = str(item.get("status") or "pending")
        local_id = str(item.get("local_id") or "")
        action = str(item.get("action") or "")
        if status == "pending" and action in {"update", "save", "edit"} and local_id:
            key = (entity, action, local_id, str(item.get("user") or ""))
            if key in pending_seen:
                duplicated_pending += 1
            pending_seen.add(key)
        try:
            payload = item.get("payload", {})
            raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
            if len(raw) > SYNC_QUEUE_MAX_PAYLOAD_CHARS:
                oversized += 1
            if _has_unredacted_secret(payload):
                secret_hits += 1
        except Exception:
            invalid += 1
    summary = queue_summary(queue)
    ok = (
        int(summary.get("total", 0) or 0) <= SYNC_QUEUE_MAX_ITEMS
        and oversized == 0
        and secret_hits == 0
        and invalid == 0
        and duplicated_pending == 0
    )
    return {
        "ok": ok,
        "total": int(summary.get("total", 0) or 0),
        "pending": int(summary.get("pending", 0) or 0),
        "failed": int(summary.get("failed", 0) or 0),
        "done": int(summary.get("done", 0) or 0),
        "oversized": oversized,
        "secret_hits": secret_hits,
        "invalid": invalid,
        "duplicated_pending": duplicated_pending,
        "entity_counts": entity_counts,
        "max_items": SYNC_QUEUE_MAX_ITEMS,
        "max_payload_chars": SYNC_QUEUE_MAX_PAYLOAD_CHARS,
        "last_repaired_at": queue.get("last_repaired_at", ""),
    }


def sync_cutover_dry_run(queue=None):
    """Build a future-server dry-run plan without connecting to a server."""
    if not isinstance(queue, dict):
        queue = load_sync_queue()
    repaired = repair_sync_queue(queue)
    integrity = sync_queue_integrity_report(repaired)
    items = repaired.get("items", []) if isinstance(repaired.get("items", []), list) else []
    pending = [item for item in items if item.get("status") == "pending"]
    batches = []
    batch_size = 50
    for idx in range(0, len(pending), batch_size):
        chunk = pending[idx:idx + batch_size]
        batches.append({
            "index": len(batches) + 1,
            "count": len(chunk),
            "entities": sorted({str(item.get("entity_type") or "unknown") for item in chunk}),
        })
    return {
        "ok": bool(integrity.get("ok", False)),
        "mode": "dry_run",
        "will_connect": False,
        "pending": len(pending),
        "batches": batches,
        "batch_size": batch_size,
        "integrity": integrity,
    }
