"""
Brief Synthesis Engine for Baymax
=================================
The missing piece: synthesizing rules ACROSS multiple cases to generate
comprehensive RULE sections and APPLICATION drafts for legal briefs.

Core capabilities:
1. Cross-case rule hierarchy synthesis
2. Circuit split detection
3. Analogy/distinction generation
4. Citation string builder
5. Element-by-element drafting
"""

import re
import os
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Set, Union
from dataclasses import dataclass, field
from enum import Enum
from collections import defaultdict

# Import existing rule extraction
from enhanced_rules import extract_enhanced_rules, EnhancedRule, RuleType, RuleLevel

# Optional PDF support
try:
    import PyPDF2
    PDF_SUPPORT = True
except ImportError:
    try:
        import pdfplumber
        PDF_SUPPORT = True
    except ImportError:
        PDF_SUPPORT = False


# ============================================================================
# DATA STRUCTURES
# ============================================================================

class CourtLevel(Enum):
    """Federal court hierarchy."""
    SUPREME_COURT = 4
    CIRCUIT_COURT = 3
    DISTRICT_COURT = 2
    STATE_COURT = 1
    UNKNOWN = 0


class RuleRelationship(Enum):
    """How rules relate across cases."""
    IDENTICAL = "identical"          # Same rule, different cases
    ELABORATION = "elaboration"      # Adds detail to prior rule
    EXCEPTION = "exception"          # Carves out exception
    CONFLICT = "conflict"            # Contradicts (circuit split)
    FACTOR = "factor"                # Part of multi-factor test
    APPLICATION = "application"      # Applies rule to specific facts


@dataclass
class SynthesizedRuleNode:
    """A node in the cross-case rule hierarchy."""
    rule_text: str
    sources: List[Tuple[str, str, Optional[str]]]  # [(case_name, citation, pinpoint), ...]
    court_level: CourtLevel
    year: Optional[int]
    relationship: RuleRelationship
    children: List['SynthesizedRuleNode'] = field(default_factory=list)
    elements: List[str] = field(default_factory=list)
    factors: List[str] = field(default_factory=list)
    # Enhanced fields
    is_binding: bool = True  # Binding vs. persuasive
    policy_rationale: Optional[str] = None  # Why this rule exists
    has_ambiguity: bool = False  # Courts state differently
    alternative_formulations: List[str] = field(default_factory=list)  # Different wordings
    rule_status: str = "majority"  # "majority", "minority", "split", "evolving"
    temporal_note: Optional[str] = None  # Evolution note


@dataclass
class CircuitPosition:
    """A circuit's position on an issue."""
    circuit: str
    rule_text: str
    cases: List[Tuple[str, str]]  # [(case_name, citation), ...]
    is_binding: bool = False


@dataclass
class CircuitSplitAnalysis:
    """Analysis of circuit split on an issue."""
    issue: str
    positions: List[CircuitPosition]
    supreme_court_status: str  # "resolved", "unresolved", "cert_pending"
    majority_position: Optional[str]
    your_jurisdiction: Optional[CircuitPosition]


@dataclass
class FactRuleMapping:
    """Maps client facts to applicable rules."""
    client_fact: str
    triggered_rules: List[Tuple[str, str, str]]  # [(rule, case, why)]
    strength: str  # "strong", "moderate", "weak"


@dataclass
class AnalogyDistinction:
    """Generated analogy or distinction."""
    type: str  # "analogy" or "distinction"
    precedent_case: str
    precedent_fact: str
    client_fact: str
    draft_language: str
    rule_applied: str
    element_addressed: Optional[str] = None  # Which legal element this supports
    record_cite: Optional[str] = None  # Record citation for client fact
    fact_significance: str = "supporting"  # "determinative", "supporting", "background"


@dataclass
class EvenIfArgument:
    """Alternative argument structure."""
    concession: str  # "Even if the Court finds X"
    alternative_argument: str  # "Plaintiff still loses because Y"
    supporting_authority: str  # Case citation
    draft_language: str


# ============================================================================
# COURT DETECTION
# ============================================================================

def _ordinal_suffix(n: int) -> str:
    """Return correct ordinal suffix for a number."""
    if 11 <= n % 100 <= 13:
        return 'th'
    return {1: 'st', 2: 'nd', 3: 'rd'}.get(n % 10, 'th')


def detect_court_level(citation: str) -> Tuple[CourtLevel, Optional[str], Optional[int]]:
    """
    Detect court level, circuit, and year from citation.

    Returns:
        (court_level, circuit_or_court, year)
    """
    citation = citation.strip()

    # Year extraction
    year_match = re.search(r'\((\d{4})\)', citation)
    year = int(year_match.group(1)) if year_match else None

    # Supreme Court
    if re.search(r'\d+\s+U\.?S\.?\s+\d+', citation) or re.search(r'\d+\s+S\.?\s*Ct\.?\s+\d+', citation):
        return CourtLevel.SUPREME_COURT, "Supreme Court", year

    # Circuit Courts - multiple patterns for flexibility
    # Pattern 1: "(5th Cir. 2020)" or "(5th Cir.2020)"
    circuit_match = re.search(r'\((\d{1,2})(?:st|nd|rd|th)\s*Cir\.?\s*\d{0,4}\)', citation, re.IGNORECASE)
    if circuit_match:
        circuit_num = int(circuit_match.group(1))
        suffix = _ordinal_suffix(circuit_num)
        return CourtLevel.CIRCUIT_COURT, f"{circuit_num}{suffix} Circuit", year

    # Pattern 2: "5th Cir." anywhere in citation (common in F.2d/F.3d cites)
    circuit_match = re.search(r'(\d{1,2})(?:st|nd|rd|th)\s*Cir\.?', citation, re.IGNORECASE)
    if circuit_match:
        circuit_num = int(circuit_match.group(1))
        suffix = _ordinal_suffix(circuit_num)
        return CourtLevel.CIRCUIT_COURT, f"{circuit_num}{suffix} Circuit", year

    # D.C. Circuit
    if re.search(r'D\.?C\.?\s*Cir\.?', citation, re.IGNORECASE):
        return CourtLevel.CIRCUIT_COURT, "D.C. Circuit", year

    # Federal Circuit
    if re.search(r'Fed\.?\s*Cir\.?', citation, re.IGNORECASE):
        return CourtLevel.CIRCUIT_COURT, "Federal Circuit", year

    # Circuit from F.2d/F.3d/F.4th reporter (implies circuit court even without explicit circuit)
    if re.search(r'F\.?\s*(?:2d|3d|4th)', citation):
        # Check for circuit info in parenthetical
        paren_match = re.search(r'\(([^)]+)\)', citation)
        if paren_match:
            paren_content = paren_match.group(1)
            # Extract circuit number from parenthetical
            num_match = re.search(r'(\d{1,2})(?:st|nd|rd|th)', paren_content, re.IGNORECASE)
            if num_match:
                circuit_num = int(num_match.group(1))
                suffix = _ordinal_suffix(circuit_num)
                return CourtLevel.CIRCUIT_COURT, f"{circuit_num}{suffix} Circuit", year
        # F.2d/F.3d/F.4th is circuit court even if we can't identify which
        return CourtLevel.CIRCUIT_COURT, "Circuit Court", year

    # District Courts
    if re.search(r'F\.?\s*Supp', citation):
        district_match = re.search(r'\(([A-Z]\.?[A-Z]?\.?\s*(?:N|S|E|W|C)?\.\s*\w+\.?)\s*\d{4}\)', citation)
        district = district_match.group(1) if district_match else "District Court"
        return CourtLevel.DISTRICT_COURT, district, year

    # State courts
    state_reporters = ['N.E.', 'S.E.', 'S.W.', 'N.W.', 'P.', 'A.', 'So.',
                       'Cal.', 'N.Y.', 'Tex.', 'Ill.', 'Mass.']
    for reporter in state_reporters:
        if reporter in citation:
            return CourtLevel.STATE_COURT, "State Court", year

    return CourtLevel.UNKNOWN, None, year


# ============================================================================
# RULE SIMILARITY AND MATCHING
# ============================================================================

def normalize_rule_text(text: str) -> str:
    """Normalize rule text for comparison."""
    text = text.lower()
    text = re.sub(r'\s+', ' ', text)
    # Remove common legal filler
    fillers = ['the court held that', 'we hold that', 'it is well established that',
               'under this standard', 'accordingly', 'therefore', 'thus']
    for filler in fillers:
        text = text.replace(filler, '')
    return text.strip()


def extract_key_legal_concepts(text: str) -> Set[str]:
    """Extract key legal concepts from rule text."""
    concepts = set()

    # Common legal concepts to look for
    concept_patterns = [
        r'\b(scienter|intent|knowledge|recklessness|negligence)\b',
        r'\b(pleading|plead|allege|allegation)\b',
        r'\b(particularity|specific|specificity)\b',
        r'\b(inference|infer|plausible|cogent|compelling)\b',
        r'\b(burden|prove|proof|establish|demonstrate|show)\b',
        r'\b(element|factor|prong|requirement|standard|test)\b',
        r'\b(materiality|material|immaterial)\b',
        r'\b(reliance|causation|damages|loss)\b',
        r'\b(fraud|misrepresentation|omission|misleading)\b',
        r'\b(duty|breach|harm|injury)\b',
        r'\b(motion to dismiss|summary judgment|12\(b\)\(6\))\b',
    ]

    for pattern in concept_patterns:
        matches = re.findall(pattern, text, re.IGNORECASE)
        concepts.update(m.lower() for m in matches)

    return concepts


def calculate_rule_similarity(rule_a: str, rule_b: str) -> float:
    """
    Calculate semantic similarity between two rules.
    Returns 0.0 to 1.0.
    """
    norm_a = normalize_rule_text(rule_a)
    norm_b = normalize_rule_text(rule_b)

    # Word overlap
    words_a = set(norm_a.split())
    words_b = set(norm_b.split())

    if not words_a or not words_b:
        return 0.0

    # Jaccard similarity
    intersection = words_a & words_b
    union = words_a | words_b
    word_sim = len(intersection) / len(union)

    # Concept overlap (weighted higher)
    concepts_a = extract_key_legal_concepts(rule_a)
    concepts_b = extract_key_legal_concepts(rule_b)

    if concepts_a and concepts_b:
        concept_intersection = concepts_a & concepts_b
        concept_union = concepts_a | concepts_b
        concept_sim = len(concept_intersection) / len(concept_union)
    else:
        concept_sim = 0.0

    # Combined score (concepts weighted 2x)
    return (word_sim + 2 * concept_sim) / 3


def detect_rule_relationship(rule_a: EnhancedRule, rule_b: EnhancedRule,
                             similarity: float) -> RuleRelationship:
    """Determine the relationship between two rules."""

    # High similarity = same rule or elaboration
    if similarity > 0.7:
        # Check if one elaborates the other (longer, more detail)
        if len(rule_b.text) > len(rule_a.text) * 1.3:
            return RuleRelationship.ELABORATION
        return RuleRelationship.IDENTICAL

    # Check for exception language
    exception_markers = ['except', 'unless', 'however', 'but', 'notwithstanding',
                         'does not apply', 'exception', 'carve-out']
    if any(marker in rule_b.text.lower() for marker in exception_markers):
        return RuleRelationship.EXCEPTION

    # Check for conflict markers
    conflict_markers = ['reject', 'disagree', 'decline to follow', 'contrary to',
                        'we hold otherwise', 'respectfully disagree']
    if any(marker in rule_b.text.lower() for marker in conflict_markers):
        return RuleRelationship.CONFLICT

    # Low similarity with shared concepts = might be different aspects
    concepts_a = extract_key_legal_concepts(rule_a.text)
    concepts_b = extract_key_legal_concepts(rule_b.text)

    if concepts_a & concepts_b:
        if rule_b.rule_type == RuleType.MULTI_FACTOR:
            return RuleRelationship.FACTOR
        return RuleRelationship.ELABORATION

    return RuleRelationship.APPLICATION


# ============================================================================
# CROSS-CASE RULE SYNTHESIS
# ============================================================================

def synthesize_rules_across_cases(
    cases: Dict[str, Tuple[str, str]],  # case_name -> (text, citation)
    issue: Optional[str] = None,
    your_jurisdiction: Optional[str] = None
) -> List[SynthesizedRuleNode]:
    """
    The core function: synthesize rules ACROSS multiple cases.

    Enhanced features:
    - Identifies binding vs. persuasive based on jurisdiction
    - Extracts policy rationales behind rules
    - Flags ambiguities where courts state rule differently
    - Shows temporal evolution of rules
    - Classifies as majority/minority/split

    Args:
        cases: Dictionary mapping case name to (full_text, citation)
        issue: Optional issue to focus on
        your_jurisdiction: Your circuit/jurisdiction for binding determination

    Returns:
        List of SynthesizedRuleNode forming a hierarchy
    """
    # Step 1: Extract rules from each case
    all_rules: List[Tuple[EnhancedRule, str, str, CourtLevel, int, str]] = []  # Added jurisdiction

    for case_name, (text, citation) in cases.items():
        court_level, court, year = detect_court_level(citation)
        rules = extract_enhanced_rules(text, case_name, citation)

        # Extract jurisdiction from citation
        jurisdiction = _extract_jurisdiction_from_citation(citation)

        for rule in rules:
            all_rules.append((rule, case_name, citation, court_level, year or 2000, jurisdiction, text))

    if not all_rules:
        return []

    # Step 2: Sort by authority (Supreme Court first, then by year)
    all_rules.sort(key=lambda x: (-x[3].value, -x[4]))

    # Step 3: Group similar rules together
    rule_groups: List[List[Tuple]] = []
    used = set()

    for i, rule_tuple_a in enumerate(all_rules):
        if i in used:
            continue

        rule_a = rule_tuple_a[0]
        group = [rule_tuple_a]
        used.add(i)

        for j, rule_tuple_b in enumerate(all_rules):
            if j in used:
                continue

            rule_b = rule_tuple_b[0]
            similarity = calculate_rule_similarity(rule_a.text, rule_b.text)
            if similarity > 0.4:  # Related rules
                group.append(rule_tuple_b)
                used.add(j)

        rule_groups.append(group)

    # Step 4: Build hierarchy from groups with enhanced analysis
    hierarchy = []

    for group in rule_groups:
        # Primary rule is from highest authority
        primary = group[0]
        rule, case_name, citation, court_level, year, jurisdiction, case_text = primary

        # NEW: Determine binding vs. persuasive
        is_binding = _is_binding_authority(court_level, jurisdiction, your_jurisdiction)

        # NEW: Extract policy rationale
        policy_rationale = _extract_policy_rationale(case_text, rule.text)

        # NEW: Check for ambiguities across formulations
        alternative_formulations = []
        has_ambiguity = False
        for other in group[1:]:
            other_rule = other[0]
            if other_rule.text != rule.text:
                alternative_formulations.append(other_rule.text)
                # Check if formulations differ significantly
                if calculate_rule_similarity(rule.text, other_rule.text) < 0.7:
                    has_ambiguity = True

        # NEW: Determine rule status (majority/minority/split)
        rule_status = _determine_rule_status(group, rule_groups)

        # NEW: Temporal analysis
        temporal_note = None
        years_in_group = [g[4] for g in group if g[4]]
        if years_in_group and len(years_in_group) > 1:
            earliest = min(years_in_group)
            latest = max(years_in_group)
            if latest - earliest > 10:
                temporal_note = f"Rule evolved from {earliest} to {latest}; cite {latest} version"

        # Create primary node with enhanced fields (include pinpoint page)
        primary_node = SynthesizedRuleNode(
            rule_text=rule.text,
            sources=[(case_name, citation, rule.pinpoint_page)],
            court_level=court_level,
            year=year,
            relationship=RuleRelationship.IDENTICAL,
            elements=rule.elements,
            factors=rule.factors,
            is_binding=is_binding,
            policy_rationale=policy_rationale,
            has_ambiguity=has_ambiguity,
            alternative_formulations=alternative_formulations[:3],
            rule_status=rule_status,
            temporal_note=temporal_note
        )

        # Add related rules as children
        for secondary in group[1:]:
            sec_rule, sec_case, sec_cite, sec_court, sec_year, sec_jur, _ = secondary

            similarity = calculate_rule_similarity(rule.text, sec_rule.text)
            relationship = detect_rule_relationship(rule, sec_rule, similarity)
            sec_is_binding = _is_binding_authority(sec_court, sec_jur, your_jurisdiction)

            if relationship == RuleRelationship.IDENTICAL:
                # Same rule - just add source (with pinpoint)
                primary_node.sources.append((sec_case, sec_cite, sec_rule.pinpoint_page))
            else:
                # Different relationship - add as child (with pinpoint)
                child = SynthesizedRuleNode(
                    rule_text=sec_rule.text,
                    sources=[(sec_case, sec_cite, sec_rule.pinpoint_page)],
                    court_level=sec_court,
                    year=sec_year,
                    relationship=relationship,
                    elements=sec_rule.elements,
                    factors=sec_rule.factors,
                    is_binding=sec_is_binding
                )
                primary_node.children.append(child)

        hierarchy.append(primary_node)

    return hierarchy


