import numpy as np
import math
import random
from datetime import datetime
import json
import re
from collections import defaultdict
import pickle
import os
from pathlib import Path


class NeuralNetwork:
    def __init__(self, input_size, hidden_sizes, output_size):
        self.layers = []
        self.weights = []
        self.biases = []
        self.learning_rate = 0.01

        # Build network architecture
        layer_sizes = [input_size] + hidden_sizes + [output_size]
        for i in range(len(layer_sizes) - 1):
            # He initialization (suitable for ReLU, also a good general choice)
            self.weights.append(
                np.random.randn(layer_sizes[i], layer_sizes[i + 1])
                * np.sqrt(2.0 / layer_sizes[i])
            )
            self.biases.append(np.zeros((1, layer_sizes[i + 1])))

    def activation(self, x, derivative=False):
        # Sigmoid activation function
        if derivative:
            # Derivative of sigmoid: sigma(x) * (1 - sigma(x))
            # x here is assumed to be the *output* of the sigmoid activation (i.e., a value between 0 and 1)
            return x * (1 - x)
        # Apply sigmoid, clipping values to prevent overflow/underflow issues with np.exp for very large/small x
        return 1 / (1 + np.exp(-np.clip(x, -500, 500)))

    def forward_propagation(self, input_data):
        # Store the input data as the 'output' of the first (input) layer
        self.layers = [input_data]
        for i in range(len(self.weights)):
            # Calculate the weighted sum of the previous layer's output plus bias
            current_layer_input = (
                np.dot(self.layers[-1], self.weights[i]) + self.biases[i]
            )
            # Apply activation and store the result as the current layer's output
            self.layers.append(self.activation(current_layer_input))
        return self.layers[-1]  # Return the final output of the network

    def backward_propagation(self, input_data, target):
        # Ensure self.layers is populated with the results of a forward pass
        output = self.forward_propagation(input_data)

        # Calculate initial error (difference between target and actual output)
        error = target - output
        # Calculate delta for the output layer
        # Multiply error by the derivative of the activation function of the output layer's output
        output_delta = error * self.activation(self.layers[-1], derivative=True)
        deltas = [output_delta]  # Store deltas for each layer, starting from the output

        # Backpropagate through hidden layers
        # Iterate backwards from the last hidden layer (index len(self.weights) - 1)
        # down to the first hidden layer (index 1 for self.layers, index 0 for weights)
        # We start from len(self.weights) - 1 because deltas[-1] corresponds to the output layer
        # and self.weights[i] connects layer i to layer i+1.
        # The loop condition `i > 0` ensures we process deltas for hidden layers.
        for i in range(len(self.weights) - 1, 0, -1):
            # Calculate delta for the current hidden layer
            # Propagate delta from the next layer (deltas[-1]) back through the weights
            # Then multiply by the derivative of the current layer's activation output
            delta = np.dot(deltas[-1], self.weights[i].T) * self.activation(
                self.layers[i], derivative=True
            )
            deltas.append(delta)

        deltas.reverse()  # Reverse the deltas list so deltas[i] corresponds to self.layers[i]

        # Update weights and biases for all layers
        for i in range(len(self.weights)):
            # Update weights: learning_rate * (transpose of previous layer's output) * current layer's delta
            self.weights[i] += self.learning_rate * np.dot(self.layers[i].T, deltas[i])
            # Update biases: learning_rate * sum of current layer's delta (summing across batch samples)
            self.biases[i] += self.learning_rate * np.sum(
                deltas[i], axis=0, keepdims=True
            )


