"""Contains the question class which allows interacting with a question through class methods."""

from typing import List, Dict, Any, Callable, Optional
from io import StringIO
import contextlib
import inspect
import ast
import os

from .results import TestResult


class Question:
    def __init__(self, id_or_token, func=None):
        """
        Initialize Question with either question_id or token.
        
        Args:
            id_or_token: Either an integer question_id or a string token
            func: The function to test (optional)
        """
        self.token = None
        self.id = None
        self.func = func
        
        if isinstance(id_or_token, str):
            # It's a token - fetch question info from server
            self.token = id_or_token
            self.id = self._get_question_id_from_token()
        else:
            # It's a question ID
            self.id = id_or_token

    def show(self):
        """Print the question text to the termninal."""
        raise NotImplementedError()
    
    def _get_question_id_from_token(self) -> int:
        """Get question ID from token by calling API"""
        import requests
        
        server_url = self._server_url
        endpoint = f"{server_url}/api/token/info"
        headers = {'Authorization': f'Bearer {self.token}'}
        
        try:
            response = requests.get(endpoint, headers=headers, timeout=10)
            response.raise_for_status()
            data = response.json()
            return data['question_id']
        except requests.ConnectionError:
            raise Exception(
                f"Could not connect to server at {server_url}. "
                f"Is the dev server running? Start it with: python run.py"
            )
        except requests.JSONDecodeError:
            raise Exception(
                f"Server at {server_url} returned invalid response. "
                f"Check if the server is running correctly."
            )
        except Exception as e:
            raise Exception(f"Failed to get question info from token: {str(e)}")

    def submit(self, token: Optional[str] = None, skip_local: bool = False, quiet: bool = False) -> bool:
        """Submit a solution to the question.

        Args:
            token: Optional token for authentication. If not provided, uses token from __init__
            skip_local: Skip local testing before submission
            quiet: Suppress output
        
        Returns:
            bool: True if submission passed, False otherwise
        """
        if self.func is None:
            raise ValueError("The Question must have a function associated with it for submission.")
        
        # Use provided token or instance token
        submit_token = token or self.token
        if not submit_token:
            raise ValueError("No token provided. Either initialize Question with a token or pass one to submit()")
        
        # Test locally before submission (optional but recommended)
        if not skip_local:
            if not quiet:
                print("Running local tests before submission...")
            local_results = self.test(quiet=quiet)
            
            if not local_results['success']:
                if not quiet:
                    print("❌ Local tests failed. Fix issues before submitting to server.")
                    print(f"   Passed: {local_results['passed']}/{local_results['total']}")
                return False
            
            if not quiet:
                print(f"✓ Local tests passed ({local_results['passed']}/{local_results['total']})")
        
        # Extract user's code for submission
        try:
            user_code = self._extract_user_code()
        except Exception as e:
            if not quiet:
                print(f"❌ Failed to extract code: {e}")
            return False
        
        # Submit to server for official validation
        if not quiet:
            print("\nSubmitting to server for validation...")
        
        try:
            server_results = self._submit_to_server(user_code, submit_token)
            
            if not quiet:
                print(f"\n{'='*50}")
                print(f"Official Server Results for Question {self.id}")
                self._print_results(server_results)
            
            return server_results['success']
            
        except Exception as e:
            if not quiet:
                print(f"❌ Submission failed: {e}")
            return False
    
    def _extract_user_code(self) -> str:
        """
        Extract the user's code from the entire module.
        Removes @question decorators for ALL functions (since we send function_name separately).
        Removes zest imports.
        Keeps helper functions and other code.
        
        Returns:
            str: Complete module code filtered for this question
        """
        # Get the module where the function is defined
        module = inspect.getmodule(self.func)
        if not module:
            # If no module, just return the function source
            return inspect.getsource(self.func)
        
        try:
            module_source = inspect.getsource(module)
        except (OSError, TypeError):
            # Fallback to just the function source
            return inspect.getsource(self.func)
        
        # Parse the module and filter out unwanted content
        lines = module_source.split('\n')
        filtered_lines = []
        i = 0
        
        while i < len(lines):
            line = lines[i]
            stripped = line.strip()
            
            # Check if this is a @question decorator
            if stripped.startswith('@question'):
                # Always skip @question decorators (we send function_name separately)
                i += 1
                continue
            
            # Check if this is a zest import - skip it
            if stripped.startswith('from zest import') or stripped.startswith('import zest'):
                i += 1
                continue
            
            # Check if this line contains calls to Question methods
            # Match patterns like: obj.submit(...), obj.test(...), etc.
            if ('.test(' in stripped or '.submit(' in stripped or 
                '._extract_user_code(' in stripped or '._print_results(' in stripped):
                i += 1
                continue
            
            # Keep everything else
            filtered_lines.append(line)
            i += 1
        
        return '\n'.join(filtered_lines)
    
    @property
    def _server_url(self):
        return 'http://localhost:5001' if os.environ.get('DATABASE_DEV', 'False') == 'True' else 'http://zest.zolnailucas.com'

    
    def _submit_to_server(self, user_code: str, token: str) -> Dict[str, Any]:
        """
        Submit user code to the server for official validation.
        
        Args:
            user_code: The complete Python code to test
            token: The question token for authentication
            
        Returns:
            Dictionary with server test results
        """
        import requests
        
        # Get server endpoint from environment
        server_url = self._server_url

        endpoint = f"{server_url}/api/submit"
        
        headers = {
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {token}'
        }
        
        payload = {
            'code': user_code,
            'function_name': self.func.__name__  # Send actual function name
        }
        
        try:
            response = requests.post(endpoint, json=payload, headers=headers, timeout=30)
            response.raise_for_status()
            return response.json()
            
        except requests.Timeout:
            raise Exception("Server request timed out")
        except requests.ConnectionError:
            raise Exception(f"Could not connect to server at {server_url}")
        except requests.HTTPError as e:
            if e.response.status_code == 401:
                raise Exception("Authentication failed - check your question token")
            elif e.response.status_code == 404:
                raise Exception(f"Question {self.id} not found on server")
            else:
                raise Exception(f"Server error: {e.response.text}")
        except Exception as e:
            raise Exception(f"Submission failed: {str(e)}")
    
    @staticmethod
    def _normalize_result(result):
        """
        Normalize result for comparison (convert tuples to lists recursively).
        
        Args:
            result: The result to normalize
            
        Returns:
            Normalized result with tuples converted to lists
        """
        if isinstance(result, tuple):
            return list(result)
        elif isinstance(result, list):
            return [Question._normalize_result(item) for item in result]
        elif isinstance(result, dict):
            return {k: Question._normalize_result(v) for k, v in result.items()}
        else:
            return result
    
    @staticmethod
    def _results_match(actual, expected):
        """
        Check if actual result matches expected result(s).
        
        Supports:
        - Single expected value
        - Multiple valid expected values (list with 'any_of' key)
        - Set comparison for unordered results
        
        Args:
            actual: The actual result from the function
            expected: Expected result or dict with 'any_of' containing list of valid results
            
        Returns:
            True if results match, False otherwise
        """
        # Check if expected is a dict with 'any_of' key for multiple valid answers
        if isinstance(expected, dict) and 'any_of' in expected:
            valid_answers = expected['any_of']
            return any(Question._normalize_result(actual) == Question._normalize_result(ans) 
                      for ans in valid_answers)
        
        # Check if expected is a dict with 'any_order' key for set comparison
        if isinstance(expected, dict) and 'any_order' in expected:
            expected_val = expected['any_order']
            try:
                return set(Question._normalize_result(actual)) == set(Question._normalize_result(expected_val))
            except TypeError:
                # If items aren't hashable, fall back to regular comparison
                return Question._normalize_result(actual) == Question._normalize_result(expected_val)
        
        # Regular comparison
        return actual == Question._normalize_result(expected)

    def test(self, test_cases: Optional[List[Dict[str, Any]]] = None, 
             no_cache: bool = False, quiet: bool = False) -> Dict[str, Any]:
        """
        Test the question response locally. Test inputs are pulled and cached from the server.
        
        Args:
            test_cases: Optional test cases to use (for executors). If None, fetches from server.
            no_cache: If True, force refetch from server instead of using cache
            quiet: If True, suppress output
            
        Returns:
            Dictionary with test results
        """
        if self.func is None:
            raise ValueError("The Question must have a function associated with it for testing.")
        
        # If test_cases not provided, fetch from server (with caching)
        if test_cases is None:
            test_cases = self._fetch_test_cases(no_cache=no_cache)
        
        test_results: List[TestResult] = []
        errors: List[str] = []
        passed = 0
        failed = 0
        
        # Run each test case
        for i, test_case in enumerate(test_cases):
            test_result = TestResult(
                test_number=i + 1,
                description=test_case.get('description', f'Test {i + 1}'),
                is_hidden=test_case.get('is_hidden', False)
            )
            
            try:
                args = test_case.get('args', [])
                kwargs = test_case.get('kwargs', {})
                expected = test_case.get('expected')
                
                test_result.expected = expected
                
                # Execute the function with captured output
                actual_result, stdout, stderr = Question._capture_output(
                    self.func, *args, **kwargs
                )
                
                test_result.actual = actual_result
                test_result.stdout = stdout
                test_result.stderr = stderr
                
                # Normalize actual result (convert tuples to lists for comparison)
                normalized_actual = self._normalize_result(actual_result)
                
                # Compare result with expected (supporting multiple valid answers)
                if self._results_match(normalized_actual, expected):
                    test_result.passed = True
                    passed += 1
                else:
                    failed += 1
                
            except Exception as e:
                failed += 1
                test_result.error = f"{type(e).__name__}: {str(e)}"
                errors.append(f"Test {i+1}: {str(e)}")
            
            test_results.append(test_result)
        
        result = {
            "passed": passed,
            "failed": failed,
            "total": len(test_cases),
            "test_results": [tr.to_dict() for tr in test_results],
            "errors": errors,
            "syntax_error": None,
            "timeout": False,
            "success": passed == len(test_cases) and not errors
        }
        
        if not quiet:
            self._print_results(result)
        
        return result
    
    def _fetch_test_cases(self, no_cache: bool = False) -> List[Dict[str, Any]]:
        """
        Fetch test cases from the server, with caching.
        
        Args:
            no_cache: If True, bypass cache and fetch fresh data
            
        Returns:
            List of test case dictionaries
        """
        import requests
        
        # Cache key based on question ID
        cache_key = f"test_cases_{self.id}"
        
        # Check cache first (unless no_cache is True)
        if not no_cache and hasattr(Question, '_cache'):
            if cache_key in Question._cache:
                return Question._cache[cache_key]
        
        # Initialize cache if it doesn't exist
        if not hasattr(Question, '_cache'):
            Question._cache = {}
        
        # Fetch from server
        server_url = self._server_url
        api_key = os.getenv('ZEST_API_KEY')
        
        endpoint = f"{server_url}/api/question/{self.id}/test_cases"
        
        headers = {}
        if api_key:
            headers['Authorization'] = f'Bearer {api_key}'
        
        try:
            response = requests.get(endpoint, headers=headers, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            test_cases = data.get('test_cases', [])
            
            # Cache the results
            Question._cache[cache_key] = test_cases
            
            return test_cases
            
        except requests.Timeout:
            raise Exception(f"Server request timed out while fetching test cases for question {self.id}")
        except requests.ConnectionError:
            raise Exception(f"Could not connect to server at {server_url}. Is the server running?")
        except requests.HTTPError as e:
            if e.response.status_code == 401:
                raise Exception("Authentication failed - check ZEST_API_KEY environment variable")
            elif e.response.status_code == 404:
                raise Exception(f"Question {self.id} not found on server")
            else:
                raise Exception(f"Server error while fetching test cases: {e.response.text}")
        except Exception as e:
            raise Exception(f"Failed to fetch test cases: {str(e)}")
    
    def _print_results(self, result: Dict[str, Any]) -> None:
        """Print test results to console"""
        print(f"\nTest Results for Question {self.id}")
        print(f"{'='*50}")
        print(f"Passed: {result['passed']}/{result['total']}")
        print(f"Failed: {result['failed']}/{result['total']}")
        print(f"Success: {'✓' if result['success'] else '✗'}")
        
        # Print details for failed tests
        if result.get('test_results'):
            failed_tests = [tr for tr in result['test_results'] if not tr.get('passed')]
            if failed_tests:
                print(f"\nFailed Tests:")
                for test in failed_tests:
                    is_hidden = test.get('is_hidden', False)
                    if is_hidden:
                        # For hidden tests, only show that they failed
                        print(f"\n  Test {test['test_number']}: Hidden test case (no details available)")
                    else:
                        # For public tests, show full details
                        print(f"\n  Test {test['test_number']}: {test.get('description', 'No description')}")
                        if test.get('error'):
                            print(f"    Error: {test['error']}")
                        else:
                            print(f"    Expected: {test.get('expected')}")
                            print(f"    Got:      {test.get('actual')}")
                        if test.get('stdout'):
                            print(f"    Output: {test['stdout']}")
        
        if result['errors']:
            print(f"\nOther Errors:")
            for error in result['errors']:
                print(f"  - {error}")
    
    @staticmethod
    def from_code(question_id: int, user_code: str, function_name: str,
                  namespace: Optional[Dict[str, Any]] = None) -> 'Question':
        """
        Create a Question instance from user code string.
        
        This is the method executors should use to instantiate a Question from submitted code.
        
        Args:
            question_id: The ID of the question
            user_code: The Python code containing the function definition
            function_name: Name of the function to extract from the code
            namespace: Optional namespace to execute code in (defaults to restricted builtins)
        
        Returns:
            Question instance with the function from user code
            
        Raises:
            SyntaxError: If the code has syntax errors
            NameError: If the function is not found in the code
            Exception: If code execution fails
        """
        # Use provided namespace or create a minimal one
        if namespace is None:
            namespace = {'__builtins__': __builtins__, '__name__': '__main__'}
        
        # First, check for syntax errors
        compile(user_code, '<string>', 'exec')
        
        # Execute user's function definition
        exec(user_code, namespace)
        
        # Check if function exists
        if function_name not in namespace:
            raise NameError(f"Function '{function_name}' not found in submitted code")
        
        # Create Question instance with the function
        return Question(question_id, func=namespace[function_name])
    
    @staticmethod
    def _capture_output(func: Callable, *args, **kwargs):
        """
        Capture stdout and stderr from function execution.
        
        Returns:
            tuple: (result, stdout, stderr)
        """
        stdout_capture = StringIO()
        stderr_capture = StringIO()
        
        with contextlib.redirect_stdout(stdout_capture), \
             contextlib.redirect_stderr(stderr_capture):
            try:
                result = func(*args, **kwargs)
            except Exception as e:
                raise e
        
        return result, stdout_capture.getvalue(), stderr_capture.getvalue()
