"""
marcus_memory.py — Session & Place Memory
==========================================
Project  : Marcus | YS Lootah Technology
Hardware : Unitree G1 EDU + Jetson Orin NX
Purpose  : Persistent memory across sessions.
           - Place memory: save named robot positions, navigate back
           - Session memory: log all commands, detections, alerts per session
           - Cross-session recall: "what did you see last session?"

Folder structure
----------------
  Data/History/Places/places.json          ← persistent named places (all sessions)
  Data/Brain/Sessions/
    session_001_2026-04-05/
      commands.json                        ← [{time, cmd, response, duration_s}]
      detections.json                      ← [{time, class, position, distance, x, y}]
      places.json                          ← places saved THIS session
      alerts.json                          ← [{time, type, detail}]
      summary.txt                          ← auto-generated session summary

Import in marcus_brain.py
-------------------------
  from marcus_memory import Memory
  mem = Memory()                           ← call once at startup
  mem.start_session()                      ← begins logging
  mem.log_command(cmd, response)           ← after every command
  mem.log_detection(class_name, pos, dist) ← from YOLO loop
  mem.save_place("door", x, y, heading)   ← when user says "remember this as door"
  mem.get_place("door")                    ← returns {x, y, heading} or None
  mem.list_places()                        ← sorted list of place names
  mem.delete_place("door")                ← removes place
  mem.last_session_summary()              ← text summary of previous session
  mem.end_session()                        ← saves everything, call on shutdown

Date     : April 2026
"""

import os
import json
import time
import re
import threading
import shutil
import difflib
from datetime import datetime
from pathlib import Path


# ══════════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ══════════════════════════════════════════════════════════════════════════════

_PROJECT_ROOT = Path(__file__).resolve().parent.parent
BASE_DIR      = _PROJECT_ROOT / "Data" / "Brain"
SESSIONS_DIR  = BASE_DIR / "Sessions"
PLACES_FILE   = _PROJECT_ROOT / "Data" / "History" / "Places" / "places.json"
MAX_CMD_LEN   = 500      # truncate very long commands
MAX_SESSIONS  = 50       # keep last N sessions — older ones auto-deleted
DETECT_DEDUPE = 5.0      # seconds — suppress duplicate YOLO detections


# ══════════════════════════════════════════════════════════════════════════════
# HELPER — SAFE JSON READ/WRITE
# ══════════════════════════════════════════════════════════════════════════════

def _read_json(path: Path, default):
    """
    Read JSON file. Returns default if file missing, unreadable, or corrupt.
    Backs up corrupt files before resetting.
    """
    if not path.exists():
        return default
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError:
        # Back up the corrupt file and return default
        backup = path.with_suffix(".bak")
        try:
            shutil.copy(path, backup)
            print(f"  [Memory] ⚠️  Corrupt JSON at {path.name} — backed up as {backup.name}")
        except Exception:
            pass
        return default
    except OSError as e:
        print(f"  [Memory] ⚠️  Cannot read {path.name}: {e}")
        return default