def synthesize_rules_across_cases_with_meta(
    cases: Dict[str, Tuple[str, str]],
    issue: Optional[str] = None,
    your_jurisdiction: Optional[str] = None
) -> Dict[str, Any]:
    """
    Synthesize rules across cases with extraction metadata.

    This wrapper tracks which cases yielded rules, which failed,
    and provides confidence scores for the synthesis.

    Returns:
        Dict with "_meta" and "hierarchy" (List[SynthesizedRuleNode])
    """
    # Track per-case extraction success
    cases_with_rules = []
    cases_without_rules = []
    per_case_confidence = {}
    suggestions = []

    # Call the original function
    hierarchy = synthesize_rules_across_cases(cases, issue, your_jurisdiction)

    # Analyze each case's contribution
    for case_name, (text, citation) in cases.items():
        rules = extract_enhanced_rules(text, case_name, citation)
        if rules:
            cases_with_rules.append(case_name)
            # Higher confidence if holding found
            has_holding = any(
                r.rule_type.value == 'holding' if hasattr(r.rule_type, 'value') else False
                for r in rules
            )
            per_case_confidence[case_name] = 0.85 if has_holding else 0.65
        else:
            cases_without_rules.append(case_name)
            per_case_confidence[case_name] = 0.0
            suggestions.append(
                f"Case '{case_name}' yielded no extractable rules. "
                "Text may be too short, procedural, or lack explicit holdings."
            )

    # Calculate overall synthesis confidence
    if cases_with_rules:
        avg_confidence = sum(per_case_confidence[c] for c in cases_with_rules) / len(cases_with_rules)
        coverage = len(cases_with_rules) / len(cases) if cases else 0
        overall_confidence = avg_confidence * coverage
    else:
        overall_confidence = 0.0

    # Build targets
    targets_found = cases_with_rules.copy()
    if hierarchy:
        targets_found.append("synthesis")

    targets_missing = cases_without_rules.copy()
    if not hierarchy:
        targets_missing.append("synthesis")
        suggestions.append(
            "No rule hierarchy could be synthesized. Ensure cases contain explicit legal rules."
        )

    # Add synthesis quality suggestions
    if hierarchy:
        binding_count = sum(1 for node in hierarchy if node.is_binding)
        if binding_count == 0 and your_jurisdiction:
            suggestions.append(
                f"No binding authority for {your_jurisdiction}. "
                "Consider adding cases from your circuit or Supreme Court."
            )

    return {
        "_meta": {
            "extraction_complete": len(cases_without_rules) == 0 and len(hierarchy) > 0,
            "targets_found": targets_found,
            "targets_missing": targets_missing,
            "confidence_scores": {
                **per_case_confidence,
                "synthesis": round(overall_confidence, 2),
                "overall": round(overall_confidence, 2)
            },
            "suggestions": suggestions,
            "cases_processed": len(cases),
            "cases_with_rules": len(cases_with_rules),
            "rules_synthesized": len(hierarchy)
        },
        "hierarchy": hierarchy
    }


def _extract_jurisdiction_from_citation(citation: str) -> str:
    """Extract jurisdiction/circuit from citation."""
    # Federal circuits
    circuit_match = re.search(r'(\d+)(?:st|nd|rd|th)\s*Cir\.?', citation, re.IGNORECASE)
    if circuit_match:
        return f"{circuit_match.group(1)}th Cir."

    if re.search(r'D\.?C\.?\s*Cir\.?', citation, re.IGNORECASE):
        return "D.C. Cir."
    if re.search(r'Fed\.?\s*Cir\.?', citation, re.IGNORECASE):
        return "Fed. Cir."

    # Supreme Court
    if re.search(r'U\.?S\.?\s*\d+|S\.?\s*Ct\.?', citation, re.IGNORECASE):
        return "SCOTUS"

    # State courts
    state_pattern = r'\(([A-Z][a-z]+\.?)\s*(?:App\.?)?\s*\d{4}\)'
    state_match = re.search(state_pattern, citation)
    if state_match:
        return state_match.group(1)

    return "Unknown"


def _normalize_jurisdiction(jurisdiction: str) -> str:
    """
    Normalize jurisdiction string for comparison.

    Handles variations:
    - "5th Circuit", "5th Cir.", "Fifth Circuit" → "5cir"
    - "D.C. Circuit", "DC Cir." → "dccir"
    - "Federal Circuit", "Fed. Cir." → "fedcir"
    - "Supreme Court", "SCOTUS" → "scotus"
    """
    j = jurisdiction.lower().strip()

    # Word-form ordinals to numbers
    ordinal_words = {
        'first': '1', 'second': '2', 'third': '3', 'fourth': '4',
        'fifth': '5', 'sixth': '6', 'seventh': '7', 'eighth': '8',
        'ninth': '9', 'tenth': '10', 'eleventh': '11', 'twelfth': '12',
    }
    for word, num in ordinal_words.items():
        if word in j:
            return f"{num}cir"

    # Extract numeric circuit (handles "5th", "5th Cir.", etc.)
    circuit_num = re.search(r'(\d+)\s*(?:st|nd|rd|th)?', j)
    if circuit_num:
        return f"{circuit_num.group(1)}cir"

    # Handle D.C. Circuit variations
    if 'd.c.' in j or 'dc' in j or 'district of columbia' in j:
        return "dccir"

    # Handle Federal Circuit
    if 'fed' in j and 'cir' in j:
        return "fedcir"

    # Handle Supreme Court
    if 'scotus' in j or 'supreme' in j or 'u.s.' in j.replace(' ', ''):
        return "scotus"

    # Default: strip punctuation and spaces
    return j.replace('.', '').replace(' ', '').replace(',', '')


def _is_binding_authority(
    court_level: CourtLevel,
    case_jurisdiction: str,
    your_jurisdiction: Optional[str]
) -> bool:
    """Determine if authority is binding in your jurisdiction."""
    # Supreme Court binds everyone
    if court_level == CourtLevel.SUPREME_COURT:
        return True

    # If no jurisdiction specified, can't determine
    if not your_jurisdiction:
        return court_level.value >= 3  # Assume circuit+ is binding

    # Same circuit = binding (normalize for comparison)
    if _normalize_jurisdiction(case_jurisdiction) == _normalize_jurisdiction(your_jurisdiction):
        return True

    # Other circuit = persuasive only
    return False


def _extract_policy_rationale(case_text: str, rule_text: str) -> Optional[str]:
    """Extract the policy rationale behind a rule."""
    # Look for policy language near the rule
    rule_position = case_text.lower().find(rule_text.lower()[:50])
    if rule_position == -1:
        rule_position = 0

    # Search around the rule for policy language
    search_start = max(0, rule_position - 500)
    search_end = min(len(case_text), rule_position + 500)
    context = case_text[search_start:search_end]

    policy_patterns = [
        r'(?:this\s+rule|the\s+standard)\s+(?:serves?|promotes?|protects?|ensures?)\s+([^.]+\.)',
        r'(?:the\s+purpose|the\s+goal|the\s+rationale)\s+(?:of|behind|for)\s+(?:this\s+)?(?:rule|standard|test)\s+(?:is\s+)?([^.]+\.)',
        r'(?:designed|intended|meant)\s+to\s+([^.]+\.)',
        r'(?:to\s+ensure|to\s+protect|to\s+balance|to\s+prevent)\s+([^.]+\.)',
        r'(?:promotes?|furthers?|serves?)\s+(?:the\s+)?(?:interests?\s+(?:of|in)|goal\s+of)\s+([^.]+\.)',
    ]

    for pattern in policy_patterns:
        match = re.search(pattern, context, re.IGNORECASE)
        if match:
            return match.group(1).strip()[:150]

    return None


def _determine_rule_status(
    group: List[Tuple],
    all_groups: List[List[Tuple]]
) -> str:
    """
    Determine rule status based on authority support.

    Returns:
        - "established": Supreme Court has ruled
        - "majority": Multiple circuits agree (4+)
        - "split": Circuits disagree on the rule
        - "circuit rule": Only one circuit, no conflict detected
        - "developing": Insufficient data
    """
    # Count supporting jurisdictions
    jurisdictions = set(g[5] for g in group)  # Index 5 is jurisdiction

    # If Supreme Court supports, it's established law
    if any(g[3] == CourtLevel.SUPREME_COURT for g in group):
        return "established"

    # Count circuits supporting this rule
    circuits = set(j for j in jurisdictions if 'Cir' in j)

    # Check if other groups have conflicting rules from different circuits
    conflicting_circuits = set()
    for other_group in all_groups:
        if other_group != group:
            for g in other_group:
                if 'Cir' in str(g[5]):
                    conflicting_circuits.add(g[5])

    # Determine status based on circuit coverage
    if len(circuits) >= 4:
        return "majority"
    elif conflicting_circuits - circuits:
        # Different circuits have different rules
        return "split"
    elif len(circuits) >= 2:
        return "majority"
    elif len(circuits) == 1:
        # Single circuit - not minority, just single circuit rule
        return "circuit rule"

    return "developing"


def _format_citation_with_pinpoint(citation: str, pinpoint: Optional[str]) -> str:
    """Format citation with pinpoint page if available."""
    if not pinpoint:
        return citation
    # Check if pinpoint already in citation
    if f", {pinpoint}" in citation or f",{pinpoint}" in citation:
        return citation
    # Insert before parenthetical
    paren_match = re.search(r'\s*\([^)]+\)\s*$', citation)
    if paren_match:
        pre_paren = citation[:paren_match.start()]
        paren = paren_match.group()
        return f"{pre_paren}, {pinpoint}{paren}"
    return f"{citation}, {pinpoint}"


def format_rule_hierarchy(hierarchy: List[SynthesizedRuleNode],
                          target_jurisdiction: Optional[str] = None) -> str:
    """Format the rule hierarchy for output with enhanced metadata."""
    output = []
    output.append("SYNTHESIZED RULE HIERARCHY")
    output.append("=" * 70)

    if target_jurisdiction:
        output.append(f"Target Jurisdiction: {target_jurisdiction}")
        output.append("")

    for i, node in enumerate(hierarchy, 1):
        # Primary rule - format with pinpoints
        def format_source(src):
            if len(src) == 3:
                case, cite, pinpoint = src
                return f"{case}, {_format_citation_with_pinpoint(cite, pinpoint)}"
            else:
                case, cite = src
                return f"{case}, {cite}"

        sources_str = "; ".join(format_source(src) for src in node.sources)
        court_str = node.court_level.name.replace("_", " ").title()

        output.append(f"\n## PRIMARY RULE {i} ({court_str})")
        output.append("-" * 70)
        output.append(f'"{node.rule_text}"')
        output.append(f"Sources: {sources_str}")

        # Authority status
        binding_str = "BINDING" if node.is_binding else "PERSUASIVE"
        status_str = node.rule_status.upper() if node.rule_status else "MAJORITY"
        output.append(f"Authority: {binding_str} | Status: {status_str}")

        # Temporal note if present
        if node.temporal_note:
            output.append(f"Temporal Note: {node.temporal_note}")

        # Ambiguity flag
        if node.has_ambiguity:
            output.append("⚠ AMBIGUITY DETECTED - Rule formulation varies across authorities")

        # Policy rationale if extracted
        if node.policy_rationale:
            output.append(f"\nPolicy Rationale: {node.policy_rationale}")

        # Alternative formulations if any
        if node.alternative_formulations:
            output.append("\nAlternative Formulations:")
            for j, alt in enumerate(node.alternative_formulations, 1):
                output.append(f"  ALT {j}: \"{alt[:150]}...\"" if len(alt) > 150 else f"  ALT {j}: \"{alt}\"")

        if node.elements:
            output.append("\nElements:")
            for j, elem in enumerate(node.elements, 1):
                output.append(f"  {j}. {elem}")

        if node.factors:
            output.append("\nFactors:")
            for j, factor in enumerate(node.factors, 1):
                output.append(f"  {j}. {factor}")

        # Child rules (elaborations, exceptions, etc.)
        for child in node.children:
            rel_str = child.relationship.value.upper()
            child_sources = "; ".join(format_source(src) for src in child.sources)
            child_binding = "BINDING" if child.is_binding else "PERSUASIVE"

            output.append(f"\n  -> {rel_str} ({child_binding}):")
            output.append(f'     "{child.rule_text[:200]}..."' if len(child.rule_text) > 200 else f'     "{child.rule_text}"')
            output.append(f"     Source: {child_sources}")

            if child.policy_rationale:
                output.append(f"     Policy: {child.policy_rationale[:100]}..." if len(child.policy_rationale) > 100 else f"     Policy: {child.policy_rationale}")

            if child.has_ambiguity:
                output.append("     ⚠ Ambiguity in this rule")

    # Summary statistics
    output.append("\n" + "=" * 70)
    output.append("SYNTHESIS SUMMARY")
    output.append("-" * 70)

    binding_count = sum(1 for n in hierarchy if n.is_binding)
    persuasive_count = len(hierarchy) - binding_count
    ambiguity_count = sum(1 for n in hierarchy if n.has_ambiguity)

    status_counts = {}
    for n in hierarchy:
        status = n.rule_status or "majority"
        status_counts[status] = status_counts.get(status, 0) + 1

    output.append(f"Total Rules: {len(hierarchy)} ({binding_count} binding, {persuasive_count} persuasive)")
    output.append(f"Rule Status: {', '.join(f'{v} {k}' for k, v in status_counts.items())}")
    if ambiguity_count > 0:
        output.append(f"⚠ {ambiguity_count} rule(s) have ambiguous formulations - review alternative formulations")

    return "\n".join(output)


# ============================================================================
# CIRCUIT SPLIT DETECTION
# ============================================================================

def detect_circuit_splits(
    cases: Dict[str, Tuple[str, str]],
    issue: str,
    your_jurisdiction: Optional[str] = None
) -> CircuitSplitAnalysis:
    """
    Detect circuit splits on an issue.

    Args:
        cases: Dictionary of case_name -> (text, citation)
        issue: The legal issue being analyzed
        your_jurisdiction: Your target circuit (e.g., "5th Circuit")

    Returns:
        CircuitSplitAnalysis with positions by circuit
    """
    # Extract rules and group by circuit
    circuit_rules: Dict[str, List[Tuple[str, str, str]]] = defaultdict(list)
    supreme_court_rules: List[Tuple[str, str, str]] = []

    for case_name, (text, citation) in cases.items():
        court_level, circuit, year = detect_court_level(citation)
        rules = extract_enhanced_rules(text, case_name, citation)

        for rule in rules:
            if court_level == CourtLevel.SUPREME_COURT:
                supreme_court_rules.append((rule.text, case_name, citation))
            elif court_level == CourtLevel.CIRCUIT_COURT and circuit:
                circuit_rules[circuit].append((rule.text, case_name, citation))

    # Build positions
    positions = []

    for circuit, rules in circuit_rules.items():
        if rules:
            # Use the first (usually most authoritative) rule as the position
            primary_rule = rules[0][0]
            cases_list = [(r[1], r[2]) for r in rules]

            position = CircuitPosition(
                circuit=circuit,
                rule_text=primary_rule,
                cases=cases_list,
                is_binding=(circuit == your_jurisdiction)
            )
            positions.append(position)

    # Determine if there's actually a split
    if len(positions) >= 2:
        # Check if rules differ significantly
        rules_text = [p.rule_text for p in positions]
        has_split = False

        for i, rule_a in enumerate(rules_text):
            for rule_b in rules_text[i+1:]:
                if calculate_rule_similarity(rule_a, rule_b) < 0.5:
                    has_split = True
                    break
    else:
        has_split = False

    # Supreme Court status
    if supreme_court_rules:
        sc_status = "resolved"
    else:
        sc_status = "unresolved" if has_split else "no_split"

    # Find your jurisdiction's position
    your_position = None
    if your_jurisdiction:
        for pos in positions:
            if pos.circuit == your_jurisdiction:
                your_position = pos
                break

    # Determine majority position
    majority = None
    if positions:
        # Simple heuristic: most cases
        majority = max(positions, key=lambda p: len(p.cases))

    return CircuitSplitAnalysis(
        issue=issue,
        positions=positions,
        supreme_court_status=sc_status,
        majority_position=majority.circuit if majority else None,
        your_jurisdiction=your_position
    )


