"""
Enhanced Rule Extraction with Hierarchy and Cross-References
============================================================
Builds a rule knowledge structure with:
- Rule hierarchy (primary rules, sub-rules, exceptions)
- Rule type classification (bright-line, balancing, multi-factor, totality)
- Cross-references to establishing cases
- Conflicting rule detection
- Policy rationale extraction
"""

import re
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set, Tuple, Any
from enum import Enum
from collections import defaultdict


class RuleType(Enum):
    """Classification of rule structure."""
    HOLDING = "holding"                 # Court's actual decision/conclusion
    BRIGHT_LINE = "bright_line"         # Clear yes/no test
    BALANCING = "balancing"             # Weighing competing interests
    MULTI_FACTOR = "multi_factor"       # List of factors to consider
    TOTALITY = "totality"               # Totality of circumstances
    ELEMENTS = "elements"               # Required elements to prove
    STANDARD = "standard"               # Standard of review/proof
    PRESUMPTION = "presumption"         # Rebuttable or irrebuttable presumption
    DEFINITION = "definition"           # Defining a legal term
    TEST = "test"                       # Legal test to apply


class RuleLevel(Enum):
    """Position in rule hierarchy."""
    PRIMARY = "primary"                 # Main governing rule
    SUB_RULE = "sub_rule"              # Rule that refines primary
    EXCEPTION = "exception"             # Exception to a rule
    LIMITATION = "limitation"           # Limitation on rule scope
    COROLLARY = "corollary"            # Derived from another rule


class RuleStrength(Enum):
    """How firmly established the rule is."""
    MANDATORY = "mandatory"             # Must be followed
    DEFAULT = "default"                 # Applies unless exception
    PERMISSIVE = "permissive"          # May be applied
    EMERGING = "emerging"              # Newly developing
    DISPUTED = "disputed"              # Subject to circuit split


@dataclass
class EnhancedRule:
    """Rich rule object with full metadata."""
    text: str
    source_case: str
    source_citation: Optional[str] = None
    pinpoint_page: Optional[str] = None  # Specific page where rule appears

    # Classification
    rule_type: RuleType = RuleType.ELEMENTS
    level: RuleLevel = RuleLevel.PRIMARY
    strength: RuleStrength = RuleStrength.MANDATORY

    # Structure
    elements: List[str] = field(default_factory=list)
    factors: List[str] = field(default_factory=list)

    # Hierarchy
    parent_rule: Optional[str] = None  # Text of rule this modifies
    child_rules: List[str] = field(default_factory=list)
    exceptions: List[str] = field(default_factory=list)

    # Context
    legal_area: str = ""  # e.g., "contract", "tort", "constitutional"
    key_terms: List[str] = field(default_factory=list)

    # Policy
    policy_rationale: str = ""

    # Cross-references
    supporting_cases: List[str] = field(default_factory=list)
    conflicting_rules: List[Tuple[str, str]] = field(default_factory=list)  # (rule_text, source)

    # Surrounding context
    context_before: str = ""
    context_after: str = ""

    def to_dict(self) -> Dict:
        """Convert to dictionary for JSON serialization."""
        return {
            'text': self.text,
            'source_case': self.source_case,
            'source_citation': self.source_citation,
            'rule_type': self.rule_type.value,
            'level': self.level.value,
            'strength': self.strength.value,
            'elements': self.elements,
            'factors': self.factors,
            'parent_rule': self.parent_rule,
            'exceptions': self.exceptions,
            'legal_area': self.legal_area,
            'key_terms': self.key_terms,
            'policy_rationale': self.policy_rationale,
            'supporting_cases': self.supporting_cases,
            'conflicting_rules': self.conflicting_rules
        }


# ============================================================================
# VALIDATION FUNCTIONS
# ============================================================================

def is_complete_legal_statement(text: str) -> bool:
    """
    Check if text appears to be a complete legal statement, not a fragment.

    This is now more permissive to avoid rejecting valid rules,
    but filters out factual narrative statements.
    """
    text = text.strip()

    if not text:
        return False

    # Reject if starts with parenthetical or is a citation
    if text.startswith('(') or re.match(r'^\d+\s+[A-Z]\.?\s', text):
        return False

    # Must have at least 4 words (more permissive than before)
    if text.count(' ') < 3:
        return False

    # Reject obvious fragments - starts lowercase (unless it's a common legal term)
    if text[0].islower() and not text.startswith(('a ', 'an ', 'the ')):
        # Check if first word might be a continuation
        first_word = text.split()[0].lower()
        if first_word in {'and', 'or', 'but', 'which', 'that', 'where', 'who', 'whose', 'by', 'to', 'from', 'with'}:
            return False

    # Reject factual narrative statements (not legal rules)
    # These describe what happened in a case, not what the law is
    factual_narrative_patterns = [
        r'^(?:by|to|from|with|at|on|in|for)\s+(?:the\s+)?(?:prosecution|defense|trial|hearing)',
        r'(?:did not come to|was (?:not )?(?:shown|disclosed|revealed))',
        r'(?:was|were)\s+(?:tried|convicted|sentenced|arrested|charged)',
        r'(?:after|before|during|until)\s+(?:he|she|they|the\s+(?:petitioner|defendant))',
        r'^(?:he|she|they|it)\s+(?:was|were|had|did)',
        r'(?:testified|stated|claimed|alleged|argued)\s+(?:that|at)',
    ]
    for pattern in factual_narrative_patterns:
        if re.search(pattern, text, re.IGNORECASE):
            return False

    # Accept if it has legal rule keywords indicating substance
    legal_rule_indicators = [
        r'\b(?:must|shall|should|may|requires?)\b',
        r'\b(?:prove|show|establish|demonstrate)\b',
        r'\b(?:violates?|prohibits?|guarantees?|mandates?)\b',
        r'\b(?:standard|test|burden|element|factor)\b',
        r'\b(?:is\s+(?:required|necessary|sufficient|material))\b',
        r'\b(?:due\s+process|equal\s+protection)\b',
    ]

    for pattern in legal_rule_indicators:
        if re.search(pattern, text, re.IGNORECASE):
            return True

    # Accept if it has party references with legal action verbs
    party_legal_patterns = [
        r'\b(?:plaintiff|defendant|party|claimant|petitioner|movant)\s+(?:must|shall|may|bears?|has)\b',
        r'\b(?:court|courts?)\s+(?:must|shall|should|may|holds?|finds?)\b',
    ]

    for pattern in party_legal_patterns:
        if re.search(pattern, text, re.IGNORECASE):
            return True

    # If none of the above, still accept if it's reasonably long and sentence-like
    return len(text) >= 80 and text[0].isupper()


# ============================================================================
# RULE TYPE DETECTION PATTERNS
# ============================================================================

