#!/usr/bin/python3
"""
Validate Behavioral DNA - Pre-Generation Hard Gate

This script validates that characters.md contains proper behavioral DNA
before episode generation can begin. This is a HARD GATE - generation cannot
proceed if this fails.

Required per major character:
1. At least 3 on-screen behaviors (not backstory)
2. Specific stress behavior (not generic)
3. Signature line (passes swap test)
4. At least 1 orthogonal trait (doesn't serve theme)
5. Contradiction defined (pattern-break moment)

Usage: python3 validate_behavioral_dna.py <project_path>
Example: python3 validate_behavioral_dna.py ./leviathan

Arguments:
  project_path   Path to production project (has /bible/) or development project

Returns:
- Exit code 0: All characters have complete behavioral DNA
- Exit code 1: One or more characters missing required DNA elements
- Exit code 2: Configuration/path error

Note: Supports both characters.md (current) and character_voices.md (legacy)
"""

import sys
import re
from pathlib import Path

# Generic stress behaviors that should be flagged
GENERIC_STRESS_BEHAVIORS = [
    'gets quiet',
    'goes quiet',
    'becomes quiet',
    'tenses up',
    'gets tense',
    'freezes',
    'shuts down',
    'gets angry',
    'becomes angry',
    'gets scared',
    'gets nervous',
    'becomes nervous',
]

# Backstory indicators (things that are history, not behavior)
BACKSTORY_INDICATORS = [
    'lost his',
    'lost her',
    'lost their',
    'was betrayed',
    'was abandoned',
    'grew up',
    'raised by',
    'came from',
    'used to be',
    'formerly',
    'once was',
    'before the',
    'in the past',
    'childhood',
    'parents',
    'family history',
]

# Voice validation patterns (for soft gate)
VOICE_REQUIRED_PATTERNS = [
    'idiom',
    'speech pattern',
    'humor type',
    'tone',
]

VOICE_ANTI_PATTERN_INDICATORS = [
    'never say',
    'anti-pattern',
    'forbidden',
    'avoid',
]

# Headers that should NOT be treated as character names
SKIP_HEADERS = [
    'voice', 'speech', 'dialogue', 'patterns', 'overview',
    'summary', 'notes', 'index', 'table', 'contents',
    'behavioral', 'dna', 'consistency', 'tone', 'humor',
    'checklist', 'validation', 'pattern', 'relational',
    'sample', 'actually', 'funny', 'anti-patterns',
    'document', 'structure', 'enforcement', 'rules',  # Added for characters_template.md compatibility
]


def find_characters_file(project_path):
    """Find the characters.md file in production or development project."""
    # Search order (characters.md is now the standard):
    # 1. Production: /bible/characters.md (current standard)
    # 2. Development: /characters.md
    # 3. Legacy: /bible/character_voices.md
    # 4. Legacy: /character_voices.md

    search_paths = [
        project_path / "bible" / "characters.md",         # Production (current)
        project_path / "characters.md",                   # Development
        project_path / "bible" / "character_voices.md",   # Legacy production
        project_path / "character_voices.md",             # Legacy development
    ]

    for path in search_paths:
        if path.exists():
            return path

    return None


def extract_characters(content):
    """Extract character sections from the voices file."""
    characters = {}

    # Split content into lines for processing
    lines = content.split('\n')

    # Find all ## headers (exactly 2 hashes, not 3+)
    # Pattern: line starts with "## " followed by character name
    header_pattern = re.compile(r'^##\s+([A-Z][A-Za-z0-9\s\-\_]+)(?:\s*[-—].*)?$')

    current_char = None
    current_section_lines = []

    for i, line in enumerate(lines):
        # Check if this is a ## header (but not ### or more)
        if line.startswith('## ') and not line.startswith('### '):
            # Save previous character if exists
            if current_char:
                characters[current_char] = '\n'.join(current_section_lines)

            # Try to extract character name
            match = header_pattern.match(line)
            if match:
                char_name = match.group(1).strip()

                # Skip non-character headers
                char_lower = char_name.lower()
                if any(skip in char_lower for skip in SKIP_HEADERS):
                    current_char = None
                    current_section_lines = []
                    continue

                current_char = char_name
                current_section_lines = []
            else:
                current_char = None
                current_section_lines = []
        elif current_char:
            current_section_lines.append(line)

    # Don't forget the last character
    if current_char:
        characters[current_char] = '\n'.join(current_section_lines)

    return characters