def format_circuit_split(analysis: CircuitSplitAnalysis) -> str:
    """Format circuit split analysis for output."""
    output = []
    output.append("CIRCUIT SPLIT ANALYSIS")
    output.append("=" * 70)
    output.append(f"Issue: {analysis.issue}")
    output.append(f"Supreme Court: {analysis.supreme_court_status.upper()}")
    output.append("")

    for pos in analysis.positions:
        binding_str = " [BINDING]" if pos.is_binding else ""
        output.append(f"## {pos.circuit}{binding_str}")
        output.append(f'Rule: "{pos.rule_text[:150]}..."' if len(pos.rule_text) > 150 else f'Rule: "{pos.rule_text}"')
        output.append(f"Cases: {', '.join(c[0] for c in pos.cases)}")
        output.append("")

    if analysis.majority_position:
        output.append(f"Majority Position: {analysis.majority_position}")

    if analysis.your_jurisdiction:
        output.append(f"\nYour Jurisdiction ({analysis.your_jurisdiction.circuit}):")
        output.append(f'  "{analysis.your_jurisdiction.rule_text[:100]}..."')

    return "\n".join(output)


# ============================================================================
# ANALOGY / DISTINCTION GENERATOR
# ============================================================================

@dataclass
class StructuredCaseFacts:
    """Structured extraction of case facts for comparison."""
    operative_facts: List[str]  # What happened
    procedural_posture: str  # How case came to court
    outcome: str  # How court ruled
    determinative_facts: List[str]  # Facts court found decisive
    insufficient_facts: List[str]  # Facts court found insufficient
    disputed_facts: List[str]  # Contested factual issues
    undisputed_facts: List[str]  # Agreed facts


def extract_structured_case_facts(text: str) -> StructuredCaseFacts:
    """
    Extract structured facts from a case for analogy/distinction purposes.

    Returns facts organized by legal significance:
    - Operative facts: What actually happened
    - Procedural posture: How case reached this court
    - Outcome: How court ruled
    - Determinative facts: Facts the court found decisive
    - Insufficient facts: Facts court found did not support claim

    Args:
        text: Case opinion text

    Returns:
        StructuredCaseFacts object
    """
    text_lower = text.lower()

    # Extract operative facts (what happened)
    operative_patterns = [
        # Standard party references
        r'(?:the\s+)?(?:plaintiff|defendant|victim|suspect|officer|company)\s+(?:allegedly\s+)?([^.]+\.)',
        # SCOTUS/appellate party references
        r'(?:the\s+)?(?:petitioner|respondent|appellant|appellee)\s+(?:allegedly\s+)?([^.]+\.)',
        # Date-based facts
        r'(?:on|during|at)\s+(?:\w+\s+)?\d{1,2},?\s+\d{4},?\s+([^.]+\.)',
        # Event descriptions
        r'(?:the\s+)?(?:incident|accident|event|transaction)\s+(?:occurred|took place|happened)\s+([^.]+\.)',
        # Evidence-based facts
        r'(?:evidence|testimony|record)\s+(?:shows?|reflects?|establishes?|indicates?)\s+(?:that\s+)?([^.]+\.)',
        # SCOTUS-style background facts
        r'(?:at\s+(?:his|her|their|the)\s+trial),?\s+([^.]+\.)',
        r'(?:prior\s+to\s+trial),?\s+([^.]+\.)',
        r'(?:were\s+found\s+guilty|was\s+convicted|pleaded\s+guilty)\s+([^.]+\.)',
    ]
    operative_facts = []
    for pattern in operative_patterns:
        for match in re.finditer(pattern, text, re.IGNORECASE):
            fact = match.group(1).strip() if match.lastindex else match.group(0).strip()
            if len(fact) > 30 and len(fact) < 300:
                operative_facts.append(fact)

    # Extract procedural posture
    procedural_posture = "Unknown"
    procedural_patterns = [
        # Standard appeal
        (r'(?:appeal|appeals?)\s+from\s+([^.]+\.)', 'Appeal from'),
        # SCOTUS certiorari
        (r'(?:certiorari|cert\.?)\s+(?:to|was\s+granted)\s+([^.]+\.)', 'Certiorari to'),
        (r'(?:we|the court)\s+granted\s+(?:certiorari|cert\.?|review)\s+([^.]+\.)', 'Certiorari granted'),
        # SCOTUS/appellate party action
        (r'(?:petitioner|appellant)\s+(?:seeks?|sought|challenges?|appeals?)\s+([^.]+\.)', 'Petitioner challenges'),
        # Trial court found guilty/convicted
        (r'(?:were\s+found\s+guilty|was\s+convicted|pleaded\s+guilty)\s+(?:of\s+)?([^.]+\.)', 'Criminal conviction'),
        (r'(?:were\s+sentenced|was\s+sentenced)\s+to\s+([^.]+\.)', 'Post-sentencing'),
        # Motion practice
        (r'(?:plaintiff|defendant)\s+(?:moves?|moved)\s+for\s+([^.]+\.)', 'Motion for'),
        (r'(?:this\s+)?(?:case|matter|action)\s+(?:comes?\s+before|is\s+before)\s+([^.]+\.)', 'Before court on'),
        (r'(?:grant|deny|review)(?:ing|ed)?\s+(?:summary\s+judgment|motion\s+to\s+dismiss|preliminary\s+injunction)', 'Summary judgment/motion stage'),
        (r'(?:at\s+)?trial,?\s+(?:the\s+)?(?:jury|court)\s+([^.]+\.)', 'Post-trial'),
    ]
    for pattern, posture_type in procedural_patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            procedural_posture = f"{posture_type}: {match.group(1).strip()}" if match.lastindex else posture_type
            break

    # Extract outcome
    outcome = "Unknown"
    outcome_patterns = [
        # SCOTUS short outcome (at end of opinion)
        r'(?:affirmed|reversed|vacated|remanded)(?:\s+in\s+part)?(?:\s+and\s+(?:affirmed|reversed|vacated|remanded)(?:\s+in\s+part)?)?\.?\s*$',
        # Standard appellate outcome
        r'(?:we|the court)\s+(?:affirm|reverse|vacate|remand|grant|deny)(?:s|ed)?\s+([^.]+\.)',
        r'(?:judgment|decision|order)\s+(?:is\s+)?(?:affirmed|reversed|vacated|remanded)',
        r'(?:motion|appeal)\s+(?:is\s+)?(?:granted|denied|dismissed)',
        r'(?:plaintiff|defendant)\s+(?:prevails?|loses?|recovers?|is\s+(?:entitled|liable))',
        # SCOTUS style
        r'(?:it\s+is\s+so\s+ordered)',
        r'(?:the\s+judgment\s+(?:of\s+the\s+)?(?:court\s+of\s+appeals?|circuit\s+court|district\s+court)\s+is\s+(?:affirmed|reversed|vacated))',
    ]
    for pattern in outcome_patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            outcome = match.group(0).strip()
            break

    # Extract determinative facts (facts court found decisive)
    determinative_patterns = [
        r'(?:critical|crucial|determinative|decisive|key|dispositive)\s+(?:fact|factor|circumstance)(?:s)?\s+(?:is|are|was|were|include[sd]?)\s+([^.]+\.)',
        r'(?:because|since|given\s+that)\s+([^,]+),\s+(?:we|the court)\s+(?:hold|find|conclude)',
        r'(?:the fact that|it is significant that|importantly,?)\s+([^.]+\.)',
        r'(?:we|the court)\s+(?:find|found|note|noted)\s+(?:it\s+)?(?:significant|important|determinative)\s+that\s+([^.]+\.)',
        r'(?:this|these)\s+fact(?:s)?\s+(?:is|are)\s+(?:sufficient|enough)\s+to\s+([^.]+\.)',
    ]
    determinative_facts = []
    for pattern in determinative_patterns:
        for match in re.finditer(pattern, text, re.IGNORECASE):
            fact = match.group(1).strip() if match.lastindex else match.group(0).strip()
            if len(fact) > 20 and len(fact) < 300:
                determinative_facts.append(fact)

    # Extract insufficient facts (facts court found inadequate)
    insufficient_patterns = [
        r'(?:insufficient|inadequate|not\s+enough)\s+(?:to\s+)?(?:show|establish|prove|demonstrate)\s+([^.]+\.)',
        r'(?:mere|standing\s+alone|by\s+itself),?\s+(?:does\s+not|is\s+not\s+sufficient)\s+([^.]+\.)',
        r'(?:plaintiff|defendant)\s+(?:failed|fails)\s+to\s+(?:show|establish|prove|demonstrate)\s+([^.]+\.)',
        r'(?:the\s+)?(?:evidence|record|allegation)(?:s)?\s+(?:do(?:es)?\s+not|fail(?:s)?\s+to)\s+(?:support|establish|show)\s+([^.]+\.)',
        r'(?:we|the court)\s+(?:reject|rejected|find\s+unpersuasive)\s+([^.]+\.)',
    ]
    insufficient_facts = []
    for pattern in insufficient_patterns:
        for match in re.finditer(pattern, text, re.IGNORECASE):
            fact = match.group(1).strip() if match.lastindex else match.group(0).strip()
            if len(fact) > 20 and len(fact) < 300:
                insufficient_facts.append(fact)

    # Extract disputed vs undisputed facts
    disputed_facts = []
    undisputed_facts = []

    disputed_patterns = [
        r'(?:parties\s+)?disput(?:e|ed)\s+(?:whether|that|the\s+fact)\s+([^.]+\.)',
        r'(?:contested|in\s+dispute|at\s+issue)\s+(?:is|are|whether)\s+([^.]+\.)',
    ]
    for pattern in disputed_patterns:
        for match in re.finditer(pattern, text, re.IGNORECASE):
            disputed_facts.append(match.group(1).strip() if match.lastindex else match.group(0).strip())

    undisputed_patterns = [
        r'(?:undisputed|uncontested|stipulated|agreed)\s+(?:that|fact(?:s)?)\s+([^.]+\.)',
        r'(?:parties\s+)?(?:agree|concede|admit)\s+(?:that)?\s+([^.]+\.)',
        r'(?:it\s+is\s+)?(?:undisputed|clear|established)\s+that\s+([^.]+\.)',
    ]
    for pattern in undisputed_patterns:
        for match in re.finditer(pattern, text, re.IGNORECASE):
            undisputed_facts.append(match.group(1).strip() if match.lastindex else match.group(0).strip())

    # Fallback: if no operative facts found, use generic extraction
    if len(operative_facts) < 2:
        operative_facts.extend(extract_case_facts(text)[:5])

    return StructuredCaseFacts(
        operative_facts=list(set(operative_facts))[:10],
        procedural_posture=procedural_posture,
        outcome=outcome,
        determinative_facts=list(set(determinative_facts))[:5],
        insufficient_facts=list(set(insufficient_facts))[:5],
        disputed_facts=list(set(disputed_facts))[:5],
        undisputed_facts=list(set(undisputed_facts))[:5]
    )


def format_structured_facts(facts: StructuredCaseFacts, case_name: str = "") -> str:
    """Format structured facts for output."""
    output = []
    if case_name:
        output.append(f"STRUCTURED FACTS: {case_name}")
    else:
        output.append("STRUCTURED CASE FACTS")
    output.append("=" * 70)

    output.append(f"\nProcedural Posture: {facts.procedural_posture}")
    output.append(f"Outcome: {facts.outcome}")

    if facts.operative_facts:
        output.append("\n## OPERATIVE FACTS (What Happened)")
        for i, fact in enumerate(facts.operative_facts, 1):
            output.append(f"  {i}. {fact}")

    if facts.determinative_facts:
        output.append("\n## DETERMINATIVE FACTS (Court Found Decisive)")
        output.append("  [Use these for ANALOGIES if your facts are similar]")
        for i, fact in enumerate(facts.determinative_facts, 1):
            output.append(f"  {i}. {fact}")

    if facts.insufficient_facts:
        output.append("\n## INSUFFICIENT FACTS (Court Found Inadequate)")
        output.append("  [Use these for DISTINCTIONS if opponent's facts are similar]")
        for i, fact in enumerate(facts.insufficient_facts, 1):
            output.append(f"  {i}. {fact}")

    if facts.undisputed_facts:
        output.append("\n## UNDISPUTED FACTS")
        for i, fact in enumerate(facts.undisputed_facts, 1):
            output.append(f"  {i}. {fact}")

    if facts.disputed_facts:
        output.append("\n## DISPUTED FACTS")
        for i, fact in enumerate(facts.disputed_facts, 1):
            output.append(f"  {i}. {fact}")

    return "\n".join(output)


def extract_case_facts(text: str) -> List[str]:
    """Extract key facts from case text."""
    facts = []

    # Look for fact indicators - expanded patterns
    fact_patterns = [
        r'(?:the|this)\s+(?:plaintiff|defendant|company|corporation|officer|victim|suspect|decedent|appellant|appellee)\s+([^.]+\.)',
        r'(?:alleged|claimed|argued|contended|asserted)\s+that\s+([^.]+\.)',
        r'(?:here|in this case),?\s+([^.]+\.)',
        r'the\s+(?:complaint|pleading)\s+(?:alleged|stated)\s+([^.]+\.)',
        # Action facts - expanded
        r'(?:The\s+)?(?:officer|officers|deputy|deputies|police|plaintiff|defendant)\s+(?:shot|fired|arrested|seized|used|deployed|struck|hit|tased|tackled|grabbed|chased|pursued|stopped|detained)\s+([^.]+\.)',
        # State of being facts - expanded
        r'(?:The\s+)?(?:suspect|plaintiff|victim|decedent|individual|person)\s+(?:was|were|had been)\s+([^.]+\.)',
        # "Court held/found" facts
        r'(?:court|judge|jury|district court|circuit)\s+(?:found|determined|concluded|held)\s+that\s+([^.]+\.)',
        # Record/evidence facts
        r'(?:the\s+)?(?:record|evidence|testimony|deposition|video|bodycam)\s+(?:shows?|reflects?|demonstrates?|establishes?|indicates?)\s+([^.]+\.)',
        # Temporal facts
        r'(?:On|At|During|After|Before|When)\s+[A-Z][^.]+(?:the\s+)?(?:officer|plaintiff|defendant|suspect)\s+([^.]+\.)',
    ]

    for pattern in fact_patterns:
        matches = re.findall(pattern, text, re.IGNORECASE)
        for match in matches:
            if isinstance(match, str) and len(match) > 20:
                facts.append(match)

    # Also extract sentences from FACTS sections
    facts_section = re.search(r'(?:FACTS?|BACKGROUND|STATEMENT\s+OF\s+(?:THE\s+)?FACTS?)[:\s]+(.+?)(?:PROCEDURAL|ISSUE|HOLDING|ANALYSIS|ARGUMENT|DISCUSSION)',
                               text, re.IGNORECASE | re.DOTALL)
    if facts_section:
        sentences = re.split(r'(?<=[.!?])\s+', facts_section.group(1))
        facts.extend(s.strip() for s in sentences if len(s.strip()) > 30)

    # Enhanced fallback: extract sentences with any factual indicators
    if len(facts) < 3:
        sentences = re.split(r'(?<=[.!?])\s+', text)
        factual_indicators = [
            'was', 'were', 'had', 'did', 'shot', 'arrested', 'used', 'deployed',
            'tased', 'struck', 'seized', 'detained', 'stopped', 'fired', 'tackled',
            'injured', 'killed', 'died', 'complained', 'alleged', 'testified',
            'occurred', 'happened', 'began', 'ended', 'arrived', 'responded'
        ]
        for sent in sentences:
            sent_lower = sent.lower()
            if any(ind in sent_lower for ind in factual_indicators) and len(sent) > 30:
                # Avoid pure legal conclusions
                if not any(legal in sent_lower for legal in ['held that', 'concluded that', 'we hold', 'we conclude', 'therefore']):
                    facts.append(sent.strip())

    # Deduplicate while preserving order
    seen = set()
    unique_facts = []
    for f in facts:
        f_normalized = f.lower().strip()
        if f_normalized not in seen:
            seen.add(f_normalized)
            unique_facts.append(f)

    return unique_facts[:10]  # Limit to 10 key facts