RULE_TYPE_PATTERNS = {
    RuleType.HOLDING: [
        r'(?:we |the court )(?:hold|held|holds) that',
        r'(?:we |the court )(?:conclude|concluded|concludes) that',
        r'(?:we |the court )(?:find|found|finds) that',
        r'(?:accordingly|therefore),? (?:we |the court )?(?:hold|conclude|affirm|reverse)',
        r'(?:judgment|decision) (?:is |of the court )',
    ],
    RuleType.TEST: [
        r'(?:the )?test (?:is|for|requires)',
        r'(?:under|applying) (?:the|this) test',
        r'(?:two|three|four|multi)[- ](?:prong|part|step) (?:test|analysis|inquiry)',
        r'(?:inquiry|analysis) (?:is|requires|involves)',
    ],
    RuleType.BRIGHT_LINE: [
        r'(?:per se|automatically|always|never|categorically)',
        r'(?:bright[- ]line|clear[- ]cut|absolute)',
        r'(?:is conclusively|shall be)',
    ],
    RuleType.BALANCING: [
        r'(?:balanc(?:e|ing)|weigh(?:s|ing)|competing)',
        r'(?:interests? (?:of|in)|consideration of)',
        r'(?:on (?:the )?one hand.*on the other)',
    ],
    RuleType.MULTI_FACTOR: [
        r'(?:factors? (?:include|are|to consider))',
        r'(?:courts? (?:consider|look to|examine))',
        r'(?:non-?exclusive (?:list|factors))',
        r'(?:among (?:the|other) (?:factors|things))',
    ],
    RuleType.TOTALITY: [
        r'(?:totality of (?:the )?circumstances)',
        r'(?:all (?:relevant )?(?:factors|circumstances))',
        r'(?:no single factor (?:is )?(?:determinative|dispositive))',
    ],
    RuleType.ELEMENTS: [
        r'(?:elements? (?:of|are|include))',
        r'(?:must (?:prove|show|establish|demonstrate))',
        r'(?:requires? (?:proof|showing|demonstration) (?:of|that))',
        r'(?:to (?:prevail|succeed|recover).*must)',
    ],
    RuleType.STANDARD: [
        r'(?:standard (?:of|is|for))',
        r'(?:de novo|clear error|abuse of discretion|substantial evidence)',
        r'(?:burden (?:of|is).*(?:proof|persuasion))',
        r'(?:preponderance|beyond.*reasonable doubt|clear and convincing)',
    ],
    RuleType.PRESUMPTION: [
        r'(?:presum(?:ed|ption|ptively))',
        r'(?:rebuttable|irrebuttable)',
        r'(?:burden shifts?)',
    ],
    RuleType.DEFINITION: [
        r'(?:is defined as|means|refers to)',
        r'(?:for purposes? of.*is|the term.*means)',
    ],
}

COMPILED_TYPE_PATTERNS = {
    rt: [re.compile(p, re.IGNORECASE) for p in patterns]
    for rt, patterns in RULE_TYPE_PATTERNS.items()
}


# ============================================================================
# RULE LEVEL DETECTION PATTERNS
# ============================================================================

RULE_LEVEL_PATTERNS = {
    RuleLevel.EXCEPTION: [
        r'(?:except(?:ion)?|unless|however|notwithstanding|but (?:if|when|where))',
        r'(?:does not apply (?:to|when|if|where))',
        r'(?:excluded from|exempt from)',
    ],
    RuleLevel.LIMITATION: [
        r'(?:limit(?:ed|s|ation)|only (?:applies?|when|if|to))',
        r'(?:restricted to|confined to)',
        r'(?:does not extend to|narrow(?:ly|ed))',
    ],
    RuleLevel.SUB_RULE: [
        r'(?:more specifically|in particular|furthermore)',
        r'(?:in applying this (?:rule|test|standard))',
        r'(?:the first (?:element|factor|prong)|the second)',
    ],
}

COMPILED_LEVEL_PATTERNS = {
    level: [re.compile(p, re.IGNORECASE) for p in patterns]
    for level, patterns in RULE_LEVEL_PATTERNS.items()
}


# ============================================================================
# STRENGTH DETECTION PATTERNS
# ============================================================================

STRENGTH_PATTERNS = {
    RuleStrength.MANDATORY: [
        r'(?:must|shall|required?|mandatory)',
        r'(?:binding|controlling)',
        r'(?:well[- ]?(?:established|settled))',
    ],
    RuleStrength.PERMISSIVE: [
        r'(?:may|can|permitted|discretion(?:ary)?)',
        r'(?:courts? (?:may|have discretion))',
    ],
    RuleStrength.EMERGING: [
        r'(?:emerging|developing|recent(?:ly)?)',
        r'(?:trend|modern (?:approach|view))',
    ],
    RuleStrength.DISPUTED: [
        r'(?:circuit split|courts? (?:are )?divided)',
        r'(?:some courts?.*other courts?)',
        r'(?:disagreement|conflict(?:ing)?)',
    ],
}

COMPILED_STRENGTH_PATTERNS = {
    strength: [re.compile(p, re.IGNORECASE) for p in patterns]
    for strength, patterns in STRENGTH_PATTERNS.items()
}


# ============================================================================
# POLICY RATIONALE PATTERNS
# ============================================================================

POLICY_PATTERNS = [
    r'(?:the (?:purpose|rationale|reason) (?:for|behind|of) (?:this|the) (?:rule|requirement) is) (.+?)(?:\.|$)',
    r'(?:this (?:rule|requirement) (?:serves|promotes|protects|ensures)) (.+?)(?:\.|$)',
    r'(?:the (?:policy|concern|interest) (?:underlying|behind)) (.+?)(?:\.|$)',
    r'(?:to (?:ensure|protect|promote|further|preserve)) (.+?)(?:\.|$)',
    r'(?:because (?:the law|courts?|society) (?:seeks?|wants?|needs?)) (.+?)(?:\.|$)',
]

COMPILED_POLICY_PATTERNS = [re.compile(p, re.IGNORECASE) for p in POLICY_PATTERNS]


# ============================================================================
# LEGAL ABBREVIATION HANDLING
# ============================================================================

# Common legal abbreviations that should NOT be treated as sentence endings
LEGAL_ABBREVIATIONS = [
    # Court reporters
    'U.S.', 'S.Ct.', 'L.Ed.', 'F.2d', 'F.3d', 'F.4th', 'F.Supp.', 'F.Supp.2d', 'F.Supp.3d',
    'So.2d', 'So.3d', 'N.E.', 'N.E.2d', 'N.W.', 'N.W.2d', 'S.E.', 'S.E.2d', 'S.W.', 'S.W.2d', 'S.W.3d',
    'A.2d', 'A.3d', 'P.2d', 'P.3d', 'Cal.Rptr.', 'N.Y.S.', 'N.Y.S.2d',
    # Circuit abbreviations
    'Cir.', '1st', '2d', '2nd', '3d', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th', '11th',
    'D.C.', 'Fed.', 'App.', 'Dist.', 'Ct.', 'Bankr.',
    # Common legal
    'v.', 'vs.', 'No.', 'Nos.', 'Inc.', 'Corp.', 'Ltd.', 'Co.', 'L.L.C.', 'L.P.',
    'e.g.', 'i.e.', 'et al.', 'cf.', 'id.', 'Id.', 'ibid.', 'Ibid.',
    'U.S.C.', 'C.F.R.', 'Fed.R.Civ.P.', 'Fed.R.Evid.', 'Stat.',
    # Titles
    'Mr.', 'Mrs.', 'Ms.', 'Dr.', 'Jr.', 'Sr.', 'Prof.', 'Hon.', 'Rev.',
    # Other common
    'Jan.', 'Feb.', 'Mar.', 'Apr.', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.',
    'approx.', 'est.', 'para.', 'paras.', 'sec.', 'secs.', 'art.', 'arts.',
    'n.', 'nn.', 'p.', 'pp.', 'ch.', 'chs.', 'pt.', 'pts.',
]

# Pre-compiled pattern for protecting abbreviations
_ABBREV_PATTERN = re.compile(
    r'\b(' + '|'.join(re.escape(abbr) for abbr in LEGAL_ABBREVIATIONS) + r')',
    re.IGNORECASE
)


def protect_abbreviations(text: str) -> str:
    """Replace periods in known abbreviations with a placeholder."""
    # Protect known abbreviations
    result = _ABBREV_PATTERN.sub(lambda m: m.group(0).replace('.', '\x00'), text)
    # Also protect patterns like "123 F.3d 456" - reporter citations
    result = re.sub(r'(\d+)\s+([A-Z][a-z]*\.(?:\d[a-z]*)?)\s+(\d+)',
                    lambda m: m.group(0).replace('.', '\x00'), result)
    return result


def restore_abbreviations(text: str) -> str:
    """Restore protected periods."""
    return text.replace('\x00', '.')


