"""
Reasoning Store

Extracts and persists decision trails with full reasoning context.
This is Pillar 2 of MCAL: Reasoning Chain Preservation.

Key capabilities:
- Extract decisions with rationale from conversations
- Store alternatives considered and why they were rejected
- Track evidence supporting decisions
- Link decisions to goals and other decisions
"""

from __future__ import annotations

import json
import logging
from typing import Optional, Protocol

from .models import (
    Alternative,
    DecisionTrail,
    DecisionReconciliation,
    Evidence,
    EvidenceSource,
    InvalidatedDecision,
    ModifiedDecision,
    NewDecision,
    TradeOff,
    Turn,
    VerifiedDecision,
)

logger = logging.getLogger(__name__)


# =============================================================================
# LLM Client Protocol
# =============================================================================

class LLMClient(Protocol):
    """Protocol for LLM client implementations."""
    
    async def complete(self, prompt: str, system: Optional[str] = None) -> str:
        """Generate a completion for the given prompt."""
        ...


# =============================================================================
# Prompts
# =============================================================================

DECISION_EXTRACTION_SYSTEM = """You are an expert at analyzing conversations to extract decision-making patterns.

Your task is to identify decisions that were made and capture the FULL reasoning context:
- What was the decision?
- What alternatives were considered?
- Why was this option chosen over alternatives?
- What evidence supported the decision?
- What trade-offs were acknowledged?

This is crucial for preserving the "WHY" behind decisions, not just the "WHAT".

Output your analysis as valid JSON."""

DECISION_EXTRACTION_PROMPT = """Analyze this conversation and extract all decisions with their reasoning context.

CONVERSATION:
{conversation}

For each decision, extract:
1. The decision itself (what was chosen)
2. The context (what situation prompted this decision)
3. Alternatives that were considered (even if briefly)
4. The rationale (WHY this was chosen)
5. Evidence supporting the decision
6. Trade-offs acknowledged

Output as JSON:
{{
    "decisions": [
        {{
            "decision": "The choice that was made",
            "context": "The situation/problem being addressed",
            "alternatives": [
                {{
                    "option": "Alternative option",
                    "pros": ["advantage 1"],
                    "cons": ["disadvantage 1"],
                    "rejection_reason": "Why this wasn't chosen"
                }}
            ],
            "rationale": "Why the chosen option was selected",
            "evidence": [
                {{
                    "claim": "Supporting fact or argument",
                    "source": "user_stated|inferred|external",
                    "turn_reference": "turn_N or null"
                }}
            ],
            "trade_offs": [
                {{
                    "gained": "What was gained",
                    "sacrificed": "What was given up",
                    "justification": "Why this trade-off is acceptable"
                }}
            ],
            "confidence": 0.0-1.0,
            "related_goal": "Goal this decision serves (if identifiable)"
        }}
    ]
}}

Be thorough - capture implicit decisions too, not just explicit ones.
Output ONLY valid JSON, no explanation."""

