import os
import pyttsx3
import base64
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import io
import sys
import traceback
import re
import uuid 

from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
from langchain_core.globals import set_llm_cache
from langchain_community.cache import InMemoryCache
from .security import SecurityManager

try: import polars as pl
except ImportError: pl = None

try: from langchain_ollama import ChatOllama
except ImportError: ChatOllama = None
try: from langchain_openai import ChatOpenAI
except ImportError: ChatOpenAI = None
try: from langchain_anthropic import ChatAnthropic
except ImportError: ChatAnthropic = None
try: from langchain_google_genai import ChatGoogleGenerativeAI
except ImportError: ChatGoogleGenerativeAI = None
try: from langchain_groq import ChatGroq
except ImportError: ChatGroq = None
try: from langchain_mistralai import ChatMistralAI
except ImportError: ChatMistralAI = None

class LLMEngine:
    def __init__(self, 
                 model_name="llama3.2", 
                 provider="auto", 
                 api_key=None, 
                 base_url=None, 
                 temperature=0.1,
                 use_cache=False,
                 security_filters=None,
                 mcp_tools=None,
                 logger=None,
                 **llm_kwargs):
        
        self.model_name = model_name
        self.provider = provider.lower()
        self.temperature = temperature
        self.api_key = api_key
        self.base_url = base_url
        self.llm_kwargs = llm_kwargs
        self.mcp_tools = mcp_tools or []
        self.logger = logger
        
        self.security = SecurityManager(filters=security_filters)
        
        if use_cache:
            set_llm_cache(InMemoryCache())
            self._log("LLM cache enabled.")
        else:
            set_llm_cache(None)

        try: 
            self.tts_engine = pyttsx3.init()
            self.tts_engine.setProperty('rate', 170)
        except: self.tts_engine = None

        if self.provider == "auto":
            self.provider = self._detect_provider(model_name)
        
        self._log(f"Engine enabled: {self.provider.upper()} ({self.model_name}) | Security: {list(self.security.active_patterns.keys())}")
        self.llm = self._create_llm_instance()

    def _log(self, msg, level="info"):
        if self.logger: self.logger(msg, level)
        else: print(msg)

    def _detect_provider(self, name):
        name = name.lower()
        if "gpt" in name or "o1-" in name: return "openai"
        if "claude" in name: return "anthropic"
        if "gemini" in name: return "google"
        if "mixtral" in name or "llama3-70b" in name: return "groq"
        if "mistral" in name: return "mistral"
        if "deepseek" in name: return "deepseek"
        if "grok" in name: return "grok"
        return "ollama"

    def _create_llm_instance(self):
        kwargs = self.llm_kwargs
        if self.provider == "ollama":
            if not ChatOllama: raise ImportError("pip install langchain-ollama")
            return ChatOllama(model=self.model_name, temperature=self.temperature, base_url=self.base_url or "http://localhost:11434", **kwargs)
        elif self.provider == "openai":
            if not ChatOpenAI: raise ImportError("pip install langchain-openai")
            return ChatOpenAI(model=self.model_name, api_key=self.api_key or os.getenv("OPENAI_API_KEY"), temperature=self.temperature, **kwargs)
        elif self.provider == "anthropic":
            if not ChatAnthropic: raise ImportError("pip install langchain-anthropic")
            return ChatAnthropic(model_name=self.model_name, api_key=self.api_key or os.getenv("ANTHROPIC_API_KEY"), temperature=self.temperature, **kwargs)
        elif self.provider == "google":
            if not ChatGoogleGenerativeAI: raise ImportError("pip install langchain-google-genai")
            return ChatGoogleGenerativeAI(model=self.model_name, google_api_key=self.api_key or os.getenv("GOOGLE_API_KEY"), temperature=self.temperature, **kwargs)
        elif self.provider == "groq":
            if not ChatGroq: raise ImportError("pip install langchain-groq")
            return ChatGroq(model_name=self.model_name, api_key=self.api_key or os.getenv("GROQ_API_KEY"), temperature=self.temperature, **kwargs)
        elif self.provider == "mistral":
            if not ChatMistralAI: raise ImportError("pip install langchain-mistralai")
            return ChatMistralAI(model=self.model_name, api_key=self.api_key or os.getenv("MISTRAL_API_KEY"), temperature=self.temperature, **kwargs)
        elif self.provider in ["deepseek", "grok", "custom", "xai"]:
            if not ChatOpenAI: raise ImportError("pip install langchain-openai")
            url = self.base_url
            key = self.api_key
            if self.provider == "deepseek" and not url: url, key = "https://api.deepseek.com/v1", key or os.getenv("DEEPSEEK_API_KEY")
            elif self.provider == "grok" and not url: url, key = "https://api.x.ai/v1", key or os.getenv("XAI_API_KEY")
            return ChatOpenAI(model=self.model_name, api_key=key, base_url=url, temperature=self.temperature, **kwargs)
        else: raise ValueError(f"Unknown provider: {self.provider}")

    def _build_system_prompt(self, config):
        parts = []
        parts.append(f"ROLE: {config.get('role', 'Autonomous AI Assistant')}")
        parts.append(f"GOAL: {config.get('goal', 'Provide factual answers based on knowledge.')}")
        
        # Anti-Hallucination & Reasoning Directives
        parts.append("CRITICAL INSTRUCTIONS:")
        parts.append("- You must break down complex queries into logical sub-tasks before answering.")
        parts.append("- ONLY use the information provided in the CONTEXT. Do not invent or hallucinate data.")
        parts.append("- If the answer is completely absent from the context, explicitly state: 'I do not have enough information in my knowledge base to answer this.'")
        parts.append("- LANGUAGE RULE: Respond in the SAME LANGUAGE as the user's query.")

        if config.get("company_name"):
            c_info = f"COMPANY CONTEXT: {config.get('company_name')}"
            if config.get("company_description"): c_info += f" - {config.get('company_description')}"
            parts.append(c_info)
            
        parts.append(f"USER PROFILE: {config.get('user_profile', 'Standard User')}")
        if config.get("instructions"): parts.append(f"ADDITIONAL: {config.get('instructions')}")
        if config.get("reflection"): parts.append("MODE: Reflection Enabled. Think step-by-step internally before giving the final output.")
        if config.get("system_prompt"): parts.append(f"SYSTEM OVERRIDE: {config.get('system_prompt')}")
        
        return "\n".join(parts)

    def generate(self, prompt, context=None, context_config={}, image_path=None, vocal_out=False, stream=False, output_format="text"):
        messages = []
        sys_prompt = self._build_system_prompt(context_config)
        messages.append(SystemMessage(content=sys_prompt))

        format_instruction = ""
        if output_format == "json": format_instruction = "\nIMPORTANT: Respond only in valid JSON."
        elif output_format == "markdown": format_instruction = "\nIMPORTANT: Use Markdown format."
        elif output_format == "cartoon": format_instruction = "\nIMPORTANT: You are a friendly cartoon character!"
        
        if self.security.is_active:
            filters_list = ", ".join(self.security.active_patterns.keys())
            format_instruction += f"\nSECURITY RULE: Mask confidential data [{filters_list}]."

        final_prompt = prompt + format_instruction
        if context:
            final_prompt = f"--- CONTEXT ---\n{context}\n----------------\nTASK: {final_prompt}"
            
        content = [{"type": "text", "text": final_prompt}]

        if image_path:
            try:
                with open(image_path, "rb") as f:
                    enc = base64.b64encode(f.read()).decode("utf-8")
                content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{enc}"}})
            except: pass
            
        messages.append(HumanMessage(content=content))
        
        if stream: return self._stream_response_generator(messages, vocal_out, context_config)
        else:
            try:
                response_obj = self.llm.invoke(messages)
                clean_text = self.security.scrub(response_obj.content, context_config.get('user_profile'))
                if vocal_out and self.tts_engine: self.speak(clean_text)
                return clean_text
            except Exception as e: 
                self._log(f"Generation Error: {e}", "error")
                return f"Error: {e}"

    # =========================================================================
    # DATA ANALYST AGENT (PANDAS & POLARS OPTIMIZED)
    # =========================================================================
    def generate_analyst(self, query, df, context_config, history="", output_format="text", stream=False, canvas=None):
        # 1. Utiliser des slashes pour éviter les erreurs d'échappement Windows (\)
        unique_chart_name = f"public/graph/chart_{uuid.uuid4().hex[:8]}.png" if context_config.get("save_graph") else f"temp_chart_{uuid.uuid4().hex[:8]}.png"
        unique_chart_name = unique_chart_name.replace('\\', '/')
        
        # 2. Introspection robuste du DataFrame (Pandas vs Polars)
        df_info = ""
        df_head = ""
        df_type = "pandas"
        
        if pl is not None and isinstance(df, pl.DataFrame):
            df_type = "polars"
            try:
                schema_dict = dict(df.schema)
                df_info = "Schema (Columns and Types):\n" + "\n".join([f"- {col}: {dtype}" for col, dtype in schema_dict.items()])
            except:
                df_info = str(df.schema)
                
            try:
                # Utiliser to_pandas si pyarrow est dispo, sinon str()
                df_head = df.head(3).to_pandas().to_markdown()
            except:
                df_head = str(df.head(3))
        else:
            buffer = io.StringIO()
            df.info(buf=buffer)
            df_info = buffer.getvalue()
            df_head = df.head(3).to_markdown()

        sys_prompt = self._build_system_prompt(context_config)
        
        code_prompt = f"""
You are an Autonomous Data Scientist. You have a {df_type} DataFrame named `df`.

HISTORY:
{history}

DATAFRAME INFO:
{df_info}

FIRST 3 ROWS:
{df_head}

USER TASK: {query}

CRITICAL RULES:
1. Write ONLY pure Python code inside a ```python ``` block. Do NOT write any conversational text outside the code block.
2. If using Polars, remember it is immutable. You must reassign variables (e.g. `df = df.filter(...)`). To plot Polars data, you MUST convert it to pandas first using `df.to_pandas()`.
3. Clean data if necessary.
4. For charts, use matplotlib/seaborn.
5. MANDATORY: Save any plot using EXACTLY: `plt.savefig('{unique_chart_name}')`. Do NOT use plt.show().
6. SAFETY: Never execute destructive operations (drop/overwrite original df without copy).
7. Use `print()` to output the result of your analysis so it can be captured.
8. UNIVERSAL TEXT SEARCH & FILTERING (CRITICAL): 
   - NEVER use strict exact matching (like `==`) when searching for terms (e.g., 'sucre', 'écouteur'). Users make typos, forget accents, or use different casing.
   - ALWAYS use case-insensitive substring matching.
   - If using Pandas: `df[df['col'].astype(str).str.contains('term', case=False, na=False, regex=False)]`.
   - If using Polars: `df.filter(pl.col('col').cast(pl.Utf8).str.to_lowercase().str.contains('term'.lower()))`.
9. SMART FALLBACK: 
   - If you don't know the exact column, write a loop or condition to search across ALL string columns.
   - If your first search returns 0 rows, broaden your search by using a partial string (e.g., 'ecout' instead of 'écouteur') or ignoring accents natively.
   - ALWAYS print the data you found. If 0 rows are found after broad search, print: "No results found for X in the database."
10. CHART INTERPRETATION RULE (CRITICAL): The AI that interprets this code's result CANNOT SEE THE SAVED IMAGE. Therefore, whenever you create a chart, you MUST ALSO `print()` the key statistics or the aggregated data being plotted (e.g., `print(df['col'].describe())` or `print(data_to_plot)`). If you don't print the data, the AI will refuse to answer.
11. Always use `print()` to output the result of your analysis or the data summary.
"""
        # 6. Use `print()` to output results. | Use `print()` to output the result of your analysis so it can be captured.
        messages = [SystemMessage(content=sys_prompt), HumanMessage(content=code_prompt)]
        
        max_retries = 3
        code_to_run = ""
        execution_result = ""
        error_msg = ""
        
        old_stdout = sys.stdout 
        
        for attempt in range(max_retries):
            try:
                self._log(f"Data Analyst Attempt {attempt+1}/{max_retries}...")
                resp = self.llm.invoke(messages)
                
                # 3. Extraction de code Ultra-Robuste (ignore la casse, gère ```py et ```python)
                code_match = re.search(r"```(?:python|py)?\s*(.*?)```", resp.content, re.DOTALL | re.IGNORECASE)
                
                if code_match:
                    code_to_run = code_match.group(1).strip()
                else:
                    # Sécurité si le LLM ne met pas de backticks: Nettoie le texte non-code
                    lines = resp.content.replace("`", "").strip().split('\n')
                    code_to_run = "\n".join([l for l in lines if not l.lower().startswith(("here", "sure", "voici", "bien", "the code"))])
                
                if not code_to_run:
                    raise SyntaxError("No Python code was generated.")
                
                redirected_output = io.StringIO()
                sys.stdout = redirected_output
                
                local_scope = {"df": df, "pd": pd, "np": np, "plt": plt, "sns": sns}
                if pl: local_scope["pl"] = pl
                
                exec(code_to_run, {}, local_scope)
                
                execution_result = redirected_output.getvalue().strip()
                sys.stdout = old_stdout 
                
                if not execution_result and not os.path.exists(unique_chart_name):
                    self._log("Code executed but produced no output. Forcing retry...", "warning")
                    raise ValueError("The code executed successfully but printed nothing and saved no graph. Use print() to output results.")
                
                self._log("Code execution successful.")
                break
                
            except Exception as e:
                sys.stdout = old_stdout 
                error_msg = str(e)
                self._log(f"Execution failed: {error_msg}", "warning")
                
                messages.append(AIMessage(content=resp.content))
                # Fournir le traceback exact au LLM pour l'auto-correction
                error_feedback = f"Execution failed with error:\n{error_msg}\n\nPlease fix the python code and try again. Make sure to ONLY output pure python code inside ```python ```."
                messages.append(HumanMessage(content=error_feedback))

        if not execution_result and not os.path.exists(unique_chart_name):
            execution_result = f"Failed to analyze the data due to technical errors. Last error: {error_msg}"

        interpretation_prompt = f"USER REQUEST: {query}\nEXECUTION RESULT:\n{execution_result}\nAnalyze this result and provide a clear, business-friendly answer to the user. Do not explain the code."
        final_messages = [SystemMessage(content=sys_prompt), HumanMessage(content=interpretation_prompt)]
        
        final_answer = self.llm.invoke(final_messages).content
        
        # Gestion du graphique pour UI
        if os.path.exists(unique_chart_name):
            try:
                with open(unique_chart_name, "rb") as img_file:
                    b64_string = base64.b64encode(img_file.read()).decode("utf-8")
                
                chart_markdown = f"\n\n![Generated Chart](data:image/png;base64,{b64_string})\n"
                final_answer += chart_markdown
                
                if canvas: canvas.update(image_path=unique_chart_name)
                
                if not context_config.get("save_graph"):
                    os.remove(unique_chart_name) 
            except Exception as e: 
                self._log(f"Error handling graph image: {e}", "error")

        return self.security.scrub(final_answer, context_config.get('user_profile'))

    def _stream_response_generator(self, messages, vocal_out, context_config):
        display_buffer = ""
        full_text = ""
        try:
            for chunk in self.llm.stream(messages):
                token = chunk.content
                if token:
                    display_buffer += token
                    if len(display_buffer) > 20 or any(c in token for c in ".!?\n"):
                        clean = self.security.scrub(display_buffer, context_config.get('user_profile'))
                        yield clean
                        full_text += clean
                        display_buffer = ""
            if display_buffer:
                clean = self.security.scrub(display_buffer, context_config.get('user_profile'))
                yield clean
                full_text += clean
            if vocal_out: self.speak(full_text)
        except Exception as e:
            yield f"\n[Stream Error: {e}]"

    def speak(self, text):
        try:
            self.tts_engine.say(text)
            self.tts_engine.runAndWait()
        except: pass

