import streamlit as st
from openai import OpenAI
import os
import pandas as pd
import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import torch
import re

# Set up OpenAI client
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# Check if GPU is available
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")

# Load metadata and embeddings (ensure these files are in your working directory or update paths)
metadata_path = 'question_metadata.csv'  # Update this path if needed
embeddings_path = 'question_dataset_embeddings.npy'  # Update this path if needed

metadata = pd.read_csv(metadata_path)
embeddings = np.load(embeddings_path)

# Load the SentenceTransformer model
model = SentenceTransformer("all-MiniLM-L6-v2").to(device)

# Load prompts from files
with open("technical_interviewer_prompt.txt", "r") as file:
    technical_interviewer_prompt = file.read()

with open("question_generation_prompt.txt", "r") as file:
    question_generation_prompt = file.read()
    
st.title("Real-World Programming Question Mock Interview")

# Initialize session state variables
if "messages" not in st.session_state:
    st.session_state.messages = []

if "follow_up_mode" not in st.session_state:
    st.session_state.follow_up_mode = False  # Tracks whether we're in follow-up mode

if "generated_question" not in st.session_state:
    st.session_state.generated_question = None  # Stores the generated question for persistence

if "code_template" not in st.session_state:
    st.session_state.code_template = ""  # Stores the code template

if "sample_test_case" not in st.session_state:
    st.session_state.sample_test_case = ""  # Stores the sample test case

if "expected_output" not in st.session_state:
    st.session_state.expected_output = ""  # Stores the expected output

if "debug_logs" not in st.session_state:
    st.session_state.debug_logs = None  # Stores debug logs for toggling

# Function to find the top 1 most similar question based on user input
def find_top_question(query):
    # Generate embedding for the query
    query_embedding = model.encode(query, convert_to_tensor=True, device=device).cpu().numpy()
    
    # Reshape query_embedding to ensure it is a 2D array
    query_embedding = query_embedding.reshape(1, -1)  # Reshape to (1, n_features)

    # Compute cosine similarity between query embedding and dataset embeddings
    similarities = cosine_similarity(query_embedding, embeddings).flatten()  # Flatten to get a 1D array of similarities

    # Get the index of the most similar result (top 1)
    top_index = similarities.argsort()[-1]  # Index of highest similarity

    # Retrieve metadata for the top result
    top_result = metadata.iloc[top_index].copy()
    top_result['similarity_score'] = similarities[top_index]

    return top_result

# Function to generate response using OpenAI API with debugging logs
def generate_response(messages):
    # For debug logs, store only the follow-up conversation history
    st.session_state.debug_logs = st.session_state.messages  # Update debug logs with current conversation
    
    response = client.chat.completions.create(
        model="o1-mini",
        messages=messages,
    )
    
    return response.choices[0].message.content

# Function to extract code template and sample test case from the generated question
def extract_code_and_test_case(generated_question):
    code_template = ""
    sample_test_case = ""
    expected_output = ""
    
    # Extract code template
    code_match = re.search(r'```python(.*?)```', generated_question, re.DOTALL)
    if code_match:
        code_template = code_match.group(1).strip()
    else:
        # Default code template if none is found
        code_template = "# Write your code here\n"

    # Extract sample test case and expected output
    test_case_match = re.search(r'Sample Input:\s*(.*?)\n', generated_question, re.DOTALL)
    expected_output_match = re.search(r'Expected Output:\s*(.*?)\n', generated_question, re.DOTALL)
    if test_case_match and expected_output_match:
        sample_test_case = test_case_match.group(1).strip()
        expected_output = expected_output_match.group(1).strip()
    else:
        sample_test_case = ""
        expected_output = ""

    return code_template, sample_test_case, expected_output

# Move the input form to the sidebar to make it always visible and more compact
with st.sidebar.form(key="input_form"):
    st.markdown("## Generate a New Question")
    company = st.text_input("Company", value="Google")  # Default value: Google
    difficulty = st.selectbox("Difficulty", ["Easy", "Medium", "Hard"], index=1)  # Default: Medium
    topic = st.text_input("Topic", value="Binary Search")  # Default: Binary Search
    generate_button = st.form_submit_button(label="Generate")

