
"""
LinkedIn SSI Booster — main entrypoint
=======================================
Generates and schedules LinkedIn posts via the Buffer API to improve
Social Selling Index (SSI) across all four components.

AI backend: Ollama (local) — requires Ollama running on OLLAMA_BASE_URL

Usage:
    python main.py --schedule [--week N] [--dry-run] [--interactive] [--channel linkedin,youtube,threads]
            # --schedule: generate and schedule posts (or preview with --dry-run)
    python main.py --curate               [--dry-run] [--interactive] [--channel linkedin,youtube,threads] [--type idea|post]
    python main.py --console
    python main.py --report
"""

from __future__ import annotations

import os
import json
import argparse
import logging
import re
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from dotenv import load_dotenv
from colorama import Fore, Style, init as _colorama_init

from scheduler import PostScheduler
from content_calendar import CONTENT_CALENDAR
from services.buffer_service import BufferService, BufferQueueFullError, BufferRateLimitError, BufferChannelNotConnectedError
from services.selection_learning import ACCEPTANCE_WINDOW_DAYS


def _configure_stdio() -> None:
    """Keep emoji/Unicode status output from crashing Windows cp1252 shells."""
    for stream_name in ("stdout", "stderr"):
        stream = getattr(sys, stream_name, None)
        reconfigure = getattr(stream, "reconfigure", None)
        if reconfigure is None:
            continue
        try:
            reconfigure(encoding="utf-8", errors="replace")
        except (TypeError, ValueError, OSError):
            # Some wrapped streams do not allow reconfiguration; best effort only.
            pass


_configure_stdio()
_colorama_init(autoreset=True)
load_dotenv()

# --- Intelligent Startup Notice ---
def print_startup_notice():
    print(str(Fore.CYAN) + str(Style.BRIGHT) + "\n👋 Welcome to LinkedIn SSI Booster!" + str(Style.RESET_ALL))
    print(str(Fore.WHITE) + f"Acceptance window: {ACCEPTANCE_WINDOW_DAYS} days" + str(Style.RESET_ALL))
    today = datetime.now(timezone.utc).date()
    cutoff_date = today + timedelta(days=ACCEPTANCE_WINDOW_DAYS)
    print(str(Fore.WHITE) + f"Latest date for 'new' post acceptance: {cutoff_date}" + str(Style.RESET_ALL))
    print(str(Fore.YELLOW) + "\n⚠️  IMPORTANT: Posts scheduled beyond the cutoff may not count for SSI growth!" + str(Style.RESET_ALL))

    # Try to connect to Buffer and check scheduled posts
    buffer_api_key = os.getenv("BUFFER_API_KEY")
    if not buffer_api_key:
        print(str(Fore.YELLOW) + "\n⚠️  BUFFER_API_KEY not set. Buffer scheduling will be disabled." + str(Style.RESET_ALL))
        return
    # Buffer scheduling check logic is commented out for now
    # try:
    #     buffer = BufferService(api_key=buffer_api_key)
    #     channels = buffer.get_channels()
    #     for ch in channels:
    #         ch_id = ch.get("id")
    #         ch_name = ch.get("name")
    #         ch_service = ch.get("service")
    #         scheduled = buffer.get_scheduled_posts(ch_id)
    #         if not scheduled:
    #             continue
    #         # Find the latest scheduled post date
    #         max_due = None
    #         for post in scheduled:
    #             due = post.get("dueAt")
    #             if due:
    #                 try:
    #                     due_dt = datetime.fromisoformat(due.replace("Z", "+00:00"))
    #                     if not max_due or due_dt > max_due:
    #                         max_due = due_dt
    #                 except Exception:
    #                     continue
    #         if max_due and max_due.date() > cutoff_date:
    #             print(str(Fore.YELLOW) + f"\n⚠️  WARNING: Buffer queue for {ch_name} has posts scheduled beyond the acceptance window!" + str(Style.RESET_ALL))
    # except Exception as e:
    #     print(str(Fore.YELLOW) + f"\n⚠️  Could not check Buffer queue: {e}" + str(Style.RESET_ALL))

# --- Coloured log formatter ---
class _ColourFormatter(logging.Formatter):
    _LEVEL = {
        logging.DEBUG:    str(Fore.CYAN)   + "DEBUG"    + str(Style.RESET_ALL),
        logging.INFO:     str(Fore.GREEN)  + "INFO"     + str(Style.RESET_ALL),
        logging.WARNING:  str(Fore.YELLOW) + "WARN"     + str(Style.RESET_ALL),
        logging.ERROR:    str(Fore.RED)    + "ERROR"    + str(Style.RESET_ALL),
        logging.CRITICAL: str(Fore.RED)    + str(Style.BRIGHT) + "CRITICAL" + str(Style.RESET_ALL),
    }
    def format(self, record: logging.LogRecord) -> str:
        record = logging.makeLogRecord(record.__dict__)
        record.levelname = self._LEVEL.get(record.levelno, record.levelname)
        return super().format(record)

_handler = logging.StreamHandler()
_handler.setFormatter(_ColourFormatter("%(asctime)s [%(levelname)s] %(message)s"))
logging.basicConfig(level=logging.INFO, handlers=[_handler])
logger = logging.getLogger(__name__)


from services.console_grounding import (
    parse_query_constraints,
    retrieve_relevant_facts,
    build_deterministic_grounded_reply,
    get_latest_extracted_knowledge,
    build_learned_knowledge_context,
    build_grounding_facts_block,
)
from services.ollama_service import OllamaService
from services.github_service import build_github_profile_context
from services.piper_service import speak_text