# ------------------------------------------------------------------------
# import os
# import pyttsx3
# import base64
# import pandas as pd
# import numpy as np
# import matplotlib.pyplot as plt
# import seaborn as sns
# import io
# import sys
# import traceback
# import re
# import uuid 

# from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
# from langchain_core.globals import set_llm_cache
# from langchain_community.cache import InMemoryCache
# from .security import SecurityManager

# try: import polars as pl
# except ImportError: pl = None

# try: from langchain_ollama import ChatOllama
# except ImportError: ChatOllama = None
# try: from langchain_openai import ChatOpenAI
# except ImportError: ChatOpenAI = None
# try: from langchain_anthropic import ChatAnthropic
# except ImportError: ChatAnthropic = None
# try: from langchain_google_genai import ChatGoogleGenerativeAI
# except ImportError: ChatGoogleGenerativeAI = None
# try: from langchain_groq import ChatGroq
# except ImportError: ChatGroq = None
# try: from langchain_mistralai import ChatMistralAI
# except ImportError: ChatMistralAI = None

# class LLMEngine:
#     def __init__(self, 
#                  model_name="llama3.2", 
#                  provider="auto", 
#                  api_key=None, 
#                  base_url=None, 
#                  temperature=0.1,
#                  use_cache=False,
#                  security_filters=None,
#                  mcp_tools=None,
#                  logger=None,
#                  **llm_kwargs):
        
