"""
This module provides an interface on the grading-service backend
for peer grading

Use peer_grading_service() to get the version specified
in settings.PEER_GRADING_INTERFACE

"""
import json
import logging
import requests
from requests.exceptions import RequestException, ConnectionError, HTTPError
import sys

from django.conf import settings
from django.http import HttpResponse, Http404
from grading_service import GradingService
from grading_service import GradingServiceError

from courseware.access import has_access
from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric
from student.models import unique_id_for_user
from lxml import etree

log = logging.getLogger(__name__)

"""
This is a mock peer grading service that can be used for unit tests
without making actual service calls to the grading controller
"""
class MockPeerGradingService(object):
    def get_next_submission(self, problem_location, grader_id):
        return json.dumps({'success': True,
                'submission_id':1,
                'submission_key': "",
                'student_response': 'fake student response',
                'prompt': 'fake submission prompt',
                'rubric': 'fake rubric',
                'max_score': 4})

    def save_grade(self, location, grader_id, submission_id, 
            score, feedback, submission_key):
        return json.dumps({'success': True})

    def is_student_calibrated(self, problem_location, grader_id):
        return json.dumps({'success': True, 'calibrated': True})
    
    def show_calibration_essay(self, problem_location, grader_id):
        return json.dumps({'success': True,
                'submission_id':1,
                'submission_key': '',
                'student_response': 'fake student response',
                'prompt': 'fake submission prompt',
                'rubric': 'fake rubric',
                'max_score': 4})

    def save_calibration_essay(self, problem_location, grader_id, 
            calibration_essay_id, submission_key, score, feedback):
        return {'success': True, 'actual_score': 2}

    def get_problem_list(self, course_id, grader_id):
        return json.dumps({'success': True,
        'problem_list': [
          json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
            'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5}),
          json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
            'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5})
        ]})

class PeerGradingService(GradingService):
    """
    Interface with the grading controller for peer grading
    """
    def __init__(self, config):
        super(PeerGradingService, self).__init__(config)
        self.get_next_submission_url = self.url + '/get_next_submission/'
        self.save_grade_url = self.url + '/save_grade/'
        self.is_student_calibrated_url = self.url + '/is_student_calibrated/'
        self.show_calibration_essay_url = self.url + '/show_calibration_essay/'
        self.save_calibration_essay_url = self.url + '/save_calibration_essay/'
        self.get_problem_list_url = self.url + '/get_problem_list/'
        self.get_notifications_url = self.url + '/get_notifications/'

    def get_next_submission(self, problem_location, grader_id):
        response = self.get(self.get_next_submission_url,
                {'location': problem_location, 'grader_id': grader_id})
        return json.dumps(self._render_rubric(response))

    def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores):
        data = {'grader_id' : grader_id,
                'submission_id' : submission_id,
                'score' : score,
                'feedback' : feedback,
                'submission_key': submission_key,
                'location': location,
                'rubric_scores': rubric_scores,
                'rubric_scores_complete': True}
        return self.post(self.save_grade_url, data)

    def is_student_calibrated(self, problem_location, grader_id):
        params = {'problem_id' : problem_location, 'student_id': grader_id}
        return self.get(self.is_student_calibrated_url, params)
    
    def show_calibration_essay(self, problem_location, grader_id):
        params = {'problem_id' : problem_location, 'student_id': grader_id}
        response = self.get(self.show_calibration_essay_url, params)
        return json.dumps(self._render_rubric(response))

    def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, 
            score, feedback, rubric_scores):
        data = {'location': problem_location, 
                'student_id': grader_id, 
                'calibration_essay_id': calibration_essay_id,
                'submission_key': submission_key,
                'score': score,
                'feedback': feedback,
                'rubric_scores[]': rubric_scores,
                'rubric_scores_complete': True}
        return self.post(self.save_calibration_essay_url, data)

    def get_problem_list(self, course_id, grader_id):
        params = {'course_id': course_id, 'student_id': grader_id}
        response = self.get(self.get_problem_list_url, params)
        return response

    def get_notifications(self, course_id, grader_id):
        params = {'course_id': course_id, 'student_id': grader_id}
        response = self.get(self.get_notifications_url, params)
        return response


_service = None
def peer_grading_service():
    """
    Return a peer grading service instance--if settings.MOCK_PEER_GRADING is True,
    returns a mock one, otherwise a real one.

    Caches the result, so changing the setting after the first call to this
    function will have no effect.
    """
    global _service
    if _service is not None:
        return _service

    if settings.MOCK_PEER_GRADING:
        _service = MockPeerGradingService()
    else:
        _service = PeerGradingService(settings.PEER_GRADING_INTERFACE)

    return _service

def _err_response(msg):
    """
    Return a HttpResponse with a json dump with success=False, and the given error message.
    """
    return HttpResponse(json.dumps({'success': False, 'error': msg}),
                        mimetype="application/json")

def _check_required(request, required):
    actual = set(request.POST.keys())
    missing = required - actual
    if len(missing) > 0:
        return False, "Missing required keys: {0}".format(', '.join(missing))
    else:
        return True, ""

def _check_post(request):
    if request.method != 'POST':
        raise Http404


