"""Condensa Encoder — Natural language -> Condensa format.

This encoder uses pattern matching and heuristic rules to convert
natural language AI-to-AI messages into compact Condensa notation.
It is a prototype demonstrating the compression potential; a production
encoder would use an LLM for semantic understanding.

v0.3 changes:
  - !! -> !? for sync commands
  - // alternative operator removed; always emit fb:value
  - Verb adverbs replaced by modifiers: ' -> /each, \\ -> /scan, ^:N -> /repeat:N
  - Backward-compatible: v0.1 and v0.2 input still encodes correctly

v0.4 changes:
  - Added delegation encoder (>:@Agent action)
  - Added result encoder (=: values)
  - Added receive/acknowledge encoder (<:ack)
  - Fixed error encoder false positives on conceptual mentions
  - Status encoder now captures numeric metrics
  - Pipeline detection from "and then" / "followed by"
  - Enhanced parameter extraction (source, field, condition, paths)
  - Fallback always produces valid Condensa syntax
  - Added missing action verbs
"""

import re
from .utils import (
    ACTIONS, MSG_TYPES, URGENCY, FALLBACKS, FORMAT_MAP, ERROR_CODES,
    IMPLICIT_VARS_REV, POLICY_FIELDS,
    count_tokens,
)


class CondensaEncoder:
    """Converts natural language AI-to-AI messages to Condensa format."""

    def encode(self, text: str) -> str:
        """Encode a natural language message into Condensa format.

        Returns the most compact Condensa representation possible.
        """
        text = text.strip()

        # Try specialized encoders in order of specificity
        for encoder in [
            self._encode_error,
            self._encode_cancel,
            self._encode_status,
            self._encode_result,
            self._encode_receive,
            self._encode_policy,
            self._encode_code_definition,
            self._encode_delegation,
            self._encode_workflow,
            self._encode_correction,
            self._encode_parallel,
            self._encode_conditional,
            self._encode_context_update,
            self._encode_pipeline_command,
            self._encode_command,
        ]:
            result = encoder(text)
            if result:
                return self._add_urgency(text, result)

        # Fallback: always produce valid Condensa syntax
        return self._encode_fallback(text)

    def encode_with_stats(self, text: str) -> dict:
        """Encode and return stats about the compression.

        Includes a v01_equivalent field showing what v0.1 encoding would
        have produced, for benchmarking the improvement.
        """
        condensa = self.encode(text)
        v01 = self._encode_v01(text)
        nl_tokens = count_tokens(text)
        c_tokens = count_tokens(condensa)
        v01_tokens = count_tokens(v01)
        return {
            "original": text,
            "condensa": condensa,
            "nl_tokens": nl_tokens,
            "condensa_tokens": c_tokens,
            "reduction_pct": round((1 - c_tokens / nl_tokens) * 100, 1) if nl_tokens > 0 else 0,
            "v01_equivalent": v01,
            "v01_tokens": v01_tokens,
            "v02_improvement_pct": round((1 - c_tokens / v01_tokens) * 100, 1) if v01_tokens > 0 else 0,
        }

    # ── v0.1 compatibility encoder (for benchmarking) ────────────────

    def _encode_v01(self, text: str) -> str:
        """Produce a v0.1-style encoding (no v0.2 compact forms).

        Used internally by encode_with_stats for comparison.
        """
        text = text.strip()

        for encoder in [
            self._encode_error,
            self._encode_cancel,
            self._encode_status,
            self._encode_workflow_v01,
            self._encode_correction,
            self._encode_parallel_v01,
            self._encode_conditional,
            self._encode_context_update,
            self._encode_command_v01,
        ]:
            result = encoder(text)
            if result:
                return self._add_urgency(text, result)

        return self._encode_fallback(text)

    def _encode_workflow_v01(self, text: str) -> str | None:
        """v0.1 workflow encoder -- no bare verbs, no inline eval."""
        text_lower = text.lower()
        step_indicators = ["first", "then", "after that", "finally", "next", "step 1", "step 2"]
        if sum(1 for s in step_indicators if s in text_lower) < 2:
            return None
        steps = []
        step_texts = re.split(r'(?:first|then|after that|finally|next)[,:]?\s*', text, flags=re.IGNORECASE)
        step_texts = [s.strip().rstrip(".") for s in step_texts if s.strip()]
        for step_text in step_texts:
            step = self._encode_single_action(step_text, bare=False, sync=False, use_v02_fallbacks=False)
            if step:
                steps.append(step)
        if len(steps) >= 2:
            return "seq(" + "; ".join(steps) + ")"
        return None

    def _encode_parallel_v01(self, text: str) -> str | None:
        """v0.1 parallel encoder -- no bare verbs."""
        text_lower = text.lower()
        if not any(w in text_lower for w in ["simultaneous", "parallel", "at the same time", "concurrently"]):
            return None
        task_texts = re.findall(r'task\s*\d+[:\s]+(.+?)(?=task\s*\d+|when all|$)', text, re.IGNORECASE | re.DOTALL)
        if not task_texts:
            task_texts = re.split(r'(?:\d+\)|\d+\.)\s*', text)[1:]
        tasks = []
        for tt in task_texts:
            step = self._encode_single_action(tt.strip().rstrip("."), bare=False, sync=False, use_v02_fallbacks=False)
            if step:
                tasks.append(step)
        if len(tasks) >= 2:
            result = "par(" + "; ".join(tasks) + ")"
            if any(w in text_lower for w in ["merge", "combine", "single response"]):
                result += " | !:mrg"
            return result
        return None

    def _encode_command_v01(self, text: str) -> str | None:
        """v0.1 command encoder -- no sync, no v0.2 fallbacks."""
        step = self._encode_single_action(text, bare=False, sync=False, use_v02_fallbacks=False)
        return step

    # ── Urgency detection ────────────────────────────────────────────

    def _add_urgency(self, original: str, encoded: str) -> str:
        text_lower = original.lower()
        if any(w in text_lower for w in ["urgent", "critical", "emergency", "drop everything", "immediately"]):
            if "critical" in text_lower or "emergency" in text_lower or "drop everything" in text_lower:
                return "+++" + encoded
            return "++" + encoded
        return encoded

    # ── Error messages ───────────────────────────────────────────────

    def _encode_error(self, text: str) -> str | None:
        text_lower = text.lower()
        if not any(w in text_lower for w in ["error", "failed", "failure", "returned a 4", "returned a 5", "503", "429", "timeout"]):
            return None

        # Problem 2 fix: Don't trigger on conceptual/future mentions of errors
        # Only trigger when the text is REPORTING an error (past tense, error codes, etc.)
        _negative_patterns = [
            r'\breport\s+(?:any\s+)?errors?\b',
            r'\bcheck\s+for\s+errors?\b',
            r'\bdetect\s+errors?\b',
            r'\bfind\s+(?:any\s+)?errors?\b',
            r'\blook\s+for\s+errors?\b',
            r'\bhandle\s+errors?\b',
            r'\bcatch\s+errors?\b',
            r'\blog\s+(?:any\s+)?errors?\b',
            r'\bvalidat\w+\b.*\berrors?\b',
            r'\berror\s+handling\b',
            r'\berror\s+detection\b',
            r'\berror\s+checking\b',
        ]
        if any(re.search(p, text_lower) for p in _negative_patterns):
            # Check if there's also an actual error code present — if so, still treat as error
            has_error_code = bool(re.search(r'\b[45]\d{2}\b', text))
            has_past_tense_error = any(w in text_lower for w in ["failed", "failure", "returned a 4", "returned a 5"])
            if not has_error_code and not has_past_tense_error:
                return None

        parts = ["E:"]

        # Extract error code
        code_match = re.search(r'\b(4\d{2}|5\d{2})\b', text)
        if code_match:
            parts.append(code_match.group())
        elif "timeout" in text_lower:
            parts.append("timeout")
        elif "auth" in text_lower:
            parts.append("auth")
        else:
            parts.append("5xx")

        # Extract retry info
        retry_match = re.search(r'(\d+)\s*(?:retries|attempts|tries)', text_lower)
        wait_match = re.search(r'wait\s*(?:for\s+)?(\d+)\s*(seconds?|s|minutes?|m)', text_lower)
        if retry_match:
            parts.append(f"retry:{retry_match.group(1)}")
        if wait_match:
            unit = "s" if wait_match.group(2).startswith("s") else "m"
            parts.append(f"wait:{wait_match.group(1)}{unit}")

        # Backoff
        if "exponential" in text_lower and "backoff" in text_lower:
            parts.append("backoff:exp")

        # Fallback
        if "partial" in text_lower or "what we have" in text_lower:
            parts.append("fb:degrade")
        elif "cache" in text_lower:
            parts.append("fb:cache")
        elif "abort" in text_lower or "stop" in text_lower:
            parts.append("fb:abort")

        return " ".join(parts)

    # ── Cancel messages ──────────────────────────────────────────────

    def _encode_cancel(self, text: str) -> str | None:
        text_lower = text.lower()
        if "cancel" not in text_lower and "abort" not in text_lower:
            return None

        parts = ["X:"]
        if "current" in text_lower:
            parts.append("current")
        elif "all" in text_lower:
            parts.append("all")

        if "discard" in text_lower:
            parts.append("discard:T")
        if "no side effect" in text_lower or "no side-effect" in text_lower:
            parts.append("safe:T")
        if "confirm" in text_lower or "acknowledge" in text_lower:
            parts.append("ack:T")

        return " ".join(parts)

    # ── Status messages ──────────────────────────────────────────────

    def _encode_status(self, text: str) -> str | None:
        text_lower = text.lower()
        if not any(w in text_lower for w in ["completed", "in progress", "processing", "finished", "done", "stage"]):
            return None
        if "error" in text_lower or "failed" in text_lower:
            return None

        parts = ["#:"]

        if "completed" in text_lower or "done" in text_lower or "complete" in text_lower or "finished" in text_lower:
            parts.append("done")
        elif "in progress" in text_lower or "processing" in text_lower or "continuing" in text_lower:
            parts.append("run")
        elif "waiting" in text_lower:
            parts.append("wait")

        # Progress N/M
        progress_match = re.search(r'(\d[\d,]*)\s*(?:out of|of|/)\s*(\d[\d,]*)', text)
        if progress_match:
            n = progress_match.group(1).replace(",", "")
            m = progress_match.group(2).replace(",", "")
            parts.append(f"{n}/{m}")

        # Extract key metrics — Problem 3 fix: capture numbers with context words
        records_match = re.search(r'(\d[\d,]*)\s*records?', text_lower)
        if records_match:
            parts.append(f"records:{records_match.group(1).replace(',', '')}")

        failures_match = re.search(r'(\d+)\s*(?:failures?|failed|errors?)', text_lower)
        if failures_match:
            parts.append(f"fail:{failures_match.group(1)}")

        # Problem 3 fix: Capture additional numeric metrics with context
        _metric_patterns = [
            (r'(\d[\d,]*)\s*(items?)\b', 'items'),
            (r'(\d[\d,]*)\s*(files?)\b', 'files'),
            (r'(\d[\d,]*)\s*(rows?)\b', 'rows'),
            (r'(\d[\d,]*)\s*(entries?)\b', 'entries'),
            (r'(\d[\d,]*)\s*(documents?|docs?)\b', 'docs'),
            (r'(\d[\d,]*)\s*(users?)\b', 'users'),
            (r'(\d[\d,]*)\s*(tasks?)\b', 'tasks'),
            (r'(\d[\d,]*)\s*(bytes?|[KMGT]B)\b', 'size'),
            (r'(\d[\d,]*)\s*(requests?)\b', 'reqs'),
            (r'(\d[\d,]*)\s*(transactions?)\b', 'txns'),
            (r'(\d[\d,]*)\s*(operations?)\b', 'ops'),
            (r'(\d[\d,]*)\s*(messages?|msgs?)\b', 'msgs'),
            (r'(\d[\d,]*)\s*(objects?)\b', 'objs'),
            (r'in\s+(\d[\d,.]*)\s*(seconds?|s|minutes?|m|ms)\b', 'time'),
            (r'(\d[\d,.]*)\s*(seconds?|s|minutes?|m|ms)\s+(?:elapsed|taken)', 'time'),
        ]
        # Don't duplicate records already captured
        for pattern, label in _metric_patterns:
            m = re.search(pattern, text_lower)
            if m:
                val = m.group(1).replace(",", "")
                if label == 'time':
                    unit_raw = m.group(2)
                    unit = "ms" if unit_raw.startswith("ms") else ("s" if unit_raw.startswith("s") else "m")
                    parts.append(f"time:{val}{unit}")
                else:
                    parts.append(f"{label}:{val}")

        # Capture "successfully" as success marker
        if "successfully" in text_lower or "success" in text_lower:
            if "done" not in parts:
                # Already have 'done' state, just note success is confirmed
                pass

        # Capture percentage
        pct_match = re.search(r'(\d+(?:\.\d+)?)\s*%', text)
        if pct_match:
            parts.append(f"pct:{pct_match.group(1)}")

        # Output path
        path_match = re.search(r'(/\S+|s3://\S+)', text)
        if path_match:
            parts.append(f"out:{path_match.group(1)}")

        # Generic number+context that wasn't captured by specific patterns above
        # This catches things like "42 records were processed" when "were" sits between
        already_captured_nums = set()
        for p in parts[2:]:  # skip "#:" and state
            num_m = re.match(r'\w+:(\d+)', p)
            if num_m:
                already_captured_nums.add(num_m.group(1))
        for m in re.finditer(r'(\d[\d,]*)\s+(\w+)', text_lower):
            num = m.group(1).replace(",", "")
            word = m.group(2)
            if num not in already_captured_nums and word not in ("out", "of", "a", "the", "and", "or", "to", "in", "is", "was", "were", "are", "for", "stage", "stages"):
                if word in ("processed", "updated", "created", "deleted", "inserted", "modified", "skipped", "matched", "found"):
                    parts.append(f"{word}:{num}")
                    already_captured_nums.add(num)

        return " ".join(parts)

    # ── Result messages (Problem 4) ──────────────────────────────────

    def _encode_result(self, text: str) -> str | None:
        """Encode result/answer messages as =: type."""
        text_lower = text.lower()

        # Must contain result-indicating words
        _result_triggers = [
            r'\b(?:here\s+(?:are|is)\s+the\s+results?)\b',
            r'\b(?:the\s+answer\s+is)\b',
            r'\b(?:the\s+result\s+is)\b',
            r'\bfound\s+\d+\b',
            r'\breturning\b',
            r'\b(?:here\s+(?:are|is))\b',
            r'\boutput\s*:\s*\b',
            r'\bresults?\s*:\s*',
        ]
        if not any(re.search(p, text_lower) for p in _result_triggers):
            return None

        # Don't trigger if it looks more like a command or error
        if any(w in text_lower for w in ["error", "failed", "cancel", "abort"]):
            return None
        # Don't trigger for things like "search for results" which is a command
        if re.search(r'\b(?:search|find|get|fetch)\s+(?:for\s+)?results?\b', text_lower):
            return None

        parts = ["=:"]

        # Extract specific values
        # "The answer is 42" -> =: 42
        answer_match = re.search(r'(?:answer|result)\s+is\s+(.+?)(?:\.|$)', text, re.IGNORECASE)
        if answer_match:
            parts.append(answer_match.group(1).strip().rstrip("."))
            return " ".join(parts)

        # "Found 3 matches" -> =: found:3 matches
        found_match = re.search(r'found\s+(\d+)\s+(\w+)', text_lower)
        if found_match:
            parts.append(f"found:{found_match.group(1)}")
            parts.append(found_match.group(2))
            return " ".join(parts)

        # "Here are the results: X, Y, Z" -> =: X,Y,Z
        list_match = re.search(r'(?:here\s+(?:are|is)(?:\s+the\s+results?)?|results?\s*|output\s*):\s*(.+)', text, re.IGNORECASE)
        if list_match:
            values = list_match.group(1).strip().rstrip(".")
            # Compact comma-separated values
            values = re.sub(r',\s+', ',', values)
            parts.append(values)
            return " ".join(parts)

        # "Returning X" -> =: X
        returning_match = re.search(r'returning\s+(.+?)(?:\.|$)', text, re.IGNORECASE)
        if returning_match:
            parts.append(returning_match.group(1).strip().rstrip("."))
            return " ".join(parts)

        return None

    # ── Receive / Acknowledge (Problem 5) ────────────────────────────

    def _encode_receive(self, text: str) -> str | None:
        """Encode acknowledgment messages as <:ack."""
        text_lower = text.lower().strip().rstrip(".")

        _ack_patterns = [
            r'^acknowledged?$',
            r'^received$',
            r'^got\s+it$',
            r'^roger$',
            r'^roger\s+that$',
            r'^understood$',
            r'^copy\s+that$',
            r'^ack$',
            r'^ok(?:ay)?$',
            r'^affirmative$',
            r'^wilco$',
            r'^10-4$',
            r'^confirmed?$',
        ]
        if any(re.match(p, text_lower) for p in _ack_patterns):
            return "<:ack"

        # Also handle "Message received" / "Task acknowledged" etc.
        if re.search(r'\b(?:acknowledged?|received|confirmed)\b', text_lower) and len(text.split()) <= 5:
            return "<:ack"

        return None

    # ── Policy session defaults (@:policy) ───────────────────────────

    def _encode_policy(self, text: str) -> str | None:
        """Detect session-level policy defaults and emit @:policy."""
        text_lower = text.lower()

        # Must look like a policy-setting statement
        if not any(w in text_lower for w in [
            "set default", "default retry", "default fallback",
            "for all messages", "for all requests",
            "session timeout", "default timeout",
            "always retry", "always use",
        ]):
            return None

        parts = ["@:policy"]

        # retry:N
        retry_match = re.search(r'retry\s*(?:to|of|:)?\s*(\d+)', text_lower)
        if retry_match:
            parts.append(f"retry:{retry_match.group(1)}")

        # timeout:Ns or timeout:Nm
        timeout_match = re.search(
            r'timeout\s*(?:to|of|:)?\s*(\d+)\s*(seconds?|s|minutes?|m)',
            text_lower,
        )
        if timeout_match:
            unit = "s" if timeout_match.group(2).startswith("s") else "m"
            parts.append(f"timeout:{timeout_match.group(1)}{unit}")

        # fallback strategy
        for fb_name in ("degrade", "cache", "skip", "abort", "ask"):
            if fb_name in text_lower and "fallback" in text_lower:
                parts.append(f"fb:{fb_name}")
                break

        # backoff
        if "backoff" in text_lower:
            if "exponential" in text_lower:
                parts.append("backoff:exp")
            else:
                parts.append("backoff:linear")

        if len(parts) > 1:
            return " ".join(parts)
        return None

    # ── Code definition (Condensa Code) ────────────────────────────────

    def _encode_code_definition(self, text: str) -> str | None:
        """Detect code architecture patterns and emit Condensa Code verbs."""
        text_lower = text.lower()

        # Wire / connect pattern: "Wire A to B", "Connect A to B", "When A fires, trigger B"
        wire_match = re.search(
            r'\b(?:wire|connect)\s+(\w+)\s+to\s+(\w+)',
            text, re.IGNORECASE,
        )
        if wire_match:
            a, b = wire_match.group(1), wire_match.group(2)
            sync = self._detect_sync(text_lower)
            prefix = "!?:" if sync else "!:"
            return f"{prefix}wire {a} -> {b}"

        # "When X fires, trigger Y" pattern
        fire_match = re.search(
            r'\bwhen\s+(\w+)\s+fires?,?\s*trigger\s+(\w+)',
            text, re.IGNORECASE,
        )
        if fire_match:
            a, b = fire_match.group(1), fire_match.group(2)
            return f"!:wire {a} -> {b}"

        # API endpoint: "Define an API endpoint POST /path" or "endpoint POST /path"
        api_match = re.search(
            r'\b(?:define\s+(?:an?\s+)?)?(?:api\s+)?endpoint\s+(GET|POST|PUT|DELETE|PATCH)\s+(/\S+)',
            text, re.IGNORECASE,
        )
        if api_match:
            method = api_match.group(1).upper()
            path = api_match.group(2)
            return f"!:api {method} {path}"

        # Schema / table: "Define a schema/table X with columns/fields Y"
        schema_match = re.search(
            r'\b(?:define\s+(?:a\s+)?)?(?:schema|table)\s+(\w+)\s+(?:with\s+)?(?:columns?|fields?)\s+(.+?)(?:\.|$)',
            text, re.IGNORECASE,
        )
        if schema_match:
            name = schema_match.group(1)
            fields = schema_match.group(2).strip().rstrip(".")
            # Compact field list
            fields = re.sub(r',\s+', ', ', fields)
            return f"!:schema {name} {{ {fields} }}"

        # Type / interface: "Define a type/interface X with fields Y"
        type_match = re.search(
            r'\b(?:define\s+(?:a\s+)?)?(?:type|interface)\s+(\w+)\s+(?:with\s+)?(?:fields?|properties|members)\s+(.+?)(?:\.|$)',
            text, re.IGNORECASE,
        )
        if type_match:
            name = type_match.group(1)
            fields = type_match.group(2).strip().rstrip(".")
            fields = re.sub(r',\s+', ', ', fields)
            return f"!:type {name} {{ {fields} }}"

        # Component / function: "Define a component/function X that accepts/takes props Y"
        fn_match = re.search(
            r'\b(?:define\s+(?:a\s+)?)?(?:component|function)\s+(\w+)\s+(?:that\s+)?(?:accepts?|takes?|with)\s+(?:props?\s+)?(.+?)(?:\.|$)',
            text, re.IGNORECASE,
        )
        if fn_match:
            name = fn_match.group(1)
            props = fn_match.group(2).strip().rstrip(".")
            return f"!:fn {name} /props:({props})"

        # Module / app: "Define a module/app X with pages Y"
        mod_match = re.search(
            r'\b(?:define\s+(?:a\s+)?)?(?:module|app|application)\s+(\w+)\s+(?:with\s+)?(?:pages?|routes?|screens?)\s+(.+?)(?:\.|$)',
            text, re.IGNORECASE,
        )
        if mod_match:
            name = mod_match.group(1)
            pages = mod_match.group(2).strip().rstrip(".")
            return f"!:def {name} /pages:({pages})"

        # Assemble: "Assemble the components" / "Assemble X"
        asm_match = re.search(
            r'\bassemble\b(?:\s+(?:the\s+)?(.+?))?(?:\.|$)',
            text, re.IGNORECASE,
        )
        if asm_match:
            target = asm_match.group(1)
            if target and target.strip():
                return f"!?:asm {target.strip().rstrip('.')}"
            return "!?:asm"

        return None

    # ── Delegation (Problem 1) ───────────────────────────────────────

    def _encode_delegation(self, text: str) -> str | None:
        """Detect delegation patterns and emit >:@AgentName action."""
        text_lower = text.lower()

        # Detect delegation verbs
        delegation_match = re.search(
            r'\b(?:delegate\s+to|forward\s+to|send\s+to|route\s+to|assign\s+to)\s+'
            r'(\w+)',
            text,
            re.IGNORECASE,
        )
        if not delegation_match:
            return None

        agent_name = delegation_match.group(1)
        # Clean agent name: remove leading @ if present
        agent_name = agent_name.lstrip("@")

        # Extract the action part — everything after the agent name
        # The pattern is typically "Delegate to AgentC: action" or "Delegate to AgentC action"
        after_agent = text[delegation_match.end():]
        # Strip leading colon, comma, dash
        after_agent = re.sub(r'^[\s:,\-]+', '', after_agent).strip()

        parts = [f">:@{agent_name}"]

        if after_agent:
            # Try to encode the action part
            action_encoded = self._encode_single_action(after_agent, bare=True)
            if action_encoded:
                parts.append(action_encoded)
            else:
                # Extract verb and compact the rest
                words = after_agent.split()
                if words:
                    verb = words[0].lower()
                    action_short = ACTIONS.get(verb, verb)
                    # Keep remaining context words, skip filler
                    _skip = {"the", "a", "an", "to", "for", "of", "is", "are"}
                    rest = [w for w in words[1:] if w.lower() not in _skip]
                    compact = [action_short] + rest
                    parts.append(" ".join(compact))

        return " ".join(parts)

    # ── Workflow / multi-step ────────────────────────────────────────

    def _encode_workflow(self, text: str) -> str | None:
        text_lower = text.lower()
        step_indicators = ["first", "then", "after that", "finally", "next", "step 1", "step 2"]
        if sum(1 for s in step_indicators if s in text_lower) < 2:
            return None

        # Detect sync intent for the overall workflow
        sync = self._detect_sync(text_lower)

        # Extract individual steps
        steps = []
        step_texts = re.split(r'(?:first|then|after that|finally|next)[,:]?\s*', text, flags=re.IGNORECASE)
        step_texts = [s.strip().rstrip(".") for s in step_texts if s.strip()]

        for i, step_text in enumerate(step_texts):
            # First step gets prefix; subsequent steps use bare verbs
            step = self._encode_single_action(step_text, bare=(i > 0), sync=False)
            if step:
                steps.append(step)

        if len(steps) >= 2:
            # v0.2: For exactly 2 sequential steps, use inline evaluation []
            if len(steps) == 2:
                return self._try_inline_eval(steps, sync)

            # Pipeline: $_ is implicit, no explicit $1 references
            result = self._strip_implicit_refs(steps)

            prefix = "!?:" if sync else ""
            return prefix + "seq(" + "; ".join(result) + ")"

        return None

    def _try_inline_eval(self, steps: list[str], sync: bool) -> str:
        """For exactly 2 steps, try [] inline evaluation.

        Emit: !:verb2 [!:verb1 args]  instead of  seq(!:verb1 args; !:verb2 $1)
        """
        first, second = steps[0], steps[1]
        prefix = "!?:" if sync else "!:"

        # If second step is bare (no !: prefix), reconstruct for inline form
        if not second.startswith("!:") and not second.startswith("!!:"):
            return f"{prefix}{second} [{first}]"
        else:
            # Strip the prefix from the second step for inline form
            second_bare = re.sub(r'^!+:', '', second).strip()
            return f"{prefix}{second_bare} [{first}]"

    def _strip_implicit_refs(self, steps: list[str]) -> list[str]:
        """Remove explicit $1 references in sequential pipelines (v0.2: $_ is implicit)."""
        result = []
        for step in steps:
            # Remove $1, $_, or explicit previous-result references in sequential context
            cleaned = re.sub(r'\s*\$[1_]\b', '', step).strip()
            result.append(cleaned if cleaned else step)
        return result

    # ── Correction / revision ────────────────────────────────────────

    def _encode_correction(self, text: str) -> str | None:
        text_lower = text.lower()
        if not any(w in text_lower for w in ["correct", "revise", "revision", "incorrect", "wrong", "keep", "remove"]):
            return None
        if "cancel" in text_lower:
            return None

        parts = ["~:^"]

        # Look for section-level instructions
        sections = re.findall(r'(?:section|part|paragraph)\s*(\d+|first|second|third)\S*\s*[,:]\s*(\w+)', text_lower)
        ordinals = {"first": "1", "second": "2", "third": "3", "fourth": "4", "fifth": "5"}

        for section_id, action in sections:
            sid = ordinals.get(section_id, section_id)
            action_short = {"keep": "keep", "revise": "revise", "remove": "del", "delete": "del",
                           "update": "revise+detail", "expand": "revise+detail", "add": "add"}.get(action, action)
            parts.append(f"s{sid}:{action_short}")

        # Look for field corrections
        field_corrections = re.findall(r'(?:correct|should be|not)\s+\$?([\d,.]+\w*)\b.*?(?:should be|is|to)\s+\$?([\d,.]+\w*)', text_lower)
        for old_val, new_val in field_corrections:
            parts.append(f"{old_val}->{new_val}")

        if len(parts) > 1:
            return " ".join(parts)
        return None

    # ── Parallel dispatch ────────────────────────────────────────────

    def _encode_parallel(self, text: str) -> str | None:
        text_lower = text.lower()
        if not any(w in text_lower for w in ["simultaneous", "parallel", "at the same time", "concurrently"]):
            return None

        # Extract tasks
        task_texts = re.findall(r'task\s*\d+[:\s]+(.+?)(?=task\s*\d+|when all|$)', text, re.IGNORECASE | re.DOTALL)
        if not task_texts:
            task_texts = re.split(r'(?:\d+\)|\d+\.)\s*', text)[1:]

        tasks = []
        for i, tt in enumerate(task_texts):
            # All steps inside par() use bare verbs (v0.2)
            step = self._encode_single_action(tt.strip().rstrip("."), bare=(i > 0))
            if step:
                tasks.append(step)

        if len(tasks) >= 2:
            result = "par(" + "; ".join(tasks) + ")"
            # Check for merge/combine instruction
            if any(w in text_lower for w in ["merge", "combine", "single response"]):
                result += " | mrg"
            return result

        return None

    # ── Conditional ──────────────────────────────────────────────────

    def _encode_conditional(self, text: str) -> str | None:
        text_lower = text.lower()
        if_count = text_lower.count("if it ") + text_lower.count("if the ")
        if if_count < 1:
            return None

        branches = re.findall(r'if\s+(?:it\s+)?(.+?),\s*(.+?)(?=\.\s*if|\.\s*in all|$)', text, re.IGNORECASE)
        parts = []
        for condition, action in branches:
            cond = condition.strip().rstrip(".")[:30]
            act = self._encode_single_action(action.strip().rstrip("."))
            if act:
                parts.append(f"if({cond}) {act}")

        # "In all cases" / "always"
        always_match = re.search(r'(?:in all cases|always|regardless)[,:]?\s*(.+?)$', text, re.IGNORECASE)
        if always_match:
            always_act = self._encode_single_action(always_match.group(1).strip().rstrip("."))
            if always_act:
                parts.append(f"always {always_act}")

        if parts:
            return "; ".join(parts)
        return None

    # ── Context update ───────────────────────────────────────────────

    def _encode_context_update(self, text: str) -> str | None:
        text_lower = text.lower()
        if not any(w in text_lower for w in ["update", "change", "changed", "modify", "set"]):
            return None
        if any(w in text_lower for w in ["search", "fetch", "process", "generate"]):
            return None
        # Avoid capturing policy-setting as context update
        if any(w in text_lower for w in ["set default", "default retry", "default fallback", "for all messages"]):
            return None

        parts = ["~:&"]

        # Look for key:old->new patterns
        changes = re.findall(
            r'(\w[\w\s]*?)\s+(?:from|was|changed from)\s+(\S+)\s+to\s+(\S+)',
            text, re.IGNORECASE
        )
        for field, old, new in changes:
            field_short = field.strip().lower().replace(" ", "_")[:15]
            parts.append(f"{field_short}:{old}->{new}")

        # Look for key=value patterns
        assignments = re.findall(
            r"(\w[\w\s]*?)\s+(?:is|are|set to|=)\s+(\S+)",
            text_lower
        )
        for field, val in assignments:
            if field.strip() not in ("it", "this", "that", "there", "which", "what"):
                field_short = field.strip().replace(" ", "_")[:15]
                parts.append(f"{field_short}:{val}")

        if len(parts) > 1:
            return " ".join(parts)
        return None

    # ── Pipeline command (Problem 9) ─────────────────────────────────

    def _encode_pipeline_command(self, text: str) -> str | None:
        """Detect 'and then' / 'followed by' pipeline patterns in commands."""
        text_lower = text.lower()

        # Pipeline connectors (different from workflow step indicators)
        _pipeline_connectors = [
            r'\band\s+then\b',
            r'\bfollowed\s+by\b',
            r'\band\s+summarize\b',
            r'\band\s+format\b',
            r'\band\s+sort\b',
            r'\band\s+filter\b',
            r'\band\s+group\b',
            r'\band\s+count\b',
            r'\band\s+merge\b',
            r'\bthen\s+summarize\b',
            r'\bthen\s+format\b',
            r'\bthen\s+sort\b',
        ]

        if not any(re.search(p, text_lower) for p in _pipeline_connectors):
            return None

        # Don't conflict with workflow encoder (which needs first/then/finally step indicators)
        step_indicators = ["first", "after that", "finally", "step 1", "step 2"]
        if sum(1 for s in step_indicators if s in text_lower) >= 1:
            return None

        # Split on pipeline connectors
        segments = re.split(
            r'\s+and\s+then\s+|\s+followed\s+by\s+|\s+and\s+(?=summarize|format|sort|filter|group|count|merge)\s*|\s+then\s+(?=summarize|format|sort)',
            text,
            flags=re.IGNORECASE,
        )
        segments = [s.strip().rstrip(".") for s in segments if s.strip()]

        if len(segments) < 2:
            return None

        steps = []
        for i, seg in enumerate(segments):
            step = self._encode_single_action(seg, bare=(i > 0), sync=False)
            if step:
                steps.append(step)

        if len(steps) >= 2:
            return " | ".join(steps)

        return None

    # ── Generic command ──────────────────────────────────────────────

    def _encode_command(self, text: str) -> str | None:
        sync = self._detect_sync(text.lower())
        step = self._encode_single_action(text, sync=sync)
        if step:
            return step
        return None

    # ── Sync detection helper ────────────────────────────────────────

    def _detect_sync(self, text_lower: str) -> bool:
        """Detect if the text implies a synchronous / blocking command."""
        sync_keywords = [
            "wait for response", "synchronous", "blocking",
            "and return", "send back", "respond immediately",
            "wait for result", "wait for reply",
        ]
        return any(kw in text_lower for kw in sync_keywords)

    # ── Single action helper ─────────────────────────────────────────

    def _encode_single_action(
        self,
        text: str,
        *,
        bare: bool = False,
        sync: bool = False,
        use_v02_fallbacks: bool = True,
    ) -> str | None:
        """Try to encode a single action from text.

        Parameters
        ----------
        bare : bool
            When True, omit the ``!:`` prefix (used for subsequent steps
            in pipelines and ``par()`` blocks).
        sync : bool
            When True, use ``!!:`` instead of ``!:`` for synchronous /
            blocking commands.
        use_v02_fallbacks : bool
            When True, emit ``// value`` for simple fallbacks instead of
            ``fb:value``.  Set to False for v0.1-compatible output.
        """
        text_lower = text.lower().strip()
        if not text_lower:
            return None

        # Determine prefix
        if bare:
            parts = []
        elif sync:
            parts = ["!?:"]
        else:
            parts = ["!:"]

        # Find the primary action verb
        action_found = None
        matched_verb = None
        for nl_verb, short in sorted(ACTIONS.items(), key=lambda x: -len(x[0])):
            if nl_verb in text_lower:
                action_found = short
                matched_verb = nl_verb
                break

        if not action_found:
            return None

        # ── Verb modifiers (v0.3: /each, /scan, /repeat:N) ──────────
        parts.append(action_found)
        modifier = self._detect_adverb(text_lower, matched_verb)
        if modifier:
            parts.append(modifier)

        # ── Retry with count as modifier: /repeat:N ──────────────────
        if action_found == "retry":
            retry_n = re.search(r'(\d+)\s*times?', text_lower)
            if retry_n:
                parts.append(f"/repeat:{retry_n.group(1)}")

        # ── Implicit context variables (v0.2) ────────────────────────
        text_lower_for_vars = text_lower

        # ── Extract quoted strings as primary arguments ──────────────
        quoted = re.findall(r'"([^"]+)"|\'([^\']+)\'', text)
        for q in quoted:
            val = q[0] or q[1]
            if " " in val:
                parts.append(f"'{val}'")
            else:
                parts.append(val)

        # ── Bulk mode: detect complex JSON-like content (v0.2) ───────
        bulk_content = self._detect_bulk_content(text)
        if bulk_content:
            parts.append(f"<<<{bulk_content}>>>")

        # ── Key parameters ───────────────────────────────────────────

        # Numbers with context
        limit_match = re.search(r'(?:top|first|max(?:imum)?|limit)\s+(\d+)', text_lower)
        if limit_match:
            parts.append(f"/n:{limit_match.group(1)}")

        count_match = re.search(r'(\d+)\s+(?:results?|items?|records?|products?)', text_lower)
        if count_match and not limit_match:
            parts.append(f"/n:{count_match.group(1)}")

        # Format
        for nl_fmt, short_fmt in FORMAT_MAP.items():
            if nl_fmt in text_lower:
                parts.append(f"/fmt:{short_fmt}")
                break

        # Language
        lang_match = re.search(r'(?:to|in|into)\s+(English|Spanish|French|German|Chinese|Japanese|Korean)', text, re.IGNORECASE)
        if lang_match:
            lang_codes = {"english": "en", "spanish": "es", "french": "fr", "german": "de",
                         "chinese": "zh", "japanese": "ja", "korean": "ko"}
            parts.append(f"/lang:{lang_codes.get(lang_match.group(1).lower(), lang_match.group(1)[:2].lower())}")

        # Sort order
        if "descending" in text_lower or "desc" in text_lower:
            parts.append("/desc")
        elif "ascending" in text_lower or "asc" in text_lower:
            parts.append("/asc")

        # Timeout
        timeout_match = re.search(r'timeout\s*(?:of|:)?\s*(\d+)\s*(s|seconds?|m|minutes?)', text_lower)
        if timeout_match:
            unit = "s" if timeout_match.group(2).startswith("s") else "m"
            parts.append(f"/timeout:{timeout_match.group(1)}{unit}")

        # ── Problem 8: Enhanced parameter extraction ─────────────────

        # Source extraction: "from the database", "from the API", "from file X"
        source = self._extract_source(text_lower)
        if source:
            parts.append(f"/src:{source}")

        # Field extraction: "by region", "by category"
        field = self._extract_field(text_lower, matched_verb)
        if field:
            parts.append(f"/by:{field}")

        # Condition extraction: "where status is active"
        condition = self._extract_condition(text)
        if condition:
            parts.append(f"/where:{condition}")

        # URL/path extraction
        paths = self._extract_paths(text)
        for p in paths:
            parts.append(p)

        # ── Implicit context variables (v0.2) ────────────────────────
        _CONTEXT_PATTERNS = {
            "$turn": [r"\bturn\s*number\b", r"\bturn\s*#\b", r"\bturn\s*count\b"],
            "$agent": [r"\bcurrent\s*agent\b", r"\bthis\s*agent\b"],
            "$t": [r"\btimestamp\b", r"\bcurrent\s*time\b", r"\bnow\b"],
        }
        for var, patterns in _CONTEXT_PATTERNS.items():
            if any(re.search(p, text_lower_for_vars) for p in patterns):
                parts.append(var)

        # ── Fallback (v0.2: // for simple, fb: for complex) ─────────
        self._append_fallback(parts, text_lower, use_v02_fallbacks)

        # ── Optional field with safe access (v0.2: ?) ────────────────
        if re.search(r'(?:optional|if available|if exists?|if present)', text_lower):
            # Append ? to the last argument that looks like a field ref
            for i in range(len(parts) - 1, 0, -1):
                if not parts[i].startswith("/") and not parts[i].startswith("fb:") and not parts[i].startswith("//"):
                    if not parts[i].endswith("?"):
                        parts[i] = parts[i] + "?"
                    break

        return " ".join(parts)

    # ── Problem 8: Parameter extraction helpers ──────────────────────

    def _extract_source(self, text_lower: str) -> str | None:
        """Extract source from 'from the database/API/file X' patterns."""
        # Don't extract source from URLs (handled by _extract_paths)
        if re.search(r'\bfrom\s+https?://', text_lower):
            return None

        m = re.search(
            r'\bfrom\s+(?:the\s+)?(\w+)(?:\s+(\w+))?',
            text_lower,
        )
        if m:
            first_word = m.group(1).strip()
            second_word = m.group(2).strip() if m.group(2) else None

            # Filter out common non-source words
            _ignore = {"the", "a", "an", "this", "that", "it", "each", "every", "all",
                        "top", "first", "last", "most", "least", "web", "scratch"}
            if first_word in _ignore:
                return None

            # Compact common sources
            _source_map = {
                "database": "db",
                "api": "api",
                "file": "file",
                "cache": "cache",
                "server": "srv",
                "storage": "store",
                "queue": "queue",
                "stream": "stream",
                "bucket": "bucket",
                "table": "tbl",
                "collection": "coll",
                "index": "idx",
                "registry": "reg",
            }
            if first_word in _source_map:
                result = _source_map[first_word]
                # Only append second word if it looks like a name, not a preposition
                _stop = {"for", "and", "or", "to", "in", "on", "at", "by", "with",
                         "the", "a", "an", "is", "are", "was", "were", "has", "have"}
                if second_word and second_word not in _stop:
                    result += ":" + second_word
                return result
            # If the first word itself isn't a known source but looks like a specific name
            _stop = {"for", "and", "or", "to", "in", "on", "at", "by", "with",
                     "the", "a", "an", "is", "are", "was", "were", "has", "have"}
            if first_word not in _stop and len(first_word) > 2:
                return first_word
        return None

    def _extract_field(self, text_lower: str, matched_verb: str) -> str | None:
        """Extract grouping/sorting field from 'by region' patterns."""
        # "by" for grouping/sorting — but not "by" in other contexts
        m = re.search(r'\bby\s+(\w+)', text_lower)
        if m:
            field = m.group(1)
            # Filter out non-field words
            _ignore = {"the", "a", "an", "it", "this", "that", "using", "default"}
            if field in _ignore:
                return None
            return field
        return None

    def _extract_condition(self, text: str) -> str | None:
        """Extract filter conditions from 'where status is active' patterns."""
        m = re.search(
            r'\bwhere\s+(.+?)(?:\s+and\s+(?:then|also)|(?:\.|,\s*(?:and|or)\s+(?!where))|$)',
            text,
            re.IGNORECASE,
        )
        if m:
            condition = m.group(1).strip().rstrip(".")
            # Compact the condition
            condition = condition.replace(" is ", "=").replace(" are ", "=")
            condition = condition.replace(" greater than ", ">").replace(" less than ", "<")
            condition = condition.replace(" more than ", ">").replace(" fewer than ", "<")
            condition = re.sub(r'\s*>\s*', '>', condition)
            condition = re.sub(r'\s*<\s*', '<', condition)
            condition = re.sub(r'\s*=\s*', '=', condition)
            return condition[:50]  # cap length
        return None

    def _extract_paths(self, text: str) -> list[str]:
        """Extract URLs and file paths from text."""
        results = []
        url_spans = set()
        # URLs
        for m in re.finditer(r'(https?://\S+)', text):
            url = m.group(1).rstrip(".,;)")
            results.append(url)
            # Track character span so we don't double-extract path fragments
            url_spans.add((m.start(), m.end()))
        # File paths (but not ones inside URLs)
        for m in re.finditer(r'(?<!\w)(/[\w/._-]+(?:\.\w+)?)', text):
            # Skip if this path is inside a URL span
            if any(s <= m.start() < e for s, e in url_spans):
                continue
            p = m.group(1)
            if p not in results and len(p) > 2:
                results.append(p)
        return results

    # ── Adverb detection (v0.2) ──────────────────────────────────────

    def _detect_adverb(self, text_lower: str, matched_verb: str) -> str:
        """Detect adverb modifiers and return the modifier string (v0.3)."""
        # "for each" / "summarize each" / "every"
        if re.search(r'\b(?:for\s+)?each\b', text_lower) or re.search(r'\bevery\b', text_lower):
            return "/each"

        # "cumulative" / "running total" / "scan"
        if any(w in text_lower for w in ["cumulative", "running total", "running sum", "scan"]):
            return "/scan"

        return ""

    # ── Bulk content detection (v0.2) ────────────────────────────────

    def _detect_bulk_content(self, text: str) -> str | None:
        """Detect complex JSON-like content and return it for <<<>>> wrapping."""
        # Look for content with nested braces/brackets
        match = re.search(r'(\{[^{}]*\{.+?\}[^{}]*\}|\[[^\[\]]*\[.+?\][^\[\]]*\])', text, re.DOTALL)
        if match:
            return match.group(1)
        return None

    # ── Fallback append helper ───────────────────────────────────────

    def _append_fallback(self, parts: list, text_lower: str, use_v02: bool) -> None:
        """Append fallback notation to parts list.

        v0.3: always emit fb:value (// operator removed).
        """
        for nl_fb, short_fb in FALLBACKS.items():
            if nl_fb in text_lower:
                parts.append(short_fb)
                break

    # ── Fallback encoder (Problem 6) ─────────────────────────────────

    def _encode_fallback(self, text: str) -> str:
        """Last resort: always produce valid Condensa syntax.

        Try to extract an action verb. If none found, use 'exec' as default
        action and produce a compact summary.
        """
        text_lower = text.lower()

        # Strip filler words first
        filler = [
            r'\bhey\b', r'\bhi\b', r'\bhello\b', r'\bplease\b', r'\bkindly\b',
            r'\bcould you\b', r'\bwould you\b', r'\bcan you\b',
            r'\bjust wanted to\b', r'\blet you know\b',
            r'\bI need you to\b', r'\bI would like you to\b',
            r'\bif you could\b', r'\bthat would be great\b',
        ]
        cleaned = text
        for pattern in filler:
            cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
        cleaned = re.sub(r'\s+', ' ', cleaned).strip().rstrip('.')

        if not cleaned:
            return "!:exec"

        cleaned_lower = cleaned.lower()

        # Try to find any action verb in the cleaned text
        for nl_verb, short in sorted(ACTIONS.items(), key=lambda x: -len(x[0])):
            if nl_verb in cleaned_lower:
                # Found a verb — build a minimal valid command
                parts = [f"!:{short}"]
                # Extract quoted strings
                quoted = re.findall(r'"([^"]+)"|\'([^\']+)\'', cleaned)
                for q in quoted:
                    val = q[0] or q[1]
                    parts.append(f"'{val}'" if " " in val else val)
                # Extract key nouns after the verb
                verb_pos = cleaned_lower.find(nl_verb)
                remainder = cleaned[verb_pos + len(nl_verb):].strip()
                if remainder and not quoted:
                    # Compact: take key words, skip articles/prepositions
                    _skip = {"the", "a", "an", "to", "for", "of", "in", "on", "at", "by",
                             "with", "and", "or", "is", "are", "it", "its", "this", "that"}
                    key_words = [w for w in remainder.split() if w.lower() not in _skip][:4]
                    if key_words:
                        parts.append(" ".join(key_words))
                return " ".join(parts)

        # Truly can't find any verb — use exec with compact summary
        _skip = {"the", "a", "an", "to", "for", "of", "in", "on", "at", "by",
                 "with", "and", "or", "is", "are", "it", "its", "this", "that",
                 "i", "you", "we", "they", "he", "she", "me", "my", "your"}
        words = [w for w in cleaned.split() if w.lower() not in _skip][:6]
        summary = " ".join(words) if words else cleaned[:30]
        return f"!:exec {summary}"


def encode(text: str) -> str:
    """Convenience function to encode text to Condensa."""
    return CondensaEncoder().encode(text)


def encode_with_stats(text: str) -> dict:
    """Convenience function to encode with compression stats."""
    return CondensaEncoder().encode_with_stats(text)
