"""
Hardin Memory — official Python client (zero dependencies)
==========================================================

Drop this single file into your project and you have hardened, poison-proof,
verifiable memory for your AI agent in a few lines. Standard library only.

    from hardin_memory import HardinMemory

    mem = HardinMemory("YOUR_API_KEY")
    mem.remember("Customer prefers email over phone.", kind="fact", source="crm")

    hits = mem.recall("how does the customer like to be contacted?")
    for h in hits["results"]:
        print(h["score"], h["text"], "verified:", h["verified"])

    proof = mem.audit()          # {"verdict": "INTACT", ...}

Every call is hardened and verified automatically — lies can't get in, and you
can prove the memory is clean. Get a key at https://memory.hardinai.co.uk

© 2026 Hardin Enterprises Ltd. Provided for use with the Hardin Memory service.
"""

import json
import urllib.request
import urllib.error
import urllib.parse

DEFAULT_BASE = "https://memory.hardinai.co.uk"


class HardinMemoryError(Exception):
    """Raised when the API returns an error or is unreachable."""


class HardinMemory:
    def __init__(self, api_key: str, base_url: str = DEFAULT_BASE, timeout: int = 30):
        if not api_key:
            raise ValueError("api_key is required")
        self.api_key = api_key
        self.base = base_url.rstrip("/")
        self.timeout = timeout

    def _call(self, method: str, path: str, body: dict = None) -> dict:
        url = self.base + path
        data = json.dumps(body).encode() if body is not None else None
        req = urllib.request.Request(url, data=data, method=method)
        req.add_header("Authorization", "Bearer " + self.api_key)
        req.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(req, timeout=self.timeout) as r:
                return json.loads(r.read().decode())
        except urllib.error.HTTPError as e:
            detail = e.read().decode(errors="replace")
            raise HardinMemoryError(f"HTTP {e.code}: {detail}") from None
        except urllib.error.URLError as e:
            raise HardinMemoryError(f"connection error: {e.reason}") from None

    def remember(self, text: str, kind: str = "fact", source: str = "api") -> dict:
        """Store a memory. Returns {shard_id, signed, chain_index, created}."""
        return self._call("POST", "/v1/memory/remember",
                          {"text": text, "kind": kind, "source": source})

    def recall(self, query: str, k: int = 5) -> dict:
        """Find the most relevant VERIFIED memories. Poisoned/forged ones are
        never returned. Returns {query, results:[{text, score, verified, ...}]}."""
        return self._call("POST", "/v1/memory/recall", {"query": query, "k": k})

    def verify(self, shard_id: str) -> dict:
        """Check one memory's seal. Returns {found, valid, ...}."""
        return self._call("GET", "/v1/memory/verify/" + shard_id)

    def audit(self) -> dict:
        """Prove the whole memory is clean. Returns {verdict, ...}."""
        return self._call("GET", "/v1/memory/audit")

    def usage(self) -> dict:
        """Your current usage and plan."""
        return self._call("GET", "/v1/memory/usage")

    # ── 3-tier trust split ────────────────────────────────────────────
    # Tier 1 = facts of record (immutable, signed) — the ONLY entitlement source.
    # Tier 2 = preferences (mutable, low-trust) — can never grant a right.
    # Tier 3 = learned policy (gated) — propose, then a human approves.
    def _q(self, path: str, params: dict) -> str:
        clean = {k: v for k, v in params.items() if v is not None}
        return path + ("?" + urllib.parse.urlencode(clean) if clean else "")

    # Tier 1 — facts of record
    def record_fact(self, fact_type: str, subject: str, data: dict, source: str) -> dict:
        """Append a SIGNED fact of record (refund issued, entitlement granted, …).
        `source` must name the verified system event, e.g. 'stripe:invoice.paid'.
        This is the only thing entitlement decisions trust. Cannot come from user text."""
        return self._call("POST", "/v1/memory/facts", {
            "fact_type": fact_type, "subject": subject, "data": data, "source": source})

    def get_facts(self, fact_type: str = None, subject: str = None) -> dict:
        """Read signed facts by reference (never semantic guessing)."""
        return self._call("GET", self._q("/v1/memory/facts",
                          {"fact_type": fact_type, "subject": subject}))

    def check_entitlement(self, subject: str, entitlement: str) -> dict:
        """Decision-grade check. Reads ONLY Tier-1 signed facts; fails closed.
        Returns {granted: bool, source, ...}."""
        return self._call("GET", self._q("/v1/memory/entitlement",
                          {"subject": subject, "entitlement": entitlement}))

    def revoke_entitlement(self, subject: str, entitlement: str, source: str, reason: str = "") -> dict:
        """Officially cancel an entitlement by appending a signed superseding fact.
        `source` must name the verified system event (e.g. 'stripe:refund.created')."""
        return self._call("POST", "/v1/memory/facts/revoke",
                          {"subject": subject, "entitlement": entitlement,
                           "source": source, "reason": reason})

    def verify_decision(self, receipt: dict) -> dict:
        """Independently verify a signed decision receipt against the public key.
        No API key needed — anyone can verify a decision your agent made."""
        return self._call("POST", "/v1/memory/decision/verify", {"receipt": receipt})

    def facts_audit(self) -> dict:
        """Whole-store integrity proof over the facts of record."""
        return self._call("GET", "/v1/memory/facts/audit")

    # Tier 2 — preferences
    def set_preference(self, key: str, value, source: str = "user") -> dict:
        """Store a soft preference (tone, language, UI). Entitlement-shaped keys
        are refused — Tier-2 can never grant a right."""
        return self._call("PUT", "/v1/memory/preferences",
                          {"key": key, "value": value, "source": source})

    def get_preferences(self) -> dict:
        return self._call("GET", "/v1/memory/preferences")

    # Tier 3 — learned policy (gated)
    def propose_policy(self, name: str, rule: dict, rationale: str = "",
                       proposed_by: str = "learning") -> dict:
        """Propose a learned policy. It stays INERT until approved."""
        return self._call("POST", "/v1/memory/policy/propose",
                          {"name": name, "rule": rule, "rationale": rationale,
                           "proposed_by": proposed_by})

    def pending_policies(self) -> dict:
        return self._call("GET", "/v1/memory/policy/pending")

    def approve_policy(self, proposal_id: str, approver: str) -> dict:
        """Activate a proposal (human/governance step). Signs it on approval."""
        return self._call("POST", "/v1/memory/policy/approve",
                          {"proposal_id": proposal_id, "approver": approver})

    def reject_policy(self, proposal_id: str, approver: str, reason: str = "") -> dict:
        return self._call("POST", "/v1/memory/policy/reject",
                          {"proposal_id": proposal_id, "approver": approver, "reason": reason})

    def active_policies(self) -> dict:
        return self._call("GET", "/v1/memory/policy")

    def decide(self, subject: str, entitlement: str) -> dict:
        """The safe decision: YES/NO from Tier-1 facts only, personalised by
        Tier-2 prefs, handled per Tier-3 policy. Returns
        {granted, decision_source, personalisation, active_policies}."""
        return self._call("GET", self._q("/v1/memory/decide",
                          {"subject": subject, "entitlement": entitlement}))

    # ── Feedback & learning (Tier 2/3, safe by construction) ───────────
    def record_outcome(self, subject: str, decision: str, outcome: str,
                       sentiment: str = None, signal: dict = None,
                       weight: float = 1.0, context: dict = None) -> dict:
        """Log an outcome (resolved / disputed / escalated / …) for an interaction.
        signal={"pref": {...}} carries preference hints the learner may adopt;
        context={"topic": ...} feeds policy mining. Low-trust — never a decision input."""
        return self._call("POST", "/v1/memory/feedback",
                          {"subject": subject, "decision": decision, "outcome": outcome,
                           "sentiment": sentiment, "signal": signal, "weight": weight,
                           "context": context})

    def feedback_stats(self) -> dict:
        return self._call("GET", "/v1/memory/feedback/stats")

    def learn(self, min_support: int = 3, consistency: float = 0.6) -> dict:
        """Run the learner: adopt consistent preferences (Tier 2) and PROPOSE
        policies (Tier 3, gated). Never grants a right. Returns the report."""
        return self._call("POST", "/v1/memory/learn",
                          {"min_support": min_support, "consistency": consistency})

    def profile(self, subject: str) -> dict:
        """Learned per-subject preference profile (safe to personalise with)."""
        return self._call("GET", self._q("/v1/memory/profile", {"subject": subject}))


if __name__ == "__main__":
    import os, sys
    key = os.environ.get("HARDIN_MEMORY_KEY") or (sys.argv[1] if len(sys.argv) > 1 else "")
    if not key:
        print("usage: HARDIN_MEMORY_KEY=... python hardin_memory.py")
        raise SystemExit(1)
    m = HardinMemory(key)
    print("remember:", m.remember("Hello from the Hardin Memory client."))
    print("recall:", m.recall("hello"))
    print("audit:", m.audit())