class ConversationalAI:
    def __init__(self):
        # Core neural network components
        self.input_size = 1000
        self.hidden_sizes = [512, 256, 128]
        self.output_size = (
            1000  # Output size matches input size for autoencoder-like behavior
        )
        self.learning_rate = (
            0.01  # Global learning rate, can be adjusted or passed to NNs
        )

        # Initialize neural networks
        self.language_network = NeuralNetwork(
            self.input_size, self.hidden_sizes, self.output_size
        )
        self.understanding_network = NeuralNetwork(
            self.input_size, self.hidden_sizes, self.output_size
        )
        self.generation_network = NeuralNetwork(
            self.input_size + self.output_size, self.hidden_sizes, self.output_size
        )  # Generation network takes concatenated input and understanding vectors

        # Memory systems
        self.short_term_memory = []  # Recent interactions
        self.long_term_memory = {}  # Indexed by timestamp/key for retrieval
        self.episodic_memory = []  # Detailed records of past conversations
        self.semantic_memory = {}  # Conceptual knowledge and associations
        self.procedural_memory = {}  # Not implemented, but for future actions/routines

        # Language processing components
        self.vocabulary = set()  # Set of all known words
        self.word_embeddings = {}  # Dictionary for word -> vector mappings
        self.word_associations = defaultdict(list)  # Word co-occurrence associations
        self.phrase_patterns = defaultdict(
            list
        )  # Not extensively used, but for future phrase learning

        # Knowledge base and response mechanisms
        self.knowledge_base = (
            self.initialize_knowledge()
        )  # Default conversational knowledge
        self.learned_patterns = defaultdict(
            list
        )  # User input patterns mapped to responses
        self.response_templates = (
            self.initialize_responses()
        )  # Templates for dynamic responses
        self.conversation_history = []  # Full log of user-AI interactions

        # Learning and adaptation parameters
        self.learning_enabled = True
        self.adaptation_rate = (
            0.1  # Controls how aggressively the AI adapts to feedback
        )
        self.confidence_threshold = 0.7  # Threshold for AI to express certainty

        # Model path for JSON knowledge base (for human-readable and incrementally updatable knowledge)
        self.model_path = None  # Initialize model_path; will be set by initialize_model

        # Load pre-existing knowledge/state when the AI starts
        # initialize_model handles loading JSON knowledge and default setup
        self.initialize_model()
        # load_state handles loading pickled NN weights and dynamic AI parameters
        self.load_state()

    # --- Persistence and Knowledge Management Methods ---

    def load_model(self, model_path):
        """
        Loads knowledge base, responses, patterns, vocabulary, word embeddings,
        and semantic memory from a JSON file. This is for human-editable,
        incremental knowledge.
        """
        try:
            self.model_path = model_path  # Set the current model path

            # Adjust model_path to be absolute if a relative path is provided
            if not os.path.isabs(model_path):
                # Get the directory of the current script file (__file__)
                script_dir = Path(__file__).parent
                # Construct the path relative to the script's parent directory
                model_path = script_dir.parent / "models" / model_path

            print(f"Loading model (JSON) from: {model_path}")

            if not os.path.exists(model_path):
                print(
                    f"⚠️ Model file not found at {model_path}. A new default model will be initialized."
                )
                return False  # Indicate that loading was not successful

            with open(model_path, "r", encoding="utf-8") as f:
                data = json.load(f)

            # Merge loaded data with existing (default or previously loaded) data
            if "knowledge_base" in data:
                for key, value in data["knowledge_base"].items():
                    if key not in self.knowledge_base:
                        self.knowledge_base[key] = value
                    else:
                        # Extend lists for responses and patterns to merge, avoiding duplicates
                        self.knowledge_base[key]["responses"].extend(
                            [
                                r
                                for r in value.get("responses", [])
                                if r not in self.knowledge_base[key]["responses"]
                            ]
                        )
                        self.knowledge_base[key]["patterns"].extend(
                            [
                                p
                                for p in value.get("patterns", [])
                                if p not in self.knowledge_base[key]["patterns"]
                            ]
                        )

            if "responses" in data:
                for key, value in data["responses"].items():
                    if key not in self.response_templates:
                        self.response_templates[key] = value
                    else:
                        self.response_templates[key].extend(
                            [r for r in value if r not in self.response_templates[key]]
                        )

            if "patterns" in data:
                for pattern, responses in data["patterns"].items():
                    self.learned_patterns[pattern].extend(
                        [
                            r
                            for r in responses
                            if r not in self.learned_patterns[pattern]
                        ]
                    )

            if "vocabulary" in data:
                self.vocabulary.update(data["vocabulary"])

            if "word_embeddings" in data:
                for word, embedding in data["word_embeddings"].items():
                    if (
                        word not in self.word_embeddings
                    ):  # Only add if not already present
                        self.word_embeddings[word] = np.array(embedding)

            if "semantic_memory" in data:
                for concept, info in data["semantic_memory"].items():
                    if concept not in self.semantic_memory:
                        self.semantic_memory[concept] = info
                    else:
                        self.semantic_memory[concept]["examples"].extend(
                            [
                                e
                                for e in info.get("examples", [])
                                if e not in self.semantic_memory[concept]["examples"]
                            ]
                        )
                        self.semantic_memory[concept]["associations"].extend(
                            [
                                a
                                for a in info.get("associations", [])
                                if a
                                not in self.semantic_memory[concept]["associations"]
                            ]
                        )
                        self.semantic_memory[concept]["frequency"] = (
                            self.semantic_memory[concept].get("frequency", 0)
                            + info.get("frequency", 0)
                        )

            print(f"✅ Model (JSON) loaded successfully from {model_path}")
            print(f"📚 Knowledge base entries: {len(self.knowledge_base)}")
            print(f"🔤 Vocabulary size: {len(self.vocabulary)}")
            print(f"🧠 Semantic memory concepts: {len(self.semantic_memory)}")
            return True

        except Exception as e:
            print(f"❌ Error loading model (JSON): {e}")
            return False

    def save_model(self, model_path=None):
        """
        Saves the human-readable parts of the model (knowledge base, patterns,
        vocabulary, embeddings, semantic memory) to a JSON file.
        """
        try:
            # Determine the path to save the model
            if model_path is None:
                model_path = (
                    self.model_path
                )  # Use the path set during loading/initialization

            # Fallback if no path was ever set or if it's None (e.g., first run)
            if model_path is None:
                script_dir = Path(__file__).parent
                model_path = script_dir.parent / "models" / "enhanced_assistant.json"

            # Ensure the path is absolute if relative, and directory exists
            if not os.path.isabs(model_path):
                script_dir = Path(__file__).parent
                model_path = script_dir.parent / "models" / model_path

            os.makedirs(os.path.dirname(model_path), exist_ok=True)

            # Prepare data for saving (convert NumPy arrays to lists for JSON compatibility)
            data = {
                "knowledge_base": self.knowledge_base,
                "responses": self.response_templates,
                "patterns": dict(self.learned_patterns),
                "vocabulary": list(self.vocabulary),
                "word_embeddings": {
                    word: emb.tolist() for word, emb in self.word_embeddings.items()
                },
                "semantic_memory": self.semantic_memory,
            }

            with open(model_path, "w", encoding="utf-8") as f:
                json.dump(data, f, indent=2, ensure_ascii=False)

            print(f"✅ Model (JSON) saved successfully to {model_path}")
            return True

        except Exception as e:
            print(f"❌ Error saving model (JSON): {e}")
            return False

    def add_to_knowledge_base(self, question, answer, category="enhanced"):
        """
        A convenience wrapper around add_to_model for adding a single
        question (pattern) and answer (response) pair to a specific category.
        """
        try:
            # Preprocess the question/pattern to match runtime input format (lowercase, cleaned)
            cleaned_pattern = self.preprocess_text(question)

            # Structure the data for add_to_model
            entries = {
                "patterns": [cleaned_pattern],
                "responses": [answer],  # Keep the answer's casing/punctuation
            }

            # Use the existing add_to_model method to update knowledge, vocab, embeddings, and save
            success = self.add_to_model(category, entries)

            if success:
                print(
                    f"✅ Successfully added knowledge to category '{category}': Pattern='{cleaned_pattern}', Response='{answer}'"
                )
                return True
            else:
                print(f"❌ Failed to add knowledge via add_to_model.")
                return False

        except Exception as e:
            print(f"❌ Error in add_to_knowledge_base: {e}")
            return False

    def add_to_model(self, category, entries):
        """
        Adds new entries to the model's knowledge base and updates vocabulary/embeddings.
        This method automatically saves the updated model.
        """
        try:
            # Ensure the category exists in the knowledge base
            if category not in self.knowledge_base:
                self.knowledge_base[category] = {"patterns": [], "responses": []}

            # Add new patterns, avoiding duplicates
            if "patterns" in entries:
                for p in entries["patterns"]:
                    if p not in self.knowledge_base[category]["patterns"]:
                        self.knowledge_base[category]["patterns"].append(p)
            # Add new responses, avoiding duplicates
            if "responses" in entries:
                for r in entries["responses"]:
                    if r not in self.knowledge_base[category]["responses"]:
                        self.knowledge_base[category]["responses"].append(r)

            # Update vocabulary and create/update embeddings for any new words found
            all_text = entries.get("patterns", []) + entries.get("responses", [])
            for text in all_text:
                # Preprocess text before tokenizing to match embedding keys
                cleaned_text = self.preprocess_text(text)
                for word in self.tokenize(cleaned_text):
                    if word not in self.vocabulary:
                        self.vocabulary.add(word)
                        # Create embedding for the new word if it doesn't exist
                        if word not in self.word_embeddings:
                            self.word_embeddings[word] = self.create_word_embedding(
                                word
                            )

            # Save the updated JSON model to persist changes
            self.save_model()

            print(f"➕ Added entries to category '{category}'.")
            return True

        except Exception as e:
            print(f"❌ Error adding to model: {e}")
            return False

    def initialize_model(self):
        """
        Initializes the model with default knowledge if the JSON model file
        doesn't exist, then loads the model from the file.
        """
        try:
            # Define the default model path relative to the script location
            script_dir = Path(__file__).parent
            self.model_path = script_dir.parent / "models" / "enhanced_assistant.json"

            # Create the 'models' directory if it doesn't exist
            os.makedirs(os.path.dirname(self.model_path), exist_ok=True)

            # If the model file does not exist, create it with default data
            if not os.path.exists(self.model_path):
                print(
                    f"No existing JSON model found at {self.model_path}. Creating default model."
                )
                default_model_data = {
                    "knowledge_base": self.initialize_knowledge(),
                    "responses": self.initialize_responses(),
                    "patterns": {},  # No learned patterns initially
                    "vocabulary": [],
                    "word_embeddings": {},
                    "semantic_memory": {},
                }

                # Populate vocabulary and embeddings for the default knowledge
                # This ensures the default knowledge is immediately usable
                all_texts = []
                for category_data in default_model_data["knowledge_base"].values():
                    all_texts.extend(category_data.get("patterns", []))
                    all_texts.extend(category_data.get("responses", []))

                for response_list in default_model_data["responses"].values():
                    all_texts.extend(response_list)

                for text in all_texts:
                    # Clean up placeholders in responses and preprocess for embedding creation
                    clean_text = re.sub(r"\{.*?\}", "", text).strip()
                    clean_text = self.preprocess_text(clean_text)

                    for word in self.tokenize(clean_text):
                        if word and word not in default_model_data["vocabulary"]:
                            default_model_data["vocabulary"].append(word)
                            # Convert tolist() because JSON can't directly serialize numpy arrays
                            default_model_data["word_embeddings"][word] = (
                                self.create_word_embedding(word).tolist()
                            )

                with open(self.model_path, "w", encoding="utf-8") as f:
                    json.dump(default_model_data, f, indent=2, ensure_ascii=False)
                print(f"✨ Created new default model file at {self.model_path}")

            # Load the model (this will merge with existing data if any, or load the newly created default)
            self.load_model(self.model_path)

            return True

        except Exception as e:
            print(f"❌ Error initializing model: {e}")
            return False

    def initialize_knowledge(self):
        """Initializes a basic knowledge base with predefined patterns and responses."""
        return {
            "greetings": {
                "patterns": [
                    "hello",
                    "hi",
                    "hey",
                    "greetings",
                    "how are you",
                    "good morning",
                    "good evening",
                ],
                "responses": [
                    "Hello! How can I help you today?",
                    "Hi there! What can I do for you?",
                    "Greetings! How may I assist you?",
                    "I am functioning optimally, thank you for asking!",
                ],
            },
            "farewells": {
                "patterns": [
                    "goodbye",
                    "bye",
                    "see you",
                    "farewell",
                    "talk to you later",
                ],
                "responses": [
                    "Goodbye! Have a great day!",
                    "Farewell! Feel free to return if you need help!",
                    "Bye! Take care!",
                    "See you around!",
                ],
            },
            "gratitude": {
                "patterns": ["thank you", "thanks", "appreciate it", "cheers"],
                "responses": [
                    "You're welcome!",
                    "Happy to help!",
                    "My pleasure!",
                    "Anytime!",
                ],
            },
            "questions_general": {  # Category for general questions
                "patterns": [
                    "what is",
                    "tell me about",
                    "who is",
                    "how do i",
                    "can you explain",
                ],
                "responses": [
                    "Let me analyze that for you...",
                    "Based on my knowledge...",
                    "Here's what I understand...",
                    "What specifically would you like to know?",
                ],
            },
            "self_introduction": {
                "patterns": [
                    "who are you",
                    "what is your name",
                    "are you a bot",
                    "what are you",
                ],
                "responses": [
                    "I am a Conversational AI, designed to assist you.",
                    "You can call me AI. I'm here to help.",
                    "I am an artificial intelligence.",
                ],
            },
            "capabilities": {
                "patterns": [
                    "what can you do",
                    "can you help me",
                    "your functions",
                    "what is your purpose",
                ],
                "responses": [
                    "I can answer questions, generate text, and learn from our conversations.",
                    "I can process natural language, learn from interactions, and assist with various tasks.",
                    "Tell me what you need help with, and I'll do my best!",
                ],
            },
        }

    def initialize_responses(self):
        """Initializes response templates for dynamic sentence generation."""
        return {
            "general": [
                "I understand you're asking about {topic}. {response}",
                "Regarding {topic}, {response}",
                "Let me explain about {topic}: {response}",
                "I'm not sure how to respond to that, but I can tell you about {topic}.",
            ],
            "question": [
                "Based on my analysis, {response}",
                "I believe that {response}",
                "According to my understanding, {response}",
                "That's a great question! {response}",
            ],
            "command": [
                "Executing command related to {topic}. {response}",
                "I'll try to {topic}. {response}",
                "Processing your request concerning {topic}. {response}",
            ],
            "statement": [
                "I see. You mentioned {topic}. {response}",
                "That's an interesting statement about {topic}. {response}",
                "Understood. Regarding {topic}: {response}",
            ],
            "greeting": [
                "Hello!",
                "Hi there!",
                "Greetings!",
            ],
            "farewell": [
                "Goodbye!",
                "Farewell!",
                "Take care!",
            ],
            "gratitude": [
                "You're welcome!",
                "Happy to help!",
                "My pleasure!",
            ],
            "clarification": [
                "Could you tell me more about {topic}?",
                "I need more information about {topic}.",
                "Could you clarify what you mean by {topic}?",
                "To understand better, could you elaborate on {topic}?",
            ],
            "error": [
                "I apologize, but I'm not sure about {topic}.",
                "I need to learn more about {topic}.",
                "Could you help me understand {topic} better?",
                "My apologies, I seem to be having trouble with {topic}. Could you rephrase?",
            ],
            "self_introduction": [
                "I am a conversational AI.",
                "You can call me AI.",
                "I'm an artificial intelligence designed to assist you.",
            ],
            "capabilities": [
                "I can answer questions and learn from our conversation.",
                "I'm here to help you with information and tasks.",
                "My abilities include understanding natural language and generating responses.",
            ],
        }

    def save_state(self):
        """
        Saves the complete state of the AI, including neural network weights,
        using pickle for efficiency and direct NumPy array serialization.
        Also triggers a save of the JSON model for human-readable parts.
        """
        # Convert NumPy arrays to lists for pickling, as direct object pickling can be fragile
        # This allows reconstructing the networks correctly on load.
        state = {
            "knowledge_base": self.knowledge_base,
            "word_embeddings": {
                word: emb.tolist() for word, emb in self.word_embeddings.items()
            },
            "learned_patterns": dict(self.learned_patterns),
            "semantic_memory": self.semantic_memory,
            "vocabulary": list(self.vocabulary),
            "adaptation_rate": self.adaptation_rate,
            "confidence_threshold": self.confidence_threshold,
            # Neural network parameters (weights and biases)
            "language_network_weights": [
                w.tolist() for w in self.language_network.weights
            ],
            "language_network_biases": [
                b.tolist() for b in self.language_network.biases
            ],
            "understanding_network_weights": [
                w.tolist() for w in self.understanding_network.weights
            ],
            "understanding_network_biases": [
                b.tolist() for b in self.understanding_network.biases
            ],
            "generation_network_weights": [
                w.tolist() for w in self.generation_network.weights
            ],
            "generation_network_biases": [
                b.tolist() for b in self.generation_network.biases
            ],
        }

        try:
            # Define the path for the pickle state file
            script_dir = Path(__file__).parent
            state_path = script_dir.parent / "models" / "ai_state.pkl"
            os.makedirs(
                os.path.dirname(state_path), exist_ok=True
            )  # Ensure directory exists

            with open(state_path, "wb") as f:
                pickle.dump(state, f)
            print(f"✅ AI state (pickle) saved successfully to {state_path}")
            # Also save the JSON parts to ensure human-readable knowledge is up-to-date
            self.save_model()
        except Exception as e:
            print(f"❌ Error saving AI state: {e}")

    def load_state(self):
        """
        Loads the complete state of the AI, including neural network weights,
        from a pickle file.
        """
        try:
            script_dir = Path(__file__).parent
            state_path = script_dir.parent / "models" / "ai_state.pkl"

            if os.path.exists(state_path):
                with open(state_path, "rb") as f:
                    state = pickle.load(f)

                # Load general AI parameters
                # Use .update() for dictionaries to merge and .get() for safe retrieval
                self.knowledge_base.update(state.get("knowledge_base", {}))
                self.word_embeddings = {
                    word: np.array(emb)
                    for word, emb in state.get("word_embeddings", {}).items()
                }
                self.learned_patterns = defaultdict(
                    list, state.get("learned_patterns", {})
                )
                self.semantic_memory = state.get("semantic_memory", {})
                self.vocabulary = set(state.get("vocabulary", []))
                self.adaptation_rate = state.get(
                    "adaptation_rate", self.adaptation_rate
                )
                self.confidence_threshold = state.get(
                    "confidence_threshold", self.confidence_threshold
                )

                # Load neural network weights and biases
                # Convert back to NumPy arrays
                if "language_network_weights" in state:
                    self.language_network.weights = [
                        np.array(w) for w in state["language_network_weights"]
                    ]
                    self.language_network.biases = [
                        np.array(b) for b in state["language_network_biases"]
                    ]
                if "understanding_network_weights" in state:
                    self.understanding_network.weights = [
                        np.array(w) for w in state["understanding_network_weights"]
                    ]
                    self.understanding_network.biases = [
                        np.array(b) for b in state["understanding_network_biases"]
                    ]
                if "generation_network_weights" in state:
                    self.generation_network.weights = [
                        np.array(w) for w in state["generation_network_weights"]
                    ]
                    self.generation_network.biases = [
                        np.array(b) for b in state["generation_network_biases"]
                    ]

                print(f"✅ AI state (pickle) loaded successfully from {state_path}")
            else:
                print(
                    "ℹ️ No saved AI state found (ai_state.pkl). Neural network parameters will start fresh."
                )
        except Exception as e:
            print(f"❌ Error loading AI state: {e}. Re-initializing neural networks.")
            # If loading fails, re-initialize networks to ensure a runnable state
            self.language_network = NeuralNetwork(
                self.input_size, self.hidden_sizes, self.output_size
            )
            self.understanding_network = NeuralNetwork(
                self.input_size, self.hidden_sizes, self.output_size
            )
            self.generation_network = NeuralNetwork(
                self.input_size + self.output_size, self.hidden_sizes, self.output_size
            )

    # --- Core AI Logic ---

    def process_input(self, user_input):
        """Processes user input, understands it, generates a response, and learns from the interaction."""
        try:
            # Preprocess input text (lowercase, clean punctuation, normalize whitespace)
            cleaned_input = self.preprocess_text(user_input)
            # Convert cleaned text to a numerical vector representation
            input_vector = self.text_to_vector(cleaned_input)

            # Analyze the input to understand its intent, patterns, and concepts
            understanding = self.understand_input(cleaned_input, input_vector)

            # Generate a response based on the understanding
            response = self.generate_creative_response(understanding)

            # If learning is enabled, update the AI's knowledge and networks
            if self.learning_enabled:
                self.learn_from_interaction(cleaned_input, response, understanding)

            # Update various memory systems (short-term, long-term, episodic, semantic)
            self.update_memory(cleaned_input, response, understanding)

            # Keep a full history of the conversation
            self.conversation_history.append(
                {
                    "user": user_input,
                    "ai": response,
                    "timestamp": datetime.now().isoformat(),
                }
            )

            return response

        except Exception as e:
            print(f"Error during input processing: {e}")
            return f"I encountered an internal error while processing that: {str(e)}"

    def understand_input(self, text, text_vector):
        """Processes the input text and its vector through the understanding network to derive meaning."""
        understanding = {
            "text": text,
            "vector": text_vector,
            "intent": self.detect_intent(text),
            "patterns": self.find_patterns(text),
            "concepts": self.extract_concepts(text),
            "confidence": 0.0,  # Will be calculated later
        }

        # Pass the raw input vector through the understanding network
        # This refines the input vector into a more abstract 'understanding' representation
        understanding["processed_vector"] = (
            self.understanding_network.forward_propagation(text_vector)
        )

        # Calculate a confidence score for the AI's understanding
        understanding["confidence"] = self.calculate_confidence(understanding)

        return understanding

    def detect_intent(self, text):
        """Detects the likely intent (purpose) of the user's input based on keywords and patterns."""
        intents = {
            "question": r"\b(what|why|how|when|where|who|can you|is there|do you know)\b",
            "command": r"\b(do|show|tell|find|search|create|make|get me|give me|check)\b",
            "statement": r"\b(is|are|was|were|i am|you are|it is|i think|my name is)\b",
            "greeting": r"\b(hello|hi|hey|greetings|good morning|good evening)\b",
            "farewell": r"\b(goodbye|bye|farewell|see you|talk later)\b",
            "gratitude": r"\b(thank you|thanks|appreciate it|cheers)\b",
            "self_introduction": r"\b(who are you|what is your name|are you a bot|what are you)\b",
            "capabilities": r"\b(what can you do|can you help me|your functions|your purpose|abilities)\b",
        }

        for intent, pattern in intents.items():
            if re.search(pattern, text, re.IGNORECASE):
                return intent
        return "general"  # Default intent if no specific intent is detected

    def find_patterns(self, text):
        """Identifies predefined patterns within the input text from the knowledge base."""
        patterns_found = []
        # Check patterns in the knowledge base
        for category, data in self.knowledge_base.items():
            for pattern in data.get("patterns", []):
                # Use regex for whole word matching (`\b` word boundary) and escaping special chars
                # We compare against the preprocessed (cleaned) text
                if re.search(r"\b" + re.escape(pattern) + r"\b", text, re.IGNORECASE):
                    patterns_found.append({"category": category, "pattern": pattern})
        # Check patterns in learned patterns
        for pattern_key in self.learned_patterns.keys():
            if re.search(r"\b" + re.escape(pattern_key) + r"\b", text, re.IGNORECASE):
                patterns_found.append({"category": "learned", "pattern": pattern_key})

        return patterns_found

    def extract_concepts(self, text):
        """Extracts key concepts (nouns, important terms) from the input text."""
        # Common English stop words
        stop_words = {
            "a",
            "an",
            "the",
            "is",
            "are",
            "was",
            "were",
            "will",
            "be",
            "to",
            "of",
            "and",
            "in",
            "on",
            "for",
            "with",
            "it",
            "that",
            "this",
            "do",
            "can",
            "what",
            "how",
            "why",
            "who",
            "when",
            "where",
            "me",
            "you",
            "my",
            "your",
            "its",
            "i",
            "we",
            "he",
            "she",
            "they",
            "them",
            "us",
            "him",
            "her",
            "or",
            "but",
            "not",
            "just",
            "about",
            "from",
            "at",
            "by",
            "as",
            "if",
            "then",
            "than",
            "so",
            "up",
            "down",
            "out",
            "in",
            "off",
            "on",
            "into",
            "onto",
            "off",
            "no",
            "yes",
        }

        words = self.tokenize(text)
        # Filter out stop words and very short words
        concepts = [word for word in words if word not in stop_words and len(word) > 2]

        # Identify multi-word concepts (phrases)
        # Collect all multi-word patterns from knowledge base and semantic memory keys
        known_phrases = set()
        for category_data in self.knowledge_base.values():
            for p in category_data.get("patterns", []):
                if " " in p:
                    known_phrases.add(p)
        for concept_key in self.semantic_memory.keys():
            if " " in concept_key:
                known_phrases.add(concept_key)

        for phrase in known_phrases:
            if phrase.lower() in text.lower():
                concepts.append(phrase)

        return list(set(concepts))  # Return unique concepts

    def calculate_confidence(self, understanding):
        """Calculates a heuristic confidence score for the AI's understanding of the input."""
        confidence = 0.0

        # Contribution from direct pattern matches
        if understanding["patterns"]:
            # More patterns matching mean higher confidence
            confidence += 0.35 * min(
                1.0, len(understanding["patterns"]) / 3.0
            )  # Cap contribution

        # Contribution from intent detection
        if understanding["intent"] != "general":
            confidence += 0.25
            # Higher weight for very specific, often simple intents
            if understanding["intent"] in [
                "greeting",
                "farewell",
                "gratitude",
                "self_introduction",
                "capabilities",
            ]:
                confidence += 0.1

        # Contribution from extracted concepts
        if understanding["concepts"]:
            confidence += 0.2

        # Contribution from the 'distinctness' of the processed vector (how well the NN understood)
        # Using the L2 norm as a simple proxy for distinctness
        vector_distinctness = np.linalg.norm(understanding["processed_vector"])
        confidence += (
            np.clip(vector_distinctness, 0, 1) * 0.2
        )  # Normalize and clip value

        return min(confidence, 1.0)  # Ensure confidence does not exceed 1.0

    def generate_response(self, understanding):
        """
        Generates a direct, retrieval-based response primarily using predefined
        knowledge base entries and templates. (Simpler than creative response).
        """
        # Priority 1: Direct pattern matches with associated responses
        for pattern_match in understanding["patterns"]:
            category = pattern_match["category"]
            if category in self.knowledge_base and self.knowledge_base[category].get(
                "responses"
            ):
                return random.choice(self.knowledge_base[category]["responses"])
            # Also check learned patterns
            if (
                category == "learned"
                and pattern_match["pattern"] in self.learned_patterns
            ):
                if self.learned_patterns[pattern_match["pattern"]]:
                    return random.choice(
                        self.learned_patterns[pattern_match["pattern"]]
                    )

        # Priority 2: Use response templates based on detected intent
        if understanding["intent"] in self.response_templates:
            # Try to pick a template that doesn't require placeholders first, if appropriate
            direct_templates = [
                r
                for r in self.response_templates[understanding["intent"]]
                if "{" not in r
            ]
            if direct_templates:
                return random.choice(direct_templates)

            # If no direct template, or if placeholders are necessary, use a template with placeholders
            template = random.choice(self.response_templates[understanding["intent"]])
            topic = (
                ", ".join(understanding["concepts"])
                if understanding["concepts"]
                else "that"
            )

            # Generate a base response using the generation network to fill the template
            input_for_gen_net = np.concatenate(
                [understanding["vector"], understanding["processed_vector"]], axis=1
            )
            response_vector = self.generation_network.forward_propagation(
                input_for_gen_net
            )
            base_response_from_vector = self.vector_to_text(response_vector)

            # Fallback for base_response if vector_to_text can't find a good match
            if "not sure how to respond" in base_response_from_vector.lower():
                base_response_from_vector = "I'm still learning in this area."

            return template.format(topic=topic, response=base_response_from_vector)

        # Priority 3: Fallback if no specific intent or pattern leads to a response
        input_for_gen_net = np.concatenate(
            [understanding["vector"], understanding["processed_vector"]], axis=1
        )
        response_vector = self.generation_network.forward_propagation(input_for_gen_net)
        base_response = self.vector_to_text(response_vector)

        return (
            base_response if base_response else "I'm not sure how to respond to that."
        )

    def get_relevant_knowledge(self, understanding):
        """Retrieves relevant knowledge snippets based on understanding's patterns and concepts."""
        relevant = []

        # Knowledge from direct pattern matches
        for pattern_match in understanding["patterns"]:
            category = pattern_match["category"]
            if category in self.knowledge_base:
                relevant.extend(self.knowledge_base[category].get("responses", []))

        # Knowledge from semantic memory linked to extracted concepts
        for concept in understanding["concepts"]:
            if concept in self.semantic_memory:
                relevant.extend(self.semantic_memory[concept].get("examples", []))
                # If a concept itself is a knowledge base category key
                if concept in self.knowledge_base and self.knowledge_base[concept].get(
                    "responses"
                ):
                    relevant.extend(self.knowledge_base[concept]["responses"])

        return list(set(relevant))  # Return unique knowledge entries

    def vector_to_text(self, vector):
        """
        Converts a numerical vector (generated by a neural network) back into a
        text response by finding the closest match among known responses.
        """
        best_response = None
        best_similarity = -1.0  # Cosine similarity ranges from -1 to 1

        # Collect all unique candidate responses from knowledge base and learned patterns
        all_candidate_responses = set()
        for category_data in self.knowledge_base.values():
            all_candidate_responses.update(category_data.get("responses", []))
        for pattern_responses in self.learned_patterns.values():
            all_candidate_responses.update(pattern_responses)

        if not all_candidate_responses:
            return "I'm not sure how to respond to that."

        # Ensure the input vector is 2D (1, N) for matrix operations
        if vector.ndim == 1:
            vector = vector.reshape(1, -1)

        # Compare the generated vector with the vectors of all candidate responses
        for response_text in all_candidate_responses:
            if not response_text.strip():  # Skip empty strings
                continue

            # Preprocess the response text to match the format of the embeddings
            cleaned_response_text = self.preprocess_text(response_text)
            response_vector = self.text_to_vector(cleaned_response_text)

            # Ensure response_vector is also 2D (1, N)
            if response_vector.ndim == 1:
                response_vector = response_vector.reshape(1, -1)

            # Calculate cosine similarity using dot product (vectors are normalized)
            # (A . B) / (||A|| * ||B||). Since ||A|| and ||B|| are ~1 for normalized vectors,
            # the dot product directly gives cosine similarity.
            similarity = np.dot(vector, response_vector.T)[
                0, 0
            ]  # Extract scalar from (1,1) array

            if similarity > best_similarity:
                best_similarity = similarity
                best_response = (
                    response_text  # Return the original (non-preprocessed) text
                )

        # Return the best match if its similarity is above a certain threshold
        if best_similarity > 0.6:  # A heuristic threshold for a "good enough" match
            return best_response
        else:
            return "I'm not sure how to respond to that."  # Fallback

    def learn_from_interaction(self, input_text, response, understanding):
        """
        Updates the AI's neural networks, knowledge base, and word associations
        based on a new user interaction.
        """
        try:
            # Convert the raw input text and the AI's actual response to numerical vectors
            input_vector = understanding["vector"]

            # Preprocess the response before converting to vector for training consistency
            cleaned_response = self.preprocess_text(response)
            target_response_vector = self.text_to_vector(cleaned_response)

            # 1. Update Language Network: (conceptual: map input to some internal language representation)
            # Here, we train it to map the raw input vector to the understanding network's processed vector.
            self.language_network.backward_propagation(
                input_vector, understanding["processed_vector"]
            )

            # 2. Update Understanding Network: (conceptual: refine raw input into 'understood' features)
            # Train it to produce a processed_vector that is consistent with its own forward pass.
            # This helps it stabilize its internal representation.
            self.understanding_network.backward_propagation(
                input_vector, understanding["processed_vector"]
            )

            # 3. Update Generation Network: (conceptual: generate a response vector from input + understanding)
            # The input to the generation network is a concatenation of the raw input and the processed understanding.
            input_for_generation_net = np.concatenate(
                [input_vector, understanding["processed_vector"]], axis=1
            )
            # Train it to produce a vector that, when converted to text, matches the actual response given.
            self.generation_network.backward_propagation(
                input_for_generation_net, target_response_vector
            )

            # Update the knowledge base with new patterns learned from this interaction
            self.update_knowledge_base(understanding, response)

            # Learn new word associations based on words co-occurring in input and response
            self.learn_word_associations(input_text, cleaned_response)

        except Exception as e:
            print(f"Learning error during interaction: {str(e)}")

    def update_knowledge_base(self, understanding, response):
        """
        Dynamically updates the knowledge base and learned patterns based on
        successful (or even unsuccessful for later correction) interactions.
        """
        # Add the full user input (cleaned) as a learned pattern mapped to the AI's response (original casing)
        # This allows the AI to learn specific phrases and their appropriate replies.
        if (
            understanding["text"] not in self.learned_patterns
            or response not in self.learned_patterns[understanding["text"]]
        ):
            self.learned_patterns[understanding["text"]].append(response)
            # print(f"Learned new pattern-response pair: '{understanding['text']}' -> '{response}'")

        # Update semantic memory with extracted concepts
        for concept in understanding["concepts"]:
            if concept not in self.semantic_memory:
                self.semantic_memory[concept] = {
                    "examples": [],
                    "associations": [],
                    "frequency": 0,
                }
            # Add the current interaction's input as an example for this concept
            if understanding["text"] not in self.semantic_memory[concept]["examples"]:
                self.semantic_memory[concept]["examples"].append(understanding["text"])
            self.semantic_memory[concept]["frequency"] += 1

            # If a concept itself isn't a category but becomes a key for a pattern, add it to KB
            # Use a specific category for dynamically learned concepts if desired, e.g., "dynamic_concepts"
            concept_category = "dynamic_concepts"
            if concept_category not in self.knowledge_base:
                self.knowledge_base[concept_category] = {
                    "patterns": [],
                    "responses": [],
                }

            # Add the concept as a pattern if it's new
            if concept not in self.knowledge_base[concept_category]["patterns"]:
                self.knowledge_base[concept_category]["patterns"].append(concept)

            # Add the response associated with this concept if it's new
            if response not in self.knowledge_base[concept_category]["responses"]:
                self.knowledge_base[concept_category]["responses"].append(response)

    def learn_word_associations(self, input_text, response_text):
        """
        Learns statistical associations between words by observing which words
        appear together in user input and AI responses.
        Input and response text should be preprocessed (cleaned/lowercased).
        """
        input_words = set(self.tokenize(input_text))
        response_words = set(self.tokenize(response_text))

        # Create bidirectional associations
        for word1 in input_words:
            for word2 in response_words:
                if word1 != word2:  # Avoid self-associations
                    if word2 not in self.word_associations[word1]:
                        self.word_associations[word1].append(word2)
                    if word1 not in self.word_associations[word2]:
                        self.word_associations[word2].append(word1)

    def update_memory(self, input_text, response, understanding):
        """
        Manages the AI's different memory systems: short-term, long-term,
        and episodic memory. Semantic memory is updated in `update_knowledge_base`.
        """
        # Update short-term memory (stores recent interactions for context)
        self.short_term_memory.append(
            {
                "input": input_text,
                "response": response,
                "understanding": understanding,
                "timestamp": datetime.now().isoformat(),
                "ai": response,  # Store AI's response explicitly
            }
        )

        # Maintain short-term memory size; transfer older items to long-term memory
        if len(self.short_term_memory) > 50:  # Cap at 50 recent interactions
            oldest_memory = self.short_term_memory.pop(0)  # Remove the oldest
            self.store_in_long_term_memory(oldest_memory)  # Store it in long-term

        # Update episodic memory (detailed chronological record of interactions)
        self.episodic_memory.append(
            {
                "interaction": {"input": input_text, "response": response},
                "timestamp": datetime.now().isoformat(),
                "understanding": understanding,
            }
        )
        # Prune episodic memory to prevent unlimited growth (e.g., keep last 1000)
        if len(self.episodic_memory) > 1000:
            self.episodic_memory = self.episodic_memory[-1000:]

    def store_in_long_term_memory(self, memory_item):
        """Stores a memory item in long-term memory and creates semantic associations."""
        # Create a unique key for the memory item
        key = datetime.now().strftime(
            "%Y%m%d%H%M%S_%f"
        )  # Add microseconds for higher uniqueness
        self.long_term_memory[key] = memory_item

        # Link this memory item to relevant concepts in semantic memory
        for concept in memory_item["understanding"]["concepts"]:
            if concept not in self.semantic_memory:
                self.semantic_memory[concept] = {
                    "examples": [],
                    "associations": [],
                    "frequency": 0,
                }
            # Add the memory item's unique key to the concept's associations
            if key not in self.semantic_memory[concept]["associations"]:
                self.semantic_memory[concept]["associations"].append(key)

    def preprocess_text(self, text):
        """Cleans and standardizes input text: lowercase, remove special characters, normalize whitespace."""
        text = text.lower()
        text = re.sub(
            r"[^\w\s]", " ", text
        )  # Replace non-alphanumeric/whitespace with space
        text = " ".join(text.split())  # Normalize multiple spaces to single space
        return text

    def tokenize(self, text):
        """Splits text into individual words or tokens."""
        return text.split()

    def text_to_vector(self, text):
        """Converts a given text string into a numerical vector representation using word embeddings."""
        vector = np.zeros(self.input_size)
        tokens = self.tokenize(text)

        if not tokens:  # Handle empty string input gracefully
            return vector.reshape(1, -1)

        new_words_found = False
        for token in tokens:
            # If the word is not in our current embeddings, create one on the fly
            if token not in self.word_embeddings:
                self.word_embeddings[token] = self.create_word_embedding(token)
                new_words_found = True

                # If learning is enabled, update vocabulary
                if self.learning_enabled:
                    self.vocabulary.add(token)

            # Add the word's embedding to the total vector for the text
            embedding = self.word_embeddings[token]
            vector += embedding

        # Save the model if new words were encountered and learning is enabled
        if new_words_found and self.learning_enabled:
            self.save_model()  # Save JSON model to persist new vocabulary/embeddings

        # Normalize the final text vector to unit length
        vector = vector / (
            np.linalg.norm(vector) + 1e-8
        )  # Add epsilon to prevent division by zero
        return vector.reshape(
            1, -1
        )  # Ensure output is 2D (1, input_size) for network input

    def create_word_embedding(self, word):
        """
        Generates a simple, fixed-size numerical embedding for a given word
        based on character n-grams and positional features.
        """
        embedding = np.zeros(self.input_size)
        if not word:
            return embedding  # Handle empty word

        # Feature 1: Character N-grams (trigrams)
        # Hashing trigrams into the vector space
        for i in range(len(word) - 2):
            trigram = word[i : i + 3]
            hash_val = hash(trigram) % self.input_size
            embedding[hash_val] += 1  # Increment count for this trigram's bin

        # Feature 2: Positional character features
        # Distributes character presence weighted by their position
        for i, char in enumerate(word):
            pos_val = (i + 1) / len(word)  # Normalized position (0 to 1)
            hash_val = hash(char) % self.input_size
            embedding[hash_val] += pos_val  # Add weighted by position

        # Normalize the embedding vector to unit length
        embedding = embedding / (np.linalg.norm(embedding) + 1e-8)
        return embedding

    def get_conversation_summary(self):
        """Provides a brief summary of the AI's current conversational state."""
        summary = {
            "total_interactions": len(self.conversation_history),
            "unique_concepts": len(self.semantic_memory),
            "learned_patterns": len(self.learned_patterns),
            "vocabulary_size": len(self.vocabulary),
            "recent_topics": self.get_recent_topics(),
        }
        return summary

    def get_recent_topics(self):
        """Extracts the most recently discussed topics from short-term memory."""
        topics = []
        # Iterate through the last 5 interactions in short-term memory
        for memory in reversed(self.short_term_memory[-5:]):
            topics.extend(memory["understanding"]["concepts"])
        return list(set(topics))  # Return unique topics

    def generate_creative_response(self, understanding):
        """
        Generates a more creative and contextually rich response by blending
        various knowledge sources and AI's understanding.
        """
        try:
            # 1. Check for direct, high-confidence pattern matches first (optimization)
            if understanding["confidence"] > 0.9 and understanding["patterns"]:
                direct_response = self.generate_response(understanding)
                if "not sure" not in direct_response.lower():
                    return self.enhance_response(direct_response, understanding)

            # 2. Neural Network Generation Process
            # Prepare input for generation network (concatenating raw input vector and processed understanding vector)
            input_for_gen_net = np.concatenate(
                [understanding["vector"], understanding["processed_vector"]], axis=1
            )
            base_response_vector = self.generation_network.forward_propagation(
                input_for_gen_net
            )

            # 3. Enhance with Associated Concepts
            associated_concepts = self.get_associated_concepts(
                understanding["concepts"]
            )
            if associated_concepts:
                associated_text = " ".join(associated_concepts)
                # Use preprocessed text for vector creation
                cleaned_associated_text = self.preprocess_text(associated_text)
                association_vector = self.text_to_vector(cleaned_associated_text)
                # Blend the base response vector with the association vector
                base_response_vector = (base_response_vector + association_vector) / 2

            # 4. Incorporate Recent Context
            recent_context = self.get_recent_context()
            if recent_context and understanding["confidence"] < 0.9:
                # Use preprocessed context for vector creation
                cleaned_context = self.preprocess_text(recent_context)
                context_vector = self.text_to_vector(cleaned_context)
                # Blend, giving more weight to the generated response
                base_response_vector = base_response_vector * 0.7 + context_vector * 0.3

            # 5. Convert the final blended vector to a text response
            response = self.vector_to_text(base_response_vector)

            # --- Fallback and Enhancement Logic ---
            # If the vector-to-text conversion yields a generic or unsure response:
            if (
                "not sure" in response.lower()
                or not response.strip()
                or response == "I'm not sure how to respond to that."
            ):
                # Try to use a relevant template based on intent
                if understanding["intent"] in self.response_templates:
                    template_list = self.response_templates[understanding["intent"]]
                    # Prioritize templates that don't need a direct 'response' fill-in if the generated one is bad
                    template = random.choice(
                        [t for t in template_list if "{response}" not in t]
                        or template_list
                    )
                    topic_str = (
                        ", ".join(understanding["concepts"])
                        if understanding["concepts"]
                        else "that"
                    )
                    response = template.format(
                        topic=topic_str, response="I'm still learning in this area."
                    )
                else:  # Generic fallback if no specific template
                    topic_str = (
                        ", ".join(understanding["concepts"])
                        if understanding["concepts"]
                        else "that"
                    )
                    response = random.choice(self.response_templates["error"]).format(
                        topic=topic_str, response="I need more information."
                    )

            # Apply final enhancements (greetings, confidence notes, etc.)
            final_response = self.enhance_response(response, understanding)

            return final_response

        except Exception as e:
            print(f"Creative response generation error: {str(e)}")
            return f"I had trouble generating a creative response: {str(e)}"

    def get_associated_concepts(self, concepts):
        """
        Retrieves concepts associated with the input concepts from word associations
        and semantic memory links.
        """
        associations = []
        for concept in concepts:
            # Check direct word associations (co-occurrence)
            if concept in self.word_associations:
                associations.extend(
                    self.word_associations[concept][:5]
                )  # Get top 5 associations

            # Check semantic memory for memory items associated with this concept
            if (
                concept in self.semantic_memory
                and "associations" in self.semantic_memory[concept]
            ):
                for mem_key in self.semantic_memory[concept]["associations"]:
                    if mem_key in self.long_term_memory:
                        # Extract concepts from the associated memory item's understanding
                        mem_concepts = self.long_term_memory[mem_key]["understanding"][
                            "concepts"
                        ]
                        # Add only new concepts that are not the original concept itself
                        associations.extend(
                            [
                                c
                                for c in mem_concepts
                                if c != concept and c not in associations
                            ]
                        )

        return list(set(associations))  # Return unique associated concepts

    def get_recent_context(self):
        """Constructs a string of recent conversation (user input + AI response) for contextual awareness."""
        if not self.short_term_memory:
            return ""

        # Use the last 3 interactions to build context
        recent_interactions = self.short_term_memory[-3:]
        context_phrases = []
        for memory in recent_interactions:
            context_phrases.append(memory["input"])
            # Preprocess the response when adding to context string
            context_phrases.append(self.preprocess_text(memory["response"]))

        return " ".join(context_phrases)

    def enhance_response(self, response, understanding):
        """Adds stylistic or functional elements to the generated response, like greetings or confidence indicators."""
        final_response = response

        # Add an initial greeting if the intent is greeting and the response doesn't already contain one
        if understanding["intent"] == "greeting" and not any(
            r in final_response.lower() for r in ["hello", "hi", "greetings"]
        ):
            final_response = (
                random.choice(["Hello!", "Hi!", "Greetings!"]) + " " + final_response
            )

        # Optionally add a note about related concepts to show depth of understanding
        if understanding["concepts"] and "regarding:" not in final_response.lower():
            if random.random() < 0.2:  # Add this randomly to avoid repetition
                final_response += (
                    f" (Regarding: {', '.join(understanding['concepts'])})"
                )

        # Add a confidence indicator if the AI's understanding confidence is below threshold
        if understanding["confidence"] < self.confidence_threshold:
            if (
                "not entirely certain" not in final_response.lower()
                and "still learning" not in final_response.lower()
            ):
                final_response += " (Note: I'm not entirely certain about this response, still learning.)"
            # If very low confidence, suggest clarification
            if understanding["confidence"] < 0.3 and understanding["intent"] not in [
                "farewell",
                "gratitude",
            ]:
                clarification_template = random.choice(
                    self.response_templates["clarification"]
                )
                topic_for_clarification = (
                    ", ".join(understanding["concepts"])
                    if understanding["concepts"]
                    else "what you mean"
                )
                final_response = (
                    clarification_template.format(topic=topic_for_clarification)
                    + " "
                    + final_response
                )

        return final_response.strip()  # Remove any extra whitespace

    def learn_from_feedback(self, feedback, previous_response):
        """
        Allows the AI to learn from explicit user feedback (e.g., "yes" for good,
        "no" for bad response) to adjust its learning parameters and knowledge.
        """
        feedback_lower = feedback.lower()
        processed_input = None

        # Find the user's original input that led to the `previous_response`
        # Search recent short-term memory first
        for memory in reversed(self.short_term_memory):
            if memory.get("ai") == previous_response:  # Use .get() for safety
                processed_input = memory["input"]
                break

        if not processed_input:
            print("Could not find corresponding input for feedback in recent memory.")
            # Optionally, search episodic memory for older interactions
            for memory in reversed(self.episodic_memory):
                # Check if 'ai' key exists in interaction dictionary for safety
                if memory["interaction"].get("response") == previous_response:
                    processed_input = memory["interaction"]["input"]
                    break

        if not processed_input:
            print("Could not find corresponding input for feedback in any memory.")
            return

        # Adjust learning parameters based on feedback type
        if feedback_lower in ["good", "correct", "right", "yes"]:
            self.adaptation_rate = min(
                0.5, self.adaptation_rate * 1.1
            )  # Increase adaptation rate, capped
            self.confidence_threshold = max(
                0.5, self.confidence_threshold * 0.95
            )  # Lower confidence threshold, capped
            print("👍 Positive feedback received. Adapting behavior.")
            # Update knowledge base to reinforce the correct pattern-response pair
            self.update_knowledge_base_from_feedback(
                processed_input, previous_response, True
            )
        elif feedback_lower in ["bad", "wrong", "incorrect", "no"]:
            self.adaptation_rate = max(
                0.01, self.adaptation_rate * 0.9
            )  # Decrease adaptation rate, min value
            self.confidence_threshold = min(
                0.9, self.confidence_threshold * 1.05
            )  # Raise confidence threshold, capped
            print("👎 Negative feedback received. Adapting behavior.")
            # Update knowledge base to penalize the incorrect pattern-response pair
            self.update_knowledge_base_from_feedback(
                processed_input, previous_response, False
            )
        else:
            print("Unrecognized feedback. Please use 'yes', 'no', or 'skip'.")

    def update_knowledge_base_from_feedback(
        self, input_text, previous_response, is_positive
    ):
        """
        Modifies the AI's learned patterns based on explicit positive or negative feedback.
        """
        # Retrieve the relevant learned pattern entry
        if input_text in self.learned_patterns:
            if is_positive:
                # For positive feedback, reinforce the successful response by adding it again.
                # This increases its frequency/likelihood of being chosen for this pattern.
                self.learned_patterns[input_text].append(previous_response)
                print(
                    f"Reinforced pattern '{input_text}' with response '{previous_response}'"
                )
            else:
                # For negative feedback, remove the incorrect response if it exists for this pattern.
                if previous_response in self.learned_patterns[input_text]:
                    try:
                        self.learned_patterns[input_text].remove(previous_response)
                        print(
                            f"Removed incorrect response '{previous_response}' for pattern '{input_text}'"
                        )
                    except ValueError:
                        pass  # Handle case where response might have been removed already
                # If removing the response leaves no other responses for this pattern, remove the pattern itself.
                if not self.learned_patterns[input_text]:
                    del self.learned_patterns[input_text]
                    print(
                        f"Pattern '{input_text}' removed due to no valid responses remaining."
                    )

        # After feedback, save the entire AI state to persist the changes.
        self.save_state()


