"""Critic agent for evaluating research completeness and team value adherence.

The Critic is the gatekeeper that ensures research meets quality standards.
It evaluates both:
1. Research completeness (coverage, evidence, depth, coherence)
2. Team value adherence (V1-V6)

Hard approval rules:
- evidence score < 3 → APPROVE forbidden
- team_alignment score < 4 → APPROVE forbidden
- All P0 questions must meet success criteria
- Any contradiction must be resolved or listed
"""

import json
from typing import Optional

from rich.console import Console

from emdash.agent.providers import get_provider
from emdash.agent.providers.factory import DEFAULT_MODEL
from emdash.agent.compaction import LLMCompactor
from emdash.agent.research.state import (
    EvidenceItem,
    Claim,
    Gap,
    ResearchPlan,
    ResearchQuestion,
    Critique,
    CritiqueScores,
    FollowUpQuestion,
    Contradiction,
    ValuesViolation,
    IterationResult,
)


CRITIC_SYSTEM_PROMPT = """You are a research critic that evaluates research quality and team value adherence.

TEAM VALUES YOU MUST ENFORCE:
- V1: Truth over fluency - reject claims without evidence
- V2: Evidence-first - claims must reference evidence IDs
- V3: Reviewer-first - output must support reviewer workflows
- V4: Cost awareness - flag wasteful exploration
- V5: Actionable outcomes - findings must lead to tasks
- V6: Team alignment - use team vocabulary

SCORING RUBRIC (1-5 for each dimension):

COVERAGE (Were questions addressed?):
- 5: All P0/P1 questions fully answered
- 4: All P0 answered, most P1 addressed
- 3: All P0 answered, some gaps in P1
- 2: Some P0 unanswered
- 1: Most questions unanswered

EVIDENCE (Are claims backed by tool results?):
- 5: Every claim has 2+ evidence sources
- 4: Every claim has evidence, most have 2+
- 3: Every claim has at least one evidence
- 2: Some claims lack evidence
- 1: Most claims ungrounded

DEPTH (Is analysis thorough?):
- 5: Deep understanding demonstrated
- 4: Good depth on important areas
- 3: Adequate depth overall
- 2: Surface-level analysis
- 1: Superficial / incomplete

COHERENCE (Do findings connect logically?):
- 5: Clear narrative, no contradictions
- 4: Well-connected, minor inconsistencies
- 3: Generally coherent
- 2: Some logical gaps
- 1: Disconnected / contradictory

TEAM_ALIGNMENT (Does output follow team workflows?):
- 5: Fully aligned with team values and vocabulary
- 4: Good alignment, minor adjustments needed
- 3: Acceptable alignment
- 2: Missing key team elements
- 1: Does not follow team workflows

DECISIONS:
- APPROVE: All P0 met, evidence >= 3, team_alignment >= 4
- CONTINUE: Progress made, follow-ups needed
- REJECT: Values violated, must fix issues
- ESCALATE: Need more powerful model or budget

OUTPUT FORMAT (JSON):
{
  "decision": "APPROVE|CONTINUE|REJECT|ESCALATE",
  "scores": {
    "coverage": 1-5,
    "evidence": 1-5,
    "depth": 1-5,
    "coherence": 1-5,
    "team_alignment": 1-5
  },
  "must_fix": ["issue 1", "issue 2"],
  "follow_up_questions": [
    {"question": "...", "why": "...", "suggested_tools": [...]}
  ],
  "risky_claims": ["C1", "C3"],
  "contradictions": [
    {"claim_a": "C1", "claim_b": "C3", "note": "..."}
  ],
  "values_violations": [
    {"value": "V1", "issue": "...", "affected_claims": ["C2"]}
  ],
  "reasoning": "Brief explanation of decision"
}

Be strict but fair. The goal is quality research, not blocking progress."""