def run_console(ai: OllamaService, github_context: str = "", verify: bool = False, avatar_explain: bool = False, dot_report: bool = False) -> None:
    """Run interactive persona chat mode in the terminal.
    
    When verify=True, DoT report scanning and spaCy similarity checks are displayed
    for each generated response.
    When avatar_explain=True, evidence IDs and grounding summary are printed after each LLM reply.
    When dot_report=True, Derivative of Truth report is printed after each LLM reply.
    """
    # Patterns that should always route to LLM (not deterministic fact citation)
    GENERATIVE_REQUEST_PHRASES = [
        "write", "generate", "give me", "post", "reply", "respond", "script", "make me", "create",
        "linkedin", "youtube", "x", "bluesky", "twitter", "tiktok", "thread", "mastodon", "deck", "slide"
    ]

    def _print_status() -> None:
        """Print current console mode status."""
        _status_line = str(Style.DIM) + "  Status: " + str(Style.RESET_ALL)
        _status_line += str(Fore.GREEN if verify else Fore.RED) + f"verify={'ON' if verify else 'OFF'}" + str(Style.RESET_ALL)
        _status_line += str(Style.DIM) + " | " + str(Style.RESET_ALL)
        _status_line += str(Fore.GREEN if avatar_explain else Fore.RED) + f"avatar-explain={'ON' if avatar_explain else 'OFF'}" + str(Style.RESET_ALL)
        _status_line += str(Style.DIM) + " | " + str(Style.RESET_ALL)
        _status_line += str(Fore.GREEN if dot_report else Fore.RED) + f"dot-report={'ON' if dot_report else 'OFF'}" + str(Style.RESET_ALL)
        print(_status_line)

    print(str(Fore.CYAN) + str(Style.BRIGHT) + "\n🧠 Persona Console Mode — Enhanced Query Routing" + str(Style.RESET_ALL))
    print("- No Buffer actions will be performed in this mode.")
    print()
    _print_status()
    print()
    print(str(Fore.WHITE) + str(Style.BRIGHT) + "3 Query Routing Modes:" + str(Style.RESET_ALL))
    print()
    print(str(Fore.YELLOW) + "  1️⃣  Explicit File Request → Deterministic citation (raw facts)" + str(Style.RESET_ALL))
    print("    • persona_graph")
    print("    • extracted_knowledge")
    print("    • domain_knowledge")
    print("    • narrative_memory")
    print()
    print(str(Fore.YELLOW) + "  2️⃣  Learned Knowledge → Latest 5 extracted facts as context (or search)" + str(Style.RESET_ALL))
    print("    • from your learned knowledge, what are the AI trends?")
    print("    • based on what you learned, explain...")
    print("    • search your learned knowledge for RAG techniques")
    print("    • find in your learned knowledge about microservices")
    print()
    print(str(Fore.YELLOW) + "  3️⃣  Everything Else → LLM with graph-enhanced retrieval (default)" + str(Style.RESET_ALL))
    print("    • What is RAG?")
    print("    • Explain vector search")
    print("    • What projects have you worked on?")
    print("    • write a LinkedIn post about...")
    print("    • How are you today?")
    print()
    print(str(Fore.WHITE) + str(Style.BRIGHT) + "🔗 Knowledge Graph Integration:" + str(Style.RESET_ALL))
    print("    • 70% BM25 keyword matching")
    print("    • 20% graph proximity (facts closer to persona node rank higher)")
    print("    • 10% claim support (facts with more supporting edges rank higher)")
    print()
    print(str(Fore.WHITE) + "  Commands: /help, /reset, /reload, /exit, /verify, /avatar-explain, /dot-report, /graph-stats" + str(Style.RESET_ALL))
    print()


    history: list[dict[str, str]] = []
    max_turns = 24

    # Load persona graph for deterministic grounding and persona chat context
    from services.avatar_intelligence import (
        load_avatar_state as _lav_console,
        normalize_evidence_facts,
        normalize_domain_facts,
        normalize_extracted_facts,
        evidence_facts_to_project_facts,
        domain_facts_to_project_facts,
        build_grounding_context,
    )
    
    from services.console_grounding._models import ProjectFact as _ProjectFact
    from services.knowledge_graph import KnowledgeGraphManager
    from services.hybrid_retriever import HybridRetriever

    # Initialize Knowledge Graph Manager
    _kg: KnowledgeGraphManager | None = None
    _hybrid_retriever: HybridRetriever | None = None

    def _load_knowledge_state() -> tuple[list, str, list, list, list]:
        """Load (or reload) avatar knowledge state.
        
        Returns: (_profile_facts, _grounding_context, _raw_evidence_facts, _raw_domain_facts, _raw_extracted_facts)
        """
        avatar_state = _lav_console()
        avatar_facts = normalize_evidence_facts(avatar_state)
        
        domain_facts_raw = []
        domain_pf: list = []
        try:
            domain_facts_raw = normalize_domain_facts(avatar_state)
            domain_pf = domain_facts_to_project_facts(domain_facts_raw)
        except Exception as exc:
            logger.warning("Domain knowledge not loaded for console mode: %s", exc)
        
        extracted_raw = []
        extracted_pf: list = []
        try:
            extracted_raw = normalize_extracted_facts(avatar_state)
            extracted_pf = [
                _ProjectFact(
                    project=f.source_title or "Extracted Knowledge",
                    company="",
                    years="",
                    details=f.statement,
                    source=f"extracted_knowledge:{f.evidence_id}",
                    tags=set(f.tags or []),
                )
                for f in extracted_raw
            ]
        except Exception as exc:
            logger.warning("Extracted knowledge not loaded for console mode: %s", exc)
        
        profile_facts = evidence_facts_to_project_facts(avatar_facts) + domain_pf + extracted_pf
        grounding_ctx = build_grounding_context(avatar_facts)
        if github_context:
            grounding_ctx = f"{grounding_ctx}\n\n{github_context}" if grounding_ctx else github_context
        return profile_facts, grounding_ctx, list(avatar_facts), domain_facts_raw, extracted_raw

    _profile_facts, _grounding_context, _raw_evidence_facts, _raw_domain_facts, _raw_extracted_facts = _load_knowledge_state()
    logger.debug(
        "Console mode: loaded %d grounding facts (%d total)",
        len(_profile_facts),
        len(_profile_facts),
    )

    # Bootstrap Knowledge Graph from avatar state
    try:
        _kg = KnowledgeGraphManager()
        _kg.bootstrap_from_avatar_state(_lav_console())
        _hybrid_retriever = HybridRetriever(kg=_kg)
        logger.debug(
            "Knowledge Graph initialized: %d nodes, %d edges",
            _kg.node_count,
            _kg.edge_count,
        )
    except Exception as kg_err:
        logger.warning("Knowledge Graph initialization failed: %s — using fallback retrieval", kg_err)
        _kg = None
        _hybrid_retriever = None

    from services.console_grounding import truth_gate_result as _tg_result

    def _print_truth_score(reply: str) -> None:
        """Print a minimal 1-line DoT + fact-sim bar after a generated reply.

        spaCy article sim is intentionally excluded — it requires a source article
        text and is always empty when article_text="" (console mode has no article).
        Fact-pool sim (best match across persona/domain facts) works in all contexts.
        
        Only runs when verify=True.
        """
        if not verify:
            return
        try:
            _, _meta = _tg_result(reply, "", _profile_facts)
            dot = _meta.truth_gradient

            if dot >= 0.75:
                _dot_col = str(Fore.GREEN)
                _dot_sym = "●"
            elif dot >= 0.45:
                _dot_col = str(Fore.YELLOW)
                _dot_sym = "◑"
            else:
                _dot_col = str(Fore.RED)
                _dot_sym = "○"

            _fsim_vals = list(_meta.fact_sim_scores.values())
            _fsim = max(_fsim_vals) if _fsim_vals else None

            def _bar(score: float, width: int = 20) -> str:
                filled = round(score * width)
                return "█" * filled + "░" * (width - filled)

            _dot_bar = _bar(dot)
            _line = (
                str(Style.DIM) + "  "
                + _dot_col + _dot_sym + str(Style.RESET_ALL)
                + str(Style.DIM) + f" DoT {dot:.2f}  " + str(Style.RESET_ALL)
                + _dot_col + _dot_bar + str(Style.RESET_ALL)
            )
            if _fsim is not None:
                if _fsim >= 0.75:
                    _fsim_col = str(Fore.GREEN)
                elif _fsim >= 0.45:
                    _fsim_col = str(Fore.YELLOW)
                else:
                    _fsim_col = str(Fore.RED)
                _fsim_bar = _bar(_fsim)
                _line += (
                    str(Style.DIM) + f"   fact sim {_fsim:.2f}  " + str(Style.RESET_ALL)
                    + _fsim_col + _fsim_bar + str(Style.RESET_ALL)
                )
            print(_line)
        except Exception:
            pass  # never interrupt the conversation for a scoring failure

    def print_graph_statistics(summary):
        """
        Prints the graph statistics in a visually enhanced, structured, and colored format.
        """
        print("\n" + "="*60)
        print(f"{Fore.CYAN}{Style.BRIGHT}{'GRAPH STRUCTURE ANALYSIS':^60}")
        print("="*60)

        # --- 1. Core Metrics (Table Format) ---
        print(f"\n{Fore.YELLOW}{'--- CORE METRICS ---':^60}")
        print(f"{Fore.YELLOW}{'Metric':<20}{'Count':>10}{'Details':<30}")
        print("-" * 60)

        print(f"{'Nodes':<20}{summary['nodes']:>10} | {'Edges':<20}{summary['edges']:>10}")
        
        # --- 2. Component Breakdown (Grouped Stats) ---
        print(f"\n{Fore.YELLOW}{'--- COMPONENT BREAKDOWN ---':^60}")
        
        # Node Type Breakdown
        node_types = summary.get('node_types', {})
        if node_types:
            print(f"{Fore.BLUE}{'Node Type Distribution':<20}{'Count':>10}")
            print("-" * 30)
            for node_type, count in node_types.items():
                print(f"  {Fore.BLUE}{node_type:<18}{Fore.BLUE}{count:>10}")
        else:
            print(f"{Fore.BLUE}{'No specific node type breakdown available.'}")

        print("\n" + "="*60)
        print(f"{Fore.CYAN}{Style.BRIGHT}{'ANALYSIS COMPLETE':^60}")
        print("="*60)

    while True:
        try:
            user_input = input("\nYou> ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nExiting console.")
            return

        if not user_input:
            continue

        cmd = user_input.lower()
        if cmd in {"/exit", "/quit"}:
            print("Exiting console.")
            return
        if cmd == "/help":
            print("Commands: /help, /reset, /reload, /exit, /verify, /avatar-explain, /dot-report, /graph-stats, /rei, /rei-toei")
            print("  /reload — re-read persona graph, domain packs, and extracted_knowledge.json")
            print("  /verify — toggle DoT + similarity verification on/off")
            print("  /avatar-explain — toggle avatar-explain report on/off")
            print("  /dot-report — toggle DoT report on/off")
            print("  /graph-stats — show knowledge graph statistics")
            print("  /rei or /rei-toei — switch to Rei Toei music avatar mode")
            continue
        if cmd == "/reset":
            history.clear()
            print("Conversation history cleared.")
            continue
        if cmd == "/reload":
            _profile_facts, _grounding_context, _raw_evidence_facts, _raw_domain_facts, _raw_extracted_facts = _load_knowledge_state()
            print(
                str(Fore.CYAN)
                + f"Knowledge reloaded — {len(_profile_facts)} grounding facts now active."
                + str(Style.RESET_ALL)
            )
            continue
        if cmd == "/verify":
            verify = not verify
            _status = str(Fore.GREEN) + "ON" + str(Style.RESET_ALL) if verify else str(Fore.RED) + "OFF" + str(Style.RESET_ALL)
            print(f"Verify mode: {_status}")
            _print_status()
            continue
        if cmd == "/avatar-explain":
            avatar_explain = not avatar_explain
            _status = str(Fore.GREEN) + "ON" + str(Style.RESET_ALL) if avatar_explain else str(Fore.RED) + "OFF" + str(Style.RESET_ALL)
            print(f"Avatar-explain mode: {_status}")
            _print_status()
            continue
        if cmd == "/dot-report":
            dot_report = not dot_report
            _status = str(Fore.GREEN) + "ON" + str(Style.RESET_ALL) if dot_report else str(Fore.RED) + "OFF" + str(Style.RESET_ALL)
            print(f"DoT report mode: {_status}")
            _print_status()
            continue
        if cmd == "/graph-stats":
            if _kg is None:
                print(str(Fore.RED) + "Knowledge Graph is not available." + str(Style.RESET_ALL))
            else:
                summary = _kg.summary()
                print(f"  Persona ID: {summary['persona_id']}")
                print_graph_statistics(summary)
            continue
        
        # Rei Toei routing: /rei or /rei-toei commands
        if cmd.startswith("/rei-toei") or cmd.startswith("/rei"):
            from services.console_grounding._rei_console import handle_rei_console
            import asyncio
            
            # Extract message after command (if any)
            rei_input = user_input[len("/rei-toei"):].strip() if user_input.lower().startswith("/rei-toei") else user_input[len("/rei"):].strip()
            
            # If no input, provide welcome message
            if not rei_input:
                rei_input = "Hello! Tell me about yourself."
            
            try:
                reply, history = asyncio.run(handle_rei_console(rei_input, ai, history, max_tokens=600))
                print(str(Fore.MAGENTA) + f"Rei> {reply}" + str(Style.RESET_ALL))
                speak_text(reply)
            except Exception as rei_err:
                logger.error(f"Rei console error: {rei_err}")
                print(str(Fore.RED) + f"⚠️ Rei error: {rei_err}" + str(Style.RESET_ALL))
            continue

        # Parse query to determine routing mode
        constraints = parse_query_constraints(user_input)
        
        # Route 1: Explicit file name requests → Deterministic citation (raw facts)
        if constraints.explicit_artifact_request:
            facts = retrieve_relevant_facts(_profile_facts, constraints, limit=8)
            reply = build_deterministic_grounded_reply(user_input, facts, constraints)
            history.append({"role": "user", "content": user_input})
            history.append({"role": "assistant", "content": reply})
            if len(history) > max_turns * 2:
                history = history[-max_turns * 2 :]
            print(str(Fore.GREEN) + f"Sam> {reply}" + str(Style.RESET_ALL))
            speak_text(reply)
            continue

        # Route 2: "From your learned knowledge" → Use latest 8 extracted knowledge as context
        # If user asks to search, use keyword-based search instead of latest 8
        if constraints.use_learned_knowledge:
            if constraints.search_learned_knowledge:
                from services.console_grounding._retrieval import search_learned_knowledge
                learned_facts = search_learned_knowledge(user_input, _profile_facts, limit=8)
            else:
                learned_facts = get_latest_extracted_knowledge(_profile_facts, limit=8)
            # Filter the raw lists based on the 'learned_facts' we actually retrieved
            used_extracted = [f for f in _raw_extracted_facts if any(lf.source.endswith(f.evidence_id) for lf in learned_facts)]
            learned_context = build_learned_knowledge_context(learned_facts)
            history.append({"role": "user", "content": user_input})
            if len(history) > max_turns * 2:
                history = history[-max_turns * 2 :]
            try:
                # Use learned knowledge as PRIMARY context (put it first and emphasize it)
                # The learned knowledge should be the focus, with persona context as background
                enhanced_context = f"{learned_context}\n\n{_grounding_context}" if _grounding_context else learned_context
                print(str(Fore.CYAN) + "🤔 Thinking..." + str(Style.RESET_ALL), end="", flush=True)
                reply = ai.chat_as_persona(history, grounding_context=enhanced_context, max_tokens=600)
                print("\r" + " " * 20 + "\r", end="", flush=True)  # Clear the "Thinking..." line
            except Exception as e:
                print("\r" + " " * 20 + "\r", end="", flush=True)  # Clear the "Thinking..." line
                print(str(Fore.RED) + f"Sam> Error: {e}" + str(Style.RESET_ALL))
                continue
            history.append({"role": "assistant", "content": reply})
            print(str(Fore.GREEN) + f"Sam> {reply}" + str(Style.RESET_ALL))
            speak_text(reply)
            
            if verify:
                print(str(Fore.CYAN) + "📊 Verifying..." + str(Style.RESET_ALL), end="", flush=True)
                _print_truth_score(reply)
                print("\r" + " " * 20 + "\r", end="", flush=True)  # Clear the "Verifying..." line
            
            
            from services.shared import print_validation_reports

            print_validation_reports(
                post_text=reply,
                context_text=user_input,
                grounding_facts=_profile_facts,
                raw_evidence=[],
                raw_domain=[],
                raw_extracted=used_extracted,
                facts_used_for_dot=facts,
                verify=verify,
                avatar_explain=avatar_explain,
                dot_report=dot_report
            )

            continue

        # Route 3: Everything else → LLM with artifact context (default)
        # This includes: domain/project/tech queries, generative requests, general chat
        # Always retrieve relevant facts and use as context
        if _hybrid_retriever is not None:
            # Use hybrid retriever (BM25 + graph proximity + claim support)
            # Combine all raw facts (evidence + domain + extracted) for hybrid scoring
            _all_candidates = list(_raw_evidence_facts) + _raw_domain_facts + _raw_extracted_facts
            facts_ranked = _hybrid_retriever.find_facts(user_input, _all_candidates, limit=8)
            # Convert results back to ProjectFact for compatibility with build_grounding_facts_block
            from services.avatar_intelligence import EvidenceFact, DomainEvidenceFact, ExtractedEvidenceFact
            facts = []
            for f in facts_ranked:
                if isinstance(f, EvidenceFact):
                    # Find matching ProjectFact from _profile_facts
                    matching = [pf for pf in _profile_facts if pf.source.startswith("persona:") and f.evidence_id in pf.source]
                    facts.extend(matching[:1])  # Add first match
                elif isinstance(f, DomainEvidenceFact):
                    matching = [pf for pf in _profile_facts if pf.source.startswith("domain:") and f.evidence_id in pf.source]
                    facts.extend(matching[:1])
                elif isinstance(f, ExtractedEvidenceFact):
                    matching = [pf for pf in _profile_facts if pf.source.startswith("extracted_knowledge:") and f.evidence_id in pf.source]
                    facts.extend(matching[:1])
            # Categorize only the top facts for the validation report
            # This ensures build_explain_output only sees what was actually used
            used_ev = [f for f in facts_ranked[:8] if isinstance(f, EvidenceFact)]
            used_dom = [f for f in facts_ranked[:8] if isinstance(f, DomainEvidenceFact)]
            used_ext = [f for f in facts_ranked[:8] if isinstance(f, ExtractedEvidenceFact)]
            # Ensure we have at least some facts even if conversion failed
            if not facts:
                facts = retrieve_relevant_facts(_profile_facts, constraints, limit=8)
        else:
            # Fallback to simple retrieval when graph not available
            facts = retrieve_relevant_facts(_profile_facts, constraints, limit=8)
        facts_context = build_grounding_facts_block(facts, limit=8)
        history.append({"role": "user", "content": user_input})
        if len(history) > max_turns * 2:
            history = history[-max_turns * 2 :]
        try:
            # Use retrieved facts as additional context
            enhanced_context = f"{_grounding_context}\n\n{facts_context}" if _grounding_context else facts_context
            print(str(Fore.CYAN) + "🤔 Thinking..." + str(Style.RESET_ALL), end="", flush=True)
            reply = ai.chat_as_persona(history, grounding_context=enhanced_context, max_tokens=600)
            print("\r" + " " * 20 + "\r", end="", flush=True)  # Clear the "Thinking..." line
        except Exception as e:
            print("\r" + " " * 20 + "\r", end="", flush=True)  # Clear the "Thinking..." line
            print(str(Fore.RED) + f"Sam> Error: {e}" + str(Style.RESET_ALL))
            continue
        history.append({"role": "assistant", "content": reply})
        print(str(Fore.GREEN) + f"Sam> {reply}" + str(Style.RESET_ALL))
        speak_text(reply)
        
        if verify:
            print(str(Fore.CYAN) + "📊 Verifying..." + str(Style.RESET_ALL), end="", flush=True)
            _print_truth_score(reply)
            print("\r" + " " * 20 + "\r", end="", flush=True)  # Clear the "Verifying..." line

        from services.shared import print_validation_reports
        print_validation_reports(
            post_text=reply,
            context_text=user_input,
            grounding_facts=_profile_facts,
            raw_evidence=used_ev,
            raw_domain=used_dom,
            raw_extracted=used_ext,
            facts_used_for_dot=facts,
            verify=verify,
            avatar_explain=avatar_explain,
            dot_report=dot_report
        )