# Usage example:
if __name__ == "__main__":
    # Initialize the AI
    ai = ConversationalAI()

    # Example of adding knowledge dynamically
    # ai.add_to_knowledge_base("What is the capital of France?", "The capital of France is Paris.", "geography")
    # ai.add_to_knowledge_base("Tell me a joke", "Why don't scientists trust atoms? Because they make up everything!", "humor")

    # Example conversation loop
    print("\n" + "=" * 50)
    print("AI: Hello! I'm ready to chat. Type 'quit' to exit.")
    print("=" * 50 + "\n")

    while True:
        user_input = input("You: ").strip()

        if user_input.lower() == "quit":
            ai.save_state()  # Ensure all learning and state is saved before exiting
            print("\n" + "=" * 50)
            print("AI: Goodbye! My learning state has been saved.")
            print("=" * 50)
            break

        # Special command to teach the AI
        if user_input.lower().startswith("teach:"):
            try:
                # Format: teach: [Category] Question? Answer.
                instruction = user_input[6:].strip()
                match = re.match(r"\[(.*?)\]\s*(.*?)\?\s*(.*)", instruction)
                if match:
                    category, question, answer = match.groups()
                    ai.add_to_knowledge_base(question, answer, category.strip())
                    print(
                        f"AI: Understood. I've learned that under '{category.strip()}'."
                    )
                else:
                    print(
                        "AI: Invalid teaching format. Use: teach: [Category] Question? Answer."
                    )
            except Exception as e:
                print(f"AI: Error during teaching: {e}")
            continue

        response = ai.process_input(user_input)
        print("AI:", response)

        # Optional feedback loop
        feedback = input("Was this response helpful? (yes/no/skip): ").strip()
        if feedback.lower() not in ["skip", ""]:
            ai.learn_from_feedback(feedback, response)




make that more advanced like you know it can do multiple thing and make like it don't delete anything in .json files but it reads it and eventually write something to that