import re
import mss
from PIL import Image, ImageEnhance, ImageFilter
import pytesseract
import speech_recognition as sr
import pyttsx3
import keyboard
import threading
import os
import logging
import tkinter as tk
import ctypes
import sys
import ollama
import asyncio
from datetime import datetime
from word2number import w2n  # Optional for number word conversion

# -------------------- Configuration --------------------

# Define a custom logging filter to exclude specific messages
class ExcludeFilter(logging.Filter):
    def __init__(self, exclude_keywords):
        super().__init__()
        self.exclude_keywords = exclude_keywords

    def filter(self, record):
        return not any(keyword in record.getMessage() for keyword in self.exclude_keywords)

# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)  # Set to INFO to include INFO and higher level logs

# Create a stream handler
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)

# Define the log format
formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
stream_handler.setFormatter(formatter)

# Define keywords to exclude from logging
excluded_keywords = [
    "Mouse moved to",
    "Width:",
    "Height:",
    "Monitor for capture:"
]

# Add the custom filter to the handler
stream_handler.addFilter(ExcludeFilter(excluded_keywords))

# Add the handler to the logger
if not logger.handlers:
    logger.addHandler(stream_handler)
else:
    # Remove existing handlers to prevent duplicate logs
    logger.handlers = []
    logger.addHandler(stream_handler)

tts_engine = pyttsx3.init()
is_listening = False
ocr_text = ""

lock = threading.Lock()
tts_lock = threading.Lock()

OUTPUT_DIR = r"D:\\AI\\EXP2\\TempS"

def strip_markdown(text):
    # Remove asterisks, underscores, and backticks
    text = re.sub(r'[*_`]', '', text)
    # Remove links [text](url)
    text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)
    # Remove headings ## Heading
    text = re.sub(r'#+\s?', '', text)
    # Remove other common Markdown syntax as needed
    return text

def set_dpi_awareness():
    try:
        if sys.platform.startswith('win'):
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
            logging.info("Process DPI awareness set to SYSTEM_AWARE.")
    except Exception as e:
        logging.warning(f"Could not set DPI awareness: {e}. Continuing without setting DPI awareness.")

def get_scaling_factor():
    try:
        if sys.platform.startswith('win'):
            user32 = ctypes.windll.user32
            try:
                user32.SetProcessDPIAware()
                dpi = user32.GetDpiForWindow(user32.GetForegroundWindow())
                scaling_factor = dpi / 96
                logging.info(f"DPI Scaling Factor detected: {scaling_factor}")
                return scaling_factor
            except AttributeError:
                user32.SetProcessDPIAware()
                hdc = user32.GetDC(0)
                LOGPIXELSX = 88
                dpi = ctypes.windll.gdi32.GetDeviceCaps(hdc, LOGPIXELSX)
                scaling_factor = dpi / 96
                ctypes.windll.user32.ReleaseDC(0, hdc)
                logging.info(f"DPI Scaling Factor detected via fallback: {scaling_factor}")
                return scaling_factor
        else:
            logging.info("Non-Windows OS detected. Defaulting DPI scaling factor to 1.0.")
            return 1.0
    except Exception as e:
        logging.warning(f"Could not retrieve DPI scaling factor: {e}. Defaulting to 1.0.")
        return 1.0

def convert_number_words(text):
    """
    Converts number words in the speech text to digits.
    Example: "five hundred ninety-seven" -> "597"
    """
    try:
        # Attempt to convert the entire text
        number = w2n.word_to_num(text)
        return str(number)
    except:
        # Attempt to extract digits directly if conversion fails
        digits = re.findall(r'\d+', text)
        return ''.join(digits) if digits else text  # Return the number if found, else original text

def speak_text(text):
    global tts_engine
    logging.info(f"Speaking out the response: {text}")
    with tts_lock:
        try:
            tts_engine.say(text)

            # Add hotkeys for pausing (F4) and stopping (F5)
            keyboard.add_hotkey('f4', pause_speaking)
            keyboard.add_hotkey('f5', stop_speaking)

            tts_engine.runAndWait()
            logging.info("Text-to-Speech completed.")
        except RuntimeError as e:
            logging.error(f"TTS Error: {e}")
        finally:
            keyboard.remove_hotkey('f4')
            keyboard.remove_hotkey('f5')