def main():
    parser = argparse.ArgumentParser(description="LinkedIn SSI Booster via Buffer API")
    parser.add_argument("--schedule",  action="store_true", help="Generate and schedule posts to Buffer (use with --dry-run to preview only)")
    parser.add_argument("--curate",    action="store_true", help="Curate AI news and create ideas in Buffer")
    parser.add_argument("--console",   action="store_true", help="Open interactive persona chat mode (no Buffer calls)")
    parser.add_argument("--verify",    action="store_true", help="Enable DoT report scanning and spaCy similarity checks in console mode (requires --console)")
    parser.add_argument("--report",    action="store_true", help="Print SSI component report")
    parser.add_argument("--save-ssi",  nargs=4, metavar=("BRAND", "FIND", "ENGAGE", "BUILD"),
                        type=float, help="Record today's SSI scores: --save-ssi 10.49 9.69 11.0 12.15")
    parser.add_argument("--bsky-stats", action="store_true", help="Fetch and display live Bluesky profile stats")
    parser.add_argument("--week",      type=int, default=1, help="Week number from content calendar (1-4)")
    parser.add_argument("--dry-run",   action="store_true", help="Preview posts without pushing to Buffer")
    _VALID_CHANNELS = {"linkedin", "x", "bluesky", "threads", "youtube", "facebook", "all"}

    def _parse_channels(value: str) -> list[str]:
        parts = [v.strip() for v in value.split(",") if v.strip()]
        invalid = [p for p in parts if p not in _VALID_CHANNELS]
        if invalid:
            parser.error(f"invalid --channel value(s): {', '.join(invalid)}. Choose from: {', '.join(sorted(_VALID_CHANNELS))}")
        if "all" in parts:
            # Expand "all" to individual channels for generation loop
            return ["linkedin", "x", "bluesky", "threads", "youtube", "facebook"]
        return parts

    parser.add_argument("--channel",   type=_parse_channels, default=["linkedin"],
                        help="Target channel(s) as comma-separated list: linkedin,x,bluesky,threads,youtube,all (default: linkedin)")
    parser.add_argument("--type",      choices=["idea", "post"], default="idea",
                        help="idea: add to Buffer Ideas board; post: schedule directly to next available queue slot (default: idea)")
    parser.add_argument("--debug",     action="store_true", help="Enable DEBUG-level logging (shows raw API payloads and responses)")
    parser.add_argument("--interactive", action="store_true", help="Pause for user confirmation on each truth gate removal")
    parser.add_argument("--avatar-explain", action="store_true", help="Print evidence IDs and grounding summary after each generation")
    parser.add_argument("--avatar-learn-report", action="store_true", help="Print learning report from captured moderation events and exit")
    parser.add_argument("--confidence-policy", choices=["strict", "balanced", "draft-first"], default=None,
                        help="Confidence policy for curate path: strict|balanced|draft-first (default: AVATAR_CONFIDENCE_POLICY env var, else balanced)")
    parser.add_argument("--dot-report", action="store_true",
                        help="Display Derivative of Truth (truth gradient, uncertainty, evidence breakdown) for each generated post")
    parser.add_argument("--learn", action="store_true",
                        help="Extract and persist knowledge from curated articles into extracted_knowledge.json (skipped on --dry-run unless this flag is also set)")
    parser.add_argument("--reconcile", action="store_true",
                        help="Fetch published Buffer posts and reconcile with generated candidates to build acceptance priors")
    parser.add_argument("--classify", action="store_true",
                        help="Batch-classify articles via Model2Vec before curation (requires: pip install model2vec)")
    parser.add_argument("--add-category", nargs=3, metavar=("NAME", "DESCRIPTION", "SSI_COMPONENT"),
                        help="Add a custom category: --add-category 'AI Research' 'Machine learning papers and studies' establish_brand")
    parser.add_argument("--list-categories", action="store_true",
                        help="List all available Model2Vec categories with descriptions and SSI component mapping")
    parser.add_argument("--remove-category", nargs="+", metavar="NAME",
                        help="Remove custom categories by name: --remove-category 'AI Research' 'Government Tech'")
    
    # Rei Toei Music Generation
    rei_group = parser.add_argument_group("Rei Toei Music Generation")
    rei_group.add_argument("--rei-generate", action="store_true",
                           help="Generate Suno song from recent knowledge")
    rei_group.add_argument("--rei-generate-strudel", action="store_true",
                           help="Generate Strudel pattern instead of Suno")
    rei_group.add_argument("--rei-theme", type=str,
                           help="Generate music for specific theme")
    rei_group.add_argument("--rei-explain", action="store_true",
                           help="Show reasoning for generation choices")
    rei_group.add_argument("--rei-preview", action="store_true",
                           help="Preview without saving/executing")
    rei_group.add_argument("--rei-execute", action="store_true",
                           help="Execute Strudel pattern (requires --rei-generate-strudel)")
    
    args = parser.parse_args()

    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)
        logging.getLogger("httpx").setLevel(logging.WARNING)  # suppress noisy HTTP client logs

    if args.console:
        incompatible = []
        if args.schedule:
            incompatible.append("--schedule")
        if args.curate:
            incompatible.append("--curate")
        if args.report:
            incompatible.append("--report")
        if args.save_ssi:
            incompatible.append("--save-ssi")
        if args.bsky_stats:
            incompatible.append("--bsky-stats")
        if args.reconcile:
            incompatible.append("--reconcile")
        if incompatible:
            print(
                str(Fore.YELLOW)
                + f"\n⚠️  --console cannot be combined with: {', '.join(incompatible)}"
                + str(Style.RESET_ALL)
            )
            return
    def build_buffer_service() -> BufferService:
        buffer_api_key = os.getenv("BUFFER_API_KEY")
        if not buffer_api_key:
            raise ValueError("BUFFER_API_KEY environment variable is required")
        return BufferService(api_key=buffer_api_key)

    if args.avatar_learn_report:
        from services.avatar_intelligence import build_learning_report, format_learning_report
        report = build_learning_report()
        print(format_learning_report(report))
        return

    from services.ssi_tracker import SSITracker
    tracker = SSITracker()

    if args.report:
        tracker.print_report()
        return

    if args.reconcile:
        from services.selection_learning import reconcile_published
        buffer = build_buffer_service()
        channel_ids: dict[str, str | None] = {"linkedin": buffer.get_linkedin_channel_id()}
        x_id = os.getenv("BUFFER_X_CHANNEL_ID")
        bsky_id = os.getenv("BUFFER_BLUESKY_CHANNEL_ID")
        threads_id = os.getenv("BUFFER_THREADS_CHANNEL_ID")
        facebook_id = os.getenv("BUFFER_FACEBOOK_CHANNEL_ID")
        if x_id:
            channel_ids["x"] = x_id
        if bsky_id:
            channel_ids["bluesky"] = bsky_id
        if threads_id:
            channel_ids["threads"] = threads_id
        if facebook_id:
            channel_ids["facebook"] = facebook_id
        stats = reconcile_published(buffer, {k: v for k, v in channel_ids.items() if v is not None})
        print(str(Fore.GREEN) + "\n✅  Reconcile complete" + str(Style.RESET_ALL))
        for k, v in stats.items():
            print(f"   {k}: {v}")

    if args.save_ssi:
        brand, find, engage, build = args.save_ssi
        tracker.save_scores(establish=brand, find=find, engage=engage, build=build)
        total = brand + find + engage + build
        print(str(Fore.GREEN) + f"✅  Saved SSI scores: brand={brand} find={find} engage={engage} build={build} total={total:.2f}" + str(Style.RESET_ALL))
        return

    if args.bsky_stats:
        from services.ssi_tracker import fetch_bluesky_stats
        stats = fetch_bluesky_stats()
        if stats:
            print(str(Fore.CYAN) + str(Style.BRIGHT) + f"\n📊 Bluesky stats for @{stats['handle']}" + str(Style.RESET_ALL))
            print(f"  👥 Followers    : {str(Fore.WHITE)}{stats['followers']}{str(Style.RESET_ALL)}")
            print(f"  ➡️  Following    : {stats['following']}")
            print(f"  📝 Total posts  : {stats['posts']}")
            print(str(Fore.CYAN) + f"\n  Last {stats['analysed']} posts (engagement)" + str(Style.RESET_ALL))
            print(f"  ❤️  Likes        : {stats['total_likes']}")
            print(f"  💬 Replies      : {stats['total_replies']}")
            print(f"  🔁 Reposts      : {stats['total_reposts']}")
            print(f"  🗨️  Quotes       : {stats['total_quotes']}")
            print(f"  📈 Avg / post   : {stats['avg_engagement']}")
            if stats.get("top_post"):
                tp = stats["top_post"]
                print(str(Fore.YELLOW) + f"\n  🏆 Top post ({tp['likes']}L {tp['replies']}R {tp['reposts']}RT)" + str(Style.RESET_ALL))
                print(f"  '{tp['text']}'")
                if tp["url"]:
                    print(f"  {tp['url']}")
        return

    if args.list_categories:
        from services.model2vec_service import get_model2vec_service
        svc = get_model2vec_service()
        if not svc.is_available():
            print(str(Fore.YELLOW) + "\n⚠️  Model2Vec is not available (install with: pip install model2vec numpy)" + str(Style.RESET_ALL))
            return
        categories = svc.list_categories()
        if not categories:
            print(str(Fore.YELLOW) + "\n⚠️  No categories found" + str(Style.RESET_ALL))
            return
        print(str(Fore.CYAN) + str(Style.BRIGHT) + f"\n🏷️  Model2Vec Categories ({len(categories)} total)" + str(Style.RESET_ALL))
        print()
        for name, meta in sorted(categories.items()):
            is_custom = meta.get("custom", "False") == "True"
            ssi = meta.get("ssi_component", "general")
            desc = meta.get("description", "")
            custom_marker = str(Fore.MAGENTA) + " [custom]" + str(Style.RESET_ALL) if is_custom else ""
            print(str(Fore.WHITE) + str(Style.BRIGHT) + f"  {name}" + str(Style.RESET_ALL) + custom_marker)
            print(f"    {desc[:100]}{'...' if len(desc) > 100 else ''}")
            print(str(Fore.YELLOW) + f"    → {ssi}" + str(Style.RESET_ALL))
            print()
        return

    if args.add_category:
        from services.model2vec_service import get_model2vec_service
        svc = get_model2vec_service()
        if not svc.is_available():
            print(str(Fore.YELLOW) + "\n⚠️  Model2Vec is not available (install with: pip install model2vec numpy)" + str(Style.RESET_ALL))
            return
        name, description, ssi_component = args.add_category
        valid_ssi = {"establish_brand", "find_right_people", "engage_with_insights", "build_relationships"}
        if ssi_component not in valid_ssi:
            print(str(Fore.RED) + f"\n❌  Invalid SSI component: {ssi_component}" + str(Style.RESET_ALL))
            print(f"    Valid options: {', '.join(sorted(valid_ssi))}")
            return
        success = svc.add_category(name, description, ssi_component)
        if success:
            print(str(Fore.GREEN) + f"\n✅  Added custom category: '{name}'" + str(Style.RESET_ALL))
            print(f"    Description: {description[:80]}{'...' if len(description) > 80 else ''}")
            print(str(Fore.YELLOW) + f"    SSI Component: {ssi_component}" + str(Style.RESET_ALL))
        else:
            print(str(Fore.RED) + f"\n❌  Failed to add category '{name}' (may already exist)" + str(Style.RESET_ALL))
        return

    if args.remove_category:
        from services.model2vec_service import get_model2vec_service
        svc = get_model2vec_service()
        if not svc.is_available():
            print(str(Fore.YELLOW) + "\n⚠️  Model2Vec is not available (install with: pip install model2vec numpy)" + str(Style.RESET_ALL))
            return
        names = args.remove_category
        results = svc.remove_categories(names)
        successes = [n for n, r in zip(names, results) if r]
        failures = [n for n, r in zip(names, results) if not r]
        if successes:
            print(str(Fore.GREEN) + f"\n✅  Removed {len(successes)} category(ies):" + str(Style.RESET_ALL))
            for name in successes:
                print(f"    • {name}")
        if failures:
            print(str(Fore.RED) + f"\n❌  Failed to remove {len(failures)} category(ies):" + str(Style.RESET_ALL))
            for name in failures:
                print(f"    • {name} (not found or is a default category)")
        return

    if not (args.schedule or args.curate or args.console or args.rei_generate or args.rei_generate_strudel):
        parser.print_help()
        return

    print_startup_notice()

    _github_context = build_github_profile_context(
        max_chars=int(os.getenv("GITHUB_CONTEXT_MAX_CHARS", "30000"))
    )
    if _github_context:
        logger.info("GitHub context loaded: %d chars", len(_github_context))

    ai = OllamaService(
        model=os.getenv("OLLAMA_MODEL", "llama3.2"),
        base_url=os.getenv("OLLAMA_BASE_URL", "http://localhost:11434"),
    )

    if args.console:
        run_console(ai=ai, github_context=_github_context, verify=args.verify, avatar_explain=args.avatar_explain, dot_report=args.dot_report)
        return

    # Handle Rei Toei CLI commands — Phase 1E full implementation
    if args.rei_generate or args.rei_generate_strudel:
        import asyncio
        import types as _types
        import uuid as _uuid
        from services.rei_toei_service import (
            load_rei_persona,
            load_rei_domain_knowledge,
            load_strudel_patterns,
            extract_themes,
            generate_song_concept,
            compose_lyrics,
            assemble_suno_prompt,
            submit_to_suno,
            validate_lyrics_with_dot,
            map_concept_to_pattern,
            generate_strudel_code,
            validate_strudel_syntax,
            execute_strudel_pattern,
            save_pattern_to_library,
            Theme,
            StrudelPattern,
        )
        from services.avatar_intelligence import (
            load_avatar_state as _lav_rei,
            normalize_extracted_facts as _nef_rei,
        )

        async def _handle_rei_generate() -> None:
            """Suno song generation pipeline."""
            rei_persona = load_rei_persona()
            rei_domain = load_rei_domain_knowledge()
            avatar_state = _lav_rei()
            # Use normalize_extracted_facts (same pattern as curator.py) to get
            # List[ExtractedEvidenceFact] — the normalized runtime representation.
            _extracted_facts = _nef_rei(avatar_state)

            if args.rei_theme:
                theme = Theme(
                    id=f"cli_{args.rei_theme.replace(' ', '_')}",
                    name=args.rei_theme,
                    technical_concepts=[args.rei_theme],
                    evidence_ids=[],
                    frequency=1,
                    recency_score=1.0,
                )
                print(str(Fore.CYAN) + f"\U0001f3b5 Using theme: {args.rei_theme}" + str(Style.RESET_ALL))
            else:
                themes = extract_themes(_extracted_facts, limit=5)
                if not themes:
                    print(
                        str(Fore.YELLOW)
                        + "\u26a0\ufe0f  No themes found in extracted knowledge. "
                        + "Run --curate --learn first to populate knowledge."
                        + str(Style.RESET_ALL)
                    )
                    return
                theme = themes[0]
                print(str(Fore.CYAN) + f"\U0001f3b5 Top theme: {theme.name} (frequency={theme.frequency})" + str(Style.RESET_ALL))

            print(str(Fore.CYAN) + "\U0001f3bc Generating song concept..." + str(Style.RESET_ALL))
            # generate_song_concept creates its own OllamaService internally
            concept = generate_song_concept(theme, rei_persona, rei_domain)

            print(str(Fore.CYAN) + "\u270d\ufe0f  Composing lyrics..." + str(Style.RESET_ALL))
            # compose_lyrics creates its own OllamaService internally
            lyrics = compose_lyrics(concept, rei_persona, rei_domain)

            suno_prompt = assemble_suno_prompt(concept, lyrics, rei_domain)

            # Display
            print(str(Fore.MAGENTA) + str(Style.BRIGHT) + "\n\U0001f3b5 REI TOEI \u2014 SUNO SONG" + str(Style.RESET_ALL))
            print(str(Fore.WHITE) + str(Style.BRIGHT) + f"Title: {suno_prompt.title}" + str(Style.RESET_ALL))
            print(str(Fore.CYAN) + f"Tags:  {suno_prompt.suno_prompt}" + str(Style.RESET_ALL))
            print()
            print(str(Fore.WHITE) + "Lyrics:" + str(Style.RESET_ALL))
            print(suno_prompt.lyrics)

            if args.rei_explain:
                print(str(Fore.YELLOW) + "\n\U0001f4ca Generation Reasoning:" + str(Style.RESET_ALL))
                print(f"  Theme       : {theme.name}")
                print(f"  Mood        : {concept.mood}")
                print(f"  BPM         : {concept.bpm}")
                print(f"  Genre tags  : {', '.join(concept.genre_tags)}")
                print(f"  Narrative   : {concept.narrative_arc}")
                if suno_prompt.evidence_ids:
                    print(f"  Evidence IDs: {', '.join(suno_prompt.evidence_ids[:5])}")
                dot = validate_lyrics_with_dot(lyrics, _extracted_facts)
                _dot_col = (
                    str(Fore.GREEN) if dot.overall_truth_score >= 0.7
                    else (str(Fore.YELLOW) if dot.overall_truth_score >= 0.4 else str(Fore.RED))
                )
                print(_dot_col + f"  DoT score   : {dot.overall_truth_score:.2f}" + str(Style.RESET_ALL))
                if dot.flagged_claims:
                    print(
                        str(Fore.YELLOW)
                        + f"  Flagged     : {'; '.join(dot.flagged_claims[:3])}"
                        + str(Style.RESET_ALL)
                    )

            if args.rei_preview:
                print(str(Fore.YELLOW) + "\n\U0001f441\ufe0f  Preview mode \u2014 not saved or submitted." + str(Style.RESET_ALL))
                return

            # Save to file
            output_dir = Path("yt-vid-data")
            output_dir.mkdir(exist_ok=True)
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            safe_title = re.sub(r"[^\w\-]", "_", suno_prompt.title[:60]).strip("_")
            output_path = output_dir / f"rei_{timestamp}_{safe_title}_suno.json"
            with open(output_path, "w", encoding="utf-8") as _f:
                json.dump(
                    {
                        "song_id": suno_prompt.song_id,
                        "title": suno_prompt.title,
                        "suno_prompt": suno_prompt.suno_prompt,
                        "lyrics": suno_prompt.lyrics,
                        "evidence_ids": suno_prompt.evidence_ids,
                        "generated_at": suno_prompt.generated_at,
                    },
                    _f,
                    indent=2,
                )
            print(str(Fore.GREEN) + f"\n\u2705  Saved to: {output_path}" + str(Style.RESET_ALL))

            # Submit to Suno if API key available
            suno_api_key = os.getenv("SUNO_API_KEY")
            if suno_api_key:
                print(str(Fore.CYAN) + "\U0001f680 Submitting to Suno API..." + str(Style.RESET_ALL))
                try:
                    task = await submit_to_suno(suno_prompt, wait_for_completion=False, api_key=suno_api_key)
                    print(str(Fore.GREEN) + f"\u2705  Suno task submitted: {task.id}" + str(Style.RESET_ALL))
                    print(f"   Status: {task.status}")
                    if task.audio_url:
                        print(f"   Audio: {task.audio_url}")
                except Exception as _suno_err:
                    print(str(Fore.YELLOW) + f"\u26a0\ufe0f  Suno submission failed: {_suno_err}" + str(Style.RESET_ALL))
            else:
                print(
                    str(Fore.YELLOW)
                    + "\n\U0001f4a1 Set SUNO_API_KEY in .env to submit directly to Suno API."
                    + str(Style.RESET_ALL)
                )

        async def _handle_rei_generate_strudel() -> None:
            """Strudel pattern generation pipeline."""
            rei_persona = load_rei_persona()
            rei_domain = load_rei_domain_knowledge()
            pattern_library = load_strudel_patterns()
            avatar_state = _lav_rei()
            # Use normalize_extracted_facts (same pattern as curator.py) to get
            # List[ExtractedEvidenceFact] — the normalized runtime representation.
            _extracted_facts = _nef_rei(avatar_state)

            if args.rei_theme:
                theme = Theme(
                    id=f"cli_{args.rei_theme.replace(' ', '_')}",
                    name=args.rei_theme,
                    technical_concepts=[args.rei_theme],
                    evidence_ids=[],
                    frequency=1,
                    recency_score=1.0,
                )
                print(str(Fore.CYAN) + f"\U0001f3b5 Using theme: {args.rei_theme}" + str(Style.RESET_ALL))
            else:
                themes = extract_themes(_extracted_facts, limit=5)
                if not themes:
                    print(
                        str(Fore.YELLOW)
                        + "\u26a0\ufe0f  No themes found in extracted knowledge. "
                        + "Run --curate --learn first to populate knowledge."
                        + str(Style.RESET_ALL)
                    )
                    return
                theme = themes[0]
                print(str(Fore.CYAN) + f"\U0001f3b5 Top theme: {theme.name} (frequency={theme.frequency})" + str(Style.RESET_ALL))

            template = map_concept_to_pattern(theme, pattern_library)
            if template is None:
                # Fallback: use first available template
                if pattern_library.templates:
                    template = pattern_library.templates[0]
                else:
                    print(str(Fore.YELLOW) + "\u26a0\ufe0f  No Strudel pattern templates available." + str(Style.RESET_ALL))
                    return

            print(
                str(Fore.CYAN)
                + f"\U0001f3bc Generating Strudel pattern (template: {template.name})..."
                + str(Style.RESET_ALL)
            )
            # generate_strudel_code is synchronous and returns a StrudelPattern object
            strudel_pattern = generate_strudel_code(theme, template, rei_persona, rei_domain)
            strudel_code = strudel_pattern.strudel_code
            validation = validate_strudel_syntax(strudel_code)

            # Display
            print(str(Fore.MAGENTA) + str(Style.BRIGHT) + "\n\U0001f3b5 REI TOEI \u2014 STRUDEL PATTERN" + str(Style.RESET_ALL))
            print(str(Fore.WHITE) + str(Style.BRIGHT) + f"Theme   : {theme.name}" + str(Style.RESET_ALL))
            print(str(Fore.CYAN) + f"Template: {template.name}" + str(Style.RESET_ALL))
            print()
            print(str(Fore.WHITE) + "Strudel Code:" + str(Style.RESET_ALL))
            print(str(Fore.GREEN) + strudel_code + str(Style.RESET_ALL))

            if validation.valid:
                print(str(Fore.GREEN) + "\n\u2705  Syntax valid" + str(Style.RESET_ALL))
            else:
                print(
                    str(Fore.YELLOW)
                    + f"\n\u26a0\ufe0f  Syntax warnings: {'; '.join(validation.errors)}"
                    + str(Style.RESET_ALL)
                )

            if args.rei_explain:
                print(str(Fore.YELLOW) + "\n\U0001f4ca Generation Reasoning:" + str(Style.RESET_ALL))
                print(f"  Theme        : {theme.name}")
                print(f"  Template     : {template.name}")
                print(f"  Description  : {template.description}")
                print(f"  Suitable for : {', '.join(template.suitable_for_concepts[:3])}")
                if theme.evidence_ids:
                    print(f"  Evidence IDs : {', '.join(theme.evidence_ids[:5])}")

            if args.rei_preview:
                print(str(Fore.YELLOW) + "\n\U0001f441\ufe0f  Preview mode \u2014 not saved or executed." + str(Style.RESET_ALL))
                return

            # Save pattern to library (strudel_pattern already has all fields set)
            save_pattern_to_library(strudel_pattern)
            print(str(Fore.GREEN) + f"\n\u2705  Pattern saved to library (ID: {strudel_pattern.pattern_id})" + str(Style.RESET_ALL))

            # Execute if requested
            if args.rei_execute:
                print(str(Fore.CYAN) + "\U0001f680 Executing Strudel pattern via MCP agent..." + str(Style.RESET_ALL))
                try:
                    # execute_strudel_pattern takes a StrudelPattern object
                    result = await execute_strudel_pattern(strudel_pattern)
                    if result.success:
                        print(str(Fore.GREEN) + "\u2705  Pattern executing in Strudel!" + str(Style.RESET_ALL))
                        if result.message:
                            print(str(Fore.CYAN) + f"   {result.message}" + str(Style.RESET_ALL))
                    else:
                        print(str(Fore.YELLOW) + f"\u26a0\ufe0f  Execution failed: {result.error}" + str(Style.RESET_ALL))
                except Exception as _exec_err:
                    print(
                        str(Fore.YELLOW)
                        + f"\u26a0\ufe0f  Strudel MCP agent unavailable: {_exec_err}"
                        + str(Style.RESET_ALL)
                    )
                    print("   Start the Strudel service: bash run.sh --profile full up -d")

        try:
            if args.rei_generate:
                asyncio.run(_handle_rei_generate())
            elif args.rei_generate_strudel:
                asyncio.run(_handle_rei_generate_strudel())
        except KeyboardInterrupt:
            print("\nRei Toei generation cancelled.")
        except Exception as _rei_err:
            logger.error("Rei Toei CLI error: %s", _rei_err)
            print(str(Fore.RED) + f"\n\u274c  Rei Toei error: {_rei_err}" + str(Style.RESET_ALL))
        return

    if args.curate:
        buffer = None if args.dry_run else build_buffer_service()
        from services.shared import AVATAR_CONFIDENCE_POLICY
        from services.content_curator import ContentCurator
        confidence_policy = args.confidence_policy or AVATAR_CONFIDENCE_POLICY
        curator = ContentCurator(ai_service=ai, buffer_service=buffer, confidence_policy=confidence_policy, github_context=_github_context, classify=args.classify)

        try:
            ideas = curator.curate_and_create_ideas(dry_run=args.dry_run, channel=args.channel, message_type=args.type, request_delay=5.0, interactive=args.interactive, avatar_explain=args.avatar_explain, dot_report=args.dot_report, learn=args.learn, classify=args.classify)
        except BufferQueueFullError as e:
            print(str(Fore.YELLOW) + f"\n⚠️  Buffer queue is full — no new posts were scheduled.\n   {e}\n   Free up slots at https://publish.buffer.com before running again." + str(Style.RESET_ALL))
            return
        except BufferRateLimitError as e:
            print(
                str(Fore.YELLOW)
                + f"\n⚠️  Buffer API rate limit reached.\n   {e}\n"
                + "   Wait for the retry window, then run the command again."
                + str(Style.RESET_ALL)
            )
            return
        except BufferChannelNotConnectedError as e:
            print(
                str(Fore.YELLOW)
                + f"\n⚠️  Requested channel is not connected in Buffer.\n   {e}\n"
                + "   Connect the channel in Buffer or run with a different --channel value."
                + str(Style.RESET_ALL)
            )
            return
        noun = "posts" if args.type == "post" else "ideas"

        
        return


    if args.schedule:
        week_topics = CONTENT_CALENDAR.get(f"week_{args.week}", [])
        if not week_topics:
            logger.error("No content found for week %d", args.week)
            return

        # args.channel is already a list from _parse_channels
        for channel in args.channel:
            logger.info("📝 Generating %d posts for week %d (channel: %s)...", len(week_topics), args.week, channel)
            posts = []
            from services.avatar_intelligence import (
                load_avatar_state as _lav_gen,
                normalize_evidence_facts,
                normalize_domain_facts,
                retrieve_evidence,
                evidence_facts_to_project_facts,
                EvidenceFact,
                DomainEvidenceFact,
                domain_facts_to_project_facts,
            )
            _gen_avatar_state = _lav_gen()
            if channel == "youtube" and not args.dry_run:
                Path("yt-vid-data").mkdir(exist_ok=True)
            if args.avatar_explain:
                from services.avatar_intelligence import build_explain_output, format_explain_output

            for topic in week_topics:
                logger.info("  Generating: %s", topic['title'])
                grounding_query = f"{topic['title']}. {topic['angle']}. {topic.get('ssi_component', 'establish_brand')}"
                # Combine both fact types
                _gen_avatar_facts = normalize_evidence_facts(_gen_avatar_state)
                _gen_domain_facts = normalize_domain_facts(_gen_avatar_state)
                all_facts = list(_gen_avatar_facts) + list(_gen_domain_facts)
                _ev_proj = int(os.getenv("EVIDENCE_PROJECT_COUNT", "3"))
                _ev_dom = int(os.getenv("EVIDENCE_DOMAIN_COUNT", "2"))
                relevant = retrieve_evidence(grounding_query, all_facts, limit=_ev_proj + _ev_dom)  # type: ignore[arg-type]

                # Split by type and convert
                persona_facts = [f for f in relevant if isinstance(f, EvidenceFact)]
                domain_facts = [f for f in relevant if isinstance(f, DomainEvidenceFact)]
                grounding_facts = (
                    evidence_facts_to_project_facts(persona_facts)
                    + domain_facts_to_project_facts(domain_facts)
                )

                if channel == "youtube":
                    post = ai.generate_youtube_short_script(
                        title=topic["title"],
                        angle=topic["angle"],
                        ssi_component=topic.get("ssi_component", "establish_brand"),
                        grounding_facts=grounding_facts,
                        interactive=args.interactive,
                    )
                    safe_title = re.sub(r"[^\w\-]", "_", topic["title"][:60]).strip("_")
                    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                    script_path = Path("yt-vid-data") / f"{timestamp}_{safe_title}.txt"
                    script_content = (
                        f"TITLE: {topic['title']}\n"
                        f"SSI COMPONENT: {topic.get('ssi_component', 'establish_brand')}\n\n"
                        f"{post}\n"
                    )
                    if not args.dry_run:
                        script_path.write_text(script_content, encoding="utf-8")
                    print(str(Fore.RED) + str(Style.BRIGHT) + f"🎬 YOUTUBE SHORT SCRIPT (channel: {channel}):" + str(Style.RESET_ALL))
                    print(str(Fore.WHITE) + f"📄 TITLE:  {topic['title']}" + str(Style.RESET_ALL))
                    print(str(Fore.CYAN) + f"🎯 SSI:    {topic.get('ssi_component', 'establish_brand')}" + str(Style.RESET_ALL))
                    print(f"\n{post}\n")
                    print("GitHub: https://buff.ly/tfajNLI")
                    print("Sign up for Buffer with my partner link — join.buffer.com/samjd42  — to start scheduling, publishing, and analyzing your social posts in one place while supporting my work.")
                    if not args.dry_run:
                        print(str(Fore.GREEN) + f"💾 Saved to: {script_path}" + str(Style.RESET_ALL))
                    posts.append({**topic, "generated_text": post})
                else:
                    post = ai.generate_linkedin_post(
                        title=topic["title"],
                        angle=topic["angle"],
                        ssi_component=topic.get("ssi_component", "establish_brand"),
                        hashtags=topic.get("hashtags", []),
                        grounding_facts=grounding_facts,
                        channel=channel,
                        interactive=args.interactive,
                    )
                    # Hashtags are only appended for LinkedIn-style posts.
                    if channel not in ("x", "bluesky", "threads", "youtube"):
                        hashtag_str = " ".join(f"#{h.lstrip('#')}" for h in topic.get("hashtags", []))
                        if hashtag_str and hashtag_str not in post:
                            post = post.rstrip() + f"\n\n{hashtag_str}"
                    posts.append({**topic, "generated_text": post})

                if channel != "youtube":
                    print(str(Fore.CYAN) + f"\n{'='*60}" + str(Style.RESET_ALL))
                    print(str(Fore.WHITE) + str(Style.BRIGHT) + f"📝 TOPIC: {topic['title']} (channel: {channel})" + str(Style.RESET_ALL))
                    print(str(Fore.CYAN) + f"🎯 SSI COMPONENT: {topic.get('ssi_component', 'establish_brand')}" + str(Style.RESET_ALL))
                    print(f"\n{post}\n")

                if args.dot_report:
                    try:
                        from services.derivative_of_truth import (
                            EvidencePath,
                            EVIDENCE_TYPE_SECONDARY,
                            REASONING_TYPE_LOGICAL,
                            score_claim_with_truth_gradient,
                            report_truth_gradient,
                            format_truth_gradient_report,
                        )
                        _dot_paths = [
                            EvidencePath(
                                source=f.source if hasattr(f, "source") else str(f),
                                evidence_type=EVIDENCE_TYPE_SECONDARY,
                                reasoning_type=REASONING_TYPE_LOGICAL,
                                credibility=0.7,
                            )
                            for f in grounding_facts
                        ]
                        _dot_result = score_claim_with_truth_gradient(post, _dot_paths)
                        _dot_report_dict = report_truth_gradient(post, _dot_result, verbose=True)
                        _dot_colour = str(Fore.RED) if _dot_result.flagged else str(Fore.CYAN)
                        from services.derivative_of_truth._reporting import format_dot_report_header
                        print(format_dot_report_header())
                        print(format_truth_gradient_report(_dot_report_dict))
                        print()
                    except Exception as _dot_err:
                        logger.debug("DoT report unavailable: %s", _dot_err)

                if args.avatar_explain:
                    # Combine project and domain facts for retrieval
                    from services.avatar_intelligence import EvidenceFact, DomainEvidenceFact, normalize_extracted_facts
                    from services.console_grounding import truth_gate_result as _tgr_exp
                    _all_facts = list(_gen_avatar_facts) + list(_gen_domain_facts)
                    _ev_proj2 = int(os.getenv("EVIDENCE_PROJECT_COUNT", "3"))
                    _ev_dom2 = int(os.getenv("EVIDENCE_DOMAIN_COUNT", "2"))
                    _relevant = retrieve_evidence(grounding_query, _all_facts, limit=_ev_proj2 + _ev_dom2)  # type: ignore[arg-type]
                    
                    # Score and filter extracted facts by relevance (retrieve_evidence doesn't support ExtractedEvidenceFact type)
                    _gen_extracted_facts_all = normalize_extracted_facts(_gen_avatar_state)
                    _ev_extracted = int(os.getenv("EVIDENCE_EXTRACTED_COUNT", "2"))
                    _q_tokens = set(re.findall(r"[a-zA-Z0-9_+#.-]{2,}", grounding_query.lower()))
                    _scored_extracted = []
                    for _fact in _gen_extracted_facts_all:
                        _text = " ".join([
                            getattr(_fact, "statement", ""),
                            getattr(_fact, "source_title", ""),
                            " ".join(getattr(_fact, "tags", []) or []),
                            " ".join(getattr(_fact, "entities", []) or []),
                        ])
                        _tokens = set(re.findall(r"[a-zA-Z0-9_+#.-]{2,}", _text.lower()))
                        _score = len(_q_tokens & _tokens)
                        _scored_extracted.append((_score, _fact))
                    _scored_extracted.sort(key=lambda x: x[0], reverse=True)
                    _relevant_extracted = [f for s, f in _scored_extracted if s > 0][:_ev_extracted]
                    if not _relevant_extracted and _gen_extracted_facts_all:
                        _relevant_extracted = list(_gen_extracted_facts_all)[:_ev_extracted]
                    
                    _, _gate_meta = _tgr_exp(post, topic.get("angle", ""), grounding_facts)
                    _explain = build_explain_output(
                        evidence_facts=_relevant,
                        article_ref=topic.get("title", ""),
                        channel=channel,
                        ssi_component=topic.get('ssi_component', 'establish_brand'),
                        dot_per_sentence_scores=_gate_meta.dot_per_sentence_scores,
                        spacy_sim_scores=_gate_meta.spacy_sim_scores,
                        extracted_facts=_relevant_extracted,
                    )
                    print(format_explain_output(_explain))

            # Only schedule if not dry-run and not YouTube
            if not args.dry_run:
                if channel == "youtube":
                    print(
                        str(Fore.YELLOW)
                        + "\n⚠️  YouTube scripts were generated and saved locally, but not scheduled to Buffer.\n"
                        + "   Buffer YouTube scheduling requires a video file upload (title/category/video).\n"
                        + "   Render with lipsync.video, then upload manually."
                        + str(Style.RESET_ALL)
                    )
                    continue
                buffer = build_buffer_service()
                scheduler = PostScheduler(buffer_service=buffer)
                try:
                    scheduled = scheduler.schedule_week(posts=posts, week_number=args.week, channel=channel)
                    print(str(Fore.GREEN) + f"\n✅  Scheduled {len(scheduled)} posts to Buffer ({channel})" + str(Style.RESET_ALL))
                except BufferQueueFullError as e:
                    print(str(Fore.YELLOW) + f"\n⚠️  Buffer queue is full — no new posts were scheduled.\n   {e}\n   Free up slots at https://publish.buffer.com before running again." + str(Style.RESET_ALL))
                except BufferRateLimitError as e:
                    print(
                        str(Fore.YELLOW)
                        + f"\n⚠️  Buffer API rate limit reached.\n   {e}\n"
                        + "   Wait for the retry window, then run the command again."
                        + str(Style.RESET_ALL)
                    )
                except BufferChannelNotConnectedError as e:
                    print(
                        str(Fore.YELLOW)
                        + f"\n⚠️  Requested channel is not connected in Buffer.\n   {e}\n"
                        + "   Connect the channel in Buffer or run with a different --channel value."
                        + str(Style.RESET_ALL)
                    )



if __name__ == "__main__":
    main()
