import time

from .client import DEFAULT_SCOPE_ORDER, SteleClient


def _default_scope_order():
    return list(DEFAULT_SCOPE_ORDER)


def _normalize_defaults(input_defaults):
    defaults = dict(input_defaults or {})
    if not str(defaults.get("type") or "").strip():
        defaults["type"] = "MEMORY_TYPE_EPISODIC"
    if not str(defaults.get("scope") or "").strip():
        defaults["scope"] = "SCOPE_AGENT"
    if not defaults.get("confidence"):
        defaults["confidence"] = 0.6
    if not defaults.get("utility_score"):
        defaults["utility_score"] = 0.3
    if not defaults.get("decay_rate"):
        defaults["decay_rate"] = 0.01
    if defaults.get("tags") is None:
        defaults["tags"] = []
    if defaults.get("entities") is None:
        defaults["entities"] = []

    prov = defaults.get("provenance")
    if not isinstance(prov, dict):
        prov = {}
    if prov.get("source_refs") is None and not str(prov.get("origin_turn_id") or "").strip():
        prov["source_refs"] = ["agent:auto"]
    if prov.get("source_refs") is None:
        prov["source_refs"] = []
    if prov.get("citations") is None:
        prov["citations"] = []
    defaults["provenance"] = prov
    return defaults


def _default_outcome(outcome):
    v = str(outcome or "").strip()
    return v or "UTILITY_OUTCOME_NEUTRAL"


def _build_memory_map(candidate, defaults, agent_id, session_id, now_ms):
    c = dict(candidate or {})
    content = str(c.get("content") or "").strip()
    if not content:
        raise ValueError("memory content is required")
    if not str(agent_id or "").strip() or not str(session_id or "").strip():
        raise ValueError("agent_id and session_id are required")

    mem_type = str(c.get("type") or defaults.get("type") or "").strip() or "MEMORY_TYPE_EPISODIC"
    scope = str(c.get("scope") or defaults.get("scope") or "").strip() or "SCOPE_AGENT"
    scope_ref = str(c.get("scope_ref") or defaults.get("scope_ref") or "")

    if scope in ("SCOPE_TEAM", "SCOPE_PROJECT"):
        if not scope_ref.strip():
            raise ValueError("scope_ref required for team/project scope")
    if scope in ("SCOPE_AGENT", "SCOPE_GLOBAL"):
        if scope_ref.strip():
            raise ValueError("scope_ref must be empty for agent/global scope")
        scope_ref = ""

    provenance = c.get("provenance")
    if not isinstance(provenance, dict):
        provenance = dict(defaults.get("provenance") or {})
    source_refs = provenance.get("source_refs")
    if not isinstance(source_refs, list):
        source_refs = []
    citations = provenance.get("citations")
    if not isinstance(citations, list):
        citations = []
    origin_turn_id = str(provenance.get("origin_turn_id") or "")

    timestamp = int(c.get("timestamp_ms") or 0) or int(now_ms)
    confidence = float(c.get("confidence") or 0) or float(defaults.get("confidence") or 0) or 0.6
    utility = float(c.get("utility_score") or 0) or float(defaults.get("utility_score") or 0) or 0.3
    decay = float(c.get("decay_rate") or 0) or float(defaults.get("decay_rate") or 0) or 0.01
    protected_floor = float(c.get("protected_floor") or 0) or float(defaults.get("protected_floor") or 0) or 0
    tags = c.get("tags")
    if tags is None:
        tags = defaults.get("tags") or []
    entities = c.get("entities")
    if entities is None:
        entities = defaults.get("entities") or []
    tool_id = str(c.get("tool_id") or defaults.get("tool_id") or "")

    return {
        "content": content,
        "type": mem_type,
        "scope": scope,
        "scope_ref": scope_ref,
        "agent_id": agent_id,
        "session_id": session_id,
        "tool_id": tool_id,
        "provenance": {
            "source_refs": source_refs,
            "citations": citations,
            "origin_turn_id": origin_turn_id,
        },
        "timestamp_ms": timestamp,
        "confidence": confidence,
        "utility_score": utility,
        "last_access_at_ms": 0,
        "access_count": 0,
        "decay_rate": decay,
        "protected_flag": bool(c.get("protected_flag") or defaults.get("protected_flag")),
        "protected_floor": protected_floor,
        "supersedes_id": str(c.get("supersedes_id") or ""),
        "tags": tags,
        "entities": entities,
        "edges": c.get("edges"),
    }


