import logging
import re
import subprocess
import tempfile
import os

_logger = logging.getLogger(__name__)

LANGUAGE_EXTENSIONS = {
    'python': '.py',
    'javascript': '.js',
    'java': '.java',
    'c': '.c',
    'cpp': '.cpp',
}

LANGUAGE_RUNNERS = {
    'python': ['python3', '{file}'],
    'javascript': ['node', '{file}'],
}

EXECUTION_TIMEOUT = 10


class CodeScorer:

    @staticmethod
    def score_code_output(student_answer, expected_output):
        """Compare student output against expected output (case-insensitive, stripped)."""
        norm_student = CodeScorer._normalize_output(str(student_answer))
        norm_expected = CodeScorer._normalize_output(str(expected_output))

        if norm_student.lower() == norm_expected.lower():
            return {
                'correct': True,
                'score': 1.0,
                'feedback': 'Correct output.',
            }

        student_lines = norm_student.splitlines()
        expected_lines = norm_expected.splitlines()
        if len(student_lines) == len(expected_lines):
            matching = sum(
                1 for s, e in zip(student_lines, expected_lines)
                if s.strip().lower() == e.strip().lower()
            )
            partial = matching / len(expected_lines)
            if partial > 0:
                return {
                    'correct': False,
                    'score': round(partial, 2),
                    'feedback': f'{matching}/{len(expected_lines)} lines match.',
                }

        return {
            'correct': False,
            'score': 0.0,
            'feedback': 'Output does not match the expected result.',
        }

    @staticmethod
    def score_code_completion(student_code, test_cases, language='python'):
        """Run student code against test cases in a sandboxed subprocess.

        Each test case: {'input': str, 'expected_output': str}
        """
        if language not in LANGUAGE_RUNNERS:
            return {
                'passed': 0, 'total': len(test_cases), 'score': 0.0,
                'details': [{'test_case': i + 1, 'passed': False,
                             'note': f'Language {language} execution not supported'}
                            for i in range(len(test_cases))],
            }

        total = len(test_cases)
        passed = 0
        details = []

        ext = LANGUAGE_EXTENSIONS.get(language, '.txt')

        for i, tc in enumerate(test_cases):
            tc_input = tc.get('input', '')
            tc_expected = tc.get('expected_output', '')

            try:
                with tempfile.NamedTemporaryFile(
                    mode='w', suffix=ext, delete=False, prefix='encoach_'
                ) as f:
                    f.write(student_code)
                    tmp_path = f.name

                cmd = [part.replace('{file}', tmp_path) for part in LANGUAGE_RUNNERS[language]]
                result = subprocess.run(
                    cmd, input=tc_input, capture_output=True, text=True,
                    timeout=EXECUTION_TIMEOUT,
                )

                actual_output = result.stdout.strip()
                stderr = result.stderr.strip()

                if result.returncode != 0:
                    details.append({
                        'test_case': i + 1,
                        'input': tc_input,
                        'expected_output': tc_expected,
                        'actual_output': actual_output,
                        'passed': False,
                        'error': stderr[:500] if stderr else f'Exit code {result.returncode}',
                    })
                    continue

                match = CodeScorer._normalize_output(actual_output).lower() == \
                    CodeScorer._normalize_output(tc_expected).lower()

                if match:
                    passed += 1

                details.append({
                    'test_case': i + 1,
                    'input': tc_input,
                    'expected_output': tc_expected,
                    'actual_output': actual_output,
                    'passed': match,
                })

            except subprocess.TimeoutExpired:
                details.append({
                    'test_case': i + 1,
                    'input': tc_input,
                    'expected_output': tc_expected,
                    'actual_output': None,
                    'passed': False,
                    'error': f'Execution timed out after {EXECUTION_TIMEOUT}s',
                })
            except Exception as e:
                details.append({
                    'test_case': i + 1,
                    'input': tc_input,
                    'expected_output': tc_expected,
                    'actual_output': None,
                    'passed': False,
                    'error': str(e),
                })
            finally:
                try:
                    os.unlink(tmp_path)
                except Exception:
                    pass

        return {
            'passed': passed,
            'total': total,
            'score': round(passed / total, 2) if total > 0 else 0.0,
            'details': details,
        }

    @staticmethod
    def score_sql_query(student_sql, expected_result, db_config=None):
        """Score a SQL query submission.

        With db_config: executes against a real database and compares result sets.
        Without db_config: falls back to normalized syntax comparison.
        """
        if db_config is not None:
            return CodeScorer._execute_sql(student_sql, expected_result, db_config)

        norm_student = CodeScorer._normalize_sql(str(student_sql))
        norm_expected = CodeScorer._normalize_sql(str(expected_result))

        if norm_student == norm_expected:
            return {
                'correct': True,
                'score': 1.0,
                'feedback': 'SQL syntax matches expected query.',
            }

        student_parts = CodeScorer._extract_sql_clauses(norm_student)
        expected_parts = CodeScorer._extract_sql_clauses(norm_expected)
        matching_clauses = sum(1 for k in expected_parts if student_parts.get(k) == expected_parts[k])
        total_clauses = len(expected_parts)

        if total_clauses > 0 and matching_clauses > 0:
            partial = matching_clauses / total_clauses
            return {
                'correct': False,
                'score': round(partial, 2),
                'feedback': f'{matching_clauses}/{total_clauses} SQL clauses match.',
            }

        return {
            'correct': False,
            'score': 0.0,
            'feedback': 'SQL query does not match the expected structure.',
        }

    @staticmethod
    def _execute_sql(student_sql, expected_result, db_config):
        """Execute SQL against a database and compare results."""
        try:
            import psycopg2
            conn = psycopg2.connect(**db_config)
            conn.set_session(readonly=True, autocommit=True)
            cur = conn.cursor()
            cur.execute(student_sql)
            rows = cur.fetchall()
            cur.close()
            conn.close()

            if isinstance(expected_result, list):
                expected_set = set(tuple(r) for r in expected_result)
                actual_set = set(rows)
                correct = expected_set == actual_set
                return {
                    'correct': correct,
                    'score': 1.0 if correct else 0.0,
                    'feedback': 'Correct!' if correct else 'Result set does not match.',
                    'row_count': len(rows),
                }

            return {'correct': False, 'score': 0.0, 'feedback': 'Cannot compare results.'}
        except ImportError:
            return {'correct': False, 'score': 0.0, 'feedback': 'psycopg2 not available for SQL execution.'}
        except Exception as e:
            return {'correct': False, 'score': 0.0, 'feedback': f'SQL execution error: {e}'}

    @staticmethod
    def _normalize_output(text):
        """Strip whitespace and normalize line endings."""
        text = text.replace('\r\n', '\n').replace('\r', '\n')
        return text.strip()

    @staticmethod
    def _normalize_sql(sql):
        """Normalize SQL for structural comparison: lowercase, collapse whitespace."""
        sql = sql.strip().lower()
        sql = re.sub(r'\s+', ' ', sql)
        sql = re.sub(r'\s*;\s*$', '', sql)
        return sql

    @staticmethod
    def _extract_sql_clauses(sql):
        """Extract major SQL clauses for partial matching."""
        clauses = {}
        keywords = ['select', 'from', 'where', 'group by', 'having', 'order by', 'limit', 'join']
        sql_lower = sql.lower()
        positions = []
        for kw in keywords:
            idx = sql_lower.find(kw)
            if idx >= 0:
                positions.append((idx, kw))
        positions.sort()
        for i, (pos, kw) in enumerate(positions):
            end = positions[i + 1][0] if i + 1 < len(positions) else len(sql)
            clauses[kw] = sql[pos:end].strip()
        return clauses