if generate_button:
    # Clear session state and start fresh with follow-up mode disabled
    st.session_state.messages = []
    st.session_state.follow_up_mode = False

    # Create a query from user inputs and find the most relevant question
    query = f"{company} {difficulty} {topic}"
    top_question = find_top_question(query)
    
    # Prepare a detailed prompt for GPT using the top question's details
    detailed_prompt = (
        f"Transform this LeetCode question into a real-world interview scenario.\n\n"
        f"**Company**: {top_question['company']}\n"
        f"**Question Name**: {top_question['questionName']}\n"
        f"**Difficulty Level**: {top_question['difficulty level']}\n"
        f"**Tags**: {top_question['Tags']}\n"
        f"**Content**: {top_question['Content']}\n"
        f"\nPlease create a real-world interview question based on this information. "
        f"Include the following sections:\n\n"
        f"- Problem Description\n"
        f"- Code Template (in a Python code block)\n"
        f"- Sample Input and Expected Output (clearly separated)\n"
    )
    
    # Generate response using OpenAI API with detailed prompt and debugging logs
    response = generate_response([{"role": "user", "content": detailed_prompt}])  # Question generation prompt excluded here

    # Store generated question in session state for persistence in sidebar and follow-up conversation state
    st.session_state.generated_question = response

    # Extract code template and sample test case
    code_template, sample_test_case, expected_output = extract_code_and_test_case(response)
    st.session_state.code_template = code_template
    st.session_state.sample_test_case = sample_test_case
    st.session_state.expected_output = expected_output

    # Enable follow-up mode after generating the initial question
    st.session_state.follow_up_mode = True

# Display chat messages from history on app rerun (for subsequent conversation)
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Chatbox for subsequent conversations with assistant (follow-up mode)
if st.session_state.follow_up_mode:
    if user_input := st.chat_input("Continue your conversation or ask follow-up questions here:"):
        # Display user message in chat message container and add to session history
        with st.chat_message("user"):
            st.markdown(user_input)
        
        st.session_state.messages.append({"role": "user", "content": user_input})

        # Prepare messages to send to the assistant
        # Include the technical interviewer prompt and generated question, but do not display them
        # Add an instruction for the assistant to reply as a real-world interviewer would
        assistant_instruction = (
            "As a real-world interviewer, please reply to the candidate's follow-up questions "
            "specific to the generated interview question, to the point, and in a natural, human-sounding way."
        )

        messages_to_send = [
            {"role": "user", "content": technical_interviewer_prompt},
            {"role": "assistant", "content": st.session_state.generated_question},
            {"role": "user", "content": assistant_instruction}
        ] + st.session_state.messages

        assistant_response = generate_response(messages_to_send)

        with st.chat_message("assistant"):
            st.markdown(assistant_response)
        
        st.session_state.messages.append({"role": "assistant", "content": assistant_response})

st.sidebar.markdown("---")
st.sidebar.markdown("## Generated Question")
if st.session_state.generated_question:
    st.sidebar.markdown(st.session_state.generated_question)
else:
    st.sidebar.markdown("_No question generated yet._")

st.sidebar.markdown("---")
st.sidebar.markdown("## Python Code Interpreter")

# Pre-fill code interpreter with code template after question generation
if st.session_state.code_template:
    code_input = st.sidebar.text_area("Write your Python code here:", value=st.session_state.code_template, height=300)
else:
    code_input = st.sidebar.text_area("Write your Python code here:", height=300)

if st.sidebar.button("Run Code"):
    try:
        # Prepare the code for execution
        exec_globals = {}
        # Create a function wrapper to execute the user's code
        exec(f"def user_solution():\n{code_input}", exec_globals)
        user_solution = exec_globals.get('user_solution', None)
        
        # Prepare sample test case execution
        if st.session_state.sample_test_case:
            # Assume the sample test case is in the format of arguments to the function
            test_case = st.session_state.sample_test_case
            # Evaluate the test case safely
            test_args = eval(test_case)
            if not isinstance(test_args, tuple):
                test_args = (test_args,)
            # Capture the output
            returned_output = user_solution(*test_args)
        else:
            returned_output = user_solution()
        
        # Display the expected output and returned output
        st.sidebar.markdown("### Sample Test Case Result:")
        st.sidebar.markdown(f"**Sample Input:** {st.session_state.sample_test_case}")
        st.sidebar.markdown(f"**Expected Output:** {st.session_state.expected_output}")
        st.sidebar.markdown(f"**Your Output:** {returned_output}")
        
        # Compare outputs
        if str(returned_output) == st.session_state.expected_output:
            st.sidebar.success("Your output matches the expected output!")
        else:
            st.sidebar.error("Your output does not match the expected output.")
    except Exception as e:
        st.sidebar.error(f"Error: {e}")

# Right sidebar toggleable debug logs and code interpreter section
with st.expander("Debug Logs (Toggle On/Off)", expanded=False):
    if st.session_state.debug_logs:
        st.write(st.session_state.debug_logs)