def check_explicit_behavioral_dna_section(section, char_name):
    """Check for the new explicit Behavioral DNA section format."""
    # Look for explicit "### Behavioral DNA" section
    dna_match = re.search(
        r'###\s*Behavioral\s+DNA[^\n]*\n(.*?)(?=###|\Z)',
        section,
        re.IGNORECASE | re.DOTALL
    )

    if not dna_match:
        return None  # No explicit section found, fall back to legacy detection

    dna_section = dna_match.group(1)
    results = {
        'found_explicit': True,
        'behaviors': [],
        'stress_behavior': None,
        'signature_line': None,
        'orthogonal_trait': None,
        'contradiction': None,
    }

    # Extract numbered behaviors (1. 2. 3. etc.)
    # Match lines starting with digit followed by period
    behavior_lines = re.findall(r'^\s*\d+\.\s*(.+)$', dna_section, re.MULTILINE)
    for line in behavior_lines:
        # Skip placeholder text and empty lines
        line = line.strip()
        if line and '{' not in line and '}' not in line:
            # Take text before any em-dash explanation
            if ' — ' in line:
                line = line.split(' — ')[0].strip()
            results['behaviors'].append(line[:80])

    # Extract stress behavior - look for **Stress Behavior:** pattern
    stress_match = re.search(
        r'\*\*Stress\s*Behavior[:\*]*\*?\*?[:\s]*(.+?)(?=\n\*\*|\n\n|\Z)',
        dna_section,
        re.IGNORECASE | re.DOTALL
    )
    if stress_match:
        stress_text = stress_match.group(1).strip()
        # Clean up the text
        stress_text = re.sub(r'\*+', '', stress_text).strip()
        if stress_text and '{' not in stress_text:
            results['stress_behavior'] = stress_text

    # Extract signature line - look for **Signature Line:** pattern
    sig_match = re.search(
        r'\*\*Signature\s*Line[:\*]*\*?\*?[:\s]*["\']?(.+?)["\']?(?=\n\*\*|\n\n|\Z)',
        dna_section,
        re.IGNORECASE | re.DOTALL
    )
    if sig_match:
        sig_text = sig_match.group(1).strip()
        sig_text = re.sub(r'\*+', '', sig_text).strip()
        # Remove surrounding quotes if present
        sig_text = sig_text.strip('"\'')
        if sig_text and '{' not in sig_text:
            results['signature_line'] = sig_text

    # Extract orthogonal trait
    ortho_match = re.search(
        r'\*\*Orthogonal\s*Trait[^:]*[:\*]*\*?\*?[:\s]*(.+?)(?=\n\*\*|\n\n|\Z)',
        dna_section,
        re.IGNORECASE | re.DOTALL
    )
    if ortho_match:
        ortho_text = ortho_match.group(1).strip()
        ortho_text = re.sub(r'\*+', '', ortho_text).strip()
        if ortho_text and '{' not in ortho_text:
            results['orthogonal_trait'] = ortho_text

    # Extract contradiction
    contra_match = re.search(
        r'\*\*Contradiction[^:]*[:\*]*\*?\*?[:\s]*(.+?)(?=\n\*\*|\n\n|\n###|\Z)',
        dna_section,
        re.IGNORECASE | re.DOTALL
    )
    if contra_match:
        contra_text = contra_match.group(1).strip()
        contra_text = re.sub(r'\*+', '', contra_text).strip()
        if contra_text and '{' not in contra_text:
            results['contradiction'] = contra_text

    return results