class CriticAgent:
    """Evaluates research completeness and team value adherence.
    
    The Critic acts as a quality gate. It scores research across
    multiple dimensions and decides whether to approve, continue,
    reject, or escalate.
    """
    
    # Hard approval thresholds
    MIN_EVIDENCE_SCORE = 3
    MIN_TEAM_ALIGNMENT_SCORE = 4
    
    def __init__(
        self,
        model: str = DEFAULT_MODEL,
        verbose: bool = True,
    ):
        """Initialize the critic agent.
        
        Args:
            model: LLM model to use
            verbose: Whether to print progress
        """
        self.provider = get_provider(model)
        self.model = model
        self.verbose = verbose
        self.console = Console()
        self.compactor = LLMCompactor(self.provider)
    
    def evaluate(
        self,
        plan: ResearchPlan,
        claims: list[Claim],
        evidence: list[EvidenceItem],
        gaps: list[Gap],
        history: list[IterationResult],
    ) -> Critique:
        """Evaluate research and return critique.
        
        Args:
            plan: Research plan with questions and values
            claims: Claims proposed in this iteration
            evidence: Evidence collected
            gaps: Gaps identified
            history: Results from previous iterations
            
        Returns:
            Critique with decision and scores
        """
        if self.verbose:
            self.console.print("[cyan]Evaluating research...[/cyan]")
        
        # Get all claims and evidence across iterations
        all_claims = list(claims)
        all_evidence = list(evidence)
        for result in history:
            all_claims.extend(result.claims)
            all_evidence.extend(result.evidence)
        
        # Try LLM-based evaluation
        try:
            critique = self._llm_evaluate(plan, all_claims, all_evidence, gaps)
            if critique:
                if self.verbose:
                    self._print_critique(critique)
                return critique
        except Exception as e:
            if self.verbose:
                self.console.print(f"[yellow]LLM evaluation failed: {e}. Using heuristic.[/yellow]")
        
        # Fallback to heuristic evaluation
        return self._heuristic_evaluate(plan, all_claims, all_evidence, gaps)
    
    def _llm_evaluate(
        self,
        plan: ResearchPlan,
        claims: list[Claim],
        evidence: list[EvidenceItem],
        gaps: list[Gap],
    ) -> Optional[Critique]:
        """Evaluate using LLM.
        
        Args:
            plan: Research plan
            claims: All claims
            evidence: All evidence
            gaps: All gaps
            
        Returns:
            Critique or None on failure
        """
        # Build evaluation context
        questions_text = "\n".join([
            f"- [{q.priority}] {q.qid}: {q.question}"
            for q in plan.questions
        ])
        
        claims_text = "\n".join([
            f"- {c.id} (conf={c.confidence}, evidence={c.evidence_ids}): {c.statement}"
            for c in claims
        ]) or "No claims yet"
        
        evidence_text = "\n".join([
            f"- {e.id}: {e.tool} → {e.summary}"
            for e in evidence
        ]) or "No evidence yet"
        
        gaps_text = "\n".join([
            f"- {g.question} (reason: {g.reason})"
            for g in gaps
        ]) or "No gaps identified"
        
        values_text = "\n".join(plan.team_values_checklist)
        
        payload = {
            "questions": questions_text,
            "claims": claims_text,
            "evidence": evidence_text,
            "gaps": gaps_text,
            "team_values": values_text,
        }
        compacted = self.compactor.compact_payload(payload, plan.goal)
        questions_text = compacted.get("questions", questions_text)
        claims_text = compacted.get("claims", claims_text)
        evidence_text = compacted.get("evidence", evidence_text)
        gaps_text = compacted.get("gaps", gaps_text)
        values_text = compacted.get("team_values", values_text)

        user_message = f"""Evaluate this research iteration.

RESEARCH GOAL: {plan.goal}

QUESTIONS (by priority):
{questions_text}

CLAIMS:
{claims_text}

EVIDENCE:
{evidence_text}

GAPS:
{gaps_text}

TEAM VALUES TO CHECK:
{values_text}

Evaluate the research and decide: APPROVE, CONTINUE, REJECT, or ESCALATE.
Remember: evidence >= 3 and team_alignment >= 4 required for APPROVE.

Return JSON only, no markdown code blocks."""

        messages = [
            {"role": "user", "content": user_message},
        ]
        
        response = self.provider.chat(messages, system=CRITIC_SYSTEM_PROMPT)
        content = response.content or ""
        
        return self._parse_critique(content)
    
    def _parse_critique(self, content: str) -> Optional[Critique]:
        """Parse critique from LLM response.
        
        Args:
            content: LLM response
            
        Returns:
            Critique or None
        """
        try:
            # Extract JSON
            json_str = content
            if "```" in content:
                start = content.find("```")
                end = content.find("```", start + 3)
                if end > start:
                    json_str = content[start + 3:end]
                    if json_str.startswith("json"):
                        json_str = json_str[4:]
            
            data = json.loads(json_str.strip())
            
            # Parse scores
            scores_data = data.get("scores", {})
            scores = CritiqueScores(
                coverage=scores_data.get("coverage", 3),
                evidence=scores_data.get("evidence", 3),
                depth=scores_data.get("depth", 3),
                coherence=scores_data.get("coherence", 3),
                team_alignment=scores_data.get("team_alignment", 3),
            )
            
            # Parse follow-up questions
            follow_ups = []
            for q in data.get("follow_up_questions", []):
                follow_ups.append(FollowUpQuestion(
                    question=q.get("question", ""),
                    why=q.get("why", ""),
                    suggested_tools=q.get("suggested_tools", []),
                    qid=q.get("qid"),
                ))
            
            # Parse contradictions
            contradictions = []
            for c in data.get("contradictions", []):
                contradictions.append(Contradiction(
                    claim_a=c.get("claim_a", ""),
                    claim_b=c.get("claim_b", ""),
                    note=c.get("note", ""),
                ))
            
            # Parse values violations
            violations = []
            for v in data.get("values_violations", []):
                violations.append(ValuesViolation(
                    value=v.get("value", ""),
                    issue=v.get("issue", ""),
                    affected_claims=v.get("affected_claims", []),
                ))
            
            # Validate decision against hard rules
            decision = data.get("decision", "CONTINUE")
            if decision == "APPROVE":
                if scores.evidence < self.MIN_EVIDENCE_SCORE:
                    decision = "CONTINUE"
                if scores.team_alignment < self.MIN_TEAM_ALIGNMENT_SCORE:
                    decision = "CONTINUE"
            
            return Critique(
                decision=decision,
                scores=scores,
                must_fix=data.get("must_fix", []),
                follow_up_questions=follow_ups,
                risky_claims=data.get("risky_claims", []),
                contradictions=contradictions,
                values_violations=violations,
            )
            
        except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
            if self.verbose:
                self.console.print(f"[yellow]Failed to parse critique: {e}[/yellow]")
            return None
    
    def _heuristic_evaluate(
        self,
        plan: ResearchPlan,
        claims: list[Claim],
        evidence: list[EvidenceItem],
        gaps: list[Gap],
    ) -> Critique:
        """Evaluate using heuristics.
        
        Fallback method when LLM evaluation fails.
        
        Args:
            plan: Research plan
            claims: All claims
            evidence: All evidence
            gaps: All gaps
            
        Returns:
            Critique
        """
        # Score coverage
        p0_questions = plan.get_p0_questions()
        p1_questions = plan.get_p1_questions()
        
        p0_answered = self._count_answered_questions(p0_questions, claims)
        p1_answered = self._count_answered_questions(p1_questions, claims)
        
        if len(p0_questions) == 0:
            coverage_score = 4
        elif p0_answered == len(p0_questions):
            if p1_answered >= len(p1_questions) * 0.7:
                coverage_score = 5
            elif p1_answered >= len(p1_questions) * 0.5:
                coverage_score = 4
            else:
                coverage_score = 3
        elif p0_answered >= len(p0_questions) * 0.7:
            coverage_score = 2
        else:
            coverage_score = 1
        
        # Score evidence
        claims_with_evidence = sum(1 for c in claims if c.evidence_ids)
        claims_with_multiple = sum(1 for c in claims if len(c.evidence_ids) >= 2)
        
        if len(claims) == 0:
            evidence_score = 1
        elif claims_with_evidence == len(claims):
            if claims_with_multiple >= len(claims) * 0.7:
                evidence_score = 5
            elif claims_with_multiple >= len(claims) * 0.3:
                evidence_score = 4
            else:
                evidence_score = 3
        elif claims_with_evidence >= len(claims) * 0.7:
            evidence_score = 2
        else:
            evidence_score = 1
        
        # Score depth (based on evidence count)
        if len(evidence) >= 20:
            depth_score = 5
        elif len(evidence) >= 15:
            depth_score = 4
        elif len(evidence) >= 10:
            depth_score = 3
        elif len(evidence) >= 5:
            depth_score = 2
        else:
            depth_score = 1
        
        # Score coherence (check for contradictions)
        contradictions = self._detect_contradictions(claims)
        if not contradictions:
            coherence_score = 5
        elif len(contradictions) <= 1:
            coherence_score = 4
        elif len(contradictions) <= 3:
            coherence_score = 3
        else:
            coherence_score = 2
        
        # Score team alignment (check claims have evidence)
        violations = self._check_values_violations(claims, evidence)
        if not violations:
            team_alignment_score = 5
        elif len(violations) <= 1:
            team_alignment_score = 4
        elif len(violations) <= 3:
            team_alignment_score = 3
        else:
            team_alignment_score = 2
        
        scores = CritiqueScores(
            coverage=coverage_score,
            evidence=evidence_score,
            depth=depth_score,
            coherence=coherence_score,
            team_alignment=team_alignment_score,
        )
        
        # Determine decision
        decision = self._determine_decision(scores, p0_answered, len(p0_questions), gaps)
        
        # Generate follow-up questions for gaps
        follow_ups = []
        for gap in gaps[:3]:
            follow_ups.append(FollowUpQuestion(
                question=gap.question,
                why=gap.reason,
                suggested_tools=gap.suggested_tools,
            ))
        
        critique = Critique(
            decision=decision,
            scores=scores,
            must_fix=[v.issue for v in violations] if violations else [],
            follow_up_questions=follow_ups,
            risky_claims=[c.id for c in claims if c.confidence <= 1],
            contradictions=contradictions,
            values_violations=violations,
        )
        
        if self.verbose:
            self._print_critique(critique)
        
        return critique
    
    def _count_answered_questions(
        self,
        questions: list[ResearchQuestion],
        claims: list[Claim],
    ) -> int:
        """Count how many questions have been answered.
        
        Args:
            questions: Questions to check
            claims: Available claims
            
        Returns:
            Count of answered questions
        """
        answered = 0
        
        for question in questions:
            # Simple heuristic: check if any claim relates to question
            q_keywords = self._extract_keywords(question.question)
            
            for claim in claims:
                c_keywords = self._extract_keywords(claim.statement)
                overlap = len(set(q_keywords) & set(c_keywords))
                
                if overlap >= 2:
                    answered += 1
                    break
        
        return answered
    
    def _extract_keywords(self, text: str) -> list[str]:
        """Extract keywords from text."""
        stopwords = {
            "the", "a", "an", "is", "are", "was", "were", "be", "been",
            "being", "have", "has", "had", "do", "does", "did", "will",
            "would", "could", "should", "may", "might", "must", "shall",
            "can", "need", "to", "of", "in", "for", "on", "with", "at",
            "by", "from", "as", "what", "where", "who", "how", "why",
        }
        
        words = text.lower().split()
        return [w.strip("?!.,;:'\"") for w in words if w not in stopwords and len(w) > 2]
    
    def _detect_contradictions(self, claims: list[Claim]) -> list[Contradiction]:
        """Detect contradictions between claims.
        
        Simple heuristic: look for claims about the same entity
        with conflicting statements.
        
        Args:
            claims: Claims to check
            
        Returns:
            List of contradictions
        """
        contradictions = []
        
        # Group claims by entity
        entity_claims: dict[str, list[Claim]] = {}
        
        for claim in claims:
            # Extract entities from claim
            keywords = self._extract_keywords(claim.statement)
            for kw in keywords:
                if kw not in entity_claims:
                    entity_claims[kw] = []
                entity_claims[kw].append(claim)
        
        # Check for contradictions within entity groups
        for entity, related_claims in entity_claims.items():
            if len(related_claims) < 2:
                continue
            
            # Look for negation patterns
            for i, c1 in enumerate(related_claims):
                for c2 in related_claims[i+1:]:
                    if self._claims_contradict(c1, c2):
                        contradictions.append(Contradiction(
                            claim_a=c1.id,
                            claim_b=c2.id,
                            note=f"Potential contradiction about '{entity}'",
                        ))
        
        return contradictions[:5]  # Limit
    
    def _claims_contradict(self, c1: Claim, c2: Claim) -> bool:
        """Check if two claims contradict each other.
        
        Simple heuristic based on negation words.
        """
        s1 = c1.statement.lower()
        s2 = c2.statement.lower()
        
        negation_words = ["not", "no", "never", "none", "nothing", "neither", "without"]
        
        s1_negative = any(w in s1 for w in negation_words)
        s2_negative = any(w in s2 for w in negation_words)
        
        # One positive, one negative about same topic
        return s1_negative != s2_negative
    
    def _check_values_violations(
        self,
        claims: list[Claim],
        evidence: list[EvidenceItem],
    ) -> list[ValuesViolation]:
        """Check for team values violations.
        
        Args:
            claims: Claims to check
            evidence: Available evidence
            
        Returns:
            List of violations
        """
        violations = []
        evidence_ids = {e.id for e in evidence}
        
        # V1: Truth over fluency - claims without evidence
        ungrounded = [c for c in claims if not c.evidence_ids]
        if ungrounded:
            violations.append(ValuesViolation(
                value="V1",
                issue="Claims without evidence IDs",
                affected_claims=[c.id for c in ungrounded],
            ))
        
        # V2: Evidence-first - references to non-existent evidence
        bad_refs = []
        for claim in claims:
            invalid = [eid for eid in claim.evidence_ids if eid not in evidence_ids]
            if invalid:
                bad_refs.append(claim.id)
        
        if bad_refs:
            violations.append(ValuesViolation(
                value="V2",
                issue="References to non-existent evidence",
                affected_claims=bad_refs,
            ))
        
        # V4: Cost awareness - claims with low confidence
        low_confidence = [c for c in claims if c.confidence == 0]
        if low_confidence:
            violations.append(ValuesViolation(
                value="V4",
                issue="Speculative claims (confidence=0) should be avoided",
                affected_claims=[c.id for c in low_confidence],
            ))
        
        return violations
    
    def _determine_decision(
        self,
        scores: CritiqueScores,
        p0_answered: int,
        p0_total: int,
        gaps: list[Gap],
    ) -> str:
        """Determine the critique decision.
        
        Args:
            scores: Critique scores
            p0_answered: Number of P0 questions answered
            p0_total: Total P0 questions
            gaps: Identified gaps
            
        Returns:
            Decision string
        """
        # Hard rules for APPROVE
        if scores.evidence < self.MIN_EVIDENCE_SCORE:
            if scores.evidence <= 1:
                return "REJECT"
            return "CONTINUE"
        
        if scores.team_alignment < self.MIN_TEAM_ALIGNMENT_SCORE:
            if scores.team_alignment <= 2:
                return "REJECT"
            return "CONTINUE"
        
        # All P0 must be answered
        if p0_total > 0 and p0_answered < p0_total:
            return "CONTINUE"
        
        # Check average score
        avg_score = scores.average()
        
        if avg_score >= 4.0:
            return "APPROVE"
        elif avg_score >= 3.0:
            if len(gaps) <= 2:
                return "APPROVE"
            return "CONTINUE"
        elif avg_score >= 2.0:
            return "CONTINUE"
        else:
            return "ESCALATE"
    
    def _print_critique(self, critique: Critique) -> None:
        """Print critique summary.
        
        Args:
            critique: The critique to print
        """
        decision_colors = {
            "APPROVE": "green",
            "CONTINUE": "yellow",
            "REJECT": "red",
            "ESCALATE": "magenta",
        }
        
        color = decision_colors.get(critique.decision, "white")
        
        self.console.print(f"\n[{color}]Decision: {critique.decision}[/{color}]")
        self.console.print(f"  Coverage: {critique.scores.coverage}/5")
        self.console.print(f"  Evidence: {critique.scores.evidence}/5")
        self.console.print(f"  Depth: {critique.scores.depth}/5")
        self.console.print(f"  Coherence: {critique.scores.coherence}/5")
        self.console.print(f"  Team Alignment: {critique.scores.team_alignment}/5")
        
        if critique.must_fix:
            self.console.print(f"  [red]Must fix: {len(critique.must_fix)} issues[/red]")
        
        if critique.follow_up_questions:
            self.console.print(f"  [yellow]Follow-ups: {len(critique.follow_up_questions)} questions[/yellow]")