#         self.model_name = model_name
#         self.provider = provider.lower()
#         self.temperature = temperature
#         self.api_key = api_key
#         self.base_url = base_url
#         self.llm_kwargs = llm_kwargs
#         self.mcp_tools = mcp_tools or []
#         self.logger = logger
        
#         self.security = SecurityManager(filters=security_filters)
        
#         if use_cache:
#             set_llm_cache(InMemoryCache())
#             self._log("LLM cache enabled.")
#         else:
#             set_llm_cache(None)

#         try: 
#             self.tts_engine = pyttsx3.init()
#             self.tts_engine.setProperty('rate', 170)
#         except: self.tts_engine = None

#         if self.provider == "auto":
#             self.provider = self._detect_provider(model_name)
        
#         self._log(f"Engine enabled: {self.provider.upper()} ({self.model_name}) | Security: {list(self.security.active_patterns.keys())}")
#         self.llm = self._create_llm_instance()

#     def _log(self, msg, level="info"):
#         if self.logger: self.logger(msg, level)
#         else: print(msg)

#     def _detect_provider(self, name):
#         name = name.lower()
#         if "gpt" in name or "o1-" in name: return "openai"
#         if "claude" in name: return "anthropic"
#         if "gemini" in name: return "google"
#         if "mixtral" in name or "llama3-70b" in name: return "groq"
#         if "mistral" in name: return "mistral"
#         if "deepseek" in name: return "deepseek"
#         if "grok" in name: return "grok"
#         return "ollama"