def pause_speaking():
    global tts_engine
    logging.info("Paused speaking...")
    # pyttsx3 does not support pause natively. This is a placeholder.

def stop_speaking():
    global tts_engine
    logging.info("Stopped speaking.")
    tts_engine.stop()

def save_captured_image(image, output_dir, prefix="captured_region"):
    try:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(output_dir, f"{prefix}_{timestamp}.png")
        image.save(filename)
        logging.info(f"Captured image saved as {filename}")
    except Exception as e:
        logging.error(f"Failed to save captured image: {e}")

def save_preprocessed_image(image, output_dir, prefix="preprocessed_image"):
    try:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = os.path.join(output_dir, f"{prefix}_{timestamp}.png")
        image.save(filename)
        logging.info(f"Preprocessed image saved as {filename}")
    except Exception as e:
        logging.error(f"Failed to save preprocessed image: {e}")

def perform_ocr(image):
    global ocr_text
    try:
        logging.info("Starting OCR process with image preprocessing...")

        save_captured_image(image, OUTPUT_DIR)

        gray = image.convert('L')
        # Removed DEBUG logs related to image processing

        enhancer = ImageEnhance.Contrast(gray)
        gray_enhanced = enhancer.enhance(1.0)
        # Removed DEBUG logs related to image processing

        threshold = 190
        gray_threshold = gray_enhanced.point(lambda x: 0 if x < threshold else 255, '1')
        # Removed DEBUG logs related to image processing

        final_image = gray_threshold

        save_preprocessed_image(final_image, OUTPUT_DIR)

        logging.info("Image preprocessing complete. Performing OCR...")

        custom_config = r'--oem 3 --psm 6'
        raw_text = pytesseract.image_to_string(final_image, lang='eng', config=custom_config)

        if not raw_text.strip():
            logging.warning("OCR returned empty text.")
            speak_text("I couldn't detect any text in the selected area. Please try again with a different region.")
        else:
            ocr_text = raw_text
            logging.info(f"OCR Text: {ocr_text}")  # Log the extracted OCR text
            logging.info("OCR process completed successfully.")
    except pytesseract.TesseractNotFoundError:
        logging.error("Tesseract executable not found. Please ensure Tesseract is installed and PATH is set correctly.")
        speak_text("Tesseract is not installed or not found. Please install Tesseract and try again.")
        ocr_text = ""
    except Exception as e:
        logging.error(f"OCR Error: {e}")
        speak_text("An error occurred during OCR processing. Please try again.")
        ocr_text = ""

def get_selected_region(scaling_factor):
    root = tk.Tk()
    root.title("Select Region (Press Esc to Cancel)")
    root.attributes('-alpha', 0.3)
    root.attributes("-topmost", True)
    root.overrideredirect(True)

    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    root.geometry(f"{screen_width}x{screen_height}")

    canvas = tk.Canvas(root, cursor="cross", bg="grey")
    canvas.pack(fill=tk.BOTH, expand=True)

    rect = None
    start_x = start_y = end_x = end_y = 0

    def on_button_press(event):
        nonlocal start_x, start_y, rect
        start_x = int(event.x * scaling_factor)
        start_y = int(event.y * scaling_factor)
        rect = canvas.create_rectangle(event.x, event.y, event.x, event.y, outline='red', width=2)
        logging.info("Please select the region by clicking and dragging the mouse.")

    def on_move_press(event):
        nonlocal end_x, end_y
        end_x = int(event.x * scaling_factor)
        end_y = int(event.y * scaling_factor)
        canvas.coords(rect, start_x / scaling_factor, start_y / scaling_factor, event.x, event.y)

    def on_button_release(event):
        root.quit()
        root.destroy()

    def on_escape(event):
        logging.info("Capture cancelled by user.")
        root.quit()
        root.destroy()
        sys.exit()

    canvas.bind("<ButtonPress-1>", on_button_press)
    canvas.bind("<B1-Motion>", on_move_press)
    canvas.bind("<ButtonRelease-1>", on_button_release)
    root.bind("<Escape>", on_escape)

    root.mainloop()

    left = min(start_x, end_x)
    top = min(start_y, end_y)
    right = max(start_x, end_x)
    bottom = max(start_y, end_y)

    logging.info(f"Selected region coordinates (scaled): Left={left}, Top={top}, Right={right}, Bottom={bottom}")
    return (left, top, right, bottom)

