"""Owner-scoped preference memory with expiry, version checks and real process restart."""
from contextlib import closing
import json
from pathlib import Path
import re
import sqlite3
import subprocess
import sys
import tempfile
import uuid


class Conflict(Exception):
    pass


class MemoryStore:
    def __init__(self, path, owner):
        if not isinstance(owner, str) or not owner:
            raise ValueError("Trusted owner required")
        self.path, self.owner = path, owner
        with closing(self.connect()) as db, db:
            db.execute("CREATE TABLE IF NOT EXISTS memory (owner TEXT, key TEXT, value TEXT, source TEXT, version TEXT, expires_at INTEGER, updated_at INTEGER, PRIMARY KEY(owner,key))")

    def connect(self):
        db = sqlite3.connect(self.path)
        db.row_factory = sqlite3.Row
        return db

    def get(self, key, now):
        with closing(self.connect()) as db:
            row = db.execute("SELECT key,value,source,version,expires_at,updated_at FROM memory WHERE owner=? AND key=? AND expires_at>?", (self.owner, key, now)).fetchone()
        return dict(row) if row else None

    def put(self, key, value, *, source, expires_at, now, expected_version=None):
        if not isinstance(key, str) or not re.fullmatch(r"[a-z][a-z0-9_-]{0,39}", key):
            raise ValueError("Invalid memory key")
        if not isinstance(value, str) or not 1 <= len(value) <= 500 or source not in {"explicit_user", "model_inference"}:
            raise ValueError("Invalid value or provenance")
        if type(expires_at) is not int or type(now) is not int or expires_at <= now:
            raise ValueError("Expiry must be an integer later than now")
        with closing(self.connect()) as db, db:
            db.execute("BEGIN IMMEDIATE")
            current = db.execute("SELECT * FROM memory WHERE owner=? AND key=?", (self.owner, key)).fetchone()
            if (current["version"] if current else None) != expected_version:
                raise Conflict("stale_version")
            if current and current["expires_at"] > now and current["source"] == "explicit_user" and source == "model_inference":
                raise Conflict("inference_cannot_replace_explicit_preference")
            version = uuid.uuid4().hex
            db.execute("INSERT INTO memory VALUES (?,?,?,?,?,?,?) ON CONFLICT(owner,key) DO UPDATE SET value=excluded.value,source=excluded.source,version=excluded.version,expires_at=excluded.expires_at,updated_at=excluded.updated_at", (self.owner, key, value, source, version, expires_at, now))
        return self.get(key, now)

    def delete(self, key, *, expected_version):
        with closing(self.connect()) as db, db:
            db.execute("BEGIN IMMEDIATE")
            row = db.execute("SELECT version FROM memory WHERE owner=? AND key=?", (self.owner, key)).fetchone()
            if row is None:
                return False
            if row["version"] != expected_version:
                raise Conflict("stale_version")
            db.execute("DELETE FROM memory WHERE owner=? AND key=?", (self.owner, key))
        return True

    def purge_expired(self, now):
        with closing(self.connect()) as db, db:
            count = db.execute("DELETE FROM memory WHERE owner=? AND expires_at<=?", (self.owner, now)).rowcount
        return count


def child_read(path, owner, now):
    process = subprocess.run([sys.executable, str(Path(__file__).resolve()), "--fixture-read", str(path), owner, str(now)], capture_output=True, text=True, check=True)
    return json.loads(process.stdout)


def fixtures():
    results = []
    def check(name, condition):
        if not condition:
            raise AssertionError(name)
        results.append({"case": name, "passed": True})
    def rejected(name, callback, reason):
        try:
            callback()
        except Conflict as error:
            check(name, str(error) == reason)
        else:
            raise AssertionError(name)
    with tempfile.TemporaryDirectory(prefix="memory-lesson-") as directory:
        path = Path(directory)/"memory.sqlite"
        alice, bob = MemoryStore(path, "alice"), MemoryStore(path, "bob")
        first = alice.put("tone", "concise", source="explicit_user", expires_at=200, now=100)
        check("store-and-read", alice.get("tone", 100)["value"] == "concise")
        check("other-owner-cannot-read", bob.get("tone", 100) is None)
        bob.put("tone", "verbose", source="explicit_user", expires_at=200, now=100)
        updated = alice.put("tone", "detailed", source="explicit_user", expires_at=200, now=110, expected_version=first["version"])
        check("update-rotates-version", updated["value"] == "detailed" and updated["version"] != first["version"])
        check("owners-have-separate-values", bob.get("tone", 110)["value"] == "verbose")
        rejected("stale-update", lambda: alice.put("tone", "old edit", source="explicit_user", expires_at=200, now=111, expected_version=first["version"]), "stale_version")
        rejected("inference-cannot-overwrite", lambda: alice.put("tone", "inferred", source="model_inference", expires_at=200, now=111, expected_version=updated["version"]), "inference_cannot_replace_explicit_preference")
        check("fresh-process-reads-updated-value", child_read(path, "alice", 120) == updated)
        check("delete", alice.delete("tone", expected_version=updated["version"]))
        check("fresh-process-confirms-deletion", child_read(path, "alice", 120) is None)
        check("expires-at-boundary", bob.get("tone", 199) is not None and bob.get("tone", 200) is None)
        check("purge-expired-owner-only", bob.purge_expired(200) == 1)
        recreated = alice.put("tone", "concise again", source="explicit_user", expires_at=400, now=250)
        check("recreated-record-has-new-version", recreated["version"] != updated["version"])
        rejected("old-delete-cannot-remove-recreated-record", lambda: alice.delete("tone", expected_version=updated["version"]), "stale_version")
    print(json.dumps({"mode": "local SQLite preferences; synthetic owners; no model or network", "passed": len(results), "restart": "two separate Python subprocess reads", "results": results}, indent=2))


if __name__ == "__main__":
    if len(sys.argv) == 5 and sys.argv[1] == "--fixture-read":
        print(json.dumps(MemoryStore(sys.argv[2], sys.argv[3]).get("tone", int(sys.argv[4]))))
    elif len(sys.argv) == 1:
        fixtures()
    else:
        raise SystemExit("Run python memory_lesson.py for the local exercise")