def _is_sentence_boundary(text: str, period_pos: int) -> bool:
    """
    Check if a period at the given position is a real sentence boundary.

    Returns True if the period ends a sentence, False if it's likely an abbreviation.
    """
    if period_pos < 0 or period_pos >= len(text):
        return True  # Edge case

    # Check what comes after the period
    if period_pos + 1 >= len(text):
        return True  # End of text is a sentence boundary

    next_char = text[period_pos + 1]

    # Newline = sentence boundary
    if next_char in '\n\r':
        return True

    # Space followed by uppercase = likely sentence boundary
    if next_char == ' ' and period_pos + 2 < len(text):
        char_after_space = text[period_pos + 2]
        if char_after_space.isupper() or char_after_space in '"\'(':
            # But check if the text before the period is a known abbreviation
            before_period = text[max(0, period_pos - 10):period_pos + 1].lower()
            abbrev_patterns = [
                r'u\.s\.$', r's\.ct\.$', r'f\.\d[a-z]*\.$', r'supp\.$',
                r'cir\.$', r'v\.$', r'no\.$', r'inc\.$', r'corp\.$', r'ltd\.$',
                r'co\.$', r'e\.g\.$', r'i\.e\.$', r'cf\.$', r'et al\.$',
                r'id\.$', r'n\.d\.$', r's\.d\.$', r'e\.d\.$', r'w\.d\.$',
                r'cal\.$', r'n\.y\.$', r'tex\.$', r'ill\.$', r'fla\.$',
                r'app\.$', r'rev\.$', r'stat\.$', r'ann\.$',
            ]
            for abbrev in abbrev_patterns:
                if re.search(abbrev, before_period):
                    return False
            return True

    # Space followed by lowercase = probably not a sentence boundary
    if next_char == ' ' and period_pos + 2 < len(text):
        char_after_space = text[period_pos + 2]
        if char_after_space.islower():
            return False

    # Period followed by another period or letter = abbreviation continuation
    if next_char.isalpha() or next_char == '.':
        return False

    return True


def find_sentence_end(text: str, start_pos: int) -> int:
    """
    Find the true end of a sentence in legal text, accounting for abbreviations.

    Args:
        text: The full text
        start_pos: Position to start searching from

    Returns:
        Position of the sentence-ending period (or end of text)
    """
    protected = protect_abbreviations(text)

    # Find next period that's actually a sentence end
    pos = start_pos
    while pos < len(protected):
        period_pos = protected.find('.', pos)
        if period_pos == -1:
            return len(text)

        # Check if this period ends a sentence
        # It's a sentence end if followed by: space+capital, newline, or end of text
        if period_pos + 1 >= len(protected):
            return period_pos
        next_char = protected[period_pos + 1]
        if next_char in '\n\r':
            return period_pos
        if next_char == ' ' and period_pos + 2 < len(protected):
            char_after_space = protected[period_pos + 2]
            if char_after_space.isupper() or char_after_space == '"' or char_after_space == '(':
                return period_pos

        pos = period_pos + 1

    return len(text)


def expand_to_sentence(text: str, match_start: int, match_end: int) -> str:
    """
    Expand a matched fragment to the full sentence.

    Args:
        text: Full text
        match_start: Start of the match
        match_end: End of the match

    Returns:
        The complete sentence containing the match
    """
    # Find sentence start (go back to previous sentence end + 1)
    sentence_start = match_start
    for i in range(match_start - 1, max(0, match_start - 500), -1):
        if text[i] == '.' and i + 1 < len(text):
            # Check if this was a real sentence end
            if i + 2 < len(text) and text[i + 1] == ' ' and text[i + 2].isupper():
                sentence_start = i + 2
                break
        elif text[i] == '\n':
            sentence_start = i + 1
            break

    # Find sentence end
    sentence_end = find_sentence_end(text, match_end)

    # Extract and clean
    sentence = text[sentence_start:sentence_end + 1].strip()
    return sentence


# ============================================================================
# MAIN RULE PATTERNS
# ============================================================================

