import asyncio
import os
import sys
from typing import List, Dict, Optional, Any
from .providers.base import LLMProvider
from src.config import settings

class LLMManager:
    """
    Manages LLM providers and orchestrates complex interactions.
    """
    def __init__(self):
        self.providers: Dict[str, LLMProvider] = {}
        self.provider_classes = {}
        self.semaphore = asyncio.Semaphore(settings.concurrency_limit)
        
        from src.logger import get_logger
        self.logger = get_logger("llm_manager")

        from src.security import BudgetManager
        # Default budget $2.00
        budget = float(os.getenv("DAILY_BUDGET_USD", "2.00"))
        self.budget_manager = BudgetManager(budget)

        # Initialize/Discover Providers Dynamically
        self._discover_providers()

        # Initialize Auto-Pilot components
        from src.planner.complexity_analyzer import ComplexityAnalyzer
        from src.models.model_registry import ModelRegistry
        self.complexity_analyzer = ComplexityAnalyzer()
        self.model_registry = ModelRegistry()
        
        # Background cleanup (Fire & Forget logic)
        try:
             retention_days = int(os.getenv("ARTIFACT_RETENTION_DAYS", "0"))
             if retention_days > 0:
                 from src.persistence import SessionRecorder
                 # Using a simple synchronous call for now as file ops on small folders are fast enough
                 # and threadpoolexecutor might be overkill for this stage.
                 # If it gets slow, we can wrap in asyncio.create_task with to_thread.
                 SessionRecorder().cleanup_old_sessions(retention_days)
        except Exception:
             pass # Don't crash startup on cleanup error
        
    def _discover_providers(self):
        """
        Dynamically discovers and registers LLMProvider subclasses from:
        1. src/providers (Built-in)
        2. plugins/ (User extensions)
        """
        import importlib
        import pkgutil
        import inspect
        from src.providers.base import LLMProvider

        # 1. Built-in Providers
        self._scan_package("src.providers")

        # 2. Plugins Directory
        plugins_dir = os.path.join(os.getcwd(), "plugins")
        if os.path.exists(plugins_dir):
            if plugins_dir not in sys.path:
                sys.path.append(plugins_dir)
            
            # Scan all .py files in plugins dir
            for _, name, _ in pkgutil.iter_modules([plugins_dir]):
                try:
                    module = importlib.import_module(name)
                    self._register_providers_from_module(module)
                except Exception as e:
                    self.logger.error("plugin_load_error", plugin=name, error=str(e))

    def _scan_package(self, package_name: str):
        import importlib
        import pkgutil
        
        try:
            package = importlib.import_module(package_name)
            if hasattr(package, "__path__"):
                for _, name, _ in pkgutil.iter_modules(package.__path__):
                    full_name = f"{package_name}.{name}"
                    try:
                        module = importlib.import_module(full_name)
                        self._register_providers_from_module(module)
                    except Exception as e:
                        # Some internal modules like 'base' might not have providers
                        pass
        except Exception as e:
            self.logger.error("package_scan_error", package=package_name, error=str(e))

    def _register_providers_from_module(self, module):
        import inspect
        from src.providers.base import LLMProvider

        for name, obj in inspect.getmembers(module):
            if (inspect.isclass(obj) and 
                issubclass(obj, LLMProvider) and 
                obj is not LLMProvider):
                
                # Check for PROVIDER_NAME
                if hasattr(obj, "PROVIDER_NAME") and obj.PROVIDER_NAME:
                    self.provider_classes[obj.PROVIDER_NAME] = obj
                    # self.logger.debug("provider_registered", name=obj.PROVIDER_NAME, cls=name)

    def _get_provider(self, provider_name: str) -> LLMProvider:
        if provider_name not in self.providers:
            from src.security import BudgetAwareProviderWrapper
            
            if provider_name not in self.provider_classes:
                 raise ValueError(f"Unknown provider: {provider_name}. Available: {list(self.provider_classes.keys())}")
            
            provider_cls = self.provider_classes[provider_name]
            
            try:
                # Instantiate
                raw_provider = provider_cls() 
                # Note: This assumes all providers can be instantiated without args 
                # or read their own env vars (which they currenty do).
                # Generic provider might be tricky if it expects args, 
                # but currently it defaults to env vars if no args.
            except Exception as e:
                 raise RuntimeError(f"Failed to instantiate provider '{provider_name}': {e}")
                
            # WRAP IT WITH SECURITY & BUDGET LAYER
            self.providers[provider_name] = BudgetAwareProviderWrapper(
                raw_provider, 
                self.budget_manager, 
                provider_name
            )
            
        return self.providers[provider_name]

    def _format_error(self, e: Exception) -> str:
        """
        Unpacks Tenacity RetryErrors to get the real cause for cleaner UI display.
        """
        from tenacity import RetryError
        if isinstance(e, RetryError):
            try:
                # e.last_attempt is a Future
                last_exc = e.last_attempt.exception()
                if last_exc:
                     return f"{type(last_exc).__name__}: {str(last_exc)}"
            except Exception:
                pass
        return f"{type(e).__name__}: {str(e)}"

    async def chat_completion(
        self, 
        provider: str, 
        model: str, 
        messages: List[Dict[str, str]], 
        system_prompt: Optional[str] = None
    ) -> str:
        if not self.budget_manager.check_budget():
            raise Exception(f"Daily Budget Limit Exceeded! Status: {self.budget_manager.get_status()}")

        llm = self._get_provider(provider)
        # Estimate cost (very rough: $0.01 per call for now just to trigger usage)
        # Real cost calculation requires token counting which is provider specific
        self.budget_manager.add_cost(0.001, provider, model) # Placeholder cost
        
        return await llm.generate_response(model, messages, system_prompt)

    def list_models(self) -> Dict[str, List[str]]:
        models = {}
        # Try to initialize all providers to list their models
        # In a real app we might handle missing keys gracefully here
        try:
            openai = self._get_provider("openai")
            models["openai"] = openai.list_models()
        except Exception as e:
            self.logger.error("provider_init_error", provider="openai", error=str(e))
            models["openai"] = ["(Error initializing OpenAI provider)"]
            
        try:
            anthropic = self._get_provider("anthropic")
            models["anthropic"] = anthropic.list_models()
        except Exception as e:
            self.logger.error("provider_init_error", provider="anthropic", error=str(e))
            models["anthropic"] = ["(Error initializing Anthropic provider)"]

        try:
            deepseek = self._get_provider("deepseek")
            models["deepseek"] = deepseek.list_models()
        except Exception as e:
            self.logger.error("provider_init_error", provider="deepseek", error=str(e))
            models["deepseek"] = ["(Error initializing DeepSeek provider)"]

        try:
            generic = self._get_provider("generic")
            models["generic"] = generic.list_models()
        except Exception as e:
            # Don't log error for generic if it's just not configured, 
            # but here we initialize it with defaults so it should pass.
            models["generic"] = ["(Generic Provider - Check GENERIC_BASE_URL)"]
            
        try:
            gemini = self._get_provider("gemini")
            models["gemini"] = gemini.list_models()
        except Exception as e:
            self.logger.error("provider_init_error", provider="gemini", error=str(e))
            models["gemini"] = ["(Error initializing Gemini provider)"]

        try:
            op = self._get_provider("openrouter")
            models["openrouter"] = op.list_models()
        except Exception:
            models["openrouter"] = ["(Error initializing OpenRouter)"]
            
        try:
            groq = self._get_provider("groq")
            models["groq"] = groq.list_models()
        except Exception:
            models["groq"] = ["(Error initializing Groq)"]
            
        try:
            mistral = self._get_provider("mistral")
            models["mistral"] = mistral.list_models()
        except Exception:
            models["mistral"] = ["(Error initializing Mistral)"]

        return models

    def _get_auto_pilot_panelists(self, prompt: str) -> List[Dict[str, str]]:
        """
        Auto-Pilot: Determines the best models based on prompt complexity.
        """
        tier = self.complexity_analyzer.analyze(prompt)
        print(f"✈️ Auto-Pilot: Detected complexity '{tier.name}'. Resolving models...")
        
        models = self.model_registry.resolve_tier_models(tier)
        selection_str = [f"{m['provider']}:{m['model']}" for m in models]
        print(f"✈️ Auto-Pilot: Selected panel: {selection_str}")
        return models

    async def collaborative_refine(
        self,
        prompt: str,
        drafter_model: Optional[str] = None,
        reviewers: Optional[List[Dict[str, str]]] = None,
        max_turns: int = 3,
        drafter_provider: Optional[str] = None,
        context: Optional[str] = None,
    ) -> str:
        """
        Orchestrates a debate/refinement loop with a Council of Reviewers.
        """
        import asyncio
        import os
        import json
        
        import asyncio
        import os
        import json
        
        # Priority 1: Auto-Pilot logic
        if not reviewers and (not drafter_provider or not drafter_model):
            try:
                 # Only trigger if we genuinely need models (either drafter or reviewers missing)
                 print("✈️ Auto-Pilot: No drafter/reviewers specified. Delegating to Auto-Pilot...")
                 auto_models = self._get_auto_pilot_panelists(prompt)
                 
                 if auto_models:
                     draft_conf = auto_models[0]
                     if not drafter_provider:
                         drafter_provider = draft_conf["provider"]
                     if not drafter_model:
                         drafter_model = draft_conf["model"]
                     
                     if not reviewers:
                         if len(auto_models) > 1:
                             reviewers = auto_models[1:]
                         else:
                             reviewers = [draft_conf]
            except Exception as e:
                 print(f"✈️ Auto-Pilot Error: {e}")

        # Priority 2: Env Vars (Load Defaults for anything still missing)
        if not drafter_provider:
            drafter_provider = os.getenv("DEFAULT_DRAFTER_PROVIDER", "openai")
        
        if not drafter_model:
            drafter_model = os.getenv("DEFAULT_DRAFTER_MODEL", "gpt-4o")

        if not reviewers:
            # Parse default reviewers from env
            default_revs_str = os.getenv("DEFAULT_REVIEWERS")
            if default_revs_str:
                default_revs_str = default_revs_str.strip()
                try:
                    reviewers = json.loads(default_revs_str)
                except json.JSONDecodeError:
                    reviewers = []
                    for item in default_revs_str.split(','):
                        if ':' in item:
                            p, m = item.split(':', 1)
                            reviewers.append({"provider": p.strip(), "model": m.strip()})
            
            if not reviewers:
                 # Priority 3: Hard Fallback
                 reviewers = [{"provider": "anthropic", "model": "claude-3-opus-20240229"}]

        drafter = self._get_provider(drafter_provider)
        
        # Initialize reviewers
        reviewer_instances = []
        for r in reviewers:
            prov = self._get_provider(r["provider"])
            reviewer_instances.append((prov, r["model"]))
        
        
        # Init Recorder
        from src.persistence import SessionRecorder
        recorder = SessionRecorder()
        session_dir = recorder.create_session_dir("debate", prompt)
        self.logger.info("session_created", dir=session_dir)
        print(f"\n📁 Session Artifacts will be saved to: {session_dir}")

        recorder.save_metadata(session_dir, {
            "topic": prompt,
            "drafter": f"{drafter_provider}:{drafter_model}",
            "reviewers": [f"{p['provider']}:{p['model']}" for p in reviewers] if reviewers else [],
            "max_turns": max_turns
        })

        initial_user_msg = prompt
        if context:
            initial_user_msg += f"\n\n--- ADDITIONAL CONTEXT ---\n{context}\n--------------------------"

        current_response = await drafter.generate_response(
            model=drafter_model,
            messages=[{"role": "user", "content": initial_user_msg}],
            system_prompt="You are an expert assistant. Draft the best possible response."
        )
        
        # Save Initial Draft
        recorder.save_artifact(session_dir, "00_initial_draft.md", current_response)
        
        history = [
            {"role": "user", "content": f"Original Request: {prompt}\n\nCurrent Draft:\n{current_response}"}
        ]
        
        for turn in range(max_turns):
             # Council Review Step (Parallel)
            review_prompt = """
            You are a member of the Expert Council. Analyze the draft provided.
            You MUST return your response in strictly VALID JSON format with these fields:
            {
                "status": "APPROVED" or "REVISE",
                "score": (integer 0-10),
                "feedback": "string explanation"
            }
            Do not add markdown code blocks around the JSON. Just the raw JSON string.
            """
            
            async def get_review(prov, mod, hist):
                try:
                    raw = await prov.generate_response(model=mod, messages=hist, system_prompt=review_prompt)
                    clean = raw.replace("```json", "").replace("```", "").strip()
                    import json
                    return json.loads(clean)
                except Exception as e:
                    error_msg = self._format_error(e)
                    self.logger.error("reviewer_failed", provider=prov.__class__.__name__, error=error_msg)
                    return {"status": "REVISE", "score": 0, "feedback": f"Reviewer Error ({prov.__class__.__name__}): {error_msg}"}

            # Gather reviews from all council members
            reviews = await asyncio.gather(*[
                get_review(prov, mod, history) for prov, mod in reviewer_instances
            ])
            
            # Save Reviews
            review_text = ""
            for i, r in enumerate(reviews):
                review_text += f"Reviewer {i+1}:\n{json.dumps(r, indent=2)}\n\n"
            recorder.save_artifact(session_dir, f"0{turn+1}_reviews.json", review_text)

            # Aggregate Feedback
            total_score = sum(r.get("score", 0) for r in reviews)
            avg_score = total_score / len(reviews)
            all_approved = all(r.get("status") == "APPROVED" for r in reviews)
            
            aggregated_feedback = "Council Feedback:\n"
            for i, r in enumerate(reviews):
                aggregated_feedback += f"- Reviewer {i+1}: {r.get('feedback')}\n"

            if all_approved or avg_score >= 9:
                final = f"Final Answer (Approved by Council):\n\n{current_response}"
                recorder.save_artifact(session_dir, "99_final_approved.md", final)
                return {"content": final, "session_dir": session_dir}
            
            # Drafter Refine Step
            refine_instruction = f"{aggregated_feedback}\n\nPlease parse this feedback and rewrite the draft to address ALL concerns."
            
            # Update history
            history.append({"role": "assistant", "content": aggregated_feedback})
            history.append({"role": "user", "content": refine_instruction})
            
            current_response = await drafter.generate_response(
                model=drafter_model,
                messages=history,
                system_prompt="You are an expert assistant. Synthesize the feedback and refine your work."
            )
            
            recorder.save_artifact(session_dir, f"0{turn+1}_refined_draft.md", current_response)
            
            history.append({"role": "assistant", "content": f"Updated Draft:\n{current_response}"})
            history.append({"role": "user", "content": "Please review this updated draft."})
        
        final_max = f"Result (Max turns reached):\n\n{current_response}"
        recorder.save_artifact(session_dir, "99_final_max_turns.md", final_max)
        return {"content": final_max, "session_dir": session_dir}

    async def evaluate_content(
        self,
        content: str,
        reviewers: Optional[List[Dict[str, str]]] = None,
    ) -> str:
        """
        Submits content to the Council for peer review only (no modification by server).
        """
        import asyncio
        import os
        import json
        
        # Load Defaults
        if not reviewers:
            default_revs_str = os.getenv("DEFAULT_REVIEWERS")
            if default_revs_str:
                default_revs_str = default_revs_str.strip()
                try:
                    reviewers = json.loads(default_revs_str)
                except json.JSONDecodeError:
                    reviewers = []
                    for item in default_revs_str.split(','):
                        if ':' in item:
                            p, m = item.split(':', 1)
                            reviewers.append({"provider": p.strip(), "model": m.strip()})
            
            if not reviewers:
                 reviewers = [{"provider": "anthropic", "model": "claude-3-opus-20240229"}]

        # Init Recorder
        from src.persistence import SessionRecorder
        recorder = SessionRecorder()
        session_dir = recorder.create_session_dir("review", "peer_review_content")
        self.logger.info("session_created", dir=session_dir)
        print(f"\n📁 Session Artifacts will be saved to: {session_dir}")

        # Generate a preview topic
        topic_preview = content[:50].replace("\n", " ") + "..." if len(content) > 50 else content
        
        recorder.save_metadata(session_dir, {
            "type": "peer_review",
            "topic": f"Review: {topic_preview}",
            "reviewers": [f"{p['provider']}:{p['model']}" for p in reviewers],
            "content_length": len(content)
        })
        
        # Save Subject Content
        recorder.save_artifact(session_dir, "00_subject_content.txt", content)

        # Prepare prompt
        review_prompt = """
        You are a member of the Expert Council. Analyze the content provided.
        You MUST return your response in strictly VALID JSON format with these fields:
        {
            "status": "APPROVED" or "REVISE",
            "score": (integer 0-10),
            "feedback": "string explanation"
        }
        Do not add markdown code blocks around the JSON. Just the raw JSON string.
        """
        
        async def get_review(prov_name, mod, text):
            try:
                prov = self._get_provider(prov_name)
                # Ensure the input isn't too large for context window
                messages = [{"role": "user", "content": f"Please review this content:\n\n{text}"}]
                raw = await prov.generate_response(model=mod, messages=messages, system_prompt=review_prompt)
                clean = raw.replace("```json", "").replace("```", "").strip()
                return {"provider": prov_name, "model": mod, "json": json.loads(clean)}
            except Exception as e:
                error_msg = self._format_error(e)
                self.logger.error("reviewer_failed", provider=prov_name, error=error_msg)
                return {"provider": prov_name, "model": mod, "error": error_msg, "status": "ERROR", "score": 0, "feedback": f"Reviewer Error ({prov_name}): {error_msg}"}

        # Gather reviews
        tasks = [get_review(r["provider"], r["model"], content) for r in reviewers]
        results = await asyncio.gather(*tasks)
        
        # Save Reviews
        for r in results:
            if "json" in r:
                filename = f"01_review_{r['provider']}.json"
                recorder.save_artifact(session_dir, filename, json.dumps(r['json'], indent=2))
            else:
                recorder.save_artifact(session_dir, f"error_{r['provider']}.txt", r.get('error', 'Unknown error'))

        # Aggregate
        aggregated_feedback = "## Council Feedback\n"
        aggregated_feedback += f"\n*(Detailed artifacts saved to {session_dir})*\n"
        
        for i, r in enumerate(results):
            reviewer_name = f"{r['provider']}/{r['model']}"
            if "json" in r:
                data = r["json"]
                icon = "✅" if data.get('status') == 'APPROVED' else "⚠️"
                aggregated_feedback += f"\n### {icon} Reviewer {i+1} ({reviewer_name})\n"
                aggregated_feedback += f"**Score:** {data.get('score', 0)}/10\n"
                aggregated_feedback += f"**Status:** {data.get('status')}\n"
                aggregated_feedback += f"**Feedback:** {data.get('feedback')}\n"
            else:
                aggregated_feedback += f"\n### ❌ Reviewer {i+1} ({reviewer_name})\n"
                aggregated_feedback += f"**Error:** {r.get('feedback')}\n"

        recorder.save_artifact(session_dir, "02_summary.md", aggregated_feedback)
        
        return {"content": aggregated_feedback, "session_dir": session_dir}

    async def round_table_debate(
        self,
        prompt: str,
        panelists: Optional[List[Dict[str, str]]] = None,
        moderator_provider: str = "openai",
        moderator_model: str = "gpt-4o",
        context: Optional[str] = None
    ) -> str:
        """
        Conducts a full Round Table debate:
        1. All panelists generate independent answers.
        2. Panelists critique each other's answers.
        3. Moderator synthesizes a final Consensus Answer.
        """
        import asyncio
        import os
        import json
        
        # 1. Load Defaults
        if not panelists:
             # Priority 1: Auto-Pilot (Smart Defaults)
             # Only checks this if User didn't specify --panelists
             try:
                 panelists = self._get_auto_pilot_panelists(prompt)
             except Exception as e:
                 self.logger.warning("auto_pilot_failed", error=str(e))
        
        if not panelists:
            # Priority 2: Env Vars (Static Defaults)
            default_revs_str = os.getenv("DEFAULT_REVIEWERS")
            if default_revs_str:
                try:
                    # Try simplified "p:m, p:m" format first as it's common now
                    if "{" not in default_revs_str:
                         panelists = []
                         for item in default_revs_str.split(','):
                            if ':' in item:
                                p, m = item.split(':', 1)
                                panelists.append({"provider": p.strip(), "model": m.strip()})
                    else:
                        panelists = json.loads(default_revs_str)
                except Exception:
                    pass
            
        if not panelists:
             # Priority 3: Hardcoded Fallback
             panelists = [
                 {"provider": "openai", "model": "gpt-4o"},
                 {"provider": "anthropic", "model": "claude-3-opus-20240229"},
                 {"provider": "deepseek", "model": "deepseek-coder"}
             ]
        
        # --- PERSONA INJECTION LOGIC ---
        from src.personas import MandatoryPersonas
        mandatory = MandatoryPersonas.all()
        
        # 1. Auto-Scale: Ensure we have at least 3 panelists to cover mandatory personas
        while len(panelists) < 3:
            # Cycle through defaults to fill gaps
            defaults = [
                {"provider": "openai", "model": "gpt-4o"},
                {"provider": "anthropic", "model": "claude-3-opus-20240229"},
                {"provider": "deepseek", "model": "deepseek-coder"}
            ]
            next_p = defaults[len(panelists) % 3]
            panelists.append(next_p)
            
        # 2. Assign Personas
        for i, persona in enumerate(mandatory):
            if i < len(panelists):
                panelists[i]["system_prompt"] = persona.system_prompt
                panelists[i]["persona_name"] = persona.name
                print(f"🎭 Role Assignment: {panelists[i]['provider']}/{panelists[i]['model']} -> {persona.name}")
        
        # Ensure unique list and moderator availability
        moderator = self._get_provider(moderator_provider)

        # Step 1: Independent Generation
        self.logger.info("round_table_phase_1", prompt=prompt[:100])
        
        initial_user_msg = prompt
        if context:
            initial_user_msg += f"\n\n--- ADDITIONAL CONTEXT ---\n{context}\n--------------------------"

        async def generate_draft(p_conf):
            try:
                prov = self._get_provider(p_conf["provider"])
                # Inject Persona System Prompt if available
                sys_prompt = p_conf.get("system_prompt", "You are an expert assistant.")
                
                resp = await prov.generate_response(model=p_conf["model"], messages=[{"role": "user", "content": initial_user_msg}], system_prompt=sys_prompt)
                
                # Add Header to content to make persona visible in output
                persona_header = f"**[{p_conf.get('persona_name', 'Expert')}]**\n\n"
                
                return {"provider": p_conf["provider"], "model": p_conf["model"], "content": persona_header + resp, "persona": p_conf.get('persona_name')}
            except Exception as e:
                return {"provider": p_conf["provider"], "error": self._format_error(e)}

        # Init Recorder
        from src.persistence import SessionRecorder
        recorder = SessionRecorder()
        session_dir = recorder.create_session_dir("round_table", prompt)
        self.logger.info("session_created", dir=session_dir)
        
        # Save Metadata
        recorder.save_metadata(session_dir, {
            "topic": prompt,
            "panelists": [f"{p['provider']}:{p['model']} ({p.get('persona_name', 'General')})" for p in panelists],
            "moderator": f"{moderator_provider}:{moderator_model}",
            "context_length": len(initial_user_msg)
        })

        drafts = await asyncio.gather(*[generate_draft(p) for p in panelists])
        valid_drafts = [d for d in drafts if "content" in d]
        failed_drafts = [d for d in drafts if "error" in d]

        print(f"\n📁 Session Artifacts will be saved to: {session_dir}")

        print("\n--- PHASE 1 RESULTS ---")
        if valid_drafts:
            for i, d in enumerate(valid_drafts):
                print(f"\n[Draft by {d['provider']}/{d['model']}]\n{d['content']}\n")
                # Save Draft
                filename = f"01_draft_{d['provider']}_{d['model']}.md"
                recorder.save_artifact(session_dir, filename, d['content'])
        
        if failed_drafts:
            print(f"\n[⚠️ ERRORS DETECTED]")
            for d in failed_drafts:
                error_msg = f"Provider {d['provider']} failed: {d['error']}"
                print(error_msg)
                self.logger.error("draft_generation_failed", provider=d['provider'], error=d['error'])
                recorder.save_artifact(session_dir, f"error_{d['provider']}.txt", d['error'])

        print("-----------------------\n")
        
        if not valid_drafts:
             return {"content": f"❌ CRITICAL ERROR: No panelists successfully generated a draft.\nFailures: {', '.join([d['provider'] + ': ' + d['error'] for d in failed_drafts])}", "session_dir": session_dir}

        self.logger.info("phase_1_results", successful_drafts=len(valid_drafts), failed=len(failed_drafts))

        # Step 2: Cross-Critique
        self.logger.info("round_table_phase_2")
        print("Round Table: Phase 2 - Cross Critique")
        critiques = []
        
        async def critique_others(critic_conf, all_drafts):
            # Critic reviews all drafts EXCEPT their own (or reviews all if simplified)
            context = "Here are proposed solutions from other experts:\n"
            for i, d in enumerate(all_drafts):
                if d["provider"] == critic_conf["provider"] and d["model"] == critic_conf["model"]:
                    continue # Skip self ?? Actually self-reflection is good too. Let's review ALL.
                context += f"--- Solution {i+1} ({d['provider']}) ---\n{d['content'][:2000]}...\n\n" # Truncate for tokens

            # Inject Persona Voice into Critique
            persona_voice = ""
            if "persona_name" in critic_conf:
                 persona_voice = f"You are acting as the {critic_conf['persona_name']}. Critique specifically from this perspective."
            
            sys_prompt = f"{persona_voice}\nYou are a critical expert. Compare these solutions. Identify the strongest points of each and fatal flaws."
            msg = [{"role": "user", "content": f"Original Problem: {prompt}\n\n{context}"}]
            
            try:
                prov = self._get_provider(critic_conf["provider"])
                resp = await prov.generate_response(model=critic_conf["model"], messages=msg, system_prompt=sys_prompt)
                
                # Add Header
                header = f"**[Critique by {critic_conf.get('persona_name', 'Expert')}]**\n\n"
                return {"provider": critic_conf["provider"], "content": header + resp}
            except Exception as e:
                error_msg = self._format_error(e)
                self.logger.error("critique_failed", provider=critic_conf["provider"], error=error_msg)
                return {"provider": critic_conf["provider"], "error": error_msg}

        critique_results_raw = await asyncio.gather(*[critique_others(p, valid_drafts) for p in panelists])
        
        # Save Critiques
        critique_results = []
        for cr in critique_results_raw:
            if "content" in cr:
                critique_results.append(f"Critique by {cr['provider']}:\n{cr['content']}")
                recorder.save_artifact(session_dir, f"02_critique_{cr['provider']}.md", cr['content'])
            else:
                critique_results.append(f"Critique Error ({cr['provider']}): {cr['error']}")


        # Step 3: Synthesis
        self.logger.info("round_table_phase_3")
        print("Round Table: Phase 3 - Synthesis")
        full_context = f"Original Request: {prompt}\n\n"
        
        full_context += "=== PHASE 1: PROPOSED SOLUTIONS ===\n"
        for i, d in enumerate(valid_drafts):
            full_context += f"--- Solution {i+1} by {d['provider']} ---\n{d['content']}\n\n"
            
        full_context += "=== PHASE 2: EXPERT CRITIQUES ===\n"
        for c in critique_results:
            full_context += f"{c}\n\n"
            
        if failed_drafts:
            full_context += "\nNOTE TO MODERATOR: Some panelists failed to respond. Transparency is key. Mention this in your report.\n"
            
        final_system_prompt = """
        You are the Moderator of the Expert Council.
        Your goal is to synthesize the FINAL CONSENSUS.
        
        STRUCTURE YOUR RESPONSE AS FOLLOWS:
        
        ## 1. Executive Summary
        The final unified answer.
        
        ## 2. Key Agreements
        Points where all models aligned.
        
        ## 3. Areas of Disagreement (IMPORTANT)
        Explicitly list where the models disagreed or conflicted. Explain WHY they disagreed.
        
        ## 4. Final Solution / Code
        The synthesized best-of-breed solution.
        
        ## 5. Recommendations for Improvement
        (Optional) Only if valid improvements were suggested.
        """
        
        final_answer = await moderator.generate_response(
            model=moderator_model,
            messages=[{"role": "user", "content": full_context}],
            system_prompt=final_system_prompt
        )
        
        recorder.save_artifact(session_dir, "03_consensus.md", final_answer)
        print(f"✅ Session saved to: {session_dir}")
        
        return {
            "content": f"# Round Table Consensus\n\n{final_answer}\n\n---\n*Based on inputs from: {', '.join([d['provider'] for d in valid_drafts])}*\n\n*(Full artifacts saved to {session_dir})*",
            "session_dir": session_dir
        }

    async def summarize_file(self, file_path: str, content: str, provider_name: str = "openai", model: str = "gpt-4o") -> str:
        """
        Map Phase: Generates a dense technical summary of a single file.
        """
        prompt = f"""
        You are a Technical Architech. Summarize this code file ({file_path}).
        Focus on:
        1. Key Classes and Functions (signatures).
        2. Responsibilities and Design Patterns used.
        3. Dependencies and Exports.
        4. Any complex logic, potential issues, OR AREAS FOR IMPROVEMENT.
        
        Keep the summary concise but technically dense. (Max 150 words). Use bullet points.
        Include a specific bullet point for "Improvements" if applicable.
        ----------------
        File Content:
        {content[:15000]} 
        ----------------
        """
        try:
            async with self.semaphore:
                prov = self._get_provider(provider_name)
                # Use system prompt for conciseness
                summary = await prov.generate_response(
                    model=model, 
                    messages=[{"role": "user", "content": prompt}],
                    system_prompt="You are a code summarizer. Output only the summary."
                )
                return f"### Summary of {file_path}\n{summary}"
        except Exception as e:
            return f"### Summary of {file_path}\n(Error generating summary: {self._format_error(e)})"

    async def map_reduce_context(
        self,
        file_paths: List[str],
        summarizer_provider: str = "openai",
        summarizer_model: str = "gpt-4o"
    ) -> str:
        """
        Performs the Map phase: Summarizes multiple files in parallel.
        Returns a single string containing all summaries.
        """
        import asyncio
        
        # 1. Read Files
        file_contents = {}
        for path in file_paths:
            try:
                # If path is already content (not file), handle? No, assume paths.
                # Actually, if we want to share logic with CLI's collection, 
                # we might need to handle reading there. 
                # But for now, let's keep it compatible with existing analyze_project signature.
                if os.path.exists(path):
                    with open(path, 'r', errors='ignore') as f:
                        file_contents[path] = f.read()
            except Exception as e:
                self.logger.warning("map_read_error", path=path, error=str(e))
        
        if not file_contents:
            return ""

        self.logger.info("starting_map_phase", file_count=len(file_contents))
        
        # 2. Map Phase (Parallel Summarization)
        summary_tasks = [
            self.summarize_file(path, content, summarizer_provider, summarizer_model)
            for path, content in file_contents.items()
        ]
        summaries = await asyncio.gather(*summary_tasks)
        
        return "\n\n".join(summaries)

    async def analyze_project(
        self,
        file_paths: List[str],
        prompt: str,
        panelists: Optional[List[Dict[str, str]]] = None,
        summarizer_provider: str = "openai",
        summarizer_model: str = "gpt-4o"
    ) -> str:
        """
        Orchestrates the Map-Reduce analysis:
        1. Map: Summarize all files in parallel.
        2. Reduce: Feed summaries to Round Table Debate.
        """
        
        project_knowledge = await self.map_reduce_context(
            file_paths, 
            summarizer_provider, 
            summarizer_model
        )
        
        if not project_knowledge:
             return {"content": "Error: No valid files found or empty context.", "session_dir": None}

        self.logger.info("context_reduced", size_chars=len(project_knowledge))
        
        # 3. Reduce Phase (Round Table)
        self.logger.info("starting_reduce_phase")
        
        full_prompt = (
            f"Based on the following TECHNICAL SUMMARIES of a codebase, please address the user's request.\n\n"
            f"--- PROJECT KNOWLEDGE BASE ---\n{project_knowledge}\n--- END KNOWLEDGE ---\n\n"
            f"USER REQUEST: {prompt}"
        )
        
        return await self.round_table_debate(
            prompt=full_prompt,
            panelists=panelists
        )