def generate_analogies_and_distinctions(
    precedent_case: str,
    precedent_text: str,
    precedent_citation: str,
    client_facts: str,
    rules: List[str]
) -> List[AnalogyDistinction]:
    """
    Generate draft analogy and distinction language.

    Args:
        precedent_case: Name of precedent case
        precedent_text: Full text of precedent
        precedent_citation: Citation for precedent
        client_facts: Your client's facts
        rules: Rules being applied

    Returns:
        List of analogy/distinction drafts
    """
    results = []

    # Extract precedent facts
    precedent_facts = extract_case_facts(precedent_text)

    # Parse client facts into sentences
    client_sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', client_facts)
                        if len(s.strip()) > 20]

    for p_fact in precedent_facts[:5]:
        for c_fact in client_sentences[:5]:
            # Check similarity
            similarity = calculate_rule_similarity(p_fact, c_fact)

            if similarity > 0.3:
                # Similar = analogy
                draft = f"Like {precedent_case}, where {p_fact.lower().rstrip('.')}, here {c_fact.lower().rstrip('.')}."
                results.append(AnalogyDistinction(
                    type="analogy",
                    precedent_case=precedent_case,
                    precedent_fact=p_fact,
                    client_fact=c_fact,
                    draft_language=draft,
                    rule_applied=rules[0] if rules else ""
                ))
            elif similarity < 0.2 and extract_key_legal_concepts(p_fact) & extract_key_legal_concepts(c_fact):
                # Different facts but same concepts = distinction
                draft = f"Unlike {precedent_case}, where {p_fact.lower().rstrip('.')}, here {c_fact.lower().rstrip('.')}."
                results.append(AnalogyDistinction(
                    type="distinction",
                    precedent_case=precedent_case,
                    precedent_fact=p_fact,
                    client_fact=c_fact,
                    draft_language=draft,
                    rule_applied=rules[0] if rules else ""
                ))

    return results[:6]  # Limit output


def format_analogies_distinctions(items: List[AnalogyDistinction]) -> str:
    """Format analogies and distinctions for output."""
    output = []
    output.append("ANALOGY/DISTINCTION DRAFTS")
    output.append("=" * 70)

    analogies = [i for i in items if i.type == "analogy"]
    distinctions = [i for i in items if i.type == "distinction"]

    if analogies:
        output.append("\n## ANALOGIES (use these to show similarity)")
        for a in analogies:
            output.append(f"\n  Case: {a.precedent_case}")
            output.append(f"  Draft: {a.draft_language}")

    if distinctions:
        output.append("\n## DISTINCTIONS (use these to distinguish)")
        for d in distinctions:
            output.append(f"\n  Case: {d.precedent_case}")
            output.append(f"  Draft: {d.draft_language}")

    return "\n".join(output)


# ============================================================================
# CITATION STRING BUILDER
# ============================================================================

def build_citation_string(
    cases: List[Tuple[str, str, str]],  # [(case_name, citation, pinpoint), ...]
    signal: str = "See",
    proposition: Optional[str] = None
) -> str:
    """
    Build a Bluebook-compliant citation string.

    Args:
        cases: List of (case_name, citation, pinpoint_page)
        signal: Citation signal ("See", "See, e.g.,", "Cf.", "But see", etc.)
        proposition: Optional proposition the citations support

    Returns:
        Formatted citation string
    """
    if not cases:
        return ""

    # Format individual citations
    formatted = []
    for i, (name, cite, pinpoint) in enumerate(cases):
        # Italicize case name
        case_cite = f"*{name}*, {cite}"
        if pinpoint:
            case_cite += f", at {pinpoint}"
        formatted.append(case_cite)

    # Join with semicolons
    if len(formatted) == 1:
        cite_string = formatted[0]
    elif len(formatted) == 2:
        cite_string = f"{formatted[0]}; {formatted[1]}"
    else:
        # For 3+, consider using "accord" for later citations
        cite_string = f"{formatted[0]}; {formatted[1]}; *accord* {'; '.join(formatted[2:])}"

    # Add signal
    if signal:
        cite_string = f"*{signal}* {cite_string}"

    # Add period
    cite_string = cite_string.rstrip('.') + "."

    if proposition:
        return f"{proposition} {cite_string}"

    return cite_string


def build_citation_strings_for_rules(
    hierarchy: List[SynthesizedRuleNode]
) -> List[Tuple[str, str]]:
    """
    Build citation strings for each rule in the hierarchy.

    Returns:
        List of (rule_text, citation_string) tuples
    """
    results = []

    for node in hierarchy:
        # Build citation from sources (handle both 2 and 3 element tuples)
        cases = []
        for src in node.sources:
            if len(src) == 3:
                cases.append((src[0], src[1], src[2] or ""))
            else:
                cases.append((src[0], src[1], ""))
        cite_string = build_citation_string(cases, signal="See")
        results.append((node.rule_text, cite_string))

        # Also do children
        for child in node.children:
            child_cases = []
            for src in child.sources:
                if len(src) == 3:
                    child_cases.append((src[0], src[1], src[2] or ""))
                else:
                    child_cases.append((src[0], src[1], ""))
            # Correct signal based on relationship type
            # "But see" is ONLY for contrary/conflicting authority
            signal_map = {
                RuleRelationship.ELABORATION: "See also",
                RuleRelationship.IDENTICAL: "Accord",
                RuleRelationship.CONFLICT: "But see",  # Only conflicts get "But see"
                RuleRelationship.EXCEPTION: "See",     # Exceptions still support the rule framework
                RuleRelationship.FACTOR: "See",
                RuleRelationship.APPLICATION: "See",
            }
            child_signal = signal_map.get(child.relationship, "See")
            child_cite = build_citation_string(child_cases, signal=child_signal)
            results.append((child.rule_text, child_cite))

    return results


# ============================================================================
# FACT-TO-RULE MAPPER
# ============================================================================

def map_facts_to_rules(
    client_facts: str,
    rules: List[SynthesizedRuleNode]
) -> List[FactRuleMapping]:
    """
    Map client facts to applicable rules.

    Args:
        client_facts: Client's factual situation
        rules: Synthesized rule hierarchy

    Returns:
        List of fact-to-rule mappings
    """
    mappings = []

    # Parse facts into sentences
    fact_sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', client_facts)
                      if len(s.strip()) > 20]

    for fact in fact_sentences:
        fact_concepts = extract_key_legal_concepts(fact)
        triggered = []

        for rule in rules:
            rule_concepts = extract_key_legal_concepts(rule.rule_text)
            overlap = fact_concepts & rule_concepts

            if overlap:
                # Check elements too
                for elem in rule.elements:
                    elem_concepts = extract_key_legal_concepts(elem)
                    if elem_concepts & fact_concepts:
                        source = rule.sources[0] if rule.sources else ("Unknown", "")
                        why = f"Triggers element: {elem[:50]}..."
                        triggered.append((rule.rule_text[:100], source[0], why))
                        break
                else:
                    source = rule.sources[0] if rule.sources else ("Unknown", "")
                    why = f"Implicates concepts: {', '.join(overlap)}"
                    triggered.append((rule.rule_text[:100], source[0], why))

        if triggered:
            # Determine strength based on number of matches
            strength = "strong" if len(triggered) >= 2 else "moderate" if triggered else "weak"

            mappings.append(FactRuleMapping(
                client_fact=fact,
                triggered_rules=triggered[:3],  # Limit
                strength=strength
            ))

    return mappings


def format_fact_rule_mapping(mappings: List[FactRuleMapping]) -> str:
    """Format fact-to-rule mappings for output."""
    output = []
    output.append("FACT-TO-RULE MAPPING")
    output.append("=" * 70)

    for i, mapping in enumerate(mappings, 1):
        output.append(f"\n## FACT {i} [{mapping.strength.upper()}]")
        output.append(f'"{mapping.client_fact}"')
        output.append("\nTriggered Rules:")

        for rule, case, why in mapping.triggered_rules:
            output.append(f"  - {rule}...")
            output.append(f"    From: {case}")
            output.append(f"    Why: {why}")

    return "\n".join(output)


# ============================================================================
# MCP ENTRY POINTS
# ============================================================================

def synthesize_brief_rules(
    cases: Dict[str, Tuple[str, str]],
    issue: Optional[str] = None,
    jurisdiction: Optional[str] = None
) -> str:
    """
    Main entry point for cross-case rule synthesis.

    Args:
        cases: Dict of case_name -> (full_text, citation)
        issue: The legal issue
        jurisdiction: Target jurisdiction

    Returns:
        Formatted rule hierarchy
    """
    hierarchy = synthesize_rules_across_cases(cases, issue)

    if not hierarchy:
        return "No rules could be synthesized from the provided cases. Try providing more case text or check that the cases contain substantive legal rules."

    output = []
    output.append(format_rule_hierarchy(hierarchy, jurisdiction))

    # Add citation strings
    cite_strings = build_citation_strings_for_rules(hierarchy)
    if cite_strings:
        output.append("\n\n" + "=" * 70)
        output.append("CITATION STRINGS (ready to paste)")
        output.append("=" * 70)
        for rule, cite in cite_strings[:5]:
            output.append(f'\nFor: "{rule[:80]}..."')
            output.append(f"Cite: {cite}")

    return "\n".join(output)


def analyze_circuit_split(
    cases: Dict[str, Tuple[str, str]],
    issue: str,
    your_jurisdiction: str
) -> str:
    """
    Main entry point for circuit split analysis.
    """
    analysis = detect_circuit_splits(cases, issue, your_jurisdiction)
    return format_circuit_split(analysis)


def generate_application_drafts(
    precedent_cases: Dict[str, Tuple[str, str]],
    client_facts: str,
    issue: str,
    elements: Optional[List[str]] = None,
    record_cites: Optional[Dict[str, str]] = None
) -> str:
    """
    Generate analogy/distinction drafts for APPLICATION section.

    Enhanced features:
    - Weights facts by legal significance (determinative vs. supporting)
    - Generates "even if" alternative arguments
    - Includes record citations in draft language
    - Matches analogies to specific legal elements

    Args:
        precedent_cases: Dict of case_name -> (text, citation)
        client_facts: Your client's facts
        issue: Legal issue being addressed
        elements: Optional list of legal elements to map facts to
        record_cites: Optional dict mapping facts to record citations

    Returns:
        Formatted application section drafts
    """
    output = []

    # Get rules first
    hierarchy = synthesize_rules_across_cases(precedent_cases, issue)
    rules = [node.rule_text for node in hierarchy]

    # Weight client facts by significance
    weighted_facts = _weight_facts_by_significance(client_facts, elements or [])

    # Generate for each precedent
    all_items = []
    for case_name, (text, citation) in precedent_cases.items():
        items = generate_analogies_and_distinctions_enhanced(
            case_name, text, citation, weighted_facts, rules,
            elements=elements, record_cites=record_cites
        )
        all_items.extend(items)

    if all_items:
        output.append(format_analogies_distinctions_enhanced(all_items))
    else:
        output.append("Could not generate analogies/distinctions. Ensure precedent cases contain factual details.")

    # Generate even-if arguments
    even_ifs = generate_even_if_arguments(precedent_cases, client_facts, issue)
    if even_ifs:
        output.append("\n\n" + format_even_if_arguments(even_ifs))

    # Also do fact-to-rule mapping
    if hierarchy:
        mappings = map_facts_to_rules(client_facts, hierarchy)
        if mappings:
            output.append("\n\n" + format_fact_rule_mapping(mappings))

    return "\n".join(output)


def generate_application_drafts_with_meta(
    precedent_cases: Dict[str, Tuple[str, str]],
    client_facts: str,
    issue: str,
    elements: Optional[List[str]] = None,
    record_cites: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
    """
    Generate application drafts with extraction metadata.

    This wrapper tracks what was generated (analogies, distinctions,
    even-if arguments) and provides confidence/suggestions.

    Returns:
        Dict with "_meta" and "drafts" (formatted string)
    """
    targets_found = []
    targets_missing = []
    suggestions = []
    confidence_scores = {}

    # Get the draft output
    drafts = generate_application_drafts(
        precedent_cases, client_facts, issue, elements, record_cites
    )

    # Analyze what was generated
    has_analogies = "Like " in drafts or "like " in drafts
    has_distinctions = "Unlike " in drafts or "unlike " in drafts
    has_even_if = "Even if" in drafts or "even if" in drafts
    has_failed = "Could not generate" in drafts

    if has_analogies:
        targets_found.append("analogies")
        confidence_scores["analogies"] = 0.75
    else:
        targets_missing.append("analogies")
        suggestions.append(
            "No analogies generated. Precedent cases may lack factual details "
            "that overlap with your client's facts."
        )

    if has_distinctions:
        targets_found.append("distinctions")
        confidence_scores["distinctions"] = 0.70
    else:
        targets_missing.append("distinctions")

    if has_even_if:
        targets_found.append("even_if_arguments")
        confidence_scores["even_if_arguments"] = 0.65

    if has_failed:
        confidence_scores["overall"] = 0.20
        suggestions.append(
            "Automatic generation failed. Ensure precedent cases contain "
            "factual details (not just holdings) and your client facts are specific."
        )

        # Add detailed suggestions
        for case_name, (text, citation) in precedent_cases.items():
            word_count = len(text.split())
            if word_count < 500:
                suggestions.append(
                    f"Case '{case_name}' has only {word_count} words. "
                    "Consider providing more text for better fact extraction."
                )
    else:
        # Calculate overall confidence
        if confidence_scores:
            confidence_scores["overall"] = round(
                sum(confidence_scores.values()) / len(confidence_scores), 2
            )
        else:
            confidence_scores["overall"] = 0.50

    return {
        "_meta": {
            "extraction_complete": not has_failed and len(targets_found) > 0,
            "targets_found": targets_found,
            "targets_missing": targets_missing,
            "confidence_scores": confidence_scores,
            "suggestions": suggestions,
            "precedent_cases_used": len(precedent_cases),
            "client_facts_length": len(client_facts)
        },
        "drafts": drafts
    }


def _weight_facts_by_significance(
    client_facts: str,
    elements: List[str]
) -> List[Tuple[str, str, int]]:
    """
    Weight client facts by legal significance.

    Returns list of (fact, significance, weight) tuples.
    Weight: 3=determinative, 2=supporting, 1=background
    """
    sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', client_facts)
                 if len(s.strip()) > 20]

    weighted = []
    for sent in sentences:
        sent_lower = sent.lower()

        # Determinative indicators - expanded for common legal contexts
        determinative_words = [
            # Intent/knowledge
            'knew', 'intended', 'caused', 'directly', 'admitted',
            'testified', 'proved', 'established', 'demonstrates',
            # Force/injury outcomes
            'died', 'killed', 'death', 'shot', 'tased', 'taser',
            'cardiac arrest', 'injured', 'broken', 'wounded',
            # Compliance/resistance
            'not resisting', 'complied', 'unarmed', 'handcuffed',
            'already', 'while', 'despite', 'after',
            # Evidence
            'bodycam', 'video shows', 'footage', 'recording'
        ]

        # Supporting evidence indicators - expanded
        supporting_words = [
            'stated', 'wrote', 'sent', 'received', 'met',
            'arrived', 'responded', 'called', 'reported',
            'approximately', 'lasted', 'occurred', 'began'
        ]

        # Check if fact addresses a specific element
        addresses_element = False
        if elements:
            for elem in elements:
                # Check for any word from element in sentence
                elem_words = [w for w in elem.lower().split() if len(w) > 3]
                if any(word in sent_lower for word in elem_words):
                    addresses_element = True
                    break

        # Classify
        if addresses_element or any(word in sent_lower for word in determinative_words):
            weighted.append((sent, "determinative", 3))
        elif any(word in sent_lower for word in supporting_words):
            weighted.append((sent, "supporting", 2))
        else:
            weighted.append((sent, "background", 1))

    # Sort by weight (highest first)
    weighted.sort(key=lambda x: x[2], reverse=True)
    return weighted