#     def _create_llm_instance(self):
#         kwargs = self.llm_kwargs
#         if self.provider == "ollama":
#             if not ChatOllama: raise ImportError("pip install langchain-ollama")
#             return ChatOllama(model=self.model_name, temperature=self.temperature, base_url=self.base_url or "http://localhost:11434", **kwargs)
#         elif self.provider == "openai":
#             if not ChatOpenAI: raise ImportError("pip install langchain-openai")
#             return ChatOpenAI(model=self.model_name, api_key=self.api_key or os.getenv("OPENAI_API_KEY"), temperature=self.temperature, **kwargs)
#         elif self.provider == "anthropic":
#             if not ChatAnthropic: raise ImportError("pip install langchain-anthropic")
#             return ChatAnthropic(model_name=self.model_name, api_key=self.api_key or os.getenv("ANTHROPIC_API_KEY"), temperature=self.temperature, **kwargs)
#         elif self.provider == "google":
#             if not ChatGoogleGenerativeAI: raise ImportError("pip install langchain-google-genai")
#             return ChatGoogleGenerativeAI(model=self.model_name, google_api_key=self.api_key or os.getenv("GOOGLE_API_KEY"), temperature=self.temperature, **kwargs)
#         elif self.provider == "groq":
#             if not ChatGroq: raise ImportError("pip install langchain-groq")
#             return ChatGroq(model_name=self.model_name, api_key=self.api_key or os.getenv("GROQ_API_KEY"), temperature=self.temperature, **kwargs)
#         elif self.provider == "mistral":
#             if not ChatMistralAI: raise ImportError("pip install langchain-mistralai")
#             return ChatMistralAI(model=self.model_name, api_key=self.api_key or os.getenv("MISTRAL_API_KEY"), temperature=self.temperature, **kwargs)
#         elif self.provider in ["deepseek", "grok", "custom", "xai"]:
#             if not ChatOpenAI: raise ImportError("pip install langchain-openai")
#             url = self.base_url
#             key = self.api_key
#             if self.provider == "deepseek" and not url: url, key = "https://api.deepseek.com/v1", key or os.getenv("DEEPSEEK_API_KEY")
#             elif self.provider == "grok" and not url: url, key = "https://api.x.ai/v1", key or os.getenv("XAI_API_KEY")
#             return ChatOpenAI(model=self.model_name, api_key=key, base_url=url, temperature=self.temperature, **kwargs)
#         else: raise ValueError(f"Unknown provider: {self.provider}")