def generate_final_response(ocr_text):
    logging.info("Generating final response...")
    
    context = f"""Assume the role of a strategist who excels at logical problem-solving and planning based on OCR Text. Your responses should prioritize efficiency, clarity, and foresight in addressing challenges.

Behavior: Break down complex problems into manageable parts and outline step-by-step strategies. Approach each situation with a calculated, analytical mindset, ensuring thorough analysis before offering solutions.

Mannerisms: Use structured, organized language. Ask clarifying questions to gather essential information and present your thoughts in a clear, methodical manner..
OCR Text:
{ocr_text}

Instructions:
1. Understand the OCR text and treat it as a comprehensive multiple-choice question.
2. Identify the correct answer from options (if provided within text).
3. Respond in the following format: "The answer is ((X)Answer) because (reason)".
4. Elaborate on the reason briefly, explaining why this is the correct answer.
5. Ignore these words if you come across them in OCR text like they do not exist : GPSC, GWSSB, GSSSB, GPSSB, GMB, PGVCL, MGVCL,
GSECL, GETCO, GMC, GIDC, GPHC, GMDC,NWRSW&K
6. Keep your answer strictly to 100 words
7. If there is calculation involved then follow this : We have been given (calculation_data), The formula to be used is (formula), we need to find (answer_variable)
8. Do calculation attentively.
9. Important : ((Check your answer 3 times internally before giving output, think in this manner : Initially deduced answer - Counter arguments - Answer deduced second time - Counter Arguments - Final Answer))
Provide the answer below:
"""
    try:
        # Assume the response is structured correctly by the LLM
        response = ollama.generate(model='llama3.2', prompt=context)
        raw_response = response.get('response', '').strip()

        # Perform basic checks to ensure a valid response format
        if "because" in raw_response:
            formatted_answer = raw_response
            logging.info("Generated formatted response.")
        else:
            formatted_answer = "The response could not be structured correctly."
            logging.warning(formatted_answer)

        # Provide the response
        if formatted_answer:
            speak_text(formatted_answer)
        else:
            logging.warning("No valid response could be generated.")

        # Strip Markdown formatting
        formatted_answer = strip_markdown(formatted_answer)
        
        return formatted_answer
    except Exception as e:
        logging.error(f"LLM Error during final response generation: {e}")
        return ""

def reset_state():
    global ocr_text
    ocr_text = ""

# -------------------- Helper Functions --------------------


def transcribe_speech(short_timeout=5, phrase_time_limit=5):
    """
    Transcribes speech from the microphone with a short timeout.
    If no speech is detected within the timeout, proceeds without user input.

    Args:
        short_timeout (int): Maximum number of seconds to wait for a phrase to start.
        phrase_time_limit (int): Maximum number of seconds a phrase can last.
    """
    global is_listening, speech_text
    recognizer = sr.Recognizer()
    with sr.Microphone() as source:
        logging.info("Listening for voice input... Please say your query or question.")
        recognizer.adjust_for_ambient_noise(source, duration=1)  # Adjust for noise for 1 second
        try:
            # Listen for speech with shorter timeout and phrase time limit
            audio = recognizer.listen(source, timeout=short_timeout, phrase_time_limit=phrase_time_limit)
            logging.info("Processing the audio with Google Speech Recognition...")
            text = recognizer.recognize_google(audio)
            # Convert number words to digits if necessary
            speech_text = convert_number_words(text)
            logging.info(f"Speech Transcribed: {speech_text}")

            # Provide confirmation
            speak_text(f"I heard: {speech_text}.")
        except sr.WaitTimeoutError:
            logging.warning("Listening timed out, no speech detected. Proceeding without voice input.")
            speech_text = ""  # Ensure speech_text is empty
        except sr.UnknownValueError:
            logging.warning("Speech recognition could not understand audio. Proceeding without voice input.")
            speech_text = ""  # Ensure speech_text is empty
        except sr.RequestError as e:
            logging.error(f"Could not request results from speech recognition service; {e}")
            speech_text = ""  # Ensure speech_text is empty
        finally:
            with lock:
                is_listening = False  # Ensure listening flag is reset