def generate_analogies_and_distinctions_enhanced(
    precedent_case: str,
    precedent_text: str,
    precedent_citation: str,
    weighted_facts: List[Tuple[str, str, int]],
    rules: List[str],
    elements: Optional[List[str]] = None,
    record_cites: Optional[Dict[str, str]] = None
) -> List[AnalogyDistinction]:
    """
    Enhanced analogy/distinction generation with element matching and record cites.
    """
    results = []

    # Legal concept groups - facts with any term from same group are related
    legal_concept_groups = {
        'force': ['shot', 'shooting', 'taser', 'tased', 'struck', 'hit', 'beat', 'force', 'deployed', 'used force', 'restrained', 'push', 'tackle'],
        'compliance': ['complying', 'complied', 'resisting', 'resistance', 'fleeing', 'fled', 'submitted', 'surrendered', 'cooperated', 'obeyed', 'refused'],
        'threat': ['threat', 'threatening', 'armed', 'unarmed', 'weapon', 'danger', 'dangerous', 'posed', 'risk', 'safety'],
        'injury': ['injured', 'injury', 'harm', 'harmed', 'died', 'death', 'cardiac', 'broken', 'wounded', 'killed', 'fatal'],
        'arrest': ['arrested', 'detained', 'seized', 'handcuffed', 'custody', 'stop', 'stopped', 'search', 'searched'],
        'mental_state': ['knew', 'intended', 'reckless', 'deliberate', 'willful', 'negligent', 'aware', 'believed'],
        'procedure': ['warrant', 'probable cause', 'reasonable', 'suspicion', 'investigation', 'emergency'],
    }

    def get_concept_overlap(fact1: str, fact2: str) -> int:
        """Check if two facts share legal concepts."""
        f1_lower = fact1.lower()
        f2_lower = fact2.lower()
        shared_concepts = 0

        for concept, terms in legal_concept_groups.items():
            f1_has = any(term in f1_lower for term in terms)
            f2_has = any(term in f2_lower for term in terms)
            if f1_has and f2_has:
                shared_concepts += 1

        return shared_concepts

    def has_common_words(fact1: str, fact2: str, min_common: int = 3) -> bool:
        """Check if facts share significant common words."""
        stop_words = {'the', 'a', 'an', 'is', 'was', 'were', 'are', 'been', 'be', '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', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between',
                      'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why',
                      'how', 'all', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor',
                      'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'and', 'but',
                      'or', 'if', 'that', 'this', 'these', 'those', 'what', 'which', 'who', 'whom'}

        words1 = set(w.lower() for w in re.findall(r'\b\w{3,}\b', fact1)) - stop_words
        words2 = set(w.lower() for w in re.findall(r'\b\w{3,}\b', fact2)) - stop_words
        return len(words1 & words2) >= min_common

    # Extract precedent facts
    precedent_facts = extract_case_facts(precedent_text)

    # Fallback: if no facts extracted, use rules as context
    if not precedent_facts and rules:
        precedent_facts = [r[:200] for r in rules[:3]]

    # Another fallback: extract any substantive sentences
    if not precedent_facts:
        sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', precedent_text) if len(s.strip()) > 40]
        precedent_facts = sentences[:5]

    for p_fact in precedent_facts[:7]:  # Check more precedent facts
        for c_fact, significance, weight in weighted_facts[:10]:  # Check more client facts
            # Check text similarity
            text_similarity = calculate_rule_similarity(p_fact, c_fact)

            # Check legal concept overlap
            concept_overlap = get_concept_overlap(p_fact, c_fact)

            # Check for common significant words
            common_words = has_common_words(p_fact, c_fact)

            # Combined similarity score - MORE LENIENT
            # Any of: moderate text similarity, shared legal concepts, or common significant words
            is_related = text_similarity > 0.08 or concept_overlap >= 1 or common_words

            # Find record cite for this fact
            rec_cite = None
            if record_cites:
                for fact_key, cite in record_cites.items():
                    if fact_key.lower() in c_fact.lower() or c_fact.lower() in fact_key.lower():
                        rec_cite = cite
                        break

            # Determine which element this addresses
            element_addressed = None
            if elements:
                for elem in elements:
                    if any(word in c_fact.lower() for word in elem.lower().split()):
                        element_addressed = elem
                        break

            # Analogy: related AND moderately similar text (or very high concept overlap)
            if is_related and (text_similarity > 0.06 or concept_overlap >= 2):
                # Build enhanced draft with record cite
                cite_suffix = f" ({rec_cite})" if rec_cite else ""
                draft = f"Like {precedent_case}, where {p_fact.lower().rstrip('.')}, here {c_fact.lower().rstrip('.')}{cite_suffix}. {precedent_citation}."

                results.append(AnalogyDistinction(
                    type="analogy",
                    precedent_case=precedent_case,
                    precedent_fact=p_fact,
                    client_fact=c_fact,
                    draft_language=draft,
                    rule_applied=rules[0] if rules else "",
                    element_addressed=element_addressed,
                    record_cite=rec_cite,
                    fact_significance=significance
                ))
            elif is_related and text_similarity < 0.06 and concept_overlap >= 1:
                # Same legal concepts but different specific facts = distinction
                cite_suffix = f" ({rec_cite})" if rec_cite else ""
                draft = f"Unlike {precedent_case}, where {p_fact.lower().rstrip('.')}, here {c_fact.lower().rstrip('.')}{cite_suffix}. {precedent_citation}."

                results.append(AnalogyDistinction(
                    type="distinction",
                    precedent_case=precedent_case,
                    precedent_fact=p_fact,
                    client_fact=c_fact,
                    draft_language=draft,
                    rule_applied=rules[0] if rules else "",
                    element_addressed=element_addressed,
                    record_cite=rec_cite,
                    fact_significance=significance
                ))

    # Sort by fact significance (determinative first), then by type (analogies first)
    results.sort(key=lambda x: (
        {"determinative": 0, "supporting": 1, "background": 2}.get(x.fact_significance, 2),
        0 if x.type == "analogy" else 1
    ))
    return results[:8]  # Return more results


def generate_even_if_arguments(
    precedent_cases: Dict[str, Tuple[str, str]],
    client_facts: str,
    issue: str
) -> List[EvenIfArgument]:
    """
    Generate "even if" alternative arguments.

    These provide fallback positions if the court doesn't accept primary argument.
    """
    results = []

    # Common even-if patterns
    even_if_templates = [
        ("the Court finds a genuine dispute of material fact on [element]",
         "summary judgment is still appropriate because [alternative]"),
        ("the Court accepts Defendant's characterization of [fact]",
         "Plaintiff still prevails because [reason]"),
        ("Plaintiff cannot establish [element 1]",
         "the claim still succeeds under [alternative theory]"),
        ("the evidence is viewed in the light most favorable to [opposing party]",
         "[party] still prevails because [reason]"),
    ]

    # Look for elements/issues in the case text
    for case_name, (text, citation) in precedent_cases.items():
        text_lower = text.lower()

        # Find alternative holdings or dicta about alternatives
        alt_patterns = [
            r'even\s+(?:if|assuming)\s+([^,]+),\s+([^.]+\.)',
            r'alternatively,?\s+([^.]+\.)',
            r'in\s+any\s+event,?\s+([^.]+\.)',
        ]

        for pattern in alt_patterns:
            matches = re.findall(pattern, text, re.IGNORECASE)
            for match in matches[:2]:
                if isinstance(match, tuple) and len(match) >= 2:
                    results.append(EvenIfArgument(
                        concession=f"Even if {match[0].strip()}",
                        alternative_argument=match[1].strip(),
                        supporting_authority=f"{case_name}, {citation}",
                        draft_language=f"Even if {match[0].strip()}, {match[1].strip()} {case_name}, {citation}."
                    ))

    # Generate some generic even-if arguments based on issue
    if 'negligence' in issue.lower():
        results.append(EvenIfArgument(
            concession="Even if Defendant breached a duty",
            alternative_argument="Plaintiff cannot establish proximate causation because [reason]",
            supporting_authority="[Insert authority]",
            draft_language="Even if Defendant breached a duty, Plaintiff cannot establish that the breach proximately caused Plaintiff's injuries because [specific reason]. See [authority]."
        ))

    if 'summary judgment' in issue.lower():
        results.append(EvenIfArgument(
            concession="Even if a genuine dispute exists as to [X]",
            alternative_argument="the undisputed facts establish [Y] as a matter of law",
            supporting_authority="[Insert authority]",
            draft_language="Even if a genuine dispute exists as to [X], the undisputed facts establish that [Y] as a matter of law. See [authority]."
        ))

    return results[:4]


def format_analogies_distinctions_enhanced(items: List[AnalogyDistinction]) -> str:
    """Enhanced formatting with element mapping and significance."""
    output = []
    output.append("APPLICATION SECTION DRAFTS")
    output.append("=" * 70)

    # Group by significance
    determinative = [i for i in items if i.fact_significance == "determinative"]
    supporting = [i for i in items if i.fact_significance == "supporting"]
    background = [i for i in items if i.fact_significance == "background"]

    if determinative:
        output.append("\n## DETERMINATIVE FACTS (case-critical)")
        for a in determinative:
            output.append(f"\n  [{a.type.upper()}] {a.precedent_case}")
            if a.element_addressed:
                output.append(f"  Element: {a.element_addressed}")
            output.append(f"  Draft: {a.draft_language}")
            if a.record_cite:
                output.append(f"  Record: {a.record_cite}")

    if supporting:
        output.append("\n## SUPPORTING FACTS")
        for a in supporting:
            output.append(f"\n  [{a.type.upper()}] {a.precedent_case}")
            if a.element_addressed:
                output.append(f"  Element: {a.element_addressed}")
            output.append(f"  Draft: {a.draft_language}")
            if a.record_cite:
                output.append(f"  Record: {a.record_cite}")

    if background:
        output.append("\n## BACKGROUND FACTS")
        for a in background:
            output.append(f"\n  [{a.type.upper()}] {a.precedent_case}")
            if a.element_addressed:
                output.append(f"  Element: {a.element_addressed}")
            output.append(f"  Draft: {a.draft_language}")
            if a.record_cite:
                output.append(f"  Record: {a.record_cite}")

    analogies = [i for i in items if i.type == "analogy"]
    distinctions = [i for i in items if i.type == "distinction"]

    output.append(f"\n\nSummary: {len(analogies)} analogies, {len(distinctions)} distinctions generated")

    return "\n".join(output)


def format_even_if_arguments(items: List[EvenIfArgument]) -> str:
    """Format even-if arguments."""
    output = []
    output.append("ALTERNATIVE 'EVEN IF' ARGUMENTS")
    output.append("=" * 70)
    output.append("(Use these as fallback positions)")

    for i, item in enumerate(items, 1):
        output.append(f"\n[{i}] {item.concession}")
        output.append(f"    Then: {item.alternative_argument}")
        output.append(f"    Authority: {item.supporting_authority}")
        output.append(f"    Draft: \"{item.draft_language}\"")

    return "\n".join(output)


# ============================================================================
# PARENTHETICAL GENERATION
# ============================================================================

@dataclass
class Parenthetical:
    """A case parenthetical for Bluebook citation."""
    case_name: str
    citation: str
    parenthetical_text: str
    parenthetical_type: str  # "holding", "explaining", "finding", "noting", "comparing"
    full_citation: str  # Complete citation with parenthetical