#     def _build_system_prompt(self, config):
#         parts = []
#         parts.append(f"ROLE: {config.get('role', 'Autonomous AI Assistant')}")
#         parts.append(f"GOAL: {config.get('goal', 'Provide factual answers based on knowledge.')}")
        
#         # Anti-Hallucination & Reasoning Directives
#         parts.append("CRITICAL INSTRUCTIONS:")
#         parts.append("- You must break down complex queries into logical sub-tasks before answering.")
#         parts.append("- ONLY use the information provided in the CONTEXT. Do not invent or hallucinate data.")
#         parts.append("- If the answer is completely absent from the context, explicitly state: 'I do not have enough information in my knowledge base to answer this.'")
#         parts.append("- LANGUAGE RULE: Respond in the SAME LANGUAGE as the user's query.")

#         if config.get("company_name"):
#             c_info = f"COMPANY CONTEXT: {config.get('company_name')}"
#             if config.get("company_description"): c_info += f" - {config.get('company_description')}"
#             parts.append(c_info)
            
#         parts.append(f"USER PROFILE: {config.get('user_profile', 'Standard User')}")
#         if config.get("instructions"): parts.append(f"ADDITIONAL: {config.get('instructions')}")
#         if config.get("reflection"): parts.append("MODE: Reflection Enabled. Think step-by-step internally before giving the final output.")
#         if config.get("system_prompt"): parts.append(f"SYSTEM OVERRIDE: {config.get('system_prompt')}")
        