def check_on_screen_behaviors(section, char_name):
    """Check for 3+ on-screen behaviors (not backstory)."""
    issues = []
    behaviors_found = []
    backstory_found = []

    # First, check for explicit Behavioral DNA section (new format)
    explicit_dna = check_explicit_behavioral_dna_section(section, char_name)

    if explicit_dna and explicit_dna['found_explicit']:
        # Use the explicit section data
        behaviors_found = explicit_dna['behaviors']
        if len(behaviors_found) < 3:
            issues.append({
                'type': 'missing_behaviors',
                'detail': f'Found {len(behaviors_found)} on-screen behaviors in Behavioral DNA section (need 3+)',
                'found': behaviors_found,
                'backstory': [],
            })
        return issues, len(behaviors_found), explicit_dna

    # Legacy detection: Look for behavior indicators distributed throughout the section
    section_lower = section.lower()

    # Count potential behaviors
    behavior_keywords = [
        'talks to', 'speaks in', 'never makes', 'always checks',
        'counts', 'touches', 'taps', 'fidgets', 'paces',
        'avoids', 'seeks', 'prefers', 'refuses to',
        'when stressed', 'under pressure', 'in conversation',
    ]

    for keyword in behavior_keywords:
        if keyword in section_lower:
            # Extract the line containing this keyword
            for line in section.split('\n'):
                if keyword in line.lower():
                    # Check it's not backstory
                    is_backstory = any(bi in line.lower() for bi in BACKSTORY_INDICATORS)
                    if is_backstory:
                        backstory_found.append(line.strip()[:60])
                    else:
                        behaviors_found.append(line.strip()[:60])

    # Deduplicate
    behaviors_found = list(set(behaviors_found))

    if len(behaviors_found) < 3:
        issues.append({
            'type': 'missing_behaviors',
            'detail': f'Found {len(behaviors_found)} on-screen behaviors (need 3+)',
            'found': behaviors_found,
            'backstory': backstory_found,
        })

    return issues, len(behaviors_found), None


def check_stress_behavior(section, char_name, explicit_dna=None):
    """Check for specific (non-generic) stress behavior."""
    issues = []

    # Check explicit DNA first
    if explicit_dna and explicit_dna.get('stress_behavior'):
        stress_behavior = explicit_dna['stress_behavior']
    else:
        # Look for stress behavior section
        stress_patterns = [
            r'\*\*Stress\s*Behavior[^:]*:\*?\*?\s*(.+?)(?=\n\*\*|\n\n|\Z)',
            r'(?:stress|pressure|panic|fear|crisis)\s*(?:behavior|response|reaction)?\s*[:\-]?\s*(.+)',
            r'(?:when stressed|under pressure|in crisis)\s*[:\-,]?\s*(.+)',
        ]

        stress_behavior = None
        for pattern in stress_patterns:
            match = re.search(pattern, section, re.IGNORECASE | re.DOTALL)
            if match:
                stress_behavior = match.group(1).strip()
                # Clean up
                stress_behavior = re.sub(r'\*+', '', stress_behavior).strip()
                break

    if not stress_behavior:
        issues.append({
            'type': 'missing_stress',
            'detail': 'No stress behavior defined',
        })
        return issues, False

    # Check if it's generic
    stress_lower = stress_behavior.lower()
    for generic in GENERIC_STRESS_BEHAVIORS:
        if generic in stress_lower:
            issues.append({
                'type': 'generic_stress',
                'detail': f'Stress behavior too generic: "{stress_behavior[:50]}"',
                'suggestion': 'Need specific, surprising behavior (e.g., "laughs at inappropriate moments", "becomes unnaturally calm")',
            })
            return issues, False

    return issues, True


def check_signature_line(section, char_name, explicit_dna=None):
    """Check for signature line that passes swap test."""
    issues = []

    # Check explicit DNA first
    if explicit_dna and explicit_dna.get('signature_line'):
        signature_line = explicit_dna['signature_line']
    else:
        # Look for signature line
        sig_patterns = [
            r'\*\*Signature\s*Line[^:]*:\*?\*?\s*["\']?(.+?)["\']?(?=\n\*\*|\n\n|\Z)',
            r'(?:signature|iconic|defining)\s*(?:line|dialogue|quote)\s*[:\-]?\s*["\'](.+?)["\']',
        ]

        signature_line = None
        for pattern in sig_patterns:
            match = re.search(pattern, section, re.IGNORECASE | re.DOTALL)
            if match:
                signature_line = match.group(1).strip()
                signature_line = re.sub(r'\*+', '', signature_line).strip()
                signature_line = signature_line.strip('"\'')
                break

    if not signature_line:
        # Try to find any quoted line after "Signature"
        sig_context = re.search(r'[Ss]ignature[^"\']*["\']([^"\']+)["\']', section)
        if sig_context:
            signature_line = sig_context.group(1).strip()

    if not signature_line:
        issues.append({
            'type': 'missing_signature',
            'detail': 'No signature line defined',
            'suggestion': 'Add a line only this character would say (reveals worldview, attitude)',
        })
        return issues, False

    # Basic swap test - check if line has personality
    generic_phrases = [
        'we need to', 'let\'s go', 'come on', 'watch out',
        'be careful', 'what do you', 'i don\'t know',
        'i think', 'maybe we', 'we should',
    ]

    sig_lower = signature_line.lower()
    is_generic = any(gp in sig_lower for gp in generic_phrases)

    if is_generic and len(signature_line) < 30:
        issues.append({
            'type': 'weak_signature',
            'detail': f'Signature line may be too generic: "{signature_line}"',
            'suggestion': 'Should reveal worldview/personality, not just function',
        })
        return issues, False

    return issues, True


