"""Local data integrity and preflight checkpoint helpers.

These helpers are offline-only.  They are used before future server cutover
checks so data can be validated and recoverable before any managed backend mode
is enabled.
"""
from __future__ import annotations

import hashlib
import json
import shutil
import time
import uuid
from datetime import datetime
from pathlib import Path

from .constants import DATA_DIR
from .storage import save_json

CHECKPOINT_DIR = DATA_DIR / "preflight_checkpoints"
INTEGRITY_REPORT_PATH = DATA_DIR / "data_integrity_latest.json"
CHECKPOINT_LIMIT = 6
CRITICAL_JSON_FILES = [
    "settings.json",
    "scoped_workspace.json",
    "notes.json",
    "todos.json",
    "documents.json",
    "enterprise.json",
    "reports.json",
    "attendance.json",
    "chat.json",
    "crm.json",
    "sync_queue.json",
]
SECRET_KEYS = {
    "password",
    "passcode",
    "token",
    "api_token",
    "access_token",
    "refresh_token",
    "authorization",
    "secret",
    "client_secret",
    "cookie",
    "set_cookie",
}


def _sha1_file(path: Path) -> str:
    h = hashlib.sha1()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 256), b""):
            h.update(chunk)
    return h.hexdigest()


def _redact(value, key_hint="", depth=0):
    if depth > 8:
        return str(value)[:240]
    key = str(key_hint or "").lower()
    if key in SECRET_KEYS or any(marker in key for marker in ("token", "password", "secret", "authorization")):
        return "***"
    if isinstance(value, dict):
        return {str(k): _redact(v, k, depth + 1) for k, v in value.items()}
    if isinstance(value, list):
        return [_redact(item, "", depth + 1) for item in value[:500]]
    return value


def data_integrity_report(write_report: bool = True) -> dict:
    """Validate local JSON files without exposing secret values."""
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    files = []
    missing = []
    unreadable = []
    total_bytes = 0
    for name in CRITICAL_JSON_FILES:
        path = DATA_DIR / name
        if not path.exists():
            missing.append(name)
            continue
        info = {"name": name, "size": 0, "sha1": "", "json_ok": False}
        try:
            info["size"] = int(path.stat().st_size)
            total_bytes += info["size"]
            info["sha1"] = _sha1_file(path)
            json.loads(path.read_text(encoding="utf-8"))
            info["json_ok"] = True
        except Exception:
            unreadable.append(name)
        files.append(info)
    ok = not unreadable
    report = {
        "time": datetime.now().isoformat(timespec="seconds"),
        "status": "OK" if ok else "FAILED",
        "ok": ok,
        "will_connect": False,
        "local_only": True,
        "total_bytes": total_bytes,
        "files_checked": len(files),
        "missing": missing,
        "unreadable": unreadable,
        "files": files,
        "secret_values_displayed": False,
    }
    if write_report:
        try:
            save_json(INTEGRITY_REPORT_PATH, report)
        except Exception:
            pass
    return report