#         return "\n".join(parts)

#     def generate(self, prompt, context=None, context_config={}, image_path=None, vocal_out=False, stream=False, output_format="text"):
#         messages = []
#         sys_prompt = self._build_system_prompt(context_config)
#         messages.append(SystemMessage(content=sys_prompt))

#         format_instruction = ""
#         if output_format == "json": format_instruction = "\nIMPORTANT: Respond only in valid JSON."
#         elif output_format == "markdown": format_instruction = "\nIMPORTANT: Use Markdown format."
#         elif output_format == "cartoon": format_instruction = "\nIMPORTANT: You are a friendly cartoon character!"
        
#         if self.security.is_active:
#             filters_list = ", ".join(self.security.active_patterns.keys())
#             format_instruction += f"\nSECURITY RULE: Mask confidential data [{filters_list}]."

#         final_prompt = prompt + format_instruction
#         if context:
#             final_prompt = f"--- CONTEXT ---\n{context}\n----------------\nTASK: {final_prompt}"
            
#         content = [{"type": "text", "text": final_prompt}]

#         if image_path:
#             try:
#                 with open(image_path, "rb") as f:
#                     enc = base64.b64encode(f.read()).decode("utf-8")
#                 content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{enc}"}})
#             except: pass
            
#         messages.append(HumanMessage(content=content))
        
#         if stream: return self._stream_response_generator(messages, vocal_out, context_config)
#         else:
#             try:
#                 response_obj = self.llm.invoke(messages)
#                 clean_text = self.security.scrub(response_obj.content, context_config.get('user_profile'))
#                 if vocal_out and self.tts_engine: self.speak(clean_text)
#                 return clean_text
#             except Exception as e: 
#                 self._log(f"Generation Error: {e}", "error")
#                 return f"Error: {e}"

#     # =========================================================================
#     # DATA ANALYST AGENT (PANDAS & POLARS)
#     # =========================================================================
#     def generate_analyst(self, query, df, context_config, history="", output_format="text", stream=False, canvas=None):
#         unique_chart_name = f"public/graph/chart_{uuid.uuid4().hex[:8]}.png" if context_config.get("save_graph") else f"temp_chart_{uuid.uuid4().hex[:8]}.png"
        
#         # Dataframe Introspection (Pandas vs Polars)
#         df_info = ""
#         df_head = ""
#         df_type = "pandas"
        
#         if pl is not None and isinstance(df, pl.DataFrame):
#             df_type = "polars"
#             df_info = str(df.schema)
#             df_head = df.head(3).to_pandas().to_markdown() if hasattr(df.head(3), "to_pandas") else str(df.head(3))
#         else:
#             buffer = io.StringIO()
#             df.info(buf=buffer)
#             df_info = buffer.getvalue()
#             df_head = df.head(3).to_markdown()