def check_orthogonal_trait(section, char_name, explicit_dna=None):
    """Check for at least one trait that doesn't serve the theme."""
    issues = []

    # Check explicit DNA first
    if explicit_dna and explicit_dna.get('orthogonal_trait'):
        trait_text = explicit_dna['orthogonal_trait']
        quality = assess_orthogonal_quality(trait_text, section)
        return issues, True, quality

    # Look for orthogonal trait markers
    orthogonal_patterns = [
        r'\*\*Orthogonal\s*Trait[^:]*:\*?\*?\s*(.+?)(?=\n\*\*|\n\n|\Z)',
        r'(?:orthogonal|unrelated|non[- ]?thematic|personal|quirk)\s*(?:trait)?\s*[:\-]?\s*(.+)',
    ]

    trait_text = None
    for pattern in orthogonal_patterns:
        match = re.search(pattern, section, re.IGNORECASE | re.DOTALL)
        if match:
            trait_text = match.group(1).strip() if match.lastindex else match.group(0).strip()
            quality = assess_orthogonal_quality(trait_text, section)
            return issues, True, quality

    # Also check for the general indicator
    if re.search(r'(?:has nothing to do with|doesn\'t serve|outside the theme)', section, re.IGNORECASE):
        quality = assess_orthogonal_quality('', section)
        return issues, True, quality

    issues.append({
        'type': 'missing_orthogonal',
        'detail': 'No orthogonal trait defined (trait that doesn\'t serve theme)',
        'suggestion': 'Add something about this character that has nothing to do with your story\'s theme — but DOES reveal character or create relationship beats',
    })
    return issues, False, None