def get_next_submission(request, course_id):
    """
    Makes a call to the grading controller for the next essay that should be graded
    Returns a json dict with the following keys:

    'success': bool

    'submission_id': a unique identifier for the submission, to be passed back
                     with the grade.

    'submission': the submission, rendered as read-only html for grading

    'rubric': the rubric, also rendered as html.

    'submission_key': a key associated with the submission for validation reasons

    'error': if success is False, will have an error message with more info.
    """
    _check_post(request)
    required = set(['location'])
    success, message = _check_required(request, required)
    if not success:
        return _err_response(message)
    grader_id = unique_id_for_user(request.user)
    p = request.POST
    location = p['location']

    try:
        response = peer_grading_service().get_next_submission(location, grader_id)
        return HttpResponse(response,
                        mimetype="application/json")
    except GradingServiceError:
        log.exception("Error getting next submission.  server url: {0}  location: {1}, grader_id: {2}"
                      .format(peer_grading_service().url, location, grader_id))
        return json.dumps({'success': False,
                           'error': 'Could not connect to grading service'})

def save_grade(request, course_id):
    """
    Saves the grade of a given submission.
    Input:
        The request should have the following keys:
        location - problem location
        submission_id - id associated with this submission
        submission_key - submission key given for validation purposes
        score - the grade that was given to the submission
        feedback - the feedback from the student
    Returns
        A json object with the following keys:
        success: bool indicating whether the save was a success
        error: if there was an error in the submission, this is the error message
    """
    _check_post(request)
    required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
    success, message = _check_required(request, required)
    if not success:
        return _err_response(message)
    grader_id = unique_id_for_user(request.user)
    p = request.POST
    location = p['location']
    submission_id = p['submission_id']
    score = p['score']
    feedback = p['feedback']
    submission_key = p['submission_key']
    rubric_scores = p.getlist('rubric_scores[]')
    try:
        response = peer_grading_service().save_grade(location, grader_id, submission_id, 
                score, feedback, submission_key, rubric_scores)
        return HttpResponse(response, mimetype="application/json")
    except GradingServiceError:
        log.exception("""Error saving grade.  server url: {0}, location: {1}, submission_id:{2}, 
                        submission_key: {3}, score: {4}"""
                      .format(peer_grading_service().url,
                          location, submission_id, submission_key, score)
                      )
        return json.dumps({'success': False,
                           'error': 'Could not connect to grading service'})



def is_student_calibrated(request, course_id):
    """
    Calls the grading controller to see if the given student is calibrated
    on the given problem

    Input:
        In the request, we need the following arguments:
        location - problem location

    Returns:
        Json object with the following keys
        success - bool indicating whether or not the call was successful
        calibrated - true if the grader has fully calibrated and can now move on to grading
                   - false if the grader is still working on calibration problems
        total_calibrated_on_so_far - the number of calibration essays for this problem 
            that this grader has graded
    """
    _check_post(request)
    required = set(['location'])
    success, message = _check_required(request, required)
    if not success:
        return _err_response(message)
    grader_id = unique_id_for_user(request.user)
    p = request.POST
    location = p['location']

    try:
        response = peer_grading_service().is_student_calibrated(location, grader_id)
        return HttpResponse(response, mimetype="application/json")
    except GradingServiceError:
        log.exception("Error from grading service.  server url: {0}, grader_id: {0}, location: {1}"
                      .format(peer_grading_service().url, grader_id, location))
        return json.dumps({'success': False,
                           'error': 'Could not connect to grading service'})



def show_calibration_essay(request, course_id):
    """
    Fetch the next calibration essay from the grading controller and return it
    Inputs:
        In the request
        location - problem location

    Returns:
        A json dict with the following keys
        'success': bool

        'submission_id': a unique identifier for the submission, to be passed back
                         with the grade.

        'submission': the submission, rendered as read-only html for grading

        'rubric': the rubric, also rendered as html.

        'submission_key': a key associated with the submission for validation reasons

        'error': if success is False, will have an error message with more info.

    """
    _check_post(request)

    required = set(['location'])
    success, message = _check_required(request, required)
    if not success:
        return _err_response(message)

    grader_id = unique_id_for_user(request.user)
    p = request.POST
    location = p['location']
    try:
        response = peer_grading_service().show_calibration_essay(location, grader_id)
        return HttpResponse(response, mimetype="application/json")
    except GradingServiceError:
        log.exception("Error from grading service.  server url: {0}, location: {0}"
                      .format(peer_grading_service().url, location))
        return json.dumps({'success': False,
                           'error': 'Could not connect to grading service'})
    # if we can't parse the rubric into HTML, 
    except etree.XMLSyntaxError:
        log.exception("Cannot parse rubric string. Raw string: {0}"
                      .format(rubric))
        return json.dumps({'success': False,
                           'error': 'Error displaying submission'})


def save_calibration_essay(request, course_id):
    """
    Saves the grader's grade of a given calibration.
    Input:
        The request should have the following keys:
        location - problem location
        submission_id - id associated with this submission
        submission_key - submission key given for validation purposes
        score - the grade that was given to the submission
        feedback - the feedback from the student
    Returns
        A json object with the following keys:
        success: bool indicating whether the save was a success
        error: if there was an error in the submission, this is the error message
        actual_score: the score that the instructor gave to this calibration essay 

    """
    _check_post(request)

    required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
    success, message = _check_required(request, required)
    if not success:
        return _err_response(message)
    grader_id = unique_id_for_user(request.user)
    p = request.POST
    location = p['location']
    calibration_essay_id = p['submission_id']
    submission_key = p['submission_key']
    score = p['score']
    feedback = p['feedback']
    rubric_scores = p.getlist('rubric_scores[]')

    try:
        response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id, 
                submission_key, score, feedback, rubric_scores)
        return HttpResponse(response, mimetype="application/json")
    except GradingServiceError:
        log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
        return _err_response('Could not connect to grading service')