DECISION_EXTRACTION_WITH_FULL_CONTEXT_PROMPT = """Analyze this conversation and extract all NEW decisions with their reasoning context.

=== USER'S ACTIVE GOALS ===
{active_goals}

=== PREVIOUS DECISIONS (from earlier sessions) ===
{previous_decisions}

=== CURRENT SESSION CONVERSATION ===
{conversation}

IMPORTANT CONTEXT:
- This is a CONTINUATION of an ongoing multi-session conversation
- The user has established goals and made prior decisions (listed above)
- Look for NEW decisions in this session that:
  * Advance toward the active goals
  * Update, refine, or reverse previous decisions
  * Represent choices between alternatives
  * Include implicit decisions (tool/approach/timing choices)

For each NEW decision found in this session, extract:
1. The decision itself (what was chosen)
2. The context (what prompted this decision)
3. Alternatives considered (even briefly mentioned)
4. The rationale (WHY this was chosen)
5. Evidence supporting the decision
6. Trade-offs acknowledged
7. Which goal this serves
8. Related previous decision (if any)

Output as JSON:
{{
    "decisions": [
        {{
            "decision": "The choice that was made",
            "context": "The situation/problem being addressed",
            "alternatives": [
                {{
                    "option": "Alternative option",
                    "pros": ["advantage 1"],
                    "cons": ["disadvantage 1"],
                    "rejection_reason": "Why this wasn't chosen"
                }}
            ],
            "rationale": "Why the chosen option was selected",
            "evidence": [
                {{
                    "claim": "Supporting fact or argument",
                    "source": "user_stated|inferred|external",
                    "turn_reference": "turn_N or null"
                }}
            ],
            "trade_offs": [
                {{
                    "gained": "What was gained",
                    "sacrificed": "What was given up",
                    "justification": "Why this trade-off is acceptable"
                }}
            ],
            "confidence": 0.0-1.0,
            "related_goal": "Goal this decision serves (if identifiable)",
            "related_previous_decision": "ID or summary of related previous decision if any"
        }}
    ]
}}

Only extract NEW decisions from the CURRENT SESSION - do not repeat previous decisions.
If no new decisions are found, return: {{"decisions": []}}
Output ONLY valid JSON, no explanation."""


# =============================================================================
# Decision Carry-Forward Prompt (Issue #1 Fix)
# =============================================================================

DECISION_CARRY_FORWARD_SYSTEM = """You are an expert at analyzing conversations and tracking decision evolution.

Your task is to RECONCILE prior decisions with new conversation context:
- Verify which prior decisions are still valid
- Identify any modifications or updates to prior decisions
- Extract genuinely new decisions
- Flag any decisions that were reversed or abandoned

This is crucial for maintaining accurate decision state across multiple conversation sessions.

Output your analysis as valid JSON."""

DECISION_CARRY_FORWARD_PROMPT = """You are analyzing a CONTINUATION of an ongoing conversation.

=== PRIOR DECISIONS (from earlier sessions) ===
{prior_decisions}

=== ACTIVE GOALS ===
{active_goals}

=== CURRENT SESSION MESSAGES ===
{conversation}

Your task is to RECONCILE the decision state by analyzing how this session affects prior decisions:

1. **VERIFIED**: Which prior decisions are confirmed/still valid?
   - If a prior decision is referenced positively or built upon, it's verified
   - If a prior decision is simply not mentioned, mark it verified with lower confidence (0.5)

2. **MODIFIED**: Which prior decisions were updated or refined?
   - Include decisions where the approach changed but the goal remains
   - Include decisions that were expanded or narrowed in scope

3. **NEW**: What genuinely NEW decisions were made in THIS session?
   - Only include choices that weren't captured in prior decisions
   - Include both explicit and implicit decisions

4. **INVALIDATED**: Which prior decisions were reversed or abandoned?
   - Include decisions the user explicitly changed their mind about
   - Include decisions that contradict new information

Output as JSON:
{{
    "verified": [
        {{
            "decision_id": "ID from prior decisions",
            "still_valid": true,
            "confidence": 0.0-1.0,
            "supporting_evidence": "Quote or reference from this session, or null if not mentioned"
        }}
    ],
    "modified": [
        {{
            "original_decision_id": "ID being modified",
            "original_summary": "Brief summary of original decision",
            "new_decision": "The updated decision",
            "modification_type": "refined|updated|partially_changed",
            "reason": "Why it changed",
            "confidence": 0.0-1.0
        }}
    ],
    "new": [
        {{
            "decision": "The new choice made",
            "context": "Situation prompting this",
            "rationale": "Why this was chosen",
            "confidence": 0.0-1.0,
            "related_goal": "Goal this serves, or null",
            "alternatives": [
                {{
                    "option": "Alternative considered",
                    "rejection_reason": "Why not chosen"
                }}
            ],
            "evidence": [
                {{
                    "claim": "Supporting fact",
                    "source": "user_stated|inferred|external"
                }}
            ],
            "trade_offs": [
                {{
                    "gained": "Benefit",
                    "sacrificed": "Cost"
                }}
            ]
        }}
    ],
    "invalidated": [
        {{
            "decision_id": "ID being invalidated",
            "reason": "Why it's no longer valid"
        }}
    ]
}}

IMPORTANT:
- EVERY prior decision must appear in exactly ONE category: verified, modified, or invalidated
- Use empty arrays [] for categories with no items
- Be conservative with "invalidated" - only use when clearly contradicted
- "new" should only contain genuinely NEW decisions not captured before

Output ONLY valid JSON, no explanation."""