def assess_orthogonal_quality(trait_text, section):
    """
    Assess orthogonal trait quality against the Quality Gate.
    Returns dict with score and which tests pass.

    Quality Gate (2/3 required):
    1. Character Revelation — shows who they are beneath the plot
    2. Arc Potential — can change over 60 episodes
    3. Relationship Work — another character noticing creates a beat
    """
    full_context = (trait_text + ' ' + section).lower()
    quality = {
        'revelation': False,
        'arc_potential': False,
        'relationship_work': False,
        'score': 0,
        'is_quality': False,
        'trait_text': trait_text[:100] if trait_text else '',
    }

    # --- Test 1: Character Revelation ---
    # Look for language indicating the trait reveals something about the character
    revelation_indicators = [
        'reveals', 'shows', 'exposes', 'beneath', 'underneath',
        'superstition', 'loneliness', 'vulnerability', 'fear',
        'learning', 'becoming', 'who they are', 'who she is', 'who he is',
        'the .+ who', 'despite', 'contradiction',
        'comfort', 'private', 'secret', 'hidden',
    ]
    if any(re.search(ind, full_context) for ind in revelation_indicators):
        quality['revelation'] = True

    # --- Test 2: Arc Potential ---
    # Look for language about evolution/change over time
    arc_indicators = [
        'evolves', 'changes', 'develops', 'transforms', 'shifts',
        'starts .+ becomes', 'early .+ late', 'unconscious .+ deliberate',
        'begins .+ ends', 'first .+ final', 'ep \\d+.*ep \\d+',
        'arc', 'growth', 'progression', 'over time',
        'act [123]', 'episode', 'callback',
    ]
    if any(re.search(ind, full_context) for ind in arc_indicators):
        quality['arc_potential'] = True

    # --- Test 3: Relationship Work ---
    # Look for language about other characters noticing/reacting
    relationship_indicators = [
        'notic', 'catches', 'sees', 'observes', 'comments',
        'relationship beat', 'callback', 'between them',
        'partner', 'jinx', 'kian', 'varek',  # character names
        'funny', 'humor', 'comedy', 'unsettling', 'devastating',
        'flirting', 'asking', 'confronting',
        'are you', 'why do you', 'why are you',
    ]
    if any(re.search(ind, full_context) for ind in relationship_indicators):
        quality['relationship_work'] = True

    quality['score'] = sum([
        quality['revelation'],
        quality['arc_potential'],
        quality['relationship_work'],
    ])
    quality['is_quality'] = quality['score'] >= 2

    # --- Detect low-quality "just a quirk" patterns ---
    quirk_only_indicators = [
        r'^hums?\b',
        r'^doodles?\b',
        r'^traces?\s+(?:patterns?|shapes?)',
        r'^whistles?\b',
        r'^taps?\s+(?:fingers?|feet)',
        r'^chews?\s+(?:nails?|lip)',
        r'^twirls?\s+(?:hair|pen)',
    ]
    trait_lower = trait_text.lower().strip() if trait_text else ''
    is_quirk_only = any(re.search(pat, trait_lower) for pat in quirk_only_indicators)
    if is_quirk_only and quality['score'] < 2:
        quality['is_quirk_warning'] = True
    else:
        quality['is_quirk_warning'] = False

    return quality


def check_contradiction(section, char_name, explicit_dna=None):
    """Check for defined contradiction/pattern-break moment."""
    issues = []

    # Check explicit DNA first
    if explicit_dna and explicit_dna.get('contradiction'):
        return issues, True

    # Look for contradiction
    contradiction_patterns = [
        r'\*\*Contradiction[^:]*:\*?\*?\s*(.+?)(?=\n\*\*|\n\n|\n###|\Z)',
        r'(?:contradiction|exception|breaks? pattern)',
        r'Episode\s+\d+\s*[-—:]\s*',  # "Episode X — does something"
    ]

    for pattern in contradiction_patterns:
        match = re.search(pattern, section, re.IGNORECASE)
        if match:
            return issues, True

    issues.append({
        'type': 'missing_contradiction',
        'detail': 'No contradiction defined (moment where character breaks their pattern)',
        'suggestion': 'Add: "The [type] who [does unexpected thing]"',
    })
    return issues, False