def create_preflight_data_checkpoint(reason: str = "network_preflight") -> dict:
    """Create a bounded local checkpoint before network-prep operations.

    settings.json is written as a redacted copy so tokens/passwords are not
    preserved in this checkpoint. Other app data files are copied as local-only
    recovery files.
    """
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    folder = CHECKPOINT_DIR / f"{stamp}_{str(reason or 'preflight')[:28]}_{uuid.uuid4().hex[:6]}"
    folder.mkdir(parents=True, exist_ok=True)
    manifest = {
        "created_at": datetime.now().isoformat(timespec="seconds"),
        "reason": str(reason or "network_preflight"),
        "will_connect": False,
        "secret_values_displayed": False,
        "files": [],
    }
    for name in CRITICAL_JSON_FILES:
        src = DATA_DIR / name
        if not src.exists() or not src.is_file():
            continue
        target_name = "settings.redacted.json" if name == "settings.json" else name
        dst = folder / target_name
        try:
            if name == "settings.json":
                data = json.loads(src.read_text(encoding="utf-8"))
                dst.write_text(json.dumps(_redact(data), ensure_ascii=False, indent=2), encoding="utf-8")
            else:
                shutil.copy2(src, dst)
            manifest["files"].append({
                "name": target_name,
                "size": int(dst.stat().st_size),
                "sha1": _sha1_file(dst),
            })
        except Exception as exc:
            manifest.setdefault("errors", []).append({"name": name, "error": str(exc)[:160]})
    (folder / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
    checkpoints = sorted(
        [p for p in CHECKPOINT_DIR.iterdir() if p.is_dir()],
        key=lambda p: p.stat().st_mtime,
        reverse=True,
    )
    for old in checkpoints[CHECKPOINT_LIMIT:]:
        try:
            shutil.rmtree(old)
        except Exception:
            pass
    return {
        "ok": True,
        "path": str(folder),
        "files": len(manifest.get("files", [])),
        "will_connect": False,
        "secret_values_displayed": False,
    }


def _read_checkpoint_manifest(folder: Path) -> dict:
    try:
        manifest = json.loads((folder / "manifest.json").read_text(encoding="utf-8"))
    except Exception:
        manifest = {}
    files = manifest.get("files", []) if isinstance(manifest.get("files", []), list) else []
    return {
        "name": folder.name,
        "path": str(folder),
        "created_at": manifest.get("created_at", ""),
        "reason": manifest.get("reason", ""),
        "files": len(files),
        "has_manifest": (folder / "manifest.json").exists(),
        "has_redacted_settings": (folder / "settings.redacted.json").exists(),
        "has_raw_settings": (folder / "settings.json").exists(),
        "will_connect": bool(manifest.get("will_connect", False)),
        "secret_values_displayed": bool(manifest.get("secret_values_displayed", False)),
        "size": sum(int((folder / str(item.get("name", ""))).stat().st_size) for item in files if (folder / str(item.get("name", ""))).exists()),
    }


def list_preflight_checkpoints(limit: int | None = None) -> list[dict]:
    """List local checkpoint manifests without reading sensitive content."""
    if not CHECKPOINT_DIR.exists():
        return []
    folders = sorted([p for p in CHECKPOINT_DIR.iterdir() if p.is_dir()], key=lambda p: p.stat().st_mtime, reverse=True)
    if limit is not None:
        folders = folders[:max(0, int(limit))]
    return [_read_checkpoint_manifest(folder) for folder in folders]


def _latest_checkpoint_age_seconds() -> int | None:
    checkpoints = list_preflight_checkpoints(limit=1)
    if not checkpoints:
        return None
    latest_path = Path(checkpoints[0].get("path", ""))
    try:
        return max(0, int(time.time() - latest_path.stat().st_mtime))
    except Exception:
        return None


def ensure_preflight_data_checkpoint(
    reason: str = "network_preflight",
    *,
    force: bool = False,
    min_interval_seconds: int = 6 * 60 * 60,
) -> dict:
    """Create a checkpoint only when needed.

    Startup/network bootstrap uses this to avoid copying local data on every
    launch.  Manual actions and snapshot preparation can pass force=True.
    """
    existing = list_preflight_checkpoints(limit=1)
    age = _latest_checkpoint_age_seconds()
    if (not force) and existing and age is not None and age < int(min_interval_seconds):
        report = {
            "ok": True,
            "created": False,
            "reused": True,
            "reason": str(reason or "network_preflight"),
            "path": existing[0].get("path", ""),
            "latest": existing[0],
            "age_seconds": age,
            "min_interval_seconds": int(min_interval_seconds),
            "will_connect": False,
            "secret_values_displayed": False,
        }
        try:
            save_json(DATA_DIR / "preflight_checkpoint_last_action.json", report)
        except Exception:
            pass
        return report
    created = create_preflight_data_checkpoint(reason)
    created["created"] = True
    created["reused"] = False
    created["age_seconds"] = 0
    created["min_interval_seconds"] = int(min_interval_seconds)
    try:
        save_json(DATA_DIR / "preflight_checkpoint_last_action.json", created)
    except Exception:
        pass
    return created



def checkpoint_health_report(write_report: bool = True) -> dict:
    """Summarize local checkpoint health and retention.

    This is a local-only report. It helps confirm that checkpoints are bounded,
    use redacted settings, and have a manifest that can support a future restore
    workflow.
    """
    checkpoints = list_preflight_checkpoints()
    missing_manifest = [cp["name"] for cp in checkpoints if not cp.get("has_manifest")]
    raw_settings = [cp["name"] for cp in checkpoints if cp.get("has_raw_settings")]
    missing_redacted = [cp["name"] for cp in checkpoints if not cp.get("has_redacted_settings") and cp.get("files", 0) > 0]
    total_bytes = sum(int(cp.get("size", 0) or 0) for cp in checkpoints)
    ok = not missing_manifest and not raw_settings and len(checkpoints) <= CHECKPOINT_LIMIT
    latest_age = _latest_checkpoint_age_seconds()
    report = {
        "time": datetime.now().isoformat(timespec="seconds"),
        "status": "OK" if ok else "FAILED",
        "ok": ok,
        "will_connect": False,
        "latest_age_seconds": latest_age,
        "checkpoint_count": len(checkpoints),
        "checkpoint_limit": CHECKPOINT_LIMIT,
        "total_bytes": total_bytes,
        "latest": checkpoints[0] if checkpoints else {},
        "missing_manifest": missing_manifest,
        "missing_redacted_settings": missing_redacted,
        "raw_settings_found": raw_settings,
        "checkpoints": checkpoints[:12],
    }
    if write_report:
        try:
            save_json(DATA_DIR / "checkpoint_health_latest.json", report)
        except Exception:
            pass
    return report


def checkpoint_restore_dry_run(checkpoint_name: str | None = None, write_report: bool = True) -> dict:
    """Build a local restore plan without changing current data."""
    checkpoints = list_preflight_checkpoints()
    selected = None
    if checkpoint_name:
        for cp in checkpoints:
            if cp.get("name") == checkpoint_name:
                selected = cp
                break
    elif checkpoints:
        selected = checkpoints[0]
    plan = {
        "time": datetime.now().isoformat(timespec="seconds"),
        "mode": "dry_run",
        "will_restore_now": False,
        "will_connect": False,
        "selected": selected or {},
        "steps": [
            "Close NexaOffice before any manual restore.",
            "Copy selected checkpoint files back to data directory if needed.",
            "Never restore settings.redacted.json as settings.json automatically.",
            "Restart NexaOffice and run data integrity check again.",
        ],
        "warnings": [
            "settings.redacted.json is for review only and intentionally hides secrets.",
            "This dry-run does not modify files.",
        ],
    }
    if write_report:
        try:
            save_json(DATA_DIR / "checkpoint_restore_dry_run_latest.json", plan)
        except Exception:
            pass
    return plan
