import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

class Autograder:
    def __init__(self):
        """
        Initializes the Autograder, generates the dataset, 
        and resets the score tracking.
        """
        self.completed_questions = set()
        self.total_questions = 30
        self._create_dataset()
        print("✅ Setup complete. Dataset generated and Auto-Grader ready.")

    def _create_dataset(self):
        """Internal method to generate the movie metadata CSV if it doesn't exist."""
        if not os.path.exists('movie_metadata.csv'):
            data = {
                'movie_title': ['Avatar', 'Pirates of the Caribbean', 'Spectre', 'The Dark Knight', 'Star Wars: Ep VII',
                                'Shin Godzilla', 'The Avengers', 'Frozen', 'Titanic', 'Jurassic World',
                                'Skyfall', 'Spider-Man 3', 'Iron Man 3', 'Minions', 'Civil War',
                                'Deadpool', 'The Lion King', 'Joker', 'Endgame', 'Infinity War'],
                'director_name': ['James Cameron', 'Gore Verbinski', 'Sam Mendes', 'Christopher Nolan', 'J.J. Abrams',
                                  'Hideaki Anno', 'Joss Whedon', 'Chris Buck', 'James Cameron', 'Colin Trevorrow',
                                  'Sam Mendes', 'Sam Raimi', 'Shane Black', 'Kyle Balda', 'Anthony Russo',
                                  'Tim Miller', 'Roger Allers', 'Todd Phillips', 'Anthony Russo', 'Anthony Russo'],
                'duration': [178, 169, 148, 152, 136, 120, 143, 102, 194, 124, 143, 139, 130, 91, 147, 108, 88, 122, 181, 149],
                'gross': [760, 309, 200, 533, 936, None, 623, 400, 658, 652, 304, 336, 409, 336, 408, 363, 422, 335, 858, 678],
                'genres': ['Action', 'Action', 'Action', 'Action', 'Action', 'Horror', 'Action', 'Animation', 'Drama', 'Action',
                           'Action', 'Action', 'Action', 'Comedy', 'Action', 'Comedy', 'Animation', 'Drama', 'Action', 'Action'],
                'imdb_score': [7.9, 7.1, 6.8, 9.0, 8.1, 8.2, 8.1, 7.5, 7.7, 7.0, 7.7, 6.2, 7.2, 6.4, 7.8, 8.0, 8.5, 8.4, 8.4, 8.4],
                'budget': [237, 300, 245, 185, 245, 15, 220, 150, 200, 150, 200, 258, 200, 74, 250, 58, 45, 55, 356, 316],
                'title_year': [2009, 2007, 2015, 2008, 2015, 2016, 2012, 2013, 1997, 2015, 2012, 2007, 2013, 2015, 2016, 2016, 1994, 2019, 2019, 2018]
            }
            df = pd.DataFrame(data)
            df.to_csv('movie_metadata.csv', index=False)
            print("✅ Dataset 'movie_metadata.csv' generated successfully!")

    def _grade_checkpoint(self, name, result, expected_check, hint):
        """Internal method to validate a specific checkpoint."""
        try:
            if expected_check(result):
                print(f"✅ Correct: {name}")
                self.completed_questions.add(name)
            else:
                print(f"❌ Incorrect. Hint: {hint}")
        except Exception as e:
            print(f"❌ Error in your code: {e}")

    def print_final_score(self):
        """Calculates and prints the final score based on completed unique questions."""
        score = len(self.completed_questions)
        percentage = (score / self.total_questions) * 100
        print(f"\n==============================")
        print(f"🏆 FINAL SCORE: {score} / {self.total_questions}")
        print(f"📊 PERCENTAGE:  {percentage:.1f}%")
        print(f"==============================")
        if score == self.total_questions:
            print("🌟 Perfect Score! You are ready for Machine Learning!")
        elif score >= 20:
            print("👍 Good job! Review the ones you missed.")
        else:
            print("💪 Keep practicing! You can do this.")

    # --- NUMPY CHECKS ---
    def check_np_1(self, x): self._grade_checkpoint("List conversion", x, lambda r: isinstance(r, np.ndarray) and np.array_equal(r, np.array([10, 20, 30])), "Convert the list using np.array().")
    def check_np_2(self, x): self._grade_checkpoint("Zeros Array", x, lambda r: r.shape == (4,4) and r[0,0] == 0, "Use np.zeros with tuple (4,4).")
    def check_np_3(self, x): self._grade_checkpoint("Arange", x, lambda r: r[0]==0 and r[-1]==18 and len(r)==10, "Use np.arange(start, stop, step).")
    def check_np_4(self, x): self._grade_checkpoint("Linspace", x, lambda r: len(r)==50 and r[0]==0 and r[-1]==10, "Use np.linspace(0, 10, 50).")
    def check_np_5(self, x): self._grade_checkpoint("Random Integers", x, lambda r: r.shape==(3,3) and r.max() <= 10, "Use np.random.randint.")
    def check_np_6(self, x): self._grade_checkpoint("Broadcasting", x, lambda r: np.array_equal(r, np.array([15, 25, 35])), "Simply add 5 to the array.")
    def check_np_7(self, x): self._grade_checkpoint("Element-wise Math", x, lambda r: np.array_equal(r, np.array([100, 400, 900])), "Square the array using ** 2.")
    def check_np_8(self, x): self._grade_checkpoint("Indexing", x, lambda r: r == 30, "Index is 2 (0-based).")
    def check_np_9(self, x): self._grade_checkpoint("Slicing", x, lambda r: np.array_equal(r, np.array([20, 30])), "Slice using [1:3].")
    def check_np_10(self, x): self._grade_checkpoint("Matrix Slicing", x, lambda r: r.shape==(2,2) and r[0,0]==1 and r[1,1]==5, "Use mat[:2, :2].")
    def check_np_11(self, x): self._grade_checkpoint("Boolean Masking", x, lambda r: np.array_equal(r, np.array([60, 70])), "Condition is arr > 50.")
    def check_np_12(self, x): self._grade_checkpoint("Aggregation", x, lambda r: r == 40.0, "Use .mean().")

    # --- MATPLOTLIB CHECKS ---
    def check_plt_1(self, x, y): self._grade_checkpoint("Line Data", (x,y), lambda r: len(r[0])==100 and r[1][0]==0, "x: 0-10, y: x*2.")
    def check_plt_2(self, x, y): self._grade_checkpoint("Scatter Data", (x,y), lambda r: len(r[0])==50, "Generate 50 random points.")
    def check_plt_3(self, x): self._grade_checkpoint("Hist Data", x, lambda r: len(r)==1000, "Use np.random.randn(1000).")

    # --- PANDAS CHECKS ---
    def check_pd_1(self, s): self._grade_checkpoint("Series Creation", s, lambda r: isinstance(r, pd.Series) and r['a']==10, "Use pd.Series(data, index=labels).")
    def check_pd_2(self, df): self._grade_checkpoint("DF Creation", df, lambda r: r.shape==(3,2) and 'Name' in r.columns, "Create DataFrame from dictionary.")
    def check_pd_3(self, df): self._grade_checkpoint("Read CSV", df, lambda r: r.shape[0]>=20, "Use pd.read_csv.")
    def check_pd_4(self, val): self._grade_checkpoint("Inspection", val, lambda r: r==20, "len(df) gives the rows.")
    def check_pd_5(self, df): self._grade_checkpoint("Col Selection", df, lambda r: isinstance(r, pd.Series) and r.name=='movie_title', "Select single column with brackets.")
    def check_pd_6(self, df): self._grade_checkpoint("Multiple Cols", df, lambda r: isinstance(r, pd.DataFrame) and r.shape[1]==2, "Use double brackets [['col1', 'col2']].")
    def check_pd_7(self, val): self._grade_checkpoint("Row Selection", val, lambda r: r=='Spectre', "Use .iloc[2] and column access.")
    def check_pd_8(self, df): self._grade_checkpoint("Filtering", df, lambda r: r['imdb_score'].min() > 8.0, "Condition: df['imdb_score'] > 8.0.")
    def check_pd_9(self, df): self._grade_checkpoint("Multiple Filters", df, lambda r: r['gross'].min() >= 500 and r['genres'].iloc[0]=='Action', "Combine conditions with &.")
    def check_pd_10(self, df): self._grade_checkpoint("Sorting", df, lambda r: r.iloc[0]['imdb_score'] >= 9.0, "Use sort_values(by=..., ascending=False).")
    def check_pd_11(self, val): self._grade_checkpoint("Missing Data", val, lambda r: r > 0, "Use .isnull().sum().")
    def check_pd_12(self, df): self._grade_checkpoint("Fill NA", df, lambda r: r['gross'].isnull().sum()==0, "Use fillna(0).")
    def check_pd_13(self, df): self._grade_checkpoint("Feature Eng", df, lambda r: 'net_profit' in r.columns, "gross - budget.")
    def check_pd_14(self, s): self._grade_checkpoint("Grouping", s, lambda r: r['Christopher Nolan'] > 8.0, "Group by director, mean of imdb_score.")
    def check_pd_15(self, df): self._grade_checkpoint("Value Counts", df, lambda r: 'Action' in r.index, "Use .value_counts().")