def check_character_paradox(section, char_name, explicit_dna=None):
    """
    Check for internal character paradox (Seger — Structural Gate S3).

    A paradox is an internal contradiction that drives the character's arc.
    It's deeper than a pattern-break moment — it's a fundamental tension
    within the character's identity (e.g., "the ruthless survivor who
    secretly craves connection").

    This checks that the contradiction is a genuine PARADOX, not just
    a surface quirk or plot exception.

    Returns (issues, has_paradox)
    """
    issues = []
    section_lower = section.lower()

    # Get the contradiction text
    contradiction_text = None
    if explicit_dna and explicit_dna.get('contradiction'):
        contradiction_text = explicit_dna['contradiction']
    else:
        match = re.search(
            r'\*\*Contradiction[^:]*:\*?\*?\s*(.+?)(?=\n\*\*|\n\n|\n###|\Z)',
            section,
            re.IGNORECASE | re.DOTALL
        )
        if match:
            contradiction_text = re.sub(r'\*+', '', match.group(1)).strip()

    if not contradiction_text:
        issues.append({
            'type': 'missing_paradox',
            'detail': f'{char_name}: No internal paradox defined',
            'suggestion': 'Define a fundamental tension: "The [quality] who [contradictory quality]" — this should drive the 60-episode arc',
        })
        return issues, False

    contra_lower = contradiction_text.lower()

    # Check for paradox indicators — language suggesting internal tension
    paradox_patterns = [
        r'the\s+\w+\s+who\s+',             # "the survivor who craves..."
        r'but\s+(?:secretly|actually|also)', # "but secretly..."
        r'(?:despite|although)\s+',          # "despite being..."
        r'(?:wants|needs|craves).*(?:but|yet|while)', # tension between desires
        r'(?:fears?|avoids?).*(?:wants?|needs?)',      # fear vs desire
        r'(?:publicly|outwardly).*(?:privately|inwardly|secretly)', # external vs internal
        r'(?:projects?|presents?).*(?:hides?|masks?|conceals?)',    # facade vs truth
        r'(?:strength|power).*(?:weakness|vulnerability)',          # strength/weakness
        r'(?:loyalty|trust).*(?:betrayal|doubt)',                   # loyalty/betrayal tension
    ]

    has_paradox_language = any(re.search(p, contra_lower) for p in paradox_patterns)

    # Check for oppositional word pairs (signal genuine internal tension)
    oppositions = [
        ('strong', 'vulnerable'), ('tough', 'tender'), ('ruthless', 'compassion'),
        ('cold', 'warm'), ('control', 'chaos'), ('trust', 'betray'),
        ('protect', 'destroy'), ('love', 'fear'), ('brave', 'afraid'),
        ('logic', 'emotion'), ('head', 'heart'), ('duty', 'desire'),
        ('isolation', 'connection'), ('power', 'weakness'), ('pride', 'shame'),
    ]

    has_opposition = any(
        a in contra_lower and b in contra_lower
        for a, b in oppositions
    )

    if has_paradox_language or has_opposition:
        return issues, True

    # Has a contradiction but no paradox markers detected — warn
    issues.append({
        'type': 'weak_paradox',
        'detail': f'{char_name}: Contradiction defined but may not be a driving paradox: "{contradiction_text[:60]}"',
        'suggestion': 'Strengthen into an internal tension that drives the arc: "The [quality] who [contradictory quality]"',
    })
    return issues, False


def check_voice_dna(section, char_name):
    """Check for voice DNA elements (SOFT gate - warnings only)."""
    warnings = []
    section_lower = section.lower()

    # Check for idiom/speech pattern
    has_idiom = any(pattern in section_lower for pattern in ['idiom', 'speech pattern', 'speech idiom'])
    if not has_idiom:
        warnings.append({
            'type': 'missing_idiom',
            'detail': 'No idiom/speech pattern defined',
            'suggestion': 'Add how this character frames the world (gamblers speak in bets, soldiers in tactics, etc.)',
        })

    # Check for humor type
    has_humor = any(pattern in section_lower for pattern in ['humor type', 'humor:', '### tone', 'gallows humor', 'deadpan', 'cruel wit'])
    if not has_humor:
        warnings.append({
            'type': 'missing_humor',
            'detail': 'No humor type defined',
            'suggestion': 'Add: GALLOWS HUMOR / DRY DEADPAN / CRUEL WIT / EARNEST / NONE',
        })

    # Check for anti-patterns
    has_anti_patterns = any(pattern in section_lower for pattern in ['never say', 'anti-pattern', 'would never', 'forbidden'])
    if not has_anti_patterns:
        warnings.append({
            'type': 'missing_anti_patterns',
            'detail': 'No anti-patterns defined',
            'suggestion': 'Add what this character would NEVER say',
        })

    # Check for sample dialogue
    has_samples = '"' in section or '>' in section  # Quoted text or blockquotes
    if not has_samples:
        warnings.append({
            'type': 'missing_samples',
            'detail': 'No sample dialogue found',
            'suggestion': 'Add example lines that demonstrate this character\'s voice',
        })

    voice_score = 4 - len(warnings)
    return warnings, voice_score