def configure_tts_voice():
    """
    Configures the TTS engine's voice, rate, and volume.
    """
    global tts_engine
    voices = tts_engine.getProperty('voices')

    # List available voices (optional)
    # Commented out DEBUG logs as they are now suppressed
    # for index, voice in enumerate(voices):
    #     logging.debug(f"Voice {index}: {voice.name}, {voice.gender}, {voice.languages}")

    # Change voice (select male/female, adjust the index as per your preference)
    if len(voices) > 1:
        tts_engine.setProperty('voice', voices[1].id)  # Change index to select a different voice
        logging.info(f"TTS voice set to: {voices[1].name}")
    else:
        logging.info("Only one voice available. Using default voice.")

    # Adjust speaking rate (lower is slower and more human-like)
    tts_engine.setProperty('rate', 150)  # Default is 200, lowering makes it more human-like

    # Adjust volume (0.0 to 1.0)
    tts_engine.setProperty('volume', 1.0)

def capture_and_save_image():
    global ocr_text, is_listening  # Added is_listening to globals

    region = get_selected_region(scaling_factor)
    left, top, right, bottom = region
    width = right - left
    height = bottom - top

    # Removed detailed dimension logging
    logging.info("Starting OCR process with image preprocessing...")

    if width <= 0 or height <= 0:
        logging.error("Invalid region selected. Please try again.")
        speak_text("Invalid region selected. Please try again by pressing F2.")
        reset_state()
        with lock:
            is_listening = False  # Reset the listening flag
        return

    with mss.mss() as sct:
        monitor = {
            "top": int(top / scaling_factor),
            "left": int(left / scaling_factor),
            "width": int(width / scaling_factor),
            "height": int(height / scaling_factor)
        }

        try:
            screenshot = sct.grab(monitor)
            img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
        except Exception as e:
            logging.error(f"Screen capture failed: {e}")
            speak_text("Screen capture failed. Please ensure the selected area is correct and try again.")
            reset_state()
            with lock:
                is_listening = False  # Reset the listening flag
            return

    perform_ocr(img)
    if not ocr_text.strip():
        logging.error("OCR returned empty text. Please ensure the selected region contains readable text.")
        speak_text("OCR did not detect any text. Please ensure the selected area contains readable text and try again.")
        reset_state()
        with lock:
            is_listening = False  # Reset the listening flag
        return

    generate_final_response(ocr_text)
    reset_state()
    with lock:
        is_listening = False  # Reset the listening flag after processing

def start_listening(scaling):
    global is_listening
    with lock:
        if not is_listening:
            is_listening = True
            logging.info("Started listening...")
            threading.Thread(target=capture_and_save_image, args=(), daemon=True).start()

def stop_listening():
    global is_listening
    with lock:
        if is_listening:
            is_listening = False
            logging.info("Stopped listening.")
            speak_text("Stopped listening.")

async def main():
    global scaling_factor

    configure_tts_voice()

    set_dpi_awareness()
    scaling_factor = get_scaling_factor()

    keyboard.add_hotkey('f2', start_listening, args=(scaling_factor,))
    keyboard.add_hotkey('f3', stop_listening)

    logging.info("QA Assistant is running.")
    logging.info("Press 'F2' to select a region and answer a question, 'F3' to stop listening, 'Esc' to exit.")

    while True:
        await asyncio.sleep(0.1)
        if keyboard.is_pressed('esc'):
            logging.info("Exiting...")
            speak_text("Exiting the QA Assistant. Goodbye!")
            break

    keyboard.unhook_all()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        logging.info("Program terminated by user.")
    except Exception as e:
        logging.error(f"An unexpected error occurred: {e}")
    finally:
        keyboard.unhook_all() : I want to implement RAG using local knowledge base, The documents I have are not the best formatted, they have text, tables as well as diagrams. make necesssary changes in RAG and use a bigger or better model for embeddings which can use upto 6gb of vram, because it is to be done once only for one document.