RULE_EXTRACTION_PATTERNS = [
    # ===== HOLDINGS (how courts state what they decided) =====
    # Standard holding patterns
    (r'(?:we |the court )?hold(?:s|ing)? that (.+?)\.', 'holding'),
    (r'(?:we |the court )?conclude(?:s|d)? that (.+?)\.', 'holding'),
    (r'(?:we |the court )?find(?:s|ing)? that (.+?)\.', 'holding'),
    (r'(?:we |the court )?determine(?:s|d)? that (.+?)\.', 'holding'),
    (r'we (?:have )?(?:repeatedly |consistently )?held that (.+?)\.', 'holding'),
    (r'this (?:court|circuit) has (?:held|established|recognized) that (.+?)\.', 'holding'),

    # Supreme Court "Held:" format
    (r'[Hh]eld[:\s]+(?:that\s+)?(.{50,500}?)(?:\.|$)', 'holding'),

    # SCOTUS-specific patterns
    (r'[Ww]e\s+now\s+hold\s+(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),
    (r'[Tt]he\s+[Cc]ourt\s+holds?\s+(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),
    (r'[Tt]his\s+[Cc]ourt\s+(?:has\s+)?held\s+(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),
    (r'[Oo]ur\s+(?:holding|decision)\s+(?:is\s+)?(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),

    # Explicit "we hold" variations
    (r'[Ww]e\s+hold\s+(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),
    (r'[Ww]e\s+therefore\s+hold\s+(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),
    (r'[Aa]ccordingly,?\s+we\s+hold\s+(?:that\s+)?(.{40,400}?)(?:\.|$)', 'holding'),

    # Constitutional holdings (common in SCOTUS)
    (r'(?:the\s+)?(?:due\s+process|equal\s+protection|first\s+amendment|fourth\s+amendment)\s+(?:clause\s+)?(?:requires?|prohibits?|guarantees?)\s+(?:that\s+)?(.{40,300}?)(?:\.|$)', 'holding'),
    (r'violates?\s+(?:the\s+)?(?:due\s+process|constitution|constitutional\s+rights?)\s+(?:when|where|if)\s+(.{40,300}?)(?:\.|$)', 'holding'),

    # Enumerated holdings (First, Second, Third)
    (r'[Ff]irst,?\s+(.{30,300}?)(?:\.|[Ss]econd)', 'holding'),
    (r'[Ss]econd,?\s+(.{30,300}?)(?:\.|[Tt]hird)', 'holding'),
    (r'[Tt]hird,?\s+(.{30,300}?)(?:\.|[Ff]ourth|\n\n)', 'holding'),

    # "establishes the following prescriptions" pattern
    (r'(?:establishes?|sets?\s+forth)\s+the\s+following\s+(?:prescriptions?|requirements?|rules?)[:\s]+(.{50,600}?)(?:\n\n|$)', 'holding'),

    # ===== RULES AND STANDARDS (how courts state the law) =====
    (r'(?:the )?(?:applicable |governing |controlling )?(?:rule|standard|test) (?:is|requires|provides) that (.+?)\.', 'rule'),
    (r'(?:under |applying |pursuant to )(?:the |this )?(.+?)(?:test|standard|analysis|framework)', 'rule'),
    (r'(?:the )?(?:legal |applicable )?standard (?:for|governing|applicable to) .+? (?:is|requires) (.+?)\.', 'rule'),
    (r'(?:the )?(?:general |well[- ]established |long[- ]standing )?(?:rule|principle) is that (.+?)\.', 'rule'),
    (r'(?:it is )?(?:well[- ])?(?:established|settled|black[- ]letter) (?:law )?that (.+?)\.', 'rule'),
    (r'(?:under |in )(?:the |this )?(?:circuit|jurisdiction),? (.+?)\.', 'rule'),

    # ===== ELEMENTS (what must be proven) =====
    (r'(?:the )?elements? (?:of .+? )?(?:are|include|consist of):?\s*(.+?)\.', 'elements'),
    (r'(?:to (?:establish|prove|show|demonstrate|prevail on) .+?,? )?(?:a |the )?(?:plaintiff|claimant|party|movant) must (?:show|prove|establish|demonstrate) (.+?)\.', 'elements'),
    (r'(?:a )?(?:claim|cause of action) (?:for|of) .+? requires (?:proof of |a showing of |that )?(.+?)\.', 'elements'),
    (r'(?:to |in order to )(?:succeed|prevail|recover|establish|prove) .+?,? (?:the |a )?(?:plaintiff|party|claimant) must (.+?)\.', 'elements'),
    (r'requires? (?:a )?(?:showing|proof|demonstration) (?:of |that )(.+?)\.', 'elements'),
    (r'liability (?:attaches|arises|exists) (?:only )?(?:when|where|if) (.+?)\.', 'elements'),
    (r'(?:the )?(?:dispositive|key|critical|essential) (?:inquiry|question|issue) is (?:whether )?(.+?)\.', 'elements'),

    # ===== SIMPLE "MUST" PATTERNS (catch more rules) =====
    # Simple plaintiff/defendant must patterns
    (r'(?:a |the )?(?:plaintiff|petitioner|claimant|private plaintiff)s? must (?:prove|plead|show|establish|demonstrate|allege) (?:that )?(.+?)\.', 'elements'),
    (r'(?:a |the )?(?:defendant|respondent)s? must (?:prove|show|establish|demonstrate) (?:that )?(.+?)\.', 'elements'),
    # Court duty patterns
    (r'(?:the |a )?court must (?:accept|consider|determine|evaluate|assess|weigh|examine|engage in) (.+?)\.', 'rule'),
    (r'(?:the |a )?(?:reviewing |appellate |district |trial )?court must (?:accept|consider|determine) (.+?)\.', 'rule'),
    (r'courts? (?:should|shall|must) (?:consider|weigh|evaluate|assess|examine|determine) (.+?)\.', 'rule'),
    # Comparative evaluation patterns (Tellabs-specific)
    (r'(?:the )?court (?:must )?engage[s]? in (?:a )?comparative (?:evaluation|analysis|assessment) (.+?)\.', 'rule'),
    (r'(?:in |when )(?:determining|assessing|evaluating) .+?,? (?:the )?court must (?:consider|weigh) (.+?)\.', 'rule'),

    # ===== FACTORS (what courts consider) =====
    (r'(?:the )?(?:relevant |applicable |pertinent )?factors? (?:include|are|to be considered|weighed):?\s*(.+?)\.', 'factors'),
    (r'(?:courts? |we )?(?:consider|look to|examine|weigh|evaluate|assess) (?:the following |several |various )?(.+?)\.', 'factors'),
    (r'(?:in determining|when assessing|to evaluate) .+?,? courts? (?:consider|examine|look to) (.+?)\.', 'factors'),
    (r'(?:the )?(?:relevant |applicable )?considerations (?:include|are) (.+?)\.', 'factors'),

    # ===== REQUIREMENTS AND BURDENS =====
    (r'(?:the |a )?(?:plaintiff|party|claimant|movant) (?:bears|has|carries) the burden (?:of|to) (.+?)\.', 'elements'),
    (r'(?:the )?burden (?:of proof|to prove|of establishing) .+? (?:is|requires|rests on) (.+?)\.', 'elements'),
    (r'(?:to |in order to )satisfy (?:this|the) (?:requirement|element|standard|burden),? (.+?)\.', 'elements'),

    # ===== EXPLANATORY STATEMENTS =====
    (r'(?:the court|we) explained that (.+?)\.', 'holding'),
    (r'(?:the court|we) (?:has|have) (?:made clear|emphasized|stressed|noted) that (.+?)\.', 'holding'),
    (r'(?:as|because) (?:the court|we) (?:explained|noted|observed|recognized) in .+?,? (.+?)\.', 'holding'),

    # ===== INFERENCE PATTERNS (for scienter, intent, etc.) =====
    (r'(?:a |an )?(?:strong |cogent |compelling )?inference (?:of .+? )?(?:arises|exists|may be drawn) (?:when|where|if) (.+?)\.', 'rule'),
    (r'(?:to |in order to )(?:draw|support|establish) (?:a |an |the )?(?:strong )?inference (?:of .+?)?,? (.+?)\.', 'elements'),
]

COMPILED_RULE_PATTERNS = [
    (re.compile(p, re.IGNORECASE | re.DOTALL), t)
    for p, t in RULE_EXTRACTION_PATTERNS
]


# ============================================================================
# LEGAL AREA DETECTION
# ============================================================================

LEGAL_AREA_KEYWORDS = {
    'contract': ['contract', 'agreement', 'breach', 'consideration', 'offer', 'acceptance', 'warranty'],
    'tort': ['negligence', 'duty', 'breach', 'causation', 'damages', 'tort', 'injury', 'reasonable care'],
    'constitutional': ['constitution', 'amendment', 'due process', 'equal protection', 'first amendment', 'fourth amendment'],
    'criminal': ['criminal', 'felony', 'misdemeanor', 'intent', 'mens rea', 'actus reus', 'prosecution'],
    'property': ['property', 'title', 'deed', 'easement', 'covenant', 'landlord', 'tenant'],
    'civil_procedure': ['jurisdiction', 'venue', 'motion', 'discovery', 'summary judgment', 'pleading'],
    'evidence': ['evidence', 'hearsay', 'relevance', 'witness', 'testimony', 'privilege'],
    'administrative': ['agency', 'regulation', 'rulemaking', 'administrative', 'deference', 'chevron'],
}


# ============================================================================
# PINPOINT PAGE EXTRACTION
# ============================================================================

def extract_pinpoint_page(context: str, match_position: int, full_text: str) -> Optional[str]:
    """
    Extract the pinpoint page number where a rule appears in the case text.

    Looks for page markers in legal documents:
    - Star pagination (*123)
    - "at 123" references
    - "Id. at 123"
    - Slip opinion markers (slip op. at 5)
    - Page numbers in running text

    Args:
        context: Text surrounding the rule
        match_position: Character position of the rule in full_text
        full_text: Complete case text

    Returns:
        Pinpoint page string (e.g., "324") or None
    """
    # Search in context around the rule (prefer before, as page markers often precede text)
    search_start = max(0, match_position - 500)
    search_end = min(len(full_text), match_position + 200)
    search_text = full_text[search_start:search_end]

    # Pattern priority order - most reliable first
    pinpoint_patterns = [
        # Star pagination (Westlaw/Lexis format): *324 or **324
        r'\*+(\d{1,4})\b',

        # "at [page]" - common in legal text
        r'\bat\s+(\d{1,4})\b(?!\s*(?:F\.|U\.S\.|S\.Ct\.|L\.Ed))',

        # Id. at [page]
        r'Id\.?\s+at\s+(\d{1,4})',

        # Slip opinion: slip op. at 5
        r'slip\s+op\.?\s+at\s+(\d{1,3})',

        # Page in parenthetical with year: "551 U.S. 308, 324 (2007)"
        # Look for second number after comma in citation pattern
        r'U\.S\.\s+\d+,\s*(\d+)',
        r'F\.\d[dh]\s+\d+,\s*(\d+)',
        r'S\.Ct\.\s+\d+,\s*(\d+)',
    ]

    # Find the page marker closest to the rule
    best_page = None
    best_distance = float('inf')
    rule_pos_in_search = match_position - search_start

    for pattern in pinpoint_patterns:
        for match in re.finditer(pattern, search_text, re.IGNORECASE):
            page_num = match.group(1)
            # Validate it looks like a real page number (not a year or case number)
            if page_num.isdigit():
                page_int = int(page_num)
                # Filter out likely years (1900-2030) and very small numbers
                if page_int >= 1 and (page_int < 1900 or page_int > 2030):
                    distance = abs(match.start() - rule_pos_in_search)
                    if distance < best_distance:
                        best_distance = distance
                        best_page = page_num

    return best_page


def format_citation_with_pinpoint(base_citation: str, pinpoint: Optional[str]) -> str:
    """
    Format a citation with pinpoint page if available.

    Examples:
        "551 U.S. 308 (2007)" + "324" -> "551 U.S. 308, 324 (2007)"
        "123 F.3d 456 (5th Cir. 2009)" + "460" -> "123 F.3d 456, 460 (5th Cir. 2009)"
    """
    if not pinpoint or not base_citation:
        return base_citation

    # Check if pinpoint already exists in citation
    if f", {pinpoint}" in base_citation or f",{pinpoint}" in base_citation:
        return base_citation

    # Insert pinpoint before parenthetical
    paren_match = re.search(r'\s*\([^)]+\)\s*$', base_citation)
    if paren_match:
        pre_paren = base_citation[:paren_match.start()]
        paren = paren_match.group()
        return f"{pre_paren}, {pinpoint}{paren}"

    # No parenthetical, append to end
    return f"{base_citation}, {pinpoint}"


# ============================================================================
# MAIN EXTRACTION FUNCTION
# ============================================================================

def extract_enhanced_rules(text: str, source_case: str = "Unknown",
                           source_citation: str = None) -> List[EnhancedRule]:
    """
    Extract rules with full hierarchy and classification.

    Args:
        text: Case opinion or brief text
        source_case: Name of the source case
        source_citation: Optional citation string

    Returns:
        List of EnhancedRule objects
    """
    rules: List[EnhancedRule] = []
    seen_rules: Set[str] = set()  # Avoid duplicates

    # Clean text
    clean_text = text.replace('\r\n', '\n').replace('\r', '\n')

    # Extract rules using patterns
    for pattern, pattern_type in COMPILED_RULE_PATTERNS:
        for match in pattern.finditer(clean_text):
            rule_text = match.group(1).strip()

            # Skip very short rules - must be substantial
            if len(rule_text) < 50:
                continue

            # ALWAYS verify we have a complete sentence by checking if the match
            # ended at a real sentence boundary (not an abbreviation like U.S. or F.3d)
            match_end_pos = match.end() - 1  # Position of the period in the pattern

            # Check if this period was a real sentence end
            is_real_sentence_end = _is_sentence_boundary(clean_text, match_end_pos)

            if not is_real_sentence_end:
                # The regex stopped at an abbreviation - expand to real sentence end
                real_end = find_sentence_end(clean_text, match_end_pos + 1)
                if real_end > match_end_pos and real_end < match_end_pos + 500:
                    # Extract the extended text
                    extended_text = clean_text[match.start(1):real_end].strip()
                    if len(extended_text) < 1000:  # Sanity check
                        rule_text = extended_text

            # Also check for common truncation indicators
            truncation_indicators = (
                rule_text.endswith((' and', ' or', ' the', ' a', ' an', ' to', ' of', ' in', ' for', ' that', ' which')),
                len(rule_text) < 80 and rule_text[-1] not in '.!?',
                re.search(r'(?:e\.g|i\.e|cf|see)\s*$', rule_text, re.IGNORECASE),
            )

            if any(truncation_indicators):
                # Try to expand to full sentence
                expanded = expand_to_sentence(clean_text, match.start(), match.end())
                if len(expanded) > len(rule_text) + 10 and len(expanded) < 1000:
                    # Extract just the rule part (after "hold that", "conclude that", etc.)
                    rule_intro_match = re.search(
                        r'(?:hold(?:s|ing)?|conclude[sd]?|find(?:s|ing)?|determine[sd]?|established?|require[sd]?)\s+that\s+',
                        expanded, re.IGNORECASE
                    )
                    if rule_intro_match:
                        rule_text = expanded[rule_intro_match.end():].strip()
                    elif expanded.endswith('.'):
                        rule_text = expanded

            # Final cleanup - remove trailing period for consistency
            rule_text = rule_text.rstrip('.')

            # Skip fragments that don't look like complete legal statements
            # Must contain a verb and not start mid-sentence
            if not is_complete_legal_statement(rule_text):
                continue

            # Normalize for deduplication
            normalized = ' '.join(rule_text.lower().split())
            if normalized in seen_rules:
                continue
            seen_rules.add(normalized)

            # Get context
            start_pos = max(0, match.start() - 300)
            end_pos = min(len(clean_text), match.end() + 300)
            context_before = clean_text[start_pos:match.start()].strip()
            context_after = clean_text[match.end():end_pos].strip()
            full_context = clean_text[start_pos:end_pos]

            # Detect rule type
            rule_type = detect_rule_type(rule_text, full_context, pattern_type)

            # Detect level
            level = detect_rule_level(rule_text, context_before)

            # Detect strength
            strength = detect_rule_strength(rule_text, full_context)

            # Extract elements/factors
            elements, factors = extract_elements_and_factors(rule_text)

            # Detect legal area
            legal_area = detect_legal_area(full_context)

            # Extract key terms
            key_terms = extract_key_terms(rule_text)

            # Extract policy rationale
            policy = extract_policy_rationale(full_context)

            # Extract supporting cases mentioned in context
            supporting = extract_supporting_cases(full_context)

            # Extract exceptions
            exceptions = extract_exceptions(context_after)

            # Extract pinpoint page where this rule appears
            pinpoint = extract_pinpoint_page(full_context, match.start(), clean_text)

            rule = EnhancedRule(
                text=rule_text,
                source_case=source_case,
                source_citation=source_citation,
                pinpoint_page=pinpoint,
                rule_type=rule_type,
                level=level,
                strength=strength,
                elements=elements,
                factors=factors,
                exceptions=exceptions,
                legal_area=legal_area,
                key_terms=key_terms,
                policy_rationale=policy,
                supporting_cases=supporting,
                context_before=context_before[-150:] if len(context_before) > 150 else context_before,
                context_after=context_after[:150] if len(context_after) > 150 else context_after
            )
            rules.append(rule)

    # FALLBACK: If no rules found via patterns, extract sentences with legal keywords
    if not rules:
        rules = extract_rules_fallback(clean_text, source_case, source_citation, seen_rules)

    # Build hierarchy relationships
    rules = build_rule_hierarchy(rules)

    return rules


def extract_enhanced_rules_with_meta(
    text: str,
    source_case: str = "Unknown",
    source_citation: str = None
) -> Dict[str, Any]:
    """
    Extract rules with full hierarchy and return with extraction metadata.

    This wrapper provides transparency about extraction quality, including
    what was found, what's missing, confidence scores, and suggestions.

    Args:
        text: Case opinion or brief text
        source_case: Name of the source case
        source_citation: Optional citation string

    Returns:
        Dict with "_meta" (ExtractionMeta-style dict) and "rules" (List[EnhancedRule])
    """
    # Call the original extraction function
    rules = extract_enhanced_rules(text, source_case, source_citation)

    # Analyze what was extracted
    targets_found = []
    targets_missing = []
    confidence_scores = {}
    suggestions = []

    # Expected targets based on text indicators
    text_lower = text.lower()
    expected_targets = []

    # Check for holding indicators
    if any(ind in text_lower for ind in ['we hold', 'held that', 'holding that', 'the court held']):
        expected_targets.append('holding')
    elif any(ind in text_lower for ind in ['conclude', 'determine', 'find that']):
        expected_targets.append('holding')

    # Check for elements/factors indicators
    if any(ind in text_lower for ind in ['elements', 'must prove', 'establish that', 'requires proof']):
        expected_targets.append('elements')
    if any(ind in text_lower for ind in ['factors', 'consider', 'weighing', 'totality']):
        expected_targets.append('factors')

    # Check for test/standard indicators
    if any(ind in text_lower for ind in ['test', 'standard', 'burden', 'threshold']):
        expected_targets.append('standard')

    # Analyze extracted rules
    rule_types_found = set()
    for rule in rules:
        if hasattr(rule.rule_type, 'value'):
            rule_types_found.add(rule.rule_type.value)
        else:
            rule_types_found.add(str(rule.rule_type))

    # Determine found vs missing
    if rules:
        if 'holding' in rule_types_found:
            targets_found.append('holding')
            confidence_scores['holding'] = 0.85
        if 'elements' in rule_types_found:
            targets_found.append('elements')
            confidence_scores['elements'] = 0.80
        if 'factors' in rule_types_found or 'multi_factor' in rule_types_found:
            targets_found.append('factors')
            confidence_scores['factors'] = 0.75
        if 'test' in rule_types_found or 'standard' in rule_types_found:
            targets_found.append('standard')
            confidence_scores['standard'] = 0.80

        # Add generic "rules" target if we found any rules
        if not targets_found:
            targets_found.append('rules')
            confidence_scores['rules'] = 0.60

    # What's missing?
    for target in expected_targets:
        if target not in targets_found:
            targets_missing.append(target)

    # Generate suggestions
    if 'holding' in targets_missing:
        suggestions.append(
            "No explicit holding detected. Look for 'we hold that' or 'the court concludes' language."
        )
    if 'elements' in targets_missing and 'elements' in expected_targets:
        suggestions.append(
            "Elements indicator found but no element list extracted. Check for numbered lists or 'must prove' language."
        )
    if 'factors' in targets_missing and 'factors' in expected_targets:
        suggestions.append(
            "Factors indicator found but no factor list extracted. Look for 'court considers' or weighing language."
        )
    if not rules:
        suggestions.append(
            "No rules extracted. Text may be procedural, factual, or use unusual phrasing. Try providing the holding section specifically."
        )

    # Calculate overall confidence
    if rules:
        avg_confidence = sum(confidence_scores.values()) / len(confidence_scores) if confidence_scores else 0.5
    else:
        avg_confidence = 0.0
    confidence_scores['overall'] = avg_confidence

    return {
        "_meta": {
            "extraction_complete": len(targets_missing) == 0 and len(rules) > 0,
            "targets_found": targets_found,
            "targets_missing": targets_missing,
            "confidence_scores": confidence_scores,
            "suggestions": suggestions,
            "rules_count": len(rules)
        },
        "rules": rules
    }


def extract_rules_fallback(text: str, source_case: str, source_citation: str,
                           seen_rules: Set[str]) -> List[EnhancedRule]:
    """
    Fallback extraction: find sentences containing legal rule indicators.
    Used when pattern-based extraction finds nothing.
    """
    rules = []

    # Keywords that indicate a sentence might contain a rule
    rule_indicators = [
        r'\bmust\b', r'\bshall\b', r'\brequires?\b', r'\bprohibits?\b',
        r'\bburden\b', r'\belements?\b', r'\bfactors?\b', r'\bstandard\b',
        r'\btest\b', r'\bprove\b', r'\bestablish\b', r'\bdemonstrate\b',
        r'\bshow\b', r'\bliability\b', r'\binference\b', r'\bscienter\b',
        r'\bpleading\b', r'\bclaim\b', r'\bplaintiff\b', r'\bdefendant\b',
    ]

    indicator_pattern = re.compile('|'.join(rule_indicators), re.IGNORECASE)

    # Split into sentences
    sentences = re.split(r'(?<=[.!?])\s+(?=[A-Z])', text)

    for sentence in sentences:
        sentence = sentence.strip()

        # Must be substantial
        if len(sentence) < 60 or len(sentence) > 500:
            continue

        # Must contain a rule indicator
        if not indicator_pattern.search(sentence):
            continue

        # Skip if already seen
        normalized = ' '.join(sentence.lower().split())
        if normalized in seen_rules:
            continue
        seen_rules.add(normalized)

        # Skip citations and headings
        if re.match(r'^[\d\s]+[A-Z]\.', sentence) or sentence.isupper():
            continue

        # Must pass basic validation
        if not is_complete_legal_statement(sentence):
            continue

        # Determine rule type from content
        if re.search(r'\belements?\b', sentence, re.IGNORECASE):
            rule_type = RuleType.ELEMENTS
        elif re.search(r'\bfactors?\b', sentence, re.IGNORECASE):
            rule_type = RuleType.MULTI_FACTOR
        elif re.search(r'\bstandard\b|\btest\b', sentence, re.IGNORECASE):
            rule_type = RuleType.STANDARD
        else:
            rule_type = RuleType.STANDARD

        # Extract elements/factors if present
        elements, factors = extract_elements_and_factors(sentence)

        rule = EnhancedRule(
            text=sentence,
            source_case=source_case,
            source_citation=source_citation,
            rule_type=rule_type,
            level=RuleLevel.PRIMARY,
            strength=RuleStrength.MANDATORY if 'must' in sentence.lower() else RuleStrength.DEFAULT,
            elements=elements,
            factors=factors,
            exceptions=[],
            legal_area=detect_legal_area(sentence),
            key_terms=extract_key_terms(sentence),
            policy_rationale=None,
            supporting_cases=[],
            context_before="",
            context_after=""
        )
        rules.append(rule)

        # Limit fallback results
        if len(rules) >= 10:
            break

    return rules


def detect_rule_type(rule_text: str, context: str, pattern_type: str) -> RuleType:
    """Classify the type of rule based on patterns and context."""
    combined_text = f"{rule_text} {context}"

    # Check each type's patterns
    for rule_type, patterns in COMPILED_TYPE_PATTERNS.items():
        for pattern in patterns:
            if pattern.search(combined_text):
                return rule_type

    # Fall back to pattern type
    type_mapping = {
        'holding': RuleType.HOLDING,
        'rule': RuleType.STANDARD,
        'test': RuleType.TEST,
        'elements': RuleType.ELEMENTS,
        'factors': RuleType.MULTI_FACTOR,
        'definition': RuleType.DEFINITION,
        'standard': RuleType.STANDARD,
    }
    return type_mapping.get(pattern_type, RuleType.STANDARD)


def detect_rule_level(rule_text: str, context_before: str) -> RuleLevel:
    """Determine position in rule hierarchy."""
    combined = f"{context_before} {rule_text}"

    for level, patterns in COMPILED_LEVEL_PATTERNS.items():
        for pattern in patterns:
            if pattern.search(combined):
                return level

    return RuleLevel.PRIMARY


def detect_rule_strength(rule_text: str, context: str) -> RuleStrength:
    """Determine how firmly established the rule is."""
    combined = f"{rule_text} {context}"

    for strength, patterns in COMPILED_STRENGTH_PATTERNS.items():
        for pattern in patterns:
            if pattern.search(combined):
                return strength

    return RuleStrength.MANDATORY


def extract_elements_and_factors(rule_text: str) -> Tuple[List[str], List[str]]:
    """Extract numbered/lettered elements and factors from rule text."""
    elements = []
    factors = []

    # First normalize the text - join split lines
    normalized = ' '.join(rule_text.split())

    # Look for numbered lists: (1) item; (2) item; etc.
    # Capture text between markers, stopping at semicolon, period, or next number
    numbered = re.findall(
        r'\((\d+)\)\s*([^;(]+?)(?=\s*[;.]|\s*\(\d+\)|$)',
        normalized
    )

    # Look for lettered lists: (a), (b), etc.
    lettered = re.findall(
        r'\(([a-z])\)\s*([^;(]+?)(?=\s*[;.]|\s*\([a-z]\)|$)',
        normalized
    )

    # Process numbered items
    for num, item in numbered:
        item = item.strip()
        # Clean up trailing "and" or "or"
        item = re.sub(r'\s+(?:and|or)\s*$', '', item)
        if len(item) >= 5 and len(item) <= 200:  # Reasonable element length
            elements.append(item)

    # Process lettered items
    for letter, item in lettered:
        item = item.strip()
        item = re.sub(r'\s+(?:and|or)\s*$', '', item)
        if len(item) >= 5 and len(item) <= 200:
            factors.append(item)

    # If no structured list found, try to identify inline elements
    if not elements and not factors:
        # Look for "must prove X, Y, and Z" pattern
        inline_match = re.search(
            r'must (?:prove|show|establish|demonstrate)[:\s]+(.+?)(?:\.|$)',
            normalized, re.IGNORECASE
        )
        if inline_match:
            items = re.split(r'[,;]\s*(?:and\s+)?', inline_match.group(1))
            for item in items:
                item = item.strip()
                if len(item) >= 5 and len(item) <= 100:
                    elements.append(item)

    return elements, factors


def detect_legal_area(context: str) -> str:
    """Identify the area of law from context."""
    context_lower = context.lower()

    scores = {}
    for area, keywords in LEGAL_AREA_KEYWORDS.items():
        score = sum(1 for kw in keywords if kw in context_lower)
        if score > 0:
            scores[area] = score

    if scores:
        return max(scores, key=scores.get)
    return "general"


def extract_key_terms(rule_text: str) -> List[str]:
    """Extract legally significant terms from rule text."""
    key_terms = []

    # Legal terms pattern
    legal_term_pattern = r'\b(?:reasonable|negligent|duty|breach|proximate|actual|constructive|' \
                        r'intentional|willful|malicious|substantial|material|' \
                        r'consideration|offer|acceptance|performance|' \
                        r'jurisdiction|standing|mootness|ripeness)\b'

    matches = re.findall(legal_term_pattern, rule_text, re.IGNORECASE)
    key_terms = list(set(m.lower() for m in matches))

    return key_terms


def extract_policy_rationale(context: str) -> str:
    """Extract policy reasoning behind the rule."""
    for pattern in COMPILED_POLICY_PATTERNS:
        match = pattern.search(context)
        if match:
            return match.group(1).strip()
    return ""


def extract_supporting_cases(context: str) -> List[str]:
    """Extract case names mentioned as supporting authority."""
    # Simple pattern for case names
    case_pattern = r'([A-Z][a-zA-Z\'\-]+\s+v\.?\s+[A-Z][a-zA-Z\'\-]+)'
    matches = re.findall(case_pattern, context)
    return list(set(matches))[:5]  # Limit to 5


def extract_exceptions(context_after: str) -> List[str]:
    """Extract exceptions mentioned after the rule."""
    exceptions = []

    exception_patterns = [
        r'(?:except|unless|however|but)\s+(.+?)(?:\.|$)',
        r'(?:this rule does not apply (?:to|when|if))\s+(.+?)(?:\.|$)',
    ]

    for pattern in exception_patterns:
        matches = re.findall(pattern, context_after, re.IGNORECASE)
        for match in matches:
            if len(match) > 15:
                exceptions.append(match.strip())

    return exceptions[:3]


def build_rule_hierarchy(rules: List[EnhancedRule]) -> List[EnhancedRule]:
    """
    Establish parent-child relationships between rules.
    Primary rules become parents of sub-rules and exceptions.
    """
    primary_rules = [r for r in rules if r.level == RuleLevel.PRIMARY]
    sub_rules = [r for r in rules if r.level in [RuleLevel.SUB_RULE, RuleLevel.EXCEPTION, RuleLevel.LIMITATION]]

    # For each sub-rule, find the most likely parent
    for sub in sub_rules:
        best_parent = None
        best_score = 0

        for primary in primary_rules:
            # Score based on shared key terms
            shared_terms = set(sub.key_terms) & set(primary.key_terms)
            score = len(shared_terms)

            # Bonus if same legal area
            if sub.legal_area == primary.legal_area:
                score += 2

            if score > best_score:
                best_score = score
                best_parent = primary

        if best_parent and best_score > 0:
            sub.parent_rule = best_parent.text[:100]
            best_parent.child_rules.append(sub.text[:100])

    return rules


# ============================================================================
# OUTPUT FORMATTING
# ============================================================================

def format_enhanced_rules_output(rules: List[EnhancedRule]) -> str:
    """Format enhanced rules with proper hierarchy display."""
    if not rules:
        return "No rules found in the provided text."

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

    # Get source info from first rule
    source_case = rules[0].source_case if rules else "Unknown"
    source_cite = rules[0].source_citation if rules and rules[0].source_citation else ""

    output.append(f"Case: {source_case}")
    if source_cite:
        output.append(f"Citation: {source_cite}")
    output.append(f"Rules Extracted: {len(rules)}")
    output.append("")

    # Group by level for hierarchy
    primary = [r for r in rules if r.level == RuleLevel.PRIMARY]
    sub = [r for r in rules if r.level == RuleLevel.SUB_RULE]
    exceptions = [r for r in rules if r.level == RuleLevel.EXCEPTION]
    limitations = [r for r in rules if r.level == RuleLevel.LIMITATION]
    corollaries = [r for r in rules if r.level == RuleLevel.COROLLARY]

    # Display each primary rule with its children
    for i, rule in enumerate(primary, 1):
        output.append("-" * 70)
        output.append(_format_rule_hierarchical(rule, i, is_primary=True))

        # Find related sub-rules (using text similarity)
        related_sub = _find_related_rules(rule, sub)
        related_exc = _find_related_rules(rule, exceptions)
        related_lim = _find_related_rules(rule, limitations)

        all_children = related_sub + related_exc + related_lim
        for j, child in enumerate(all_children):
            is_last = (j == len(all_children) - 1)
            prefix = "    └── " if is_last else "    ├── "
            output.append(_format_rule_hierarchical(child, None, prefix=prefix))

    # Show any orphaned sub-rules/exceptions not linked to primary
    orphan_sub = [r for r in sub if r not in [c for p in primary for c in _find_related_rules(p, sub)]]
    orphan_exc = [r for r in exceptions if r not in [c for p in primary for c in _find_related_rules(p, exceptions)]]

    if orphan_sub or orphan_exc:
        output.append("")
        output.append("-" * 70)
        output.append("ADDITIONAL RULES (not linked to primary):")
        output.append("-" * 70)
        for rule in orphan_sub + orphan_exc:
            output.append(_format_rule_hierarchical(rule, None, prefix="  • "))

    # Corollaries at the end
    if corollaries:
        output.append("")
        output.append("-" * 70)
        output.append("COROLLARY RULES:")
        output.append("-" * 70)
        for rule in corollaries:
            output.append(_format_rule_hierarchical(rule, None, prefix="  • "))

    output.append("")
    output.append("=" * 70)

    return '\n'.join(output)


def _format_rule_hierarchical(rule: EnhancedRule, number: Optional[int], is_primary: bool = False, prefix: str = "") -> str:
    """Format a single rule in hierarchical display."""
    lines = []

    # Rule type tag
    type_tag = rule.rule_type.value.upper().replace('_', ' ')

    # Strength indicator
    strength_indicator = ""
    if rule.strength == RuleStrength.MANDATORY:
        strength_indicator = "[MANDATORY] "
    elif rule.strength == RuleStrength.PERMISSIVE:
        strength_indicator = "[PERMISSIVE] "

    # Build header
    if is_primary and number:
        header = f"PRIMARY RULE {number}: [{type_tag}] {strength_indicator}"
    else:
        level_name = rule.level.value.upper().replace('_', '-')
        header = f"{prefix}[{level_name}] [{type_tag}] {strength_indicator}"

    lines.append(header)

    # Full rule text (no truncation)
    if is_primary:
        lines.append(f'"{rule.text}"')
    else:
        lines.append(f'{prefix}"{rule.text}"')

    # Citation with pinpoint
    cite_line = f"  Source: {rule.source_case}"
    if rule.source_citation:
        cite_line += f", {rule.source_citation}"
    if rule.pinpoint_page:
        cite_line += f" at {rule.pinpoint_page}"
    if is_primary:
        lines.append(cite_line)
    else:
        lines.append(f"{prefix}{cite_line}")

    # Elements if present
    if rule.elements:
        if is_primary:
            lines.append(f"  Elements ({len(rule.elements)}):")
            for idx, elem in enumerate(rule.elements, 1):
                lines.append(f"    ({idx}) {elem}")
        else:
            lines.append(f"{prefix}  Elements: {', '.join(rule.elements[:5])}")

    # Factors if present
    if rule.factors:
        if is_primary:
            lines.append(f"  Factors ({len(rule.factors)}):")
            for idx, factor in enumerate(rule.factors, 1):
                lines.append(f"    ({idx}) {factor}")
        else:
            lines.append(f"{prefix}  Factors: {', '.join(rule.factors[:5])}")

    # Key terms
    if rule.key_terms and is_primary:
        lines.append(f"  Key Terms: {', '.join(rule.key_terms[:10])}")

    lines.append("")  # Blank line after each rule

    return '\n'.join(lines)


def _find_related_rules(primary: EnhancedRule, candidates: List[EnhancedRule]) -> List[EnhancedRule]:
    """Find rules that appear related to a primary rule based on shared terms."""
    related = []
    primary_words = set(primary.text.lower().split())

    for candidate in candidates:
        candidate_words = set(candidate.text.lower().split())
        # Check for significant word overlap (excluding common words)
        stop_words = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been',
                      'to', 'of', 'in', 'for', 'on', 'with', 'that', 'this', 'and', 'or'}
        significant_primary = primary_words - stop_words
        significant_candidate = candidate_words - stop_words
        overlap = significant_primary & significant_candidate

        # If >25% overlap, consider related
        if len(overlap) >= max(3, len(significant_candidate) * 0.25):
            related.append(candidate)

    return related


def format_single_rule(rule: EnhancedRule, indent: int = 0) -> str:
    """Format a single rule with proper indentation - NO truncation."""
    prefix = " " * indent
    lines = []

    # Full rule text - no truncation per spec
    type_tag = rule.rule_type.value.upper().replace('_', ' ')
    lines.append(f"\n{prefix}RULE: \"{rule.text}\"")
    lines.append(f"{prefix}TYPE: {type_tag}")

    # Source with pinpoint citation per spec
    source_line = f"{prefix}SOURCE: {rule.source_case}"
    if rule.source_citation:
        source_line += f", {rule.source_citation}"
    lines.append(source_line)

    if rule.pinpoint_page:
        lines.append(f"{prefix}PINPOINT: {rule.pinpoint_page}")

    # Strength (mandatory vs permissive)
    lines.append(f"{prefix}STRENGTH: {rule.strength.value.upper()}")

    # Legal area if detected
    if rule.legal_area:
        lines.append(f"{prefix}AREA: {rule.legal_area}")

    # Elements - full text, no truncation
    if rule.elements:
        lines.append(f"{prefix}ELEMENTS ({len(rule.elements)}):")
        for i, elem in enumerate(rule.elements, 1):
            lines.append(f"{prefix}  ({i}) {elem}")

    # Factors - full text, no truncation
    if rule.factors:
        lines.append(f"{prefix}FACTORS ({len(rule.factors)}):")
        for i, factor in enumerate(rule.factors, 1):
            lines.append(f"{prefix}  ({i}) {factor}")

    # Exceptions - full text
    if rule.exceptions:
        lines.append(f"{prefix}EXCEPTIONS:")
        for exc in rule.exceptions:
            lines.append(f"{prefix}  - {exc}")

    # Policy rationale - full text
    if rule.policy_rationale:
        lines.append(f"{prefix}POLICY: {rule.policy_rationale}")

    # Key terms
    if rule.key_terms:
        lines.append(f"{prefix}KEY TERMS: {', '.join(rule.key_terms)}")

    # Supporting cases
    if rule.supporting_cases:
        lines.append(f"{prefix}SUPPORTING: {', '.join(rule.supporting_cases)}")

    return '\n'.join(lines)


# ============================================================================
# RULE SYNTHESIS ACROSS MULTIPLE CASES
# ============================================================================

def synthesize_enhanced_rules(case_texts: Dict[str, Tuple[str, str]]) -> Dict[str, List[EnhancedRule]]:
    """
    Synthesize rules from multiple cases with conflict detection.

    Args:
        case_texts: Dict mapping case_name -> (text, citation)

    Returns:
        Dictionary organizing rules by type
    """
    all_rules = defaultdict(list)

    # Extract rules from each case
    case_rules = {}
    for case_name, (text, citation) in case_texts.items():
        rules = extract_enhanced_rules(text, case_name, citation)
        case_rules[case_name] = rules

        for rule in rules:
            all_rules[rule.rule_type.value].append(rule)

    # Detect conflicting rules
    all_rules_flat = [r for rules_list in all_rules.values() for r in rules_list]
    detect_conflicts(all_rules_flat)

    return dict(all_rules)


def detect_conflicts(rules: List[EnhancedRule]):
    """
    Detect potentially conflicting rules and mark them.
    """
    for i, rule_a in enumerate(rules):
        for rule_b in rules[i+1:]:
            # Skip if same source
            if rule_a.source_case == rule_b.source_case:
                continue

            # Check if same legal area and key terms overlap
            if rule_a.legal_area == rule_b.legal_area:
                shared_terms = set(rule_a.key_terms) & set(rule_b.key_terms)
                if len(shared_terms) >= 2:
                    # Check for conflicting language
                    if contains_conflict_language(rule_a.text, rule_b.text):
                        rule_a.conflicting_rules.append((rule_b.text[:100], rule_b.source_case))
                        rule_b.conflicting_rules.append((rule_a.text[:100], rule_a.source_case))
                        rule_a.strength = RuleStrength.DISPUTED
                        rule_b.strength = RuleStrength.DISPUTED


def contains_conflict_language(text_a: str, text_b: str) -> bool:
    """Check if two rule texts contain conflicting language."""
    # Simple heuristic: one says "must" where other says "may"
    # or one says "always" where other says "never"

    a_lower = text_a.lower()
    b_lower = text_b.lower()

    conflict_pairs = [
        ('must', 'may'),
        ('shall', 'may'),
        ('always', 'never'),
        ('required', 'discretionary'),
    ]

    for word_a, word_b in conflict_pairs:
        if (word_a in a_lower and word_b in b_lower) or (word_b in a_lower and word_a in b_lower):
            return True

    return False


# ============================================================================
# MCP WRAPPER FUNCTIONS
# ============================================================================

def analyze_rules_enhanced(text: str, case_name: str = "Unknown", citation: str = None) -> str:
    """
    Main entry point for MCP server.

    Args:
        text: Case opinion text
        case_name: Name of the case
        citation: Optional citation

    Returns:
        Formatted analysis string
    """
    rules = extract_enhanced_rules(text, case_name, citation)
    return format_enhanced_rules_output(rules)


def get_rules_as_dict(text: str, case_name: str = "Unknown") -> List[Dict]:
    """
    Get rules as list of dictionaries for programmatic use.

    Args:
        text: Case opinion text
        case_name: Name of the case

    Returns:
        List of rule dictionaries
    """
    rules = extract_enhanced_rules(text, case_name)
    return [rule.to_dict() for rule in rules]