# =============================================================================
# Reasoning Store
# =============================================================================

class ReasoningStore:
    """
    Extracts and stores decision trails with full reasoning context.
    
    Unlike simple fact storage (e.g., "User chose PostgreSQL"), this captures:
    - What alternatives were considered
    - Why the chosen option was selected
    - What evidence supported the decision
    - What trade-offs were made
    
    Usage:
        store = ReasoningStore(llm_client)
        
        # Extract decisions from conversation
        decisions = await store.extract_decisions(turns)
        
        # Store a decision
        store.add_decision(decision_trail)
        
        # Query decisions related to a goal
        related = store.get_decisions_for_goal(goal_id)
    """
    
    def __init__(self, llm_client: LLMClient):
        """
        Initialize the reasoning store.
        
        Args:
            llm_client: LLM client for extraction
        """
        self.llm = llm_client
        self.decisions: dict[str, DecisionTrail] = {}
        self._goal_index: dict[str, list[str]] = {}  # goal_id -> [decision_ids]
    
    async def extract_decisions(
        self,
        turns: list[Turn],
        goal_ids: Optional[list[str]] = None,
        existing_decisions: Optional[list[DecisionTrail]] = None,
        active_goals_context: Optional[str] = None
    ) -> list[DecisionTrail]:
        """
        Extract decision trails from conversation.
        
        For continuation sessions with existing decisions, uses the
        Decision Carry-Forward approach to reconcile prior decisions
        with new session context.
        
        Args:
            turns: List of conversation turns
            goal_ids: Optional list of goal IDs to link decisions to
            existing_decisions: Optional list of existing decisions for context
            active_goals_context: Optional string describing active goals for context
            
        Returns:
            List of extracted DecisionTrail objects
        """
        # Use carry-forward for continuation sessions
        if existing_decisions and len(existing_decisions) > 0:
            logger.info(f"Using Decision Carry-Forward with {len(existing_decisions)} prior decisions")
            return await self._reconcile_and_extract(
                turns=turns,
                existing_decisions=existing_decisions,
                active_goals_context=active_goals_context,
                goal_ids=goal_ids
            )
        
        # First session - use smart formatting for long conversations
        conversation = await self._format_conversation_smart(turns)
        
        if active_goals_context:
            # Have goals but no prior decisions (first session with goals)
            prompt = DECISION_EXTRACTION_WITH_FULL_CONTEXT_PROMPT.format(
                active_goals=active_goals_context,
                previous_decisions="None (this is the first session)",
                conversation=conversation
            )
            logger.debug(f"Using goal-aware extraction (first session)")
        else:
            prompt = DECISION_EXTRACTION_PROMPT.format(conversation=conversation)
        
        # Call LLM for extraction
        response = await self.llm.complete(prompt, system=DECISION_EXTRACTION_SYSTEM)
        
        # Parse response
        try:
            data = json.loads(self._clean_json_response(response))
        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse decision extraction response: {e}")
            logger.debug(f"Response was: {response}")
            return []
        
        # Build decision trails
        decisions = []
        for decision_data in data.get("decisions", []):
            decision = self._build_decision_trail(decision_data, turns)
            decisions.append(decision)
            self.add_decision(decision)
        
        logger.info(f"Extracted {len(decisions)} decisions from conversation")
        
        return decisions

    async def _reconcile_and_extract(
        self,
        turns: list[Turn],
        existing_decisions: list[DecisionTrail],
        active_goals_context: Optional[str] = None,
        goal_ids: Optional[list[str]] = None
    ) -> list[DecisionTrail]:
        """
        Use Decision Carry-Forward to reconcile prior decisions with new session.
        
        This is the core fix for Issue #1: instead of asking for "NEW decisions only"
        (which returns empty), we ask the LLM to verify, modify, and extend.
        
        Args:
            turns: Current session's conversation turns
            existing_decisions: Prior decisions to reconcile
            active_goals_context: String describing active goals
            goal_ids: Goal IDs to link new decisions to
            
        Returns:
            List of all valid decisions after reconciliation
        """
        conversation = await self._format_conversation_smart(turns)
        prior_decisions = self._format_decisions_for_carry_forward(existing_decisions)
        goals_context = active_goals_context or "None specified"
        
        prompt = DECISION_CARRY_FORWARD_PROMPT.format(
            prior_decisions=prior_decisions,
            active_goals=goals_context,
            conversation=conversation
        )
        
        # Call LLM for reconciliation
        response = await self.llm.complete(prompt, system=DECISION_CARRY_FORWARD_SYSTEM)
        
        # Parse reconciliation result
        try:
            data = json.loads(self._clean_json_response(response))
        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse carry-forward response: {e}")
            logger.debug(f"Response was: {response}")
            # Fallback: return existing decisions unchanged
            return existing_decisions
        
        # Build reconciliation result
        reconciliation = self._parse_reconciliation(data, existing_decisions)
        
        # Apply reconciliation to get final decision list
        final_decisions = self._apply_reconciliation(
            existing_decisions=existing_decisions,
            reconciliation=reconciliation,
            turns=turns,
            goal_ids=goal_ids
        )
        
        logger.info(
            f"Carry-Forward: {len(reconciliation.verified)} verified, "
            f"{len(reconciliation.modified)} modified, "
            f"{len(reconciliation.new)} new, "
            f"{len(reconciliation.invalidated)} invalidated"
        )
        
        return final_decisions
    
    def _format_decisions_for_carry_forward(self, decisions: list[DecisionTrail]) -> str:
        """
        Format decisions with IDs for carry-forward prompt.
        
        Args:
            decisions: List of prior decisions
            
        Returns:
            Formatted string with decision IDs for reference
        """
        if not decisions:
            return "None"
        
        lines = []
        for decision in decisions:
            lines.append(f"[{decision.id}] Decision: {decision.decision}")
            lines.append(f"    Context: {decision.context}")
            lines.append(f"    Rationale: {decision.rationale}")
            if decision.related_goals:
                lines.append(f"    Goals: {', '.join(decision.related_goals)}")
            if decision.alternatives:
                alts = ", ".join(a.option for a in decision.alternatives[:3])
                lines.append(f"    Alternatives considered: {alts}")
            lines.append("")
        
        return "\n".join(lines)
    
    def _parse_reconciliation(
        self,
        data: dict,
        existing_decisions: list[DecisionTrail]
    ) -> DecisionReconciliation:
        """
        Parse LLM response into DecisionReconciliation object.
        
        Args:
            data: Parsed JSON from LLM
            existing_decisions: Prior decisions for ID lookup
            
        Returns:
            DecisionReconciliation object
        """
        # Build lookup for existing decision IDs
        existing_ids = {d.id for d in existing_decisions}
        
        # Parse verified
        verified = []
        for v in data.get("verified", []):
            decision_id = v.get("decision_id", "")
            if decision_id in existing_ids:
                # Handle LLM returning float/int instead of bool for still_valid
                still_valid_raw = v.get("still_valid", True)
                if isinstance(still_valid_raw, (int, float)):
                    still_valid = still_valid_raw >= 0.5
                else:
                    still_valid = bool(still_valid_raw)
                
                verified.append(VerifiedDecision(
                    decision_id=decision_id,
                    still_valid=still_valid,
                    confidence=float(v.get("confidence", 0.8)),
                    supporting_evidence=v.get("supporting_evidence")
                ))
        
        # Parse modified
        modified = []
        for m in data.get("modified", []):
            original_id = m.get("original_decision_id", "")
            if original_id in existing_ids:
                modified.append(ModifiedDecision(
                    original_decision_id=original_id,
                    original_summary=m.get("original_summary", ""),
                    new_decision=m.get("new_decision", ""),
                    modification_type=m.get("modification_type", "refined"),
                    reason=m.get("reason", ""),
                    confidence=m.get("confidence", 0.8)
                ))
        
        # Parse new
        new = []
        for n in data.get("new", []):
            new.append(NewDecision(
                decision=n.get("decision", ""),
                context=n.get("context", ""),
                rationale=n.get("rationale", ""),
                confidence=n.get("confidence", 0.8),
                related_goal=n.get("related_goal"),
                alternatives=n.get("alternatives", []),
                evidence=n.get("evidence", []),
                trade_offs=n.get("trade_offs", [])
            ))
        
        # Parse invalidated
        invalidated = []
        for i in data.get("invalidated", []):
            decision_id = i.get("decision_id", "")
            if decision_id in existing_ids:
                invalidated.append(InvalidatedDecision(
                    decision_id=decision_id,
                    reason=i.get("reason", "")
                ))
        
        return DecisionReconciliation(
            verified=verified,
            modified=modified,
            new=new,
            invalidated=invalidated
        )
    
    def _apply_reconciliation(
        self,
        existing_decisions: list[DecisionTrail],
        reconciliation: DecisionReconciliation,
        turns: list[Turn],
        goal_ids: Optional[list[str]] = None
    ) -> list[DecisionTrail]:
        """
        Apply reconciliation to build final decision list.
        
        Args:
            existing_decisions: Prior decisions
            reconciliation: Parsed reconciliation result
            turns: Current session turns
            goal_ids: Goal IDs for new decisions
            
        Returns:
            Final list of valid decisions
        """
        # Build lookup
        existing_by_id = {d.id: d for d in existing_decisions}
        
        # Track which decisions to keep
        verified_ids = {v.decision_id for v in reconciliation.verified if v.still_valid}
        modified_ids = {m.original_decision_id for m in reconciliation.modified}
        invalidated_ids = {i.decision_id for i in reconciliation.invalidated}
        
        final_decisions = []
        
        # Add verified decisions (with updated confidence)
        for verified in reconciliation.verified:
            if verified.still_valid and verified.decision_id in existing_by_id:
                decision = existing_by_id[verified.decision_id]
                # Update confidence if we have supporting evidence
                if verified.supporting_evidence:
                    decision.confidence = max(decision.confidence, verified.confidence)
                final_decisions.append(decision)
        
        # Add modified decisions (create new decision, invalidate old)
        for modified in reconciliation.modified:
            if modified.original_decision_id in existing_by_id:
                # Create new decision from modified
                new_decision = DecisionTrail(
                    decision=modified.new_decision,
                    context=f"Modified from: {modified.original_summary}",
                    rationale=modified.reason,
                    confidence=modified.confidence,
                    dependencies=[modified.original_decision_id],
                    related_goals=existing_by_id[modified.original_decision_id].related_goals
                )
                final_decisions.append(new_decision)
                self.add_decision(new_decision)
                
                # Mark original as invalidated
                self.invalidate_decision(modified.original_decision_id, new_decision.id)
        
        # Add new decisions
        for new in reconciliation.new:
            decision_data = {
                "decision": new.decision,
                "context": new.context,
                "rationale": new.rationale,
                "confidence": new.confidence,
                "related_goal": new.related_goal,
                "alternatives": new.alternatives,
                "evidence": new.evidence,
                "trade_offs": new.trade_offs
            }
            decision = self._build_decision_trail(decision_data, turns)
            if goal_ids:
                decision.related_goals = goal_ids
            final_decisions.append(decision)
            self.add_decision(decision)
        
        # Decisions in invalidated list are NOT added to final
        # (they're already excluded by not being in verified/modified)
        
        # For any existing decisions not categorized, assume still valid (conservative)
        categorized_ids = verified_ids | modified_ids | invalidated_ids
        for decision in existing_decisions:
            if decision.id not in categorized_ids:
                logger.warning(f"Decision {decision.id} not categorized, assuming still valid")
                final_decisions.append(decision)
        
        return final_decisions
    
    def add_decision(self, decision: DecisionTrail) -> str:
        """
        Add a decision to the store.
        
        Args:
            decision: DecisionTrail to store
            
        Returns:
            Decision ID
        """
        self.decisions[decision.id] = decision
        
        # Update goal index
        for goal_id in decision.related_goals:
            if goal_id not in self._goal_index:
                self._goal_index[goal_id] = []
            self._goal_index[goal_id].append(decision.id)
        
        return decision.id
    
    def get_decision(self, decision_id: str) -> Optional[DecisionTrail]:
        """Get a decision by ID."""
        return self.decisions.get(decision_id)
    
    def get_decisions_for_goal(
        self,
        goal_id: str,
        include_invalidated: bool = False
    ) -> list[DecisionTrail]:
        """
        Get all decisions related to a goal.
        
        Args:
            goal_id: Goal ID to filter by
            include_invalidated: Whether to include superseded decisions
            
        Returns:
            List of related decisions
        """
        decision_ids = self._goal_index.get(goal_id, [])
        decisions = [self.decisions[did] for did in decision_ids if did in self.decisions]
        
        if not include_invalidated:
            decisions = [d for d in decisions if d.is_valid]
        
        return decisions
    
    def get_valid_decisions(self) -> list[DecisionTrail]:
        """Get all currently valid (not superseded) decisions."""
        return [d for d in self.decisions.values() if d.is_valid]
    
    def get_decisions_with_alternative(
        self,
        alternative_keyword: str
    ) -> list[DecisionTrail]:
        """
        Find decisions where a specific alternative was considered.
        
        Useful for answering questions like "Why didn't we use MongoDB?"
        
        Args:
            alternative_keyword: Keyword to search in alternatives
            
        Returns:
            Decisions where this alternative was considered
        """
        results = []
        keyword_lower = alternative_keyword.lower()
        
        for decision in self.decisions.values():
            for alt in decision.alternatives:
                if keyword_lower in alt.option.lower():
                    results.append(decision)
                    break
        
        return results
    
    def invalidate_decision(
        self,
        decision_id: str,
        replacement_id: str
    ) -> None:
        """
        Mark a decision as superseded by a new decision.
        
        Args:
            decision_id: ID of decision to invalidate
            replacement_id: ID of decision that replaces it
        """
        if decision_id in self.decisions:
            self.decisions[decision_id].invalidate(replacement_id)
    
    def get_decision_chain(self, decision_id: str) -> list[DecisionTrail]:
        """
        Get the chain of decisions leading to this one.
        
        Follows the dependencies to build the reasoning path.
        
        Args:
            decision_id: Starting decision ID
            
        Returns:
            List of decisions in dependency order
        """
        chain = []
        visited = set()
        
        def traverse(did: str):
            if did in visited or did not in self.decisions:
                return
            visited.add(did)
            
            decision = self.decisions[did]
            for dep_id in decision.dependencies:
                traverse(dep_id)
            
            chain.append(decision)
        
        traverse(decision_id)
        return chain
    
    def _build_decision_trail(
        self,
        data: dict,
        turns: list[Turn]
    ) -> DecisionTrail:
        """Build a DecisionTrail from extracted data."""
        # Build alternatives
        alternatives = []
        for alt_data in data.get("alternatives", []):
            alternatives.append(Alternative(
                option=alt_data.get("option", ""),
                pros=alt_data.get("pros", []),
                cons=alt_data.get("cons", []),
                rejection_reason=alt_data.get("rejection_reason")
            ))
        
        # Build evidence
        evidence = []
        for ev_data in data.get("evidence", []):
            source_str = ev_data.get("source", "inferred")
            try:
                source = EvidenceSource(source_str)
            except ValueError:
                source = EvidenceSource.INFERRED
            
            evidence.append(Evidence(
                claim=ev_data.get("claim", ""),
                source=source,
                turn_id=ev_data.get("turn_reference")
            ))
        
        # Build trade-offs
        trade_offs = []
        for to_data in data.get("trade_offs", []):
            trade_offs.append(TradeOff(
                gained=to_data.get("gained", ""),
                sacrificed=to_data.get("sacrificed", ""),
                justification=to_data.get("justification")
            ))
        
        # Build related goals
        related_goals = []
        if data.get("related_goal"):
            related_goals.append(data["related_goal"])
        
        return DecisionTrail(
            decision=data.get("decision", ""),
            context=data.get("context", ""),
            alternatives=alternatives,
            rationale=data.get("rationale", ""),
            evidence=evidence,
            trade_offs=trade_offs,
            confidence=data.get("confidence", 0.8),
            related_goals=related_goals
        )
    
    def _format_conversation(self, turns: list[Turn]) -> str:
        """Format turns for prompt."""
        lines = []
        for i, turn in enumerate(turns):
            lines.append(f"[Turn {i+1}] [{turn.role}]: {turn.content}")
        return "\n\n".join(lines)
    
    def _estimate_tokens(self, text: str) -> int:
        """
        Estimate token count for text.
        
        Uses rough approximation of ~4 characters per token for English text.
        This is conservative to avoid context overflow.
        
        Args:
            text: Text to estimate tokens for
            
        Returns:
            Estimated token count
        """
        return len(text) // 4
    
    async def _summarize_turns(self, turns: list[Turn]) -> str:
        """
        Summarize a batch of conversation turns.
        
        Uses LLM to create a concise summary preserving key decisions,
        topics, and context without all the verbose back-and-forth.
        
        Args:
            turns: List of turns to summarize
            
        Returns:
            Condensed summary string
        """
        if not turns:
            return ""
        
        # Format turns for summarization
        turn_text = []
        for i, turn in enumerate(turns):
            turn_text.append(f"[{turn.role}]: {turn.content[:500]}")  # Truncate very long turns
        
        summarization_prompt = f"""Summarize the following conversation segment concisely.
Focus on:
1. Key decisions made or discussed
2. Important technical choices and their rationale
3. Context that would be needed to understand later conversation
4. Any problems identified and solutions proposed

Keep the summary under 500 words. Be factual and specific.

CONVERSATION SEGMENT:
{chr(10).join(turn_text)}

SUMMARY:"""
        
        try:
            summary = await self.llm.complete(summarization_prompt)
            return f"[SUMMARY OF MIDDLE TURNS]: {summary.strip()}"
        except Exception as e:
            logger.warning(f"Failed to summarize turns: {e}")
            # Fallback: just note what was skipped
            return f"[SUMMARY: {len(turns)} turns omitted from middle of conversation]"
    
    async def _format_conversation_smart(
        self,
        turns: list[Turn],
        max_tokens: int = 15000,
        first_n: int = 10,
        last_n: int = 30
    ) -> str:
        """
        Format conversation with smart chunking for long conversations.
        
        Strategy: Sliding Window with Summary
        - Keep first N turns (establishes context, goals, initial decisions)
        - Summarize middle turns (preserve key information compactly)
        - Keep last N turns (recent state, latest decisions)
        
        This handles conversations of any length while preserving
        the most important context for decision extraction.
        
        Args:
            turns: All conversation turns
            max_tokens: Maximum token budget for conversation text
            first_n: Number of initial turns to keep verbatim
            last_n: Number of recent turns to keep verbatim
            
        Returns:
            Formatted conversation string within token budget
        """
        if not turns:
            return ""
        
        total_turns = len(turns)
        
        # For short conversations, use simple formatting
        if total_turns <= (first_n + last_n):
            return self._format_conversation(turns)
        
        # Check if simple formatting fits within budget
        simple_format = self._format_conversation(turns)
        if self._estimate_tokens(simple_format) <= max_tokens:
            return simple_format
        
        logger.info(f"Long conversation detected ({total_turns} turns), applying sliding window")
        
        # Split into three segments
        first_turns = turns[:first_n]
        middle_turns = turns[first_n:-last_n] if last_n > 0 else turns[first_n:]
        last_turns = turns[-last_n:] if last_n > 0 else []
        
        # Format first and last turns verbatim
        first_formatted = []
        for i, turn in enumerate(first_turns):
            first_formatted.append(f"[Turn {i + 1}] [{turn.role}]: {turn.content}")
        
        last_formatted = []
        start_idx = len(turns) - len(last_turns)
        for i, turn in enumerate(last_turns):
            last_formatted.append(f"[Turn {start_idx + i + 1}] [{turn.role}]: {turn.content}")
        
        # Summarize middle section
        middle_summary = await self._summarize_turns(middle_turns)
        
        # Combine all sections
        sections = [
            "=== CONVERSATION START ===",
            "\n\n".join(first_formatted),
            "",
            "=== MIDDLE SECTION (SUMMARIZED) ===",
            middle_summary,
            "",
            "=== RECENT CONVERSATION ===",
            "\n\n".join(last_formatted)
        ]
        
        result = "\n\n".join(sections)
        
        # Log token savings
        original_tokens = self._estimate_tokens(simple_format)
        final_tokens = self._estimate_tokens(result)
        logger.info(f"Conversation chunking: {original_tokens} → {final_tokens} tokens "
                   f"(saved {original_tokens - final_tokens} tokens)")
        
        return result
    
    def _format_decisions_for_context(self, decisions: list[DecisionTrail]) -> str:
        """
        Format existing decisions for context-aware extraction prompt.
        
        Args:
            decisions: List of existing decisions
            
        Returns:
            Formatted string summary of decisions
        """
        if not decisions:
            return "None"
        
        lines = []
        for i, decision in enumerate(decisions, 1):
            lines.append(f"{i}. [{decision.id}] {decision.decision}")
            lines.append(f"   Context: {decision.context}")
            lines.append(f"   Rationale: {decision.rationale}")
            if decision.related_goals:
                lines.append(f"   Related goals: {', '.join(decision.related_goals)}")
            lines.append("")
        
        return "\n".join(lines)
    
    def _clean_json_response(self, response: str) -> str:
        """Clean LLM response to extract JSON."""
        response = response.strip()
        if response.startswith("```json"):
            response = response[7:]
        elif response.startswith("```"):
            response = response[3:]
        if response.endswith("```"):
            response = response[:-3]
        return response.strip()
    
    def format_decision_for_context(
        self,
        decision: DecisionTrail,
        include_alternatives: bool = True,
        include_evidence: bool = True
    ) -> str:
        """
        Format a decision trail for inclusion in LLM context.
        
        Args:
            decision: Decision to format
            include_alternatives: Whether to include rejected alternatives
            include_evidence: Whether to include supporting evidence
            
        Returns:
            Formatted string representation
        """
        lines = [
            f"DECISION: {decision.decision}",
            f"Context: {decision.context}",
            f"Rationale: {decision.rationale}"
        ]
        
        if include_alternatives and decision.alternatives:
            lines.append("Alternatives considered:")
            for alt in decision.alternatives:
                lines.append(f"  - {alt.option}: Rejected because {alt.rejection_reason}")
        
        if include_evidence and decision.evidence:
            lines.append("Supporting evidence:")
            for ev in decision.evidence:
                lines.append(f"  - {ev.claim} (source: {ev.source.value})")
        
        if decision.trade_offs:
            lines.append("Trade-offs:")
            for to in decision.trade_offs:
                lines.append(f"  - Gained {to.gained}, sacrificed {to.sacrificed}")
        
        return "\n".join(lines)