#         sys_prompt = self._build_system_prompt(context_config)
        
#         code_prompt = f"""
#         You are an Autonomous Data Scientist. You have a {df_type} DataFrame named `df`.
        
#         HISTORY:
#         {history}

#         DATAFRAME INFO:
#         {df_info}

#         FIRST 3 ROWS:
#         {df_head}
        
#         USER TASK: {query}
        
#         RULES:
#         1. Write ONLY python code inside ```python ``` blocks.
#         2. Clean data if necessary.
#         3. For charts, use matplotlib/seaborn.
#         4. MANDATORY: Save any plot using `plt.savefig('{unique_chart_name}')`. Do NOT use plt.show().
#         5. Use print() to output results.
#         6. SAFETY: Never execute destructive operations (drop/overwrite original df without copy).
#         """
        
#         messages = [SystemMessage(content=sys_prompt), HumanMessage(content=code_prompt)]
        
#         max_retries = 3
#         code_to_run = ""
#         execution_result = ""
        
#         # Pre-define old_stdout to prevent UnboundLocalError
#         old_stdout = sys.stdout 
        
#         for attempt in range(max_retries):
#             try:
#                 self._log(f"Data Analyst Attempt {attempt+1}/{max_retries}...")
#                 resp = self.llm.invoke(messages)
#                 code_match = re.search(r"```python(.*?)```", resp.content, re.DOTALL)
#                 code_to_run = code_match.group(1).strip() if code_match else resp.content
                
#                 redirected_output = io.StringIO()
#                 sys.stdout = redirected_output
                
#                 local_scope = {"df": df, "pd": pd, "np": np, "plt": plt, "sns": sns}
#                 if pl: local_scope["pl"] = pl
                
#                 exec(code_to_run, {}, local_scope)
                
#                 execution_result = redirected_output.getvalue()
#                 sys.stdout = old_stdout 
#                 self._log("Code execution successful.")
#                 break
#             except Exception as e:
#                 sys.stdout = old_stdout 
#                 error_msg = str(e)
#                 self._log(f"Execution failed: {error_msg}", "warning")
#                 messages.append(AIMessage(content=code_to_run))
#                 messages.append(HumanMessage(content=f"Error executing code: {error_msg}. Analyze the error, fix the code, and try again."))

#         interpretation_prompt = f"USER REQUEST: {query}\nEXECUTION RESULT:\n{execution_result}\nAnalyze this result and provide a clear, business-friendly answer to the user."
#         final_messages = [SystemMessage(content=sys_prompt), HumanMessage(content=interpretation_prompt)]
        
#         final_answer = self.llm.invoke(final_messages).content
        
#         # Manage UI Graph Display
#         if os.path.exists(unique_chart_name):
#             try:
#                 with open(unique_chart_name, "rb") as img_file:
#                     b64_string = base64.b64encode(img_file.read()).decode("utf-8")
                
#                 chart_markdown = f"\n\n![Generated Chart](data:image/png;base64,{b64_string})\n"
#                 final_answer += chart_markdown
                
#                 if canvas: canvas.update(image_path=unique_chart_name)
                
#                 if not context_config.get("save_graph"):
#                     os.remove(unique_chart_name) 
#             except Exception as e: 
#                 self._log(f"Error handling graph image: {e}", "error")

#         return self.security.scrub(final_answer, context_config.get('user_profile'))

#     def _stream_response_generator(self, messages, vocal_out, context_config):
#         display_buffer = ""
#         full_text = ""
#         try:
#             for chunk in self.llm.stream(messages):
#                 token = chunk.content
#                 if token:
#                     display_buffer += token
#                     if len(display_buffer) > 20 or any(c in token for c in ".!?\n"):
#                         clean = self.security.scrub(display_buffer, context_config.get('user_profile'))
#                         yield clean
#                         full_text += clean
#                         display_buffer = ""
#             if display_buffer:
#                 clean = self.security.scrub(display_buffer, context_config.get('user_profile'))
#                 yield clean
#                 full_text += clean
#             if vocal_out: self.speak(full_text)
#         except Exception as e:
#             yield f"\n[Stream Error: {e}]"

#     def speak(self, text):
#         try:
#             self.tts_engine.say(text)
#             self.tts_engine.runAndWait()
#         except: pass