def generate_parenthetical(
    case_name: str,
    case_text: str,
    citation: str,
    rule_text: Optional[str] = None,
    focus: Optional[str] = None
) -> Parenthetical:
    """
    Generate an explanatory parenthetical for a case citation.

    Args:
        case_name: Name of the case
        case_text: Full text of the case opinion
        citation: Case citation
        rule_text: Optional specific rule to focus parenthetical on
        focus: Optional focus area (e.g., "facts", "holding", "reasoning")

    Returns:
        Parenthetical object with generated text
    """
    # Use improved rule extraction for better holdings
    rules = extract_enhanced_rules(case_text, case_name, citation)

    paren_type = "holding"  # Default
    best_holding = None

    # First choice: use extracted rules (most complete)
    if rules:
        # Pick the best rule - prefer ones with "hold" or "conclude" nearby
        for rule in rules:
            rule_pos = case_text.lower().find(rule.text[:50].lower())
            if rule_pos > 0:
                context_before = case_text[max(0, rule_pos - 100):rule_pos].lower()
                if 'hold' in context_before or 'held' in context_before:
                    best_holding = rule.text
                    paren_type = "holding"
                    break
                elif 'conclude' in context_before:
                    best_holding = rule.text
                    paren_type = "holding"
                    break
                elif 'find' in context_before or 'found' in context_before:
                    best_holding = rule.text
                    paren_type = "finding"
                    break

        # If no match with context, just use first rule
        if not best_holding and rules:
            best_holding = rules[0].text
            paren_type = "holding"

    # Second choice: use provided rule_text
    if not best_holding and rule_text:
        best_holding = rule_text

    # Third choice: manual extraction with better sentence handling
    if not best_holding:
        # Look for holding statements and expand properly
        holding_markers = [
            (r'(?:we|the court)\s+(?:hold|held)\s+that\s+', "holding"),
            (r'(?:we|the court)\s+(?:conclude[sd]?)\s+that\s+', "holding"),
            (r'the\s+(?:standard|rule|test)\s+(?:is|requires)\s+', "holding"),
        ]

        for marker_pattern, p_type in holding_markers:
            match = re.search(marker_pattern, case_text, re.IGNORECASE)
            if match:
                # Extract text after the marker until a real sentence end
                start = match.end()
                text_after = case_text[start:start + 500]

                # Find proper sentence end (not abbreviation)
                end_pos = len(text_after)
                for i in range(len(text_after) - 1):
                    if text_after[i] == '.':
                        # Check if this is a real sentence end
                        if i + 1 < len(text_after) and text_after[i + 1] in ' \n':
                            if i + 2 >= len(text_after) or text_after[i + 2].isupper() or text_after[i + 2] in '"\n(':
                                # Avoid stopping at abbreviations
                                before_period = text_after[max(0, i-5):i].lower()
                                if not any(abbr in before_period for abbr in ['u.s', 'f.3', 'f.2', 's.ct', 'cir', 'inc', 'corp', 'e.g', 'i.e']):
                                    end_pos = i
                                    break

                best_holding = text_after[:end_pos].strip()
                paren_type = p_type
                if len(best_holding) > 30:
                    break

    # Last resort fallback
    if not best_holding or len(best_holding) < 20:
        best_holding = "establishing the applicable legal standard"

    # Clean up the holding text
    best_holding = best_holding.strip()
    if best_holding.endswith('.'):
        best_holding = best_holding[:-1]

    # Make first letter lowercase for parenthetical style (Bluebook convention)
    # Exception: proper nouns, acronyms, case names
    if best_holding and best_holding[0].isupper():
        first_word = best_holding.split()[0] if best_holding.split() else ""
        # Don't lowercase if it's a proper noun/acronym pattern
        is_proper = (
            first_word.isupper() or  # Acronym like "PSLRA"
            (len(first_word) > 1 and first_word[1].isupper()) or  # Mixed case like "McDonald"
            first_word in ('Congress', 'Court', 'Constitution', 'Amendment', 'Act', 'Rule', 'Section')
        )
        if not is_proper:
            best_holding = best_holding[0].lower() + best_holding[1:]

    # Extract key facts if present
    facts_to_include = None
    if focus == "facts" or "where" not in best_holding.lower():
        # Look for factual context
        fact_patterns = [
            r'(?:where|when)\s+(?:the\s+)?(?:plaintiff|defendant|complaint|company)\s+([^.]+)',
            r'(?:in which|because)\s+(?:the\s+)?([^.]+)',
        ]
        for fp in fact_patterns:
            fm = re.search(fp, case_text, re.IGNORECASE)
            if fm and len(fm.group(1)) < 100:
                facts_to_include = fm.group(1).strip()
                break

    # Build the parenthetical
    # First, fix grammar issues with holdings that start with bare verbs
    # E.g., "require a showing" → "requiring a showing" (gerund form)
    bare_verb_patterns = {
        # verb_regex: (gerund_replacement, remainder_pattern)
        r'^require[sd]?\s+': ('requiring ', r'^require[sd]?\s+'),
        r'^establish(?:es|ed)?\s+': ('establishing ', r'^establish(?:es|ed)?\s+'),
        r'^permit[s]?\s+': ('permitting ', r'^permit[s]?\s+'),
        r'^provide[sd]?\s+': ('providing ', r'^provide[sd]?\s+'),
        r'^define[sd]?\s+': ('defining ', r'^define[sd]?\s+'),
        r'^allow[sd]?\s+': ('allowing ', r'^allow[sd]?\s+'),
        r'^prohibit[sd]?\s+': ('prohibiting ', r'^prohibit[sd]?\s+'),
        r'^mandate[sd]?\s+': ('mandating ', r'^mandate[sd]?\s+'),
        r'^set[s]?\s+forth\s+': ('setting forth ', r'^set[s]?\s+forth\s+'),
        r'^hold[s]?\s+that\s+': ('holding that ', r'^hold[s]?\s+that\s+'),
        r'^conclude[sd]?\s+that\s+': ('concluding that ', r'^conclude[sd]?\s+that\s+'),
        r'^find[s]?\s+that\s+': ('finding that ', r'^find[s]?\s+that\s+'),
        r'^note[sd]?\s+that\s+': ('noting that ', r'^note[sd]?\s+that\s+'),
        r'^explain[sd]?\s+that\s+': ('explaining that ', r'^explain[sd]?\s+that\s+'),
        r'^recognize[sd]?\s+': ('recognizing ', r'^recognize[sd]?\s+'),
        r'^adopt[sd]?\s+': ('adopting ', r'^adopt[sd]?\s+'),
        r'^reject[sd]?\s+': ('rejecting ', r'^reject[sd]?\s+'),
        r'^apply(?:ing)?\s+': ('applying ', r'^apply(?:ing)?\s+'),
        r'^interpret[sd]?\s+': ('interpreting ', r'^interpret[sd]?\s+'),
        r'^affirm[sd]?\s+': ('affirming ', r'^affirm[sd]?\s+'),
        r'^reverse[sd]?\s+': ('reversing ', r'^reverse[sd]?\s+'),
    }

    # Check if holding starts with a bare verb (needs grammar fix)
    use_gerund_directly = False
    gerund_paren_text = None

    for pattern, (gerund, strip_pattern) in bare_verb_patterns.items():
        if re.match(pattern, best_holding, re.IGNORECASE):
            # This holding starts with a bare verb - convert to gerund form
            remainder = re.sub(strip_pattern, '', best_holding, count=1, flags=re.IGNORECASE)
            gerund_paren_text = gerund + remainder
            use_gerund_directly = True
            break

    # Standard verb map for cases with complete subject-verb structure
    verb_map = {
        "holding": "holding that",
        "finding": "finding that",
        "explaining": "explaining that",
        "noting": "noting that",
    }
    verb = verb_map.get(paren_type, "holding that")

    # Decide which format to use
    if use_gerund_directly and gerund_paren_text:
        # Use the gerund form directly (no "holding that" prefix needed)
        if facts_to_include and "where" not in gerund_paren_text.lower():
            paren_text = f"{gerund_paren_text} where {facts_to_include}"
        else:
            paren_text = gerund_paren_text
    elif facts_to_include and "where" not in best_holding.lower():
        paren_text = f"{verb} {best_holding} where {facts_to_include}"
    else:
        # Check if holding already has a proper subject (starts with article, proper noun, etc.)
        has_subject = bool(re.match(
            r'^(?:the|a|an|this|that|such|any|no|each|every|all|'
            r'[A-Z][a-z]+(?:\s+v\.?\s+[A-Z][a-z]+)?|'  # Case names or proper nouns
            r'§\s*\d+|'  # Statute sections
            r'\d+\s+U\.?S\.?C\.?|'  # USC citations
            r'plaintiffs?|defendants?|courts?|congress|'
            r'it\s+is|there\s+is|there\s+are)\s+',
            best_holding, re.IGNORECASE
        ))

        if has_subject:
            paren_text = f"{verb} {best_holding}"
        else:
            # No clear subject - likely a fragment, try gerund conversion as fallback
            # This catches edge cases not in the explicit list
            first_word = best_holding.split()[0] if best_holding.split() else ""
            # Only try gerund conversion for actual verbs:
            # - Must end in 's' (third person singular)
            # - Must NOT contain apostrophe (excludes "defendant's", "plaintiff's")
            # - Must NOT be a common noun that ends in 's'
            common_s_nouns = {'defendants', 'plaintiffs', 'courts', 'claims', 'facts',
                             'elements', 'factors', 'rights', 'laws', 'rules', 'cases',
                             'parties', 'witnesses', 'statutes', 'contracts', 'damages'}
            is_verb_candidate = (
                first_word.endswith('s') and
                len(first_word) > 3 and
                "'" not in first_word and  # Exclude possessives like "defendant's"
                first_word.lower() not in common_s_nouns  # Exclude common plural nouns
            )
            if is_verb_candidate:
                # Try to convert third-person singular to gerund
                # "requires" → "requiring", "establishes" → "establishing"
                base = first_word.rstrip('s')
                if base.endswith('e'):
                    gerund = base[:-1] + 'ing'  # "require" → "requiring"
                else:
                    gerund = base + 'ing'
                rest = ' '.join(best_holding.split()[1:])
                paren_text = f"{gerund} {rest}"
            else:
                # Fallback: use standard format
                paren_text = f"{verb} {best_holding}"

    # Truncate based on WORD count per spec
    # Target: 15-25 words, Hard max: 35 words
    # Rule: Complete thought > arbitrary length
    words = paren_text.split()
    word_count = len(words)
    hard_max_words = 35

    if word_count > hard_max_words:
        truncated = False

        # First try: find a complete sentence within word limit
        # Rebuild text word by word, looking for sentence end
        current_text = ""
        for i, word in enumerate(words):
            test_text = current_text + (" " if current_text else "") + word
            if i >= 12 and word.endswith('.'):  # At least 12 words for a meaningful thought
                # Check it's not an abbreviation
                if not any(word.lower().endswith(abbr) for abbr in ['u.s.', 'f.3d.', 'f.2d.', 's.ct.', 'cir.', 'inc.', 'e.g.', 'i.e.']):
                    paren_text = test_text
                    truncated = False
                    break
            current_text = test_text
            if i >= hard_max_words - 1:
                break

        # If still too long, try clause boundary
        if len(paren_text.split()) > hard_max_words:
            # Find last clause break within limit
            clause_breaks = ['; ', ', and ', ', but ', ', or ', ', which ']
            best_text = paren_text
            for break_pattern in clause_breaks:
                pos = paren_text.rfind(break_pattern)
                if pos > 0:
                    candidate = paren_text[:pos]
                    if 15 <= len(candidate.split()) <= hard_max_words:
                        best_text = candidate
                        break
            paren_text = best_text
            truncated = True

        # Last resort: truncate at word boundary
        if len(paren_text.split()) > hard_max_words:
            words = paren_text.split()[:hard_max_words]
            paren_text = ' '.join(words)
            truncated = True

        # Add ellipsis only if truncated mid-thought
        if truncated and not paren_text.rstrip().endswith('.'):
            paren_text = paren_text.rstrip('., ;:') + '...'

    # Clean up trailing punctuation (preserve ellipsis)
    if not paren_text.endswith('...'):
        paren_text = paren_text.rstrip('., ;:')

    # Build full citation
    full_cite = f"{case_name}, {citation} ({paren_text})"

    return Parenthetical(
        case_name=case_name,
        citation=citation,
        parenthetical_text=paren_text,
        parenthetical_type=paren_type,
        full_citation=full_cite
    )


def generate_parenthetical_with_meta(
    case_name: str,
    case_text: str,
    citation: str,
    rule_text: Optional[str] = None,
    focus: Optional[str] = None
) -> Dict[str, Any]:
    """
    Generate parenthetical with extraction metadata.

    This wrapper tracks extraction quality including which method
    succeeded and confidence in the result.

    Returns:
        Dict with "_meta" and "parenthetical" (Parenthetical object)
    """
    # Track extraction path for confidence scoring
    extraction_method = "unknown"
    targets_found = []
    targets_missing = []
    suggestions = []

    # Get rules for tracking
    rules = extract_enhanced_rules(case_text, case_name, citation)

    # Determine extraction method and build parenthetical
    paren = generate_parenthetical(case_name, case_text, citation, rule_text, focus)

    # Analyze the result to determine which path was taken
    paren_lower = paren.parenthetical_text.lower()

    if paren.parenthetical_text == "establishing the applicable legal standard":
        # Generic fallback was used
        extraction_method = "fallback"
        confidence = 0.20
        targets_missing.append("holding")
        targets_missing.append("specific_rule")
        suggestions.append(
            "Could not extract specific holding. The case may lack explicit 'we hold' language."
        )
        suggestions.append(
            "Try providing the holding section specifically or check if this is a procedural opinion."
        )
    elif rules and any(r.text[:50].lower() in paren_lower for r in rules[:3]):
        # Used extracted rules
        extraction_method = "rules"
        confidence = 0.85
        targets_found.append("holding")
        targets_found.append("specific_rule")
    elif rule_text and rule_text[:30].lower() in paren_lower:
        # Used provided rule_text
        extraction_method = "provided_rule"
        confidence = 0.75
        targets_found.append("specific_rule")
        targets_missing.append("extracted_holding")
        suggestions.append(
            "Used provided rule text. For better results, ensure case text includes the court's holding."
        )
    else:
        # Manual extraction succeeded
        extraction_method = "manual"
        confidence = 0.65
        targets_found.append("inferred_holding")
        suggestions.append(
            "Holding extracted via pattern matching. Verify against source for accuracy."
        )

    # Check for truncation (indicates incomplete extraction)
    if paren.parenthetical_text.endswith('...'):
        confidence -= 0.10
        suggestions.append(
            "Parenthetical was truncated. Full holding may be longer than extracted text."
        )

    return {
        "_meta": {
            "extraction_complete": extraction_method in ("rules", "provided_rule"),
            "targets_found": targets_found,
            "targets_missing": targets_missing,
            "confidence_scores": {
                "parenthetical": round(confidence, 2),
                "overall": round(confidence, 2)
            },
            "suggestions": suggestions,
            "extraction_method": extraction_method
        },
        "parenthetical": paren
    }


def generate_parentheticals_for_cases(
    cases: Dict[str, Tuple[str, str]],
    rule_focus: Optional[str] = None
) -> List[Parenthetical]:
    """
    Generate parentheticals for multiple cases.

    Args:
        cases: Dictionary of case_name -> (text, citation)
        rule_focus: Optional rule text to focus parentheticals on

    Returns:
        List of Parenthetical objects
    """
    parentheticals = []
    for case_name, (text, citation) in cases.items():
        paren = generate_parenthetical(case_name, text, citation, rule_focus)
        parentheticals.append(paren)
    return parentheticals


def format_parentheticals(parentheticals: List[Parenthetical]) -> str:
    """Format parentheticals for output."""
    output = []
    output.append("GENERATED PARENTHETICALS")
    output.append("=" * 70)
    output.append("(Ready to paste into brief citations)")
    output.append("")

    for i, p in enumerate(parentheticals, 1):
        output.append(f"[{i}] {p.case_name}")
        output.append(f"    Citation: {p.citation}")
        output.append(f"    Type: {p.parenthetical_type}")
        output.append(f"    Parenthetical: ({p.parenthetical_text})")
        output.append(f"")
        output.append(f"    FULL CITATION:")
        output.append(f"    {p.full_citation}")
        output.append("")

    return "\n".join(output)


# ============================================================================
# DIRECT FILE READING
# ============================================================================

def _translate_docker_path(file_path: str) -> str:
    """
    Translate Docker/WSL paths from Claude Desktop to Windows-accessible paths.

    Claude Desktop stores uploaded files at /mnt/user-data/uploads/ which is
    inside a Docker container or WSL. This function attempts to translate those
    paths to Windows-accessible equivalents.

    Args:
        file_path: Original path (may be Linux-style Docker path)

    Returns:
        Translated Windows path, or original path if no translation needed/possible
    """
    import os

    # If it's already a Windows path or exists, return as-is
    if not file_path.startswith('/') or os.path.exists(file_path):
        return file_path

    # Common Claude Desktop Docker path
    if file_path.startswith('/mnt/user-data/'):
        # Try WSL access paths
        wsl_paths_to_try = [
            # WSL2 default path format
            f"\\\\wsl.localhost\\docker-desktop-data\\data\\docker\\volumes\\claude-desktop_user-data\\_data{file_path[14:]}",
            f"\\\\wsl$\\docker-desktop-data\\data\\docker\\volumes\\claude-desktop_user-data\\_data{file_path[14:]}",
            # Alternative Docker volume location
            f"\\\\wsl.localhost\\docker-desktop\\mnt\\docker-desktop-disk\\data\\docker\\volumes\\claude-desktop_user-data\\_data{file_path[14:]}",
            # Direct WSL path attempt
            f"\\\\wsl$\\Ubuntu{file_path}",
            f"\\\\wsl.localhost\\Ubuntu{file_path}",
        ]

        for wsl_path in wsl_paths_to_try:
            try:
                if os.path.exists(wsl_path):
                    return wsl_path
            except (OSError, PermissionError):
                continue

    # Generic /mnt/ path - might be WSL mount
    if file_path.startswith('/mnt/') and len(file_path) > 5:
        # /mnt/c/Users/... -> C:\Users\...
        drive_letter = file_path[5]
        if drive_letter.isalpha() and (len(file_path) == 6 or file_path[6] == '/'):
            rest_of_path = file_path[6:].replace('/', '\\') if len(file_path) > 6 else ''
            windows_path = f"{drive_letter.upper()}:{rest_of_path}"
            if os.path.exists(windows_path):
                return windows_path

    # Return original if no translation works
    return file_path


def read_case_file(file_path: str) -> Tuple[str, str]:
    """
    Read case text from a file (PDF or text).

    Args:
        file_path: Path to the case file (supports Windows paths and Docker/WSL paths)

    Returns:
        Tuple of (extracted_text, file_type)
    """
    # Translate Docker/WSL paths to Windows paths if needed
    translated_path = _translate_docker_path(file_path)
    path = Path(translated_path)

    if not path.exists():
        # Include both original and translated path in error for debugging
        if translated_path != file_path:
            return f"Error: File not found. Original: {file_path}, Tried: {translated_path}", "error"
        return f"Error: File not found: {file_path}", "error"

    file_ext = path.suffix.lower()

    if file_ext == '.pdf':
        return _extract_pdf_text(translated_path), "pdf"
    elif file_ext in ['.txt', '.text']:
        with open(translated_path, 'r', encoding='utf-8', errors='ignore') as f:
            return f.read(), "text"
    elif file_ext in ['.md', '.markdown']:
        with open(translated_path, 'r', encoding='utf-8', errors='ignore') as f:
            return f.read(), "markdown"
    elif file_ext in ['.doc', '.docx']:
        return _extract_docx_text(translated_path), "docx"
    else:
        # Try reading as text
        try:
            with open(translated_path, 'r', encoding='utf-8', errors='ignore') as f:
                return f.read(), "unknown"
        except Exception as e:
            return f"Error reading file: {str(e)}", "error"