def validate_character(char_name, section):
    """Validate all behavioral DNA requirements for a character."""
    results = {
        'name': char_name,
        'issues': [],
        'warnings': [],  # For soft gate (voice)
        'passed': True,
        'scores': {},
    }

    # Check on-screen behaviors (also returns explicit_dna if found)
    behavior_issues, behavior_count, explicit_dna = check_on_screen_behaviors(section, char_name)
    results['issues'].extend(behavior_issues)
    results['scores']['behaviors'] = behavior_count

    # Check stress behavior
    stress_issues, stress_ok = check_stress_behavior(section, char_name, explicit_dna)
    results['issues'].extend(stress_issues)
    results['scores']['stress'] = stress_ok

    # Check signature line
    sig_issues, sig_ok = check_signature_line(section, char_name, explicit_dna)
    results['issues'].extend(sig_issues)
    results['scores']['signature'] = sig_ok

    # Check orthogonal trait (now returns quality assessment)
    ortho_issues, ortho_ok, ortho_quality = check_orthogonal_trait(section, char_name, explicit_dna)
    results['issues'].extend(ortho_issues)
    results['scores']['orthogonal'] = ortho_ok
    results['orthogonal_quality'] = ortho_quality

    # Orthogonal trait quality warning (soft gate)
    if ortho_ok and ortho_quality and not ortho_quality['is_quality']:
        quality_warning = {
            'type': 'weak_orthogonal',
            'detail': f'Orthogonal trait may be just a quirk (Quality Gate: {ortho_quality["score"]}/3)',
        }
        tests_failed = []
        if not ortho_quality['revelation']:
            tests_failed.append('Character Revelation')
        if not ortho_quality['arc_potential']:
            tests_failed.append('Arc Potential')
        if not ortho_quality['relationship_work']:
            tests_failed.append('Relationship Work')
        quality_warning['detail'] += f' — missing: {", ".join(tests_failed)}'
        quality_warning['suggestion'] = (
            'A good orthogonal trait does character work. It should pass 2/3: '
            '(1) Reveal who they are beneath the plot, '
            '(2) Evolve over 60 episodes, '
            '(3) Create relationship beats when noticed by others. '
            'Example: "Names things — talks to machines" reveals superstition, '
            'evolves when she talks to a person the same way, '
            'and creates comedy/relationship beats.'
        )
        if ortho_quality.get('is_quirk_warning'):
            quality_warning['detail'] = f'Orthogonal trait looks like a bare quirk: "{ortho_quality["trait_text"][:50]}". ' + quality_warning['detail']
        results['warnings'].append(quality_warning)

    # Check contradiction
    contra_issues, contra_ok = check_contradiction(section, char_name, explicit_dna)
    results['issues'].extend(contra_issues)
    results['scores']['contradiction'] = contra_ok

    # Check character paradox (Structural Gate S3 — Seger)
    paradox_issues, paradox_ok = check_character_paradox(section, char_name, explicit_dna)
    if paradox_issues:
        # Paradox issues are warnings, not blockers (soft gate)
        results['warnings'].extend(paradox_issues)
    results['scores']['paradox'] = paradox_ok

    # Check voice DNA (SOFT gate - warnings only, doesn't block)
    voice_warnings, voice_score = check_voice_dna(section, char_name)
    results['warnings'].extend(voice_warnings)
    results['scores']['voice'] = voice_score

    # Determine pass/fail (voice warnings don't block)
    # A character passes if they have 3+ behaviors, stress, and signature
    has_behaviors = behavior_count >= 3
    has_stress = stress_ok
    has_signature = sig_ok

    results['passed'] = has_behaviors and has_stress and has_signature

    return results


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 validate_behavioral_dna.py <project_path>")
        print("Example: python3 validate_behavioral_dna.py ./leviathan")
        sys.exit(2)

    project_path = Path(sys.argv[1]).resolve()

    if not project_path.exists():
        print(f"Error: Project path does not exist: {project_path}")
        sys.exit(2)

    # Find characters file
    voices_file = find_characters_file(project_path)

    if not voices_file:
        print(f"\n{'='*60}")
        print(f"BEHAVIORAL DNA GATE: ERROR")
        print(f"{'='*60}")
        print(f"\nCannot find characters.md in:")
        print(f"  - {project_path / 'bible' / 'characters.md'} (production)")
        print(f"  - {project_path / 'characters.md'} (development)")
        print(f"\nCreate characters.md using the template at:")
        print(f"  /templates/dev_templates/characters_template.md")
        print(f"{'='*60}\n")
        sys.exit(2)

    # Read and parse
    content = voices_file.read_text()
    characters = extract_characters(content)

    if not characters:
        print(f"\n{'='*60}")
        print(f"BEHAVIORAL DNA GATE: ERROR")
        print(f"{'='*60}")
        print(f"\nNo character sections found in {voices_file}")
        print(f"Ensure characters are marked with ## CHARACTER NAME headers")
        print(f"{'='*60}\n")
        sys.exit(2)

    # Validate each character
    all_results = []
    for char_name, section in characters.items():
        results = validate_character(char_name, section)
        all_results.append(results)

    # Report
    print(f"\n{'='*60}")
    print(f"BEHAVIORAL DNA GATE: Pre-Generation Validation")
    print(f"Project: {project_path.name}")
    print(f"File: {voices_file.relative_to(project_path)}")
    print(f"Characters found: {len(characters)}")
    print(f"{'='*60}")

    all_passed = True
    has_voice_warnings = False
    for result in all_results:
        status = "PASS" if result['passed'] else "FAIL"
        print(f"\n{result['name']}: [{status}]")

        # Show scores
        scores = result['scores']
        print(f"  Behaviors: {scores['behaviors']}/3+",
              "OK" if scores['behaviors'] >= 3 else "MISSING")
        print(f"  Stress: {'OK' if scores['stress'] else 'MISSING/GENERIC'}")
        print(f"  Signature: {'OK' if scores['signature'] else 'MISSING/WEAK'}")
        ortho_q = result.get('orthogonal_quality')
        if scores['orthogonal'] and ortho_q:
            q_score = ortho_q['score']
            q_label = 'QUALITY' if ortho_q['is_quality'] else 'QUIRK WARNING'
            passed_tests = []
            if ortho_q['revelation']: passed_tests.append('Revelation')
            if ortho_q['arc_potential']: passed_tests.append('Arc')
            if ortho_q['relationship_work']: passed_tests.append('Relationship')
            print(f"  Orthogonal: OK ({q_label} {q_score}/3: {', '.join(passed_tests) if passed_tests else 'none'})")
        else:
            print(f"  Orthogonal: {'OK' if scores['orthogonal'] else 'MISSING'}")
        print(f"  Contradiction: {'OK' if scores['contradiction'] else 'MISSING'}")
        print(f"  Paradox: {'OK' if scores.get('paradox') else 'WEAK/MISSING'}")
        print(f"  Voice DNA: {scores.get('voice', 0)}/4",
              "OK" if scores.get('voice', 0) >= 3 else "INCOMPLETE (soft)")

        if result['issues']:
            print(f"\n  Issues (BLOCKING):")
            for issue in result['issues']:
                print(f"    - {issue['detail']}")
                if 'suggestion' in issue:
                    print(f"      Suggestion: {issue['suggestion']}")

        if result.get('warnings'):
            has_voice_warnings = True
            print(f"\n  Voice Warnings (non-blocking):")
            for warning in result['warnings']:
                print(f"    - {warning['detail']}")
                if 'suggestion' in warning:
                    print(f"      Suggestion: {warning['suggestion']}")

        if not result['passed']:
            all_passed = False

    # Final result
    print(f"\n{'='*60}")

    if all_passed:
        if has_voice_warnings:
            print(f"BEHAVIORAL DNA GATE: PASSED (with voice warnings)")
            print(f"All characters have required behavioral DNA")
            print(f"\nVoice DNA incomplete (SOFT gate - doesn't block):")
            print(f"  Consider adding idiom, humor type, anti-patterns, and samples")
            print(f"\nGeneration may proceed")
        else:
            print(f"BEHAVIORAL DNA GATE: PASSED")
            print(f"All characters have required behavioral DNA and voice patterns")
            print(f"Generation may proceed")
        print(f"{'='*60}\n")
        sys.exit(0)
    else:
        print(f"BEHAVIORAL DNA GATE: FAILED")
        print(f"\nCANNOT PROCEED TO GENERATION")
        print(f"\nFix the issues above in characters.md:")
        print(f"  - Add 3+ on-screen behaviors per character")
        print(f"  - Add specific (non-generic) stress behaviors")
        print(f"  - Add signature lines that pass swap test")
        print(f"  - Add orthogonal traits (non-theme-serving)")
        print(f"  - Define contradictions (pattern-break moments)")
        print(f"\nThen re-run: python3 validate_behavioral_dna.py {project_path}")
        print(f"{'='*60}\n")
        sys.exit(1)


if __name__ == "__main__":
    main()