class Adapter:
    """
    Adapter wraps a SteleClient for agent workflows:
    - query relevant memory
    - write learnings (optional)
    - send feedback (optional)

    This mirrors the Go SDK adapter in `sdk/go/adapter.go`.
    """

    def __init__(
        self,
        client=None,
        client_config=None,
        auto_write=True,
        auto_feedback=False,
        default_memory=None,
        scope_order=None,
        budget_tokens=0,
        include_debug=False,
        now_fn=None,
    ):
        if client is None:
            client = SteleClient(**(client_config or {}))
        self.client = client
        self.auto_write = bool(auto_write)
        self.auto_feedback = bool(auto_feedback)
        self.defaults = _normalize_defaults(default_memory or {})
        self.scope_order = list(scope_order) if scope_order else _default_scope_order()
        self.budget_tokens = int(budget_tokens or 0)
        self.include_debug = bool(include_debug)
        self.now_fn = now_fn or (lambda: int(time.time() * 1000))

    def query_memories(self, query_text, opts=None):
        if self.client is None:
            raise ValueError("adapter requires client")
        query_text = str(query_text or "").strip()
        if not query_text:
            raise ValueError("query_text is required")
        opts = dict(opts or {})

        budget = int(opts.get("budget_tokens") or 0) or self.budget_tokens
        scope_order = opts.get("scope_order") or self.scope_order
        include_debug = bool(opts.get("include_debug") or self.include_debug)

        return self.client.query(
            {
                "query_text": query_text,
                "plan": str(opts.get("plan") or "").strip(),
                "task_state": opts.get("task_state"),
                "budget_tokens": budget or None,
                "budget_strategy": str(opts.get("budget_strategy") or "").strip() or None,
                "scope_order": scope_order,
                "include_debug": include_debug,
                "query_hash": opts.get("query_hash"),
                "cursor": opts.get("cursor"),
            }
        )

    def format_memories(self, memories, max_items=0, include_metadata=False):
        if not memories:
            return ""
        items = list(memories or [])
        n = int(max_items or 0)
        if n <= 0 or n > len(items):
            n = len(items)
        lines = ["MEMORY CONTEXT:"]
        for i in range(n):
            m = items[i] or {}
            content = str(m.get("content") or "").replace("\n", " ")
            if include_metadata:
                mem_type = str(m.get("type") or "")
                scope = str(m.get("scope") or "")
                mem_id = str(m.get("id") or "")
                lines.append(f"- [{mem_type} {scope} {mem_id}] {content}")
            else:
                lines.append(f"- {content}")
        return "\n".join(lines)

    def record_turn(self, turn=None):
        if self.client is None:
            raise ValueError("adapter requires client")
        turn = dict(turn or {})
        result = {"writes": [], "feedback": []}
        now_ms = int(self.now_fn() or int(time.time() * 1000))

        used_ids = turn.get("used_memory_ids") or turn.get("usedMemoryIDs") or []
        learnings = turn.get("learnings") or []
        feedback_signals = turn.get("feedback") or []

        if not feedback_signals and self.auto_feedback and used_ids:
            feedback_signals = []
            for mem_id in used_ids:
                mem_id = str(mem_id or "").strip()
                if not mem_id:
                    continue
                feedback_signals.append(
                    {
                        "memory_id": mem_id,
                        "outcome": "UTILITY_OUTCOME_NEUTRAL",
                        "explicit_feedback": 0.1,
                        "timestamp_ms": now_ms,
                    }
                )

        for signal in feedback_signals:
            signal = dict(signal or {})
            mem_id = str(signal.get("memory_id") or "").strip()
            if not mem_id:
                continue
            ts = int(signal.get("timestamp_ms") or 0) or now_ms
            resp = self.client.feedback(
                {
                    "signal": {
                        "memory_id": mem_id,
                        "agent_id": self.client.agent_id,
                        "session_id": self.client.session_id,
                        "outcome": _default_outcome(signal.get("outcome")),
                        "explicit_feedback": float(signal.get("explicit_feedback") or 0),
                        "timestamp_ms": ts,
                    }
                }
            )
            result["feedback"].append(resp)

        if self.auto_write:
            for learning in learnings:
                learning = dict(learning or {})
                mem = _build_memory_map(
                    learning.get("memory") or {},
                    self.defaults,
                    self.client.agent_id,
                    self.client.session_id,
                    now_ms,
                )
                resp = self.client.write(
                    {
                        "memory": mem,
                        "idempotency_key": learning.get("idempotency_key") or "",
                        "dedupe_key": learning.get("dedupe_key") or "",
                        "allow_update": bool(learning.get("allow_update")),
                    }
                )
                result["writes"].append(resp)

        return result


def create_adapter(cfg=None):
    return Adapter(**(cfg or {}))