def _extract_pdf_text(file_path: str) -> str:
    """Extract text from PDF file."""
    try:
        # Try PyPDF2 first
        import PyPDF2
        text_parts = []
        with open(file_path, 'rb') as f:
            reader = PyPDF2.PdfReader(f)
            for page in reader.pages:
                page_text = page.extract_text()
                if page_text:
                    text_parts.append(page_text)
        return '\n\n'.join(text_parts)
    except ImportError:
        pass

    try:
        # Try pdfplumber as fallback
        import pdfplumber
        text_parts = []
        with pdfplumber.open(file_path) as pdf:
            for page in pdf.pages:
                page_text = page.extract_text()
                if page_text:
                    text_parts.append(page_text)
        return '\n\n'.join(text_parts)
    except ImportError:
        pass

    return "Error: No PDF library available (install PyPDF2 or pdfplumber)"


def _extract_docx_text(file_path: str) -> str:
    """Extract text from DOCX file."""
    try:
        from docx import Document
        doc = Document(file_path)
        return '\n\n'.join(para.text for para in doc.paragraphs if para.text.strip())
    except ImportError:
        return "Error: python-docx not installed (pip install python-docx)"
    except Exception as e:
        return f"Error reading DOCX: {str(e)}"


def read_project_cases(
    file_paths: Dict[str, str],
    base_path: Optional[str] = None
) -> Dict[str, Tuple[str, str]]:
    """
    Read multiple case files from project.

    Args:
        file_paths: Dict of case_name -> file_path
        base_path: Optional base directory (prepended to relative paths)

    Returns:
        Dict of case_name -> (text, citation_placeholder)
    """
    cases = {}

    for case_name, file_path in file_paths.items():
        # Handle relative paths
        if base_path and not os.path.isabs(file_path):
            full_path = os.path.join(base_path, file_path)
        else:
            full_path = file_path

        text, file_type = read_case_file(full_path)

        if file_type != "error":
            # Try to extract citation from text
            citation = _extract_citation_from_text(text, case_name)
            cases[case_name] = (text, citation)
        else:
            # Include error message for debugging
            cases[case_name] = (text, "citation unknown")

    return cases


def _extract_citation_from_text(text: str, case_name: Optional[str] = None) -> str:
    """
    Extract THE case's actual citation, not citations to other cases within the text.

    Strategy:
    1. Look for citation near the case name in the header/first portion
    2. Look for standard case header patterns
    3. If case_name provided, find citation adjacent to case name
    """
    # Only search the first ~2000 chars where the case caption should be
    header_text = text[:2000] if len(text) > 2000 else text

    # Citation patterns with capture groups
    citation_patterns = [
        # Full citation with year: "482 U.S. 451 (1987)"
        r'(\d{1,3})\s+(U\.S\.|S\.\s*Ct\.|F\.\s*(?:2d|3d|4th)|F\.\s*Supp\.\s*(?:2d|3d)?|N\.W\.(?:2d)?|N\.E\.(?:2d)?|S\.W\.(?:2d|3d)?|S\.E\.(?:2d)?|P\.(?:2d|3d)?|A\.(?:2d|3d)?|So\.(?:2d|3d)?|Cal\.\s*(?:App\.\s*)?(?:2d|3d|4th|5th)?|N\.Y\.(?:2d|3d)?)\s+(\d+)(?:\s*,\s*\d+\s+\w+\.?\s*\d+)*\s*\(([^)]+\d{4}[^)]*)\)',
        # Citation without year paren but with year nearby
        r'(\d{1,3})\s+(U\.S\.|S\.\s*Ct\.|F\.\s*(?:2d|3d|4th)|F\.\s*Supp\.\s*(?:2d|3d)?|N\.W\.(?:2d)?|N\.E\.(?:2d)?|S\.W\.(?:2d|3d)?|S\.E\.(?:2d)?|P\.(?:2d|3d)?|A\.(?:2d|3d)?|So\.(?:2d|3d)?)\s+(\d+)',
    ]

    # If we have a case name, look for citation near it
    if case_name:
        # Escape special chars and create flexible pattern
        name_parts = re.split(r'\s+v\.?\s+', case_name, maxsplit=1)
        if len(name_parts) >= 2:
            # Look for "Plaintiff v. Defendant" pattern followed by citation
            plaintiff = re.escape(name_parts[0].strip()[:20])  # First 20 chars
            defendant = re.escape(name_parts[1].strip()[:20])

            # Pattern: case name followed by citation within 200 chars
            case_cite_pattern = rf'{plaintiff}.*?v\.?\s*{defendant}[^0-9]{{0,50}}(\d{{1,3}}\s+(?:U\.S\.|S\.\s*Ct\.|F\.\s*(?:2d|3d|4th)|F\.\s*Supp|N\.W\.(?:2d)?|N\.E\.(?:2d)?|S\.W\.(?:2d|3d)?|S\.E\.(?:2d)?|P\.(?:2d|3d)?|A\.(?:2d|3d)?|So\.(?:2d|3d)?)\s+\d+)'

            match = re.search(case_cite_pattern, header_text, re.IGNORECASE | re.DOTALL)
            if match:
                cite = match.group(1)
                # Try to find year
                year_match = re.search(r'\(([^)]*\d{4}[^)]*)\)', header_text[match.end():match.end()+100])
                if year_match:
                    return f"{cite} ({year_match.group(1)})"
                return cite

    # Look for standard case header patterns
    # Pattern: Case name, citation, court, year - like "State v. Morin, 736 N.W.2d 691 (Minn. App. 2007)"
    header_pattern = r'([A-Z][a-zA-Z\.\s]+)\s+v\.?\s+([A-Z][a-zA-Z\.\s,]+?),?\s*(\d{1,3})\s+(U\.S\.|S\.\s*Ct\.|F\.\s*(?:2d|3d|4th)|F\.\s*Supp\.\s*(?:2d|3d)?|N\.W\.(?:2d)?|N\.E\.(?:2d)?|S\.W\.(?:2d|3d)?|S\.E\.(?:2d)?|P\.(?:2d|3d)?|A\.(?:2d|3d)?|So\.(?:2d|3d)?|Cal\.|N\.Y\.)\s+(\d+)\s*\(([^)]+)\)'

    match = re.search(header_pattern, header_text)
    if match:
        vol, reporter, page, court_year = match.group(3), match.group(4), match.group(5), match.group(6)
        return f"{vol} {reporter} {page} ({court_year})"

    # Fallback: Find first citation in header that looks like a primary citation
    # Primary citations typically have court/year in parens
    primary_cite_pattern = r'(\d{1,3})\s+(U\.S\.|S\.\s*Ct\.|F\.\s*(?:2d|3d|4th)|F\.\s*Supp\.\s*(?:2d|3d)?|N\.W\.(?:2d)?|N\.E\.(?:2d)?|S\.W\.(?:2d|3d)?|S\.E\.(?:2d)?|P\.(?:2d|3d)?|A\.(?:2d|3d)?|So\.(?:2d|3d)?|Cal\.\s*(?:App\.\s*)?(?:2d|3d|4th)?|N\.Y\.)\s+(\d+)\s*\(([^)]+\d{4}[^)]*)\)'

    match = re.search(primary_cite_pattern, header_text)
    if match:
        return f"{match.group(1)} {match.group(2)} {match.group(3)} ({match.group(4)})"

    # Last resort: any citation pattern in header
    simple_pattern = r'(\d{1,3})\s+(U\.S\.|S\.\s*Ct\.|F\.\s*(?:2d|3d)|N\.W\.(?:2d)?|N\.E\.(?:2d)?|S\.W\.(?:2d|3d)?|P\.(?:2d|3d)?|A\.(?:2d|3d)?)\s+(\d+)'
    match = re.search(simple_pattern, header_text)
    if match:
        return f"{match.group(1)} {match.group(2)} {match.group(3)}"

    return "citation needed"


# ============================================================================
# TWO-PASS SYNTHESIS (for long cases)
# ============================================================================

def extract_rules_two_pass(
    case_text: str,
    case_name: str,
    citation: str
) -> List[Dict]:
    """
    Two-pass rule extraction for long cases.

    Pass 1: Identify rule-containing sections (holdings, conclusions, etc.)
    Pass 2: Extract complete rules from those sections only

    This prevents fragmentation from processing entire case text.
    """
    rules = []

    # Pass 1: Find rule-containing sections
    rule_sections = _identify_rule_sections(case_text)

    # Pass 2: Extract rules from each section
    for section_text, section_type in rule_sections:
        section_rules = extract_enhanced_rules(section_text, case_name, citation)
        for rule in section_rules:
            rules.append({
                'text': rule.text,
                'case_name': case_name,
                'citation': citation,
                'pinpoint': rule.pinpoint_page,
                'section_type': section_type,
                'rule_type': rule.rule_type.value if rule.rule_type else 'holding',
                'confidence': _calculate_rule_confidence(rule, section_type)
            })

    # Deduplicate by similarity
    unique_rules = _deduplicate_rules(rules)

    # Sort by confidence
    unique_rules.sort(key=lambda x: x['confidence'], reverse=True)

    return unique_rules[:10]  # Return top 10 rules


def _identify_rule_sections(text: str) -> List[Tuple[str, str]]:
    """
    Identify sections of text likely to contain rules.

    Returns list of (section_text, section_type) tuples.
    """
    sections = []

    # Section patterns with labels
    section_patterns = [
        (r'(?:HOLDING|HOLDINGS?)[:\s]+(.+?)(?=\n\n[A-Z]|\Z)', 'holding'),
        (r'(?:We\s+)?(?:hold|conclude|find)\s+that\s+(.+?)(?:\.|$)', 'holding'),
        (r'(?:CONCLUSION|CONCLUSIONS?)[:\s]+(.+?)(?=\n\n[A-Z]|\Z)', 'conclusion'),
        (r'(?:The\s+)?(?:rule|standard|test)\s+(?:is|requires)\s+(.+?)(?:\.|$)', 'rule_statement'),
        (r'(?:Under|Pursuant\s+to)\s+[^,]+,\s+(.+?)(?:\.|$)', 'rule_application'),
        (r'(?:established|well-settled)\s+that\s+(.+?)(?:\.|$)', 'established_rule'),
    ]

    for pattern, section_type in section_patterns:
        matches = re.finditer(pattern, text, re.IGNORECASE | re.DOTALL)
        for match in matches:
            section_text = match.group(1) if match.lastindex else match.group(0)
            if len(section_text) > 50:  # Minimum length
                sections.append((section_text[:2000], section_type))  # Cap length

    # If no sections found, use paragraph-based extraction
    if not sections:
        paragraphs = text.split('\n\n')
        for para in paragraphs:
            para = para.strip()
            if len(para) > 100:
                # Check for rule indicators
                indicators = ['hold', 'conclude', 'require', 'must', 'shall',
                             'standard', 'test', 'element', 'factor']
                if any(ind in para.lower() for ind in indicators):
                    sections.append((para[:1500], 'paragraph'))

    return sections[:15]  # Limit sections to process


def _calculate_rule_confidence(rule: EnhancedRule, section_type: str) -> float:
    """Calculate confidence score for a rule."""
    confidence = 0.5  # Base

    # Section type bonus
    section_bonuses = {
        'holding': 0.3,
        'conclusion': 0.2,
        'rule_statement': 0.25,
        'established_rule': 0.2,
        'rule_application': 0.1,
        'paragraph': 0.0
    }
    confidence += section_bonuses.get(section_type, 0)

    # Rule type bonus - HOLDING is highest value
    if rule.rule_type == RuleType.HOLDING:
        confidence += 0.20
    elif rule.rule_type in (RuleType.ELEMENTS, RuleType.BRIGHT_LINE, RuleType.TEST):
        confidence += 0.15
    elif rule.rule_type == RuleType.STANDARD:
        confidence += 0.1
    elif rule.rule_type in (RuleType.MULTI_FACTOR, RuleType.BALANCING):
        confidence += 0.08

    # Length penalty for very short or very long
    text_len = len(rule.text)
    if text_len < 50:
        confidence -= 0.2
    elif text_len > 500:
        confidence -= 0.1

    return min(1.0, max(0.0, confidence))


def _deduplicate_rules(rules: List[Dict]) -> List[Dict]:
    """Remove duplicate rules based on similarity."""
    if not rules:
        return []

    unique = [rules[0]]

    for rule in rules[1:]:
        is_duplicate = False
        for existing in unique:
            similarity = calculate_rule_similarity(rule['text'], existing['text'])
            if similarity > 0.7:  # High similarity = duplicate
                # Keep the one with higher confidence
                if rule['confidence'] > existing['confidence']:
                    unique.remove(existing)
                    unique.append(rule)
                is_duplicate = True
                break

        if not is_duplicate:
            unique.append(rule)

    return unique


def synthesize_rules_two_pass(
    cases: Dict[str, Tuple[str, str]],
    issue: str,
    your_jurisdiction: Optional[str] = None
) -> str:
    """
    Synthesize rules using two-pass extraction.

    Better for long cases - extracts rules first, then synthesizes.
    """
    all_rules = []

    # Pass 1 & 2: Extract rules from each case
    for case_name, (text, citation) in cases.items():
        case_rules = extract_rules_two_pass(text, case_name, citation)
        all_rules.extend(case_rules)

    if not all_rules:
        return "No rules extracted from provided cases."

    # Group by similarity to find common rules
    rule_groups = _group_similar_rules(all_rules)

    # Build synthesis output
    output = []
    output.append("SYNTHESIZED RULE HIERARCHY")
    output.append("=" * 70)
    output.append(f"Issue: {issue}")
    if your_jurisdiction:
        output.append(f"Jurisdiction: {your_jurisdiction}")
    output.append("")

    # Format each rule group
    for i, group in enumerate(rule_groups[:8], 1):
        primary_rule = group[0]
        supporting_cases = group[1:]

        # Determine if binding
        is_binding = False
        for rule in group:
            citation = rule['citation']
            court_level, circuit, _ = detect_court_level(citation)
            # Supreme Court is ALWAYS binding
            if court_level == CourtLevel.SUPREME_COURT:
                is_binding = True
                break
            # Same circuit is binding
            if your_jurisdiction and circuit:
                if _normalize_jurisdiction(circuit) == _normalize_jurisdiction(your_jurisdiction):
                    is_binding = True
                    break

        binding_tag = "[BINDING]" if is_binding else "[PERSUASIVE]"

        output.append(f"{i}. {binding_tag} {primary_rule['text']}")
        output.append(f"   Source: {primary_rule['case_name']}, {primary_rule['citation']}")

        if len(group) > 1:
            output.append(f"   Accord: {'; '.join(r['case_name'] for r in supporting_cases[:3])}")

        output.append("")

    return '\n'.join(output)


def _group_similar_rules(rules: List[Dict]) -> List[List[Dict]]:
    """Group rules by similarity."""
    if not rules:
        return []

    groups = []
    used = set()

    for i, rule in enumerate(rules):
        if i in used:
            continue

        group = [rule]
        used.add(i)

        for j, other in enumerate(rules):
            if j in used:
                continue

            similarity = calculate_rule_similarity(rule['text'], other['text'])
            if similarity > 0.5:
                group.append(other)
                used.add(j)

        groups.append(group)

    # Sort groups by size and confidence
    groups.sort(key=lambda g: (len(g), g[0]['confidence']), reverse=True)

    return groups


# ============================================================================
# PIPELINE FUNCTION - SINGLE-CALL BRIEF GENERATION
# ============================================================================

@dataclass
class BriefSection:
    """Complete brief section output."""
    rule_section: str
    application_section: str
    citation_block: str
    circuit_split_note: Optional[str]
    gaps_identified: List[str]
    metadata: Dict