def _write_json(path: Path, data, lock: threading.Lock = None):
    """
    Write JSON atomically using a temp file + rename.
    Catches disk-full and permission errors without crashing.
    Returns True on success.
    """
    def _do_write():
        try:
            path.parent.mkdir(parents=True, exist_ok=True)
            tmp = path.with_suffix(".tmp")
            with open(tmp, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
            tmp.replace(path)   # atomic rename
            return True
        except OSError as e:
            print(f"  [Memory] ⚠️  Cannot write {path.name}: {e}")
            return False

    if lock:
        with lock:
            return _do_write()
    return _do_write()


def _sanitize_name(name: str) -> str:
    """
    Clean a place name: lowercase, strip whitespace, remove unsafe chars.
    'Server Room!' → 'server_room'
    """
    name = name.strip().lower()
    name = re.sub(r"[^\w\s\-]", "", name)   # keep word chars, spaces, hyphens
    name = re.sub(r"\s+", "_", name)         # spaces → underscores
    name = name[:50]                          # max 50 chars
    return name


def _fuzzy_match(query: str, choices: list, n: int = 3) -> list:
    """Return up to n closest matches from choices for query."""
    if not choices:
        return []
    return difflib.get_close_matches(query, choices, n=n, cutoff=0.5)


def _new_session_id(sessions_dir: Path) -> str:
    """Generate next session ID: session_001, session_002..."""
    existing = [
        d.name for d in sessions_dir.iterdir()
        if d.is_dir() and d.name.startswith("session_")
    ] if sessions_dir.exists() else []

    nums = []
    for name in existing:
        parts = name.split("_")
        if len(parts) >= 2 and parts[1].isdigit():
            nums.append(int(parts[1]))

    next_num = max(nums) + 1 if nums else 1
    date_str = datetime.now().strftime("%Y-%m-%d")
    return f"session_{next_num:03d}_{date_str}"


# ══════════════════════════════════════════════════════════════════════════════
# MEMORY CLASS
# ══════════════════════════════════════════════════════════════════════════════

class Memory:
    """
    Persistent session and place memory for Marcus.

    Thread-safe. All write operations use locks.
    Saves automatically on shutdown via end_session().
    Register with atexit for crash protection.

    Usage:
        mem = Memory()
        mem.start_session()
        # ... during operation ...
        mem.log_command("turn left", "Turning left")
        mem.save_place("door", x=1.2, y=0.5, heading=90.0)
        # ... on shutdown ...
        mem.end_session()
    """

    def __init__(self):
        self._places_lock   = threading.Lock()
        self._session_lock  = threading.Lock()
        self._session_dir   = None
        self._session_id    = None
        self._session_start = None

        # In-memory buffers — flushed to disk on end_session + periodically
        self._commands   = []   # [{time, cmd, response, duration_s}]
        self._detections = []   # [{time, class, position, distance, x, y}]
        self._alerts     = []   # [{time, type, detail}]

        # Dedup tracking for YOLO detections
        self._last_detection = {}   # class → timestamp

        # Persistent places (survives all sessions)
        self._places = {}           # {name: {x, y, heading, saved_at, session}}

        # Ensure base dirs exist
        BASE_DIR.mkdir(parents=True, exist_ok=True)
        SESSIONS_DIR.mkdir(parents=True, exist_ok=True)

        # Load persistent places
        self._load_places()

        # Register auto-save on crash
        import atexit
        atexit.register(self._emergency_save)

    # ── PLACES ────────────────────────────────────────────────────────────────

    def _load_places(self):
        """Load places.json from disk into memory."""
        data = _read_json(PLACES_FILE, {})
        if isinstance(data, dict):
            self._places = data
            print(f"  [Memory] Places loaded: {len(self._places)} locations")
        else:
            print("  [Memory] ⚠️  places.json has wrong format — resetting")
            self._places = {}

    def save_place(self, name: str, x: float = None,
                   y: float = None, heading: float = None) -> bool:
        """
        Save current robot position with a name.

        Args:
            name    : human-readable name e.g. "door", "desk_a"
            x       : robot x position from odometry (None if not available)
            y       : robot y position from odometry
            heading : robot heading in degrees

        Returns:
            True on success, False on invalid name or write error.

        Edge cases handled:
            - Empty name → rejected
            - Name with special chars → sanitized
            - Duplicate name → overwrites with notification
            - Odometry not running (x/y/heading all None) → saved as landmark only
            - Disk full → error logged, returns False
        """
        # Validate name
        if not name or not name.strip():
            print("  [Memory] ⚠️  Place name cannot be empty")
            return False

        clean = _sanitize_name(name)
        if not clean:
            print(f"  [Memory] ⚠️  Place name '{name}' has no valid characters")
            return False

        # Warn if overwriting
        if clean in self._places:
            old = self._places[clean]
            print(f"  [Memory] Overwriting '{clean}' (was saved at {old.get('saved_at','?')})")

        # Warn if no odometry
        if x is None and y is None:
            print("  [Memory] ⚠️  Odometry not running — saving place as landmark only (no coordinates)")
            print("  [Memory]    'go to' navigation will not be available for this place")

        entry = {
            "name":     clean,
            "x":        round(x, 4) if x is not None else None,
            "y":        round(y, 4) if y is not None else None,
            "heading":  round(heading, 2) if heading is not None else None,
            "has_odom": x is not None,
            "saved_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "session":  self._session_id or "unknown",
        }

        with self._places_lock:
            self._places[clean] = entry
            ok = _write_json(PLACES_FILE, self._places)

        if ok:
            coord_str = f"x={x:.2f} y={y:.2f} h={heading:.1f}°" if x is not None else "no coordinates"
            print(f"  [Memory] ✅ Saved place '{clean}' — {coord_str}")
            return True
        return False

    def get_place(self, name: str) -> dict:
        """
        Get a saved place by name.

        Returns:
            dict with x, y, heading, has_odom — or None if not found.
            Suggests similar names if not found.

        Edge cases:
            - Exact match first
            - Case-insensitive match
            - Fuzzy match suggestion if no exact match
        """
        clean = _sanitize_name(name)

        with self._places_lock:
            places = dict(self._places)

        # Exact match
        if clean in places:
            return places[clean]

        # Case-insensitive match
        for k, v in places.items():
            if k.lower() == clean.lower():
                return v

        # Fuzzy suggestions
        suggestions = _fuzzy_match(clean, list(places.keys()))
        if suggestions:
            print(f"  [Memory] ⚠️  Place '{name}' not found.")
            print(f"  [Memory]    Did you mean: {', '.join(suggestions)} ?")
        else:
            print(f"  [Memory] ⚠️  Place '{name}' not found.")
            if places:
                print(f"  [Memory]    Known places: {', '.join(sorted(places.keys()))}")
            else:
                print("  [Memory]    No places saved yet. Say 'remember this as <name>'")
        return None

    def delete_place(self, name: str) -> bool:
        """
        Remove a saved place.

        Returns True if deleted, False if not found.
        """
        clean = _sanitize_name(name)

        with self._places_lock:
            if clean not in self._places:
                suggestions = _fuzzy_match(clean, list(self._places.keys()))
                if suggestions:
                    print(f"  [Memory] Place '{name}' not found. Did you mean: {', '.join(suggestions)}?")
                else:
                    print(f"  [Memory] Place '{name}' not found.")
                return False

            del self._places[clean]
            ok = _write_json(PLACES_FILE, self._places)

        if ok:
            print(f"  [Memory] ✅ Deleted place '{clean}'")
            return True

        # Restore on write failure
        self._load_places()
        return False

    def list_places(self) -> list:
        """
        Return sorted list of place name dicts.

        Returns:
            [{"name": str, "x": float|None, "y": float|None,
              "heading": float|None, "has_odom": bool, "saved_at": str}]
        """
        with self._places_lock:
            places = list(self._places.values())
        return sorted(places, key=lambda p: p.get("name", ""))

    def rename_place(self, old_name: str, new_name: str) -> bool:
        """Rename a saved place."""
        old_clean = _sanitize_name(old_name)
        new_clean = _sanitize_name(new_name)

        if not new_clean:
            print(f"  [Memory] ⚠️  New name '{new_name}' is invalid")
            return False

        with self._places_lock:
            if old_clean not in self._places:
                print(f"  [Memory] ⚠️  Place '{old_name}' not found")
                return False
            if new_clean in self._places:
                print(f"  [Memory] ⚠️  Name '{new_clean}' already exists")
                return False

            entry = self._places.pop(old_clean)
            entry["name"] = new_clean
            self._places[new_clean] = entry
            ok = _write_json(PLACES_FILE, self._places)

        if ok:
            print(f"  [Memory] ✅ Renamed '{old_clean}' → '{new_clean}'")
            return True

        self._load_places()
        return False

    # ── SESSION ───────────────────────────────────────────────────────────────

    def start_session(self):
        """
        Begin a new session. Creates session folder, sets session ID.

        Edge cases:
            - sessions/ doesn't exist → created
            - session ID collision (same date, same number) → incremented
            - Can only call once — subsequent calls are no-ops with warning
        """
        if self._session_id is not None:
            print(f"  [Memory] ⚠️  Session already running: {self._session_id}")
            return

        self._session_id    = _new_session_id(SESSIONS_DIR)
        self._session_dir   = SESSIONS_DIR / self._session_id
        self._session_start = time.time()

        try:
            self._session_dir.mkdir(parents=True, exist_ok=True)
        except OSError as e:
            print(f"  [Memory] ⚠️  Cannot create session dir: {e}")
            self._session_id  = None
            self._session_dir = None
            return

        # Load previous session summary for context
        prev = self._get_previous_session_dir()
        if prev:
            print(f"  [Memory] Previous session: {prev.name}")
        else:
            print("  [Memory] First session — no previous memory")

        print(f"  [Memory] ✅ Session started: {self._session_id}")

        # Auto-flush every 60 seconds in background
        self._start_autosave()

    def _start_autosave(self):
        """Background thread — flush session to disk every 60s."""
        def _loop():
            while self._session_id is not None:
                time.sleep(60)
                self._flush_session()
        threading.Thread(target=_loop, daemon=True).start()

    def _flush_session(self):
        """Write current session buffers to disk without closing session."""
        if self._session_dir is None:
            return
        with self._session_lock:
            _write_json(self._session_dir / "commands.json",   self._commands)
            _write_json(self._session_dir / "detections.json", self._detections)
            _write_json(self._session_dir / "alerts.json",     self._alerts)
            # Save copy of places as they were this session
            with self._places_lock:
                _write_json(self._session_dir / "places.json", self._places)

    def log_command(self, cmd: str, response: str = "", duration_s: float = 0.0):
        """
        Log a command and its response to the current session.

        Args:
            cmd        : the command string typed/spoken
            response   : Marcus's spoken response
            duration_s : how long LLaVA took to respond

        Edge cases:
            - No active session → logs to memory only, warns
            - cmd too long → truncated at MAX_CMD_LEN
            - Unicode (Arabic) → preserved via ensure_ascii=False
            - Thread-safe
        """
        if not cmd:
            return

        entry = {
            "time":       datetime.now().strftime("%H:%M:%S"),
            "cmd":        cmd[:MAX_CMD_LEN],
            "response":   response[:MAX_CMD_LEN] if response else "",
            "duration_s": round(duration_s, 2),
        }

        with self._session_lock:
            self._commands.append(entry)

        if self._session_dir is None:
            print("  [Memory] ⚠️  No active session — command logged in memory only")

    def log_detection(self, class_name: str, position: str = "",
                      distance: str = "", x: float = None, y: float = None):
        """
        Log a YOLO detection to the current session.

        Deduplicates: same class within DETECT_DEDUPE seconds is suppressed.

        Args:
            class_name : e.g. "person"
            position   : "left" / "center" / "right"
            distance   : "close" / "medium" / "far"
            x, y       : robot position when detection occurred (from odometry)
        """
        if not class_name:
            return

        # Deduplicate — don't log same class repeatedly within dedupe window
        now = time.time()
        last = self._last_detection.get(class_name, 0)
        if now - last < DETECT_DEDUPE:
            return
        self._last_detection[class_name] = now

        entry = {
            "time":     datetime.now().strftime("%H:%M:%S"),
            "class":    class_name,
            "position": position,
            "distance": distance,
            "x":        round(x, 3) if x is not None else None,
            "y":        round(y, 3) if y is not None else None,
        }

        with self._session_lock:
            self._detections.append(entry)

    def log_alert(self, alert_type: str, detail: str = ""):
        """
        Log a PPE or hazard alert to the current session.

        Args:
            alert_type : "PPE", "Hazard", "obstacle", etc.
            detail     : e.g. "no helmet (left)", "fire extinguisher missing"
        """
        entry = {
            "time":   datetime.now().strftime("%H:%M:%S"),
            "type":   alert_type,
            "detail": detail[:200],
        }
        with self._session_lock:
            self._alerts.append(entry)
        print(f"  [Memory] 🚨 Alert logged: {alert_type} — {detail}")

    def get_last_command(self) -> str:
        """Return the last command typed, or empty string."""
        with self._session_lock:
            if self._commands:
                return self._commands[-1].get("cmd", "")
        return ""

    def get_last_n_commands(self, n: int = 5) -> list:
        """Return last N command strings for LLaVA context."""
        with self._session_lock:
            recent = self._commands[-n:] if len(self._commands) >= n else self._commands[:]
        return [e.get("cmd", "") for e in recent]

    def get_session_detections(self) -> list:
        """Return all YOLO detections this session."""
        with self._session_lock:
            return list(self._detections)

    def end_session(self):
        """
        Save everything and close the current session.
        Call on clean shutdown or Ctrl+C.

        Edge cases:
            - No active session → no-op
            - Disk full → logs error but doesn't crash
            - Generates summary text file
            - Cleans up old sessions if > MAX_SESSIONS
        """
        if self._session_id is None:
            return

        print(f"\n  [Memory] Saving session {self._session_id}...")

        self._flush_session()
        self._write_summary()
        self._prune_old_sessions()

        elapsed = time.time() - (self._session_start or time.time())
        mins = int(elapsed // 60)
        secs = int(elapsed % 60)

        with self._session_lock:
            n_cmds  = len(self._commands)
            n_dets  = len(self._detections)
            n_alerts= len(self._alerts)

        print(f"  [Memory] ✅ Session saved: {self._session_id}")
        print(f"  [Memory]    Duration: {mins}m {secs}s")
        print(f"  [Memory]    Commands: {n_cmds} | Detections: {n_dets} | Alerts: {n_alerts}")

        # Reset state
        self._session_id  = None
        self._session_dir = None

    def _emergency_save(self):
        """Called by atexit on crash — saves partial session data."""
        if self._session_id is None:
            return
        print("\n  [Memory] Emergency save on exit...")
        self._flush_session()

    def _write_summary(self):
        """Generate a plain text summary of the session."""
        if self._session_dir is None:
            return
        try:
            with self._session_lock:
                n_cmds   = len(self._commands)
                n_dets   = len(self._detections)
                n_alerts = len(self._alerts)
                places   = list(self._places.keys())
                cmds     = [e.get("cmd", "") for e in self._commands[:10]]

            elapsed = time.time() - (self._session_start or time.time())
            lines = [
                f"Session: {self._session_id}",
                f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
                f"Duration: {int(elapsed//60)}m {int(elapsed%60)}s",
                f"Commands: {n_cmds}",
                f"YOLO detections: {n_dets}",
                f"Alerts: {n_alerts}",
                f"Known places: {', '.join(places) if places else 'none'}",
                "",
                "First commands:",
            ] + [f"  - {c}" for c in cmds]

            with open(self._session_dir / "summary.txt", "w", encoding="utf-8") as f:
                f.write("\n".join(lines))
        except Exception as e:
            print(f"  [Memory] ⚠️  Summary write failed: {e}")

    def _prune_old_sessions(self):
        """Delete oldest sessions if total exceeds MAX_SESSIONS."""
        try:
            sessions = sorted(
                [d for d in SESSIONS_DIR.iterdir() if d.is_dir()],
                key=lambda d: d.stat().st_mtime
            )
            while len(sessions) > MAX_SESSIONS:
                oldest = sessions.pop(0)
                shutil.rmtree(oldest, ignore_errors=True)
                print(f"  [Memory] Pruned old session: {oldest.name}")
        except Exception as e:
            print(f"  [Memory] ⚠️  Session pruning failed: {e}")

    # ── PREVIOUS SESSION RECALL ───────────────────────────────────────────────

    def _get_previous_session_dir(self) -> Path:
        """Return the most recent session dir that is NOT the current session."""
        try:
            dirs = sorted(
                [d for d in SESSIONS_DIR.iterdir()
                 if d.is_dir() and d.name != self._session_id],
                key=lambda d: d.stat().st_mtime,
                reverse=True
            )
            return dirs[0] if dirs else None
        except Exception:
            return None

    def last_session_summary(self) -> str:
        """
        Return text summary of the most recent previous session.
        Used when user says 'what did you do last session?'

        Edge cases:
            - No previous session → informative message
            - Summary file missing → reconstruct from commands.json
            - Commands file corrupt → return partial info
        """
        prev = self._get_previous_session_dir()
        if prev is None:
            return "No previous session found. This appears to be the first session."

        # Try summary.txt first
        summary_file = prev / "summary.txt"
        if summary_file.exists():
            try:
                return summary_file.read_text(encoding="utf-8")
            except Exception:
                pass

        # Reconstruct from raw data
        cmds = _read_json(prev / "commands.json", [])
        dets = _read_json(prev / "detections.json", [])
        pls  = _read_json(prev / "places.json", {})

        lines = [
            f"Previous session: {prev.name}",
            f"Commands logged: {len(cmds)}",
            f"Objects detected: {len(dets)}",
            f"Places saved: {', '.join(pls.keys()) if pls else 'none'}",
        ]
        if cmds:
            lines.append("Commands included:")
            for c in cmds[:10]:
                lines.append(f"  - {c.get('cmd','')}")
        return "\n".join(lines)

    def previous_session_detections(self) -> list:
        """
        Return list of unique object classes seen in previous session.
        Used for 'what objects did you see last time?'
        """
        prev = self._get_previous_session_dir()
        if prev is None:
            return []
        dets = _read_json(prev / "detections.json", [])
        if not isinstance(dets, list):
            return []
        return list({d.get("class", "") for d in dets if d.get("class")})

    def previous_session_places(self) -> dict:
        """
        Return places saved in the previous session.
        """
        prev = self._get_previous_session_dir()
        if prev is None:
            return {}
        data = _read_json(prev / "places.json", {})
        return data if isinstance(data, dict) else {}

    def all_sessions(self) -> list:
        """
        Return list of all session IDs with basic stats.
        """
        result = []
        try:
            for d in sorted(SESSIONS_DIR.iterdir(), reverse=True):
                if not d.is_dir():
                    continue
                cmds = _read_json(d / "commands.json", [])
                result.append({
                    "id":       d.name,
                    "commands": len(cmds) if isinstance(cmds, list) else 0,
                    "date":     "_".join(d.name.split("_")[2:]) if "_" in d.name else "",
                })
        except Exception:
            pass
        return result

    # ── QUICK LOOKUPS ─────────────────────────────────────────────────────────

    def session_duration_str(self) -> str:
        """Return human-readable session duration e.g. '14m 22s'."""
        if self._session_start is None:
            return "0m 0s"
        elapsed = time.time() - self._session_start
        return f"{int(elapsed//60)}m {int(elapsed%60)}s"

    def places_count(self) -> int:
        with self._places_lock:
            return len(self._places)

    def commands_count(self) -> int:
        with self._session_lock:
            return len(self._commands)

    def __repr__(self):
        return (f"Memory(session={self._session_id}, "
                f"places={self.places_count()}, "
                f"commands={self.commands_count()})")


# ══════════════════════════════════════════════════════════════════════════════
# STANDALONE TEST
# ══════════════════════════════════════════════════════════════════════════════

if __name__ == "__main__":
    print("Marcus Memory — Standalone Test")
    print("=================================\n")

    mem = Memory()
    mem.start_session()

    print("\n--- Place memory ---")
    mem.save_place("door",   x=0.0,  y=0.0,  heading=0.0)
    mem.save_place("desk a", x=1.5,  y=0.3,  heading=45.0)
    mem.save_place("window", x=3.0,  y=0.0,  heading=180.0)
    mem.save_place("",       x=0.0,  y=0.0,  heading=0.0)   # empty name → rejected
    mem.save_place("desk a", x=1.6,  y=0.4,  heading=50.0)  # duplicate → overwrite

    print("\n--- Get place ---")
    p = mem.get_place("door")
    print(f"door → {p}")

    p = mem.get_place("dooor")          # typo → fuzzy suggestion
    p = mem.get_place("nonexistent")    # not found + list all

    print("\n--- List places ---")
    for pl in mem.list_places():
        odom = f"x={pl['x']} y={pl['y']}" if pl['has_odom'] else "no odom"
        print(f"  {pl['name']:20} {odom}")

    print("\n--- Command logging ---")
    mem.log_command("turn left", "Turning left", duration_s=6.2)
    mem.log_command("what do you see", "I see a person at a desk", duration_s=7.1)
    mem.log_command("go to door", "Navigating to door", duration_s=0.1)
    print(f"Last command: '{mem.get_last_command()}'")
    print(f"Last 3: {mem.get_last_n_commands(3)}")

    print("\n--- Detection logging ---")
    mem.log_detection("person", "center", "close", x=1.5, y=0.3)
    mem.log_detection("person", "center", "close", x=1.5, y=0.3)  # dedupe → suppressed
    mem.log_detection("chair",  "right",  "medium", x=1.5, y=0.3)

    print("\n--- Alert logging ---")
    mem.log_alert("PPE", "no helmet (left)")
    mem.log_alert("Hazard", "fire extinguisher missing")

    print("\n--- Previous session ---")
    print(mem.last_session_summary())

    print("\n--- All sessions ---")
    for s in mem.all_sessions():
        print(f"  {s['id']:40} {s['commands']} commands")

    print(f"\n--- Session duration: {mem.session_duration_str()} ---")
    print(f"--- {repr(mem)} ---\n")

    print("\n--- Rename place ---")
    mem.rename_place("window", "window_north")

    print("\n--- Delete place ---")
    mem.delete_place("door")
    mem.delete_place("nonexistent")

    print("\n--- End session ---")
    mem.end_session()
    print("\nDone.")