def generate_brief_section(
    precedent_cases: Dict[str, Tuple[str, str]],
    issue: str,
    client_facts: str,
    jurisdiction: str,
    elements: Optional[List[str]] = None,
    record_cites: Optional[Dict[str, str]] = None,
    use_two_pass: bool = True
) -> str:
    """
    Single-call brief section generator.

    Pipeline: extract rules → synthesize hierarchy → detect splits →
              generate parentheticals → map facts → draft application → format

    Args:
        precedent_cases: Dict of case_name -> (text, citation)
        issue: Legal issue being briefed
        client_facts: Your client's factual situation
        jurisdiction: Target jurisdiction (e.g., "5th Circuit")
        elements: Optional list of legal elements
        record_cites: Optional dict mapping facts to record citations
        use_two_pass: Use two-pass extraction for long cases (recommended)

    Returns:
        Ready-to-paste brief section with RULE, APPLICATION, and citations
    """
    output_parts = []
    gaps = []

    # -------------------------------------------------------------------------
    # STEP 1: Extract and synthesize rules
    # -------------------------------------------------------------------------
    if use_two_pass:
        all_rules = []
        for case_name, (text, citation) in precedent_cases.items():
            case_rules = extract_rules_two_pass(text, case_name, citation)
            all_rules.extend(case_rules)
        rule_groups = _group_similar_rules(all_rules)
    else:
        # Use original synthesis
        hierarchy = synthesize_rules_across_cases(precedent_cases, issue, jurisdiction)
        rule_groups = [[{'text': node.rule_text,
                        'case_name': node.sources[0][0] if node.sources else '',
                        'citation': node.sources[0][1] if node.sources else '',
                        'confidence': 0.8}] for node in hierarchy]

    if not rule_groups:
        return "ERROR: No rules could be extracted from the provided cases."

    # -------------------------------------------------------------------------
    # STEP 2: Check for circuit splits
    # -------------------------------------------------------------------------
    split_analysis = detect_circuit_splits(precedent_cases, issue, jurisdiction)
    has_split = len(split_analysis.positions) >= 2

    # -------------------------------------------------------------------------
    # STEP 3: Generate parentheticals for key holdings
    # -------------------------------------------------------------------------
    parentheticals = {}
    for case_name, (text, citation) in precedent_cases.items():
        parens = generate_parentheticals_for_cases({case_name: (text, citation)}, issue)
        if parens:
            parentheticals[case_name] = parens[0].parenthetical_text

    # -------------------------------------------------------------------------
    # STEP 4: Build RULE SECTION
    # -------------------------------------------------------------------------
    output_parts.append("=" * 70)
    output_parts.append("RULE SECTION")
    output_parts.append("=" * 70)
    output_parts.append("")

    # Organize by binding vs persuasive
    binding_rules = []
    persuasive_rules = []

    norm_jurisdiction = _normalize_jurisdiction(jurisdiction)

    for group in rule_groups:
        primary = group[0]
        _, circuit, _ = detect_court_level(primary['citation'])

        # Check if binding - Supreme Court first (always binding)
        court_level, _, _ = detect_court_level(primary['citation'])
        is_binding = False
        if court_level == CourtLevel.SUPREME_COURT:
            is_binding = True
        elif circuit and _normalize_jurisdiction(circuit) == norm_jurisdiction:
            is_binding = True

        if is_binding:
            binding_rules.append((primary, group[1:]))
        else:
            persuasive_rules.append((primary, group[1:]))

    # Format binding rules first
    if binding_rules:
        output_parts.append("BINDING AUTHORITY:")
        output_parts.append("")
        for primary, supporting in binding_rules:
            paren = parentheticals.get(primary['case_name'], '')
            paren_str = f" ({paren})" if paren else ""

            output_parts.append(f"  {primary['text']}")
            output_parts.append(f"  {primary['case_name']}, {primary['citation']}{paren_str}.")

            if supporting:
                accord_str = "; ".join(f"{s['case_name']}, {s['citation']}" for s in supporting[:2])
                output_parts.append(f"  Accord {accord_str}.")
            output_parts.append("")

    # Format persuasive rules
    if persuasive_rules:
        output_parts.append("PERSUASIVE AUTHORITY:")
        output_parts.append("")
        for primary, supporting in persuasive_rules[:5]:  # Limit persuasive
            paren = parentheticals.get(primary['case_name'], '')
            paren_str = f" ({paren})" if paren else ""

            output_parts.append(f"  {primary['text']}")
            output_parts.append(f"  See {primary['case_name']}, {primary['citation']}{paren_str}.")
            output_parts.append("")

    # -------------------------------------------------------------------------
    # STEP 5: Circuit split note (if applicable)
    # -------------------------------------------------------------------------
    if has_split:
        output_parts.append("-" * 70)
        output_parts.append("CIRCUIT SPLIT NOTE:")
        output_parts.append("")
        for pos in split_analysis.positions:
            binding_note = " [YOUR CIRCUIT]" if pos.is_binding else ""
            output_parts.append(f"  {pos.circuit}{binding_note}: {pos.rule_text[:150]}...")
        output_parts.append("")

    # -------------------------------------------------------------------------
    # STEP 6: Build APPLICATION SECTION
    # -------------------------------------------------------------------------
    output_parts.append("=" * 70)
    output_parts.append("APPLICATION SECTION")
    output_parts.append("=" * 70)
    output_parts.append("")

    # Always show client facts as starting point
    output_parts.append("YOUR CLIENT'S FACTS:")
    output_parts.append("")
    fact_sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', client_facts) if len(s.strip()) > 15]
    for i, fact in enumerate(fact_sentences[:10], 1):
        rec_cite = ""
        if record_cites:
            for fact_key, cite in record_cites.items():
                if fact_key.lower() in fact.lower():
                    rec_cite = f" ({cite})"
                    break
        output_parts.append(f"  {i}. {fact}{rec_cite}")
    output_parts.append("")

    # Map facts to elements if provided
    if elements:
        output_parts.append("ELEMENT-BY-ELEMENT ANALYSIS:")
        output_parts.append("")

        hierarchy = synthesize_rules_across_cases(precedent_cases, issue, jurisdiction)
        mappings = map_facts_to_rules(client_facts, hierarchy)

        # Group by element
        element_facts = defaultdict(list)
        for mapping in mappings:
            for elem in elements:
                if any(word in mapping.client_fact.lower() for word in elem.lower().split()):
                    element_facts[elem].append(mapping)
                    break
            else:
                element_facts['General'].append(mapping)

        for elem in elements:
            facts_for_elem = element_facts.get(elem, [])
            if facts_for_elem:
                output_parts.append(f"  [{elem.upper()}]")
                for mapping in facts_for_elem[:2]:
                    rec_cite = ""
                    if record_cites:
                        for fact_key, cite in record_cites.items():
                            if fact_key.lower() in mapping.client_fact.lower():
                                rec_cite = f" ({cite})"
                                break
                    output_parts.append(f"    - {mapping.client_fact}{rec_cite}")
                    output_parts.append(f"      Supports: {mapping.triggered_rules[0][0][:100]}..." if mapping.triggered_rules else "")
                output_parts.append("")
            else:
                gaps.append(f"No facts mapped to element: {elem}")

    # Generate analogies/distinctions
    app_drafts = generate_application_drafts(
        precedent_cases, client_facts, issue,
        elements=elements, record_cites=record_cites
    )

    has_draft_content = app_drafts and "Could not generate" not in app_drafts
    if has_draft_content:
        output_parts.append("DRAFT LANGUAGE:")
        output_parts.append("")
        output_parts.append(app_drafts)
    else:
        # Fallback: provide scaffolding with precedent fact summaries
        output_parts.append("SUGGESTED APPLICATION STRUCTURE:")
        output_parts.append("")
        output_parts.append("  (No automatic analogies/distinctions could be generated.")
        output_parts.append("   Use the precedent facts below to draft your application.)")
        output_parts.append("")

        for case_name, (text, citation) in list(precedent_cases.items())[:3]:
            # Extract key facts from precedent
            prec_facts = extract_case_facts(text)
            if prec_facts:
                output_parts.append(f"  {case_name} ({citation}) - Key Facts:")
                for pf in prec_facts[:3]:
                    output_parts.append(f"    - {pf[:150]}...")
                output_parts.append("")
                output_parts.append(f"  Template: \"Like {case_name}, where [precedent fact],")
                output_parts.append(f"             here [your client's fact]. {citation}.\"")
                output_parts.append("")

    # -------------------------------------------------------------------------
    # STEP 7: Citation block (ready to paste)
    # -------------------------------------------------------------------------
    output_parts.append("")
    output_parts.append("=" * 70)
    output_parts.append("CITATION BLOCK (ready to paste)")
    output_parts.append("=" * 70)
    output_parts.append("")

    # Build citation strings
    binding_cites = []
    persuasive_cites = []

    for case_name, (_, citation) in precedent_cases.items():
        _, circuit, _ = detect_court_level(citation)
        paren = parentheticals.get(case_name, '')

        full_cite = f"{case_name}, {citation}"
        if paren:
            full_cite += f" ({paren})"

        is_binding = False
        if circuit and _normalize_jurisdiction(circuit) == norm_jurisdiction:
            is_binding = True
        if 'U.S.' in citation or 'S. Ct.' in citation or 'S.Ct.' in citation:
            is_binding = True

        if is_binding:
            binding_cites.append(full_cite)
        else:
            persuasive_cites.append(full_cite)

    if binding_cites:
        output_parts.append(f"See {'; '.join(binding_cites)}.")
        output_parts.append("")

    if persuasive_cites:
        output_parts.append(f"See also {'; '.join(persuasive_cites[:3])}.")
        output_parts.append("")

    # -------------------------------------------------------------------------
    # STEP 8: Gaps identified
    # -------------------------------------------------------------------------
    if gaps:
        output_parts.append("-" * 70)
        output_parts.append("GAPS IDENTIFIED:")
        for gap in gaps:
            output_parts.append(f"  ⚠ {gap}")
        output_parts.append("")

    return '\n'.join(output_parts)


def generate_brief_section_from_files(
    case_files: Dict[str, str],
    issue: str,
    client_facts: str,
    jurisdiction: str,
    base_path: Optional[str] = None,
    elements: Optional[List[str]] = None,
    record_cites: Optional[Dict[str, str]] = None
) -> str:
    """
    Generate brief section by reading case files directly.

    Args:
        case_files: Dict of case_name -> file_path
        issue: Legal issue being briefed
        client_facts: Your client's factual situation
        jurisdiction: Target jurisdiction
        base_path: Optional base directory for relative paths
        elements: Optional list of legal elements
        record_cites: Optional dict mapping facts to record citations

    Returns:
        Ready-to-paste brief section
    """
    # Read all case files
    cases = read_project_cases(case_files, base_path)

    # Check for read errors
    errors = [name for name, (text, _) in cases.items() if text.startswith("Error")]
    if errors:
        return f"ERROR reading files: {', '.join(errors)}\n\nCheck file paths and try again."

    # Generate brief section
    return generate_brief_section(
        cases, issue, client_facts, jurisdiction,
        elements=elements, record_cites=record_cites
    )


# ============================================================================
# ANALYZE CASE FILE - Single Command Pipeline
# ============================================================================

def analyze_case_file(file_path: str) -> str:
    """
    Single-command analysis: read file + extract_rules + analyze_rules_deep + generate_parentheticals.
    Returns structured JSON with all extraction results.

    Args:
        file_path: Path to case file (PDF, DOCX, TXT)

    Returns:
        Formatted output with structured JSON containing:
        - rules: List of extracted rules with metadata
        - parenthetical: Generated parenthetical for the case
        - metadata: File info, case name, citation
    """
    import json
    import os

    result = {
        "file_path": file_path,
        "file_type": None,
        "case_name": None,
        "citation": None,
        "rules": [],
        "parenthetical": None,
        "error": None
    }

    # Step 1: Read the file
    try:
        text, file_type = read_case_file(file_path)
        result["file_type"] = file_type
    except Exception as e:
        result["error"] = f"Failed to read file: {str(e)}"
        return _format_case_file_output(result)

    if not text or len(text.strip()) < 100:
        result["error"] = "File is empty or too short to analyze"
        return _format_case_file_output(result)

    # Step 2: Extract case name from filename
    basename = os.path.basename(file_path)
    name_without_ext = os.path.splitext(basename)[0]
    # Convert "Tellabs v Makor" to "Tellabs v. Makor"
    case_name = re.sub(r'\s+v\s+', ' v. ', name_without_ext)
    case_name = re.sub(r'[_-]', ' ', case_name)
    result["case_name"] = case_name

    # Step 3: Try to extract citation from text
    citation = _extract_citation_from_text(text)
    result["citation"] = citation

    # Step 4: Extract rules using enhanced extraction
    try:
        rules = extract_enhanced_rules(text, case_name, citation)
        result["rules"] = [
            {
                "text": r.text,
                "rule_type": r.rule_type.value if hasattr(r.rule_type, 'value') else str(r.rule_type),
                "level": r.level.value if hasattr(r.level, 'value') else str(r.level),
                "pinpoint": r.pinpoint_page,
                "elements": r.elements,
                "factors": r.factors,
                "legal_area": r.legal_area
            }
            for r in rules
        ]
    except Exception as e:
        result["rules"] = []
        result["error"] = f"Rule extraction failed: {str(e)}"

    # Step 5: Generate parenthetical
    try:
        paren = generate_parenthetical(case_name, text, citation or "")
        if paren:
            result["parenthetical"] = {
                "text": paren.parenthetical_text,
                "type": paren.parenthetical_type,
                "full_citation": paren.full_citation
            }
    except Exception as e:
        result["parenthetical"] = None

    return _format_case_file_output(result)


def _format_case_file_output(result: dict) -> str:
    """Format analyze_case_file result as text + JSON."""
    import json

    output = []
    output.append("=" * 70)
    output.append("CASE FILE ANALYSIS")
    output.append("=" * 70)

    if result.get("error"):
        output.append(f"\nERROR: {result['error']}")
        output.append("")

    output.append(f"\nFile: {result['file_path']}")
    output.append(f"Type: {result.get('file_type', 'unknown')}")
    output.append(f"Case: {result.get('case_name', 'Unknown')}")
    output.append(f"Citation: {result.get('citation', 'Not found')}")

    # Rules summary
    rules = result.get("rules", [])
    output.append(f"\n--- RULES EXTRACTED ({len(rules)}) ---")

    for i, rule in enumerate(rules[:10], 1):
        rule_type = rule.get("rule_type", "unknown").upper()
        text = rule.get("text", "")[:200]
        if len(rule.get("text", "")) > 200:
            text += "..."
        output.append(f"\n{i}. [{rule_type}] {text}")
        if rule.get("pinpoint"):
            output.append(f"   Pinpoint: {rule['pinpoint']}")

    # Parenthetical
    paren = result.get("parenthetical")
    output.append("\n--- PARENTHETICAL ---")
    if paren:
        output.append(f"({paren.get('text', '')})")
        output.append(f"Full: {paren.get('full_citation', '')}")
    else:
        output.append("(No parenthetical generated)")

    # JSON data
    output.append("\n" + "=" * 70)
    output.append("JSON_DATA:")
    output.append(json.dumps(result, indent=2, default=str))

    return "\n".join(output)


# ============================================================================
# TEST
# ============================================================================

if __name__ == "__main__":
    # Quick test
    test_cases = {
        "Tellabs v. Makor": (
            """The Supreme Court held that courts must consider the complaint in its entirety
            when evaluating whether it states a strong inference of scienter. The inference
            must be more than merely plausible—it must be cogent and at least as compelling
            as any opposing inference.""",
            "551 U.S. 308 (2007)"
        ),
        "City of Livonia v. Boeing": (
            """The Seventh Circuit applied Tellabs and held that the strong inference standard
            requires plaintiffs to allege facts showing defendants knew statements were false
            when made. The court must weigh competing inferences.""",
            "711 F.3d 754 (7th Cir. 2013)"
        )
    }

    result = synthesize_brief_rules(test_cases, "scienter pleading", "5th Circuit")
    print(result)
