import json
import logging
import re
import bleach
from html5lib.tokenizer import HTMLTokenizer
from xmodule.progress import Progress
import capa.xqueue_interface as xqueue_interface
from capa.util import *
from .peer_grading_service import PeerGradingService, MockPeerGradingService
import controller_query_service

from datetime import datetime
from pytz import UTC
from boto.s3.connection import S3Connection
from boto.s3.key import Key

log = logging.getLogger("edx.courseware")

# Set the default number of max attempts.  Should be 1 for production
# Set higher for debugging/testing
# attempts specified in xml definition overrides this.
MAX_ATTEMPTS = 1

# Set maximum available number of points.
# Overriden by max_score specified in xml.
MAX_SCORE = 1

FILE_NOT_FOUND_IN_RESPONSE_MESSAGE = "We could not find a file in your submission.  Please try choosing a file or pasting a link to your file into the answer box."
ERROR_SAVING_FILE_MESSAGE = "We are having trouble saving your file.  Please try another file or paste a link to your file into the answer box."


def upload_to_s3(file_to_upload, keyname, s3_interface):
    '''
    Upload file to S3 using provided keyname.

    Returns:
        public_url: URL to access uploaded file
    '''

    conn = S3Connection(s3_interface['access_key'], s3_interface['secret_access_key'])
    bucketname = str(s3_interface['storage_bucket_name'])
    bucket = conn.lookup(bucketname.lower())
    if not bucket:
        bucket = conn.create_bucket(bucketname.lower())

    k = Key(bucket)
    k.key = keyname
    k.set_metadata('filename', file_to_upload.name)
    k.set_contents_from_file(file_to_upload)

    k.set_acl("public-read")
    public_url = k.generate_url(60 * 60 * 24 * 365)   # URL timeout in seconds.

    return public_url

# Used by sanitize_html
ALLOWED_HTML_ATTRS = {
    '*': ['id', 'class', 'height', 'width', 'alt'],
    'a': ['href', 'title', 'rel', 'target'],
    'embed': ['src'],
    'iframe': ['src'],
    'img': ['src'],
}


class OpenEndedChild(object):
    """
    States:

    initial (prompt, textbox shown)
         |
    assessing (read-only textbox, rubric + assessment input shown for self assessment, response queued for open ended)
         |
    post_assessment (read-only textbox, read-only rubric and assessment, hint input box shown)
         |
    done (submitted msg, green checkmark, everything else read-only.  If attempts < max, shows
         a reset button that goes back to initial state.  Saves previous
         submissions too.)
    """

    DEFAULT_QUEUE = 'open-ended'
    DEFAULT_MESSAGE_QUEUE = 'open-ended-message'
    max_inputfields = 1

    STATE_VERSION = 1

    # states
    INITIAL = 'initial'
    ASSESSING = 'assessing'
    POST_ASSESSMENT = 'post_assessment'
    DONE = 'done'

    # This is used to tell students where they are at in the module
    HUMAN_NAMES = {
        'initial': 'Not started',
        'assessing': 'In progress',
        'post_assessment': 'Done',
        'done': 'Done',
        }

    def __init__(self, system, location, definition, descriptor, static_data,
                 instance_state=None, shared_state=None, **kwargs):
        # Load instance state

        if instance_state is not None:
            try:
                instance_state = json.loads(instance_state)
            except:
                log.error(
                    "Could not load instance state for open ended.  Setting it to nothing.: {0}".format(instance_state))
                instance_state = {}
        else:
            instance_state = {}

        # History is a list of tuples of (answer, score, hint), where hint may be
        # None for any element, and score and hint can be None for the last (current)
        # element.
        # Scores are on scale from 0 to max_score

        self.child_history = instance_state.get('child_history', [])
        self.child_state = instance_state.get('child_state', self.INITIAL)
        self.child_created = instance_state.get('child_created', False)
        self.child_attempts = instance_state.get('child_attempts', 0)
        self.stored_answer = instance_state.get('stored_answer', None)

        self.max_attempts = static_data['max_attempts']
        self.child_prompt = static_data['prompt']
        self.child_rubric = static_data['rubric']
        self.display_name = static_data['display_name']
        self.accept_file_upload = static_data['accept_file_upload']
        self.close_date = static_data['close_date']
        self.s3_interface = static_data['s3_interface']
        self.skip_basic_checks = static_data['skip_basic_checks']
        self._max_score = static_data['max_score']
        self.control = static_data['control']

        # Used for progress / grading.  Currently get credit just for
        # completion (doesn't matter if you self-assessed correct/incorrect).
        if system.open_ended_grading_interface:
            self.peer_gs = PeerGradingService(system.open_ended_grading_interface, system)
            self.controller_qs = controller_query_service.ControllerQueryService(
                system.open_ended_grading_interface, system
            )
        else:
            self.peer_gs = MockPeerGradingService()
            self.controller_qs = None

        self.system = system

        self.location_string = location
        try:
            self.location_string = self.location_string.url()
        except:
            pass

        self.setup_response(system, location, definition, descriptor)

    def setup_response(self, system, location, definition, descriptor):
        """
        Needs to be implemented by the inheritors of this module.  Sets up additional fields used by the child modules.
        @param system: Modulesystem
        @param location: Module location
        @param definition: XML definition
        @param descriptor: Descriptor of the module
        @return: None
        """
        pass

    def closed(self):
        if self.close_date is not None and datetime.now(UTC) > self.close_date:
            return True
        return False

    def check_if_closed(self):
        if self.closed():
            return True, {
                'success': False,
                # This is a student_facing_error
                'error': 'The problem close date has passed, and this problem is now closed.'
            }
        elif self.child_attempts > self.max_attempts:
            return True, {
                'success': False,
                # This is a student_facing_error
                'error': 'You have attempted this problem {0} times.  You are allowed {1} attempts.'.format(
                    self.child_attempts, self.max_attempts
                )
            }
        else:
            return False, {}

    def latest_answer(self):
        """Empty string if not available"""
        if not self.child_history:
            return ""
        return self.child_history[-1].get('answer', "")

    def latest_score(self):
        """None if not available"""
        if not self.child_history:
            return None
        return self.child_history[-1].get('score')

    def all_scores(self):
        """None if not available"""
        if not self.child_history:
            return None
        return [self.child_history[i].get('score') for i in xrange(0, len(self.child_history))]

    def latest_post_assessment(self, system):
        """Empty string if not available"""
        if not self.child_history:
            return ""
        return self.child_history[-1].get('post_assessment', "")

    @staticmethod
    def sanitize_html(answer):
        """
        Take a student response and sanitize the HTML to prevent malicious script injection
        or other unwanted content.
        answer - any string
        return - a cleaned version of the string
        """
        clean_html = bleach.clean(answer,
                                  tags=['embed', 'iframe', 'a', 'img', 'br'],
                                  attributes=ALLOWED_HTML_ATTRS,
                                  strip=True)
        autolinked = bleach.linkify(clean_html,
                                    callbacks=[bleach.callbacks.target_blank],
                                    skip_pre=True,
                                    tokenizer=HTMLTokenizer)
        return OpenEndedChild.replace_newlines(autolinked)

    @staticmethod
    def replace_newlines(html):
        """
        Replaces "\n" newlines with <br/>
        """
        retv = re.sub(r'</p>$', '', re.sub(r'^<p>', '', html))
        return re.sub("\n","<br/>", retv)

    def new_history_entry(self, answer):
        """
        Adds a new entry to the history dictionary
        @param answer: The student supplied answer
        @return: None
        """
        answer = OpenEndedChild.sanitize_html(answer)
        self.child_history.append({'answer': answer})
        self.stored_answer = None

    def record_latest_score(self, score):
        """Assumes that state is right, so we're adding a score to the latest
        history element"""
        self.child_history[-1]['score'] = score

    def record_latest_post_assessment(self, post_assessment):
        """Assumes that state is right, so we're adding a score to the latest
        history element"""
        self.child_history[-1]['post_assessment'] = post_assessment

    def change_state(self, new_state):
        """
        A centralized place for state changes--allows for hooks.  If the
        current state matches the old state, don't run any hooks.
        """
        if self.child_state == new_state:
            return

        self.child_state = new_state

        if self.child_state == self.DONE:
            self.child_attempts += 1

    def get_instance_state(self):
        """
        Get the current score and state
        """

        state = {
            'version': self.STATE_VERSION,
            'child_history': self.child_history,
            'child_state': self.child_state,
            'max_score': self._max_score,
            'child_attempts': self.child_attempts,
            'child_created': self.child_created,
            'stored_answer': self.stored_answer,
            }
        return json.dumps(state)

    def _allow_reset(self):
        """Can the module be reset?"""
        return (self.child_state == self.DONE and self.child_attempts < self.max_attempts)

    def max_score(self):
        """
        Return max_score
        """
        return self._max_score

    def get_score(self):
        """
        Returns the last score in the list
        """
        score = self.latest_score()
        return {'score': score if score is not None else 0,
                'total': self._max_score}

    def reset(self, system):
        """
        If resetting is allowed, reset the state.

        Returns {'success': bool, 'error': msg}
        (error only present if not success)
        """
        self.change_state(self.INITIAL)
        return {'success': True}

    def get_display_answer(self):
        latest = self.latest_answer()
        if self.child_state == self.INITIAL:
            if self.stored_answer is not None:
                previous_answer = self.stored_answer
            elif latest is not None and len(latest) > 0:
                previous_answer = latest
            else:
                previous_answer = ""
            previous_answer = previous_answer.replace("<br/>","\n").replace("<br>", "\n")
        else:
            if latest is not None and len(latest) > 0:
                previous_answer = latest
            else:
                previous_answer = ""
            previous_answer = previous_answer.replace("\n","<br/>")

        return previous_answer

    def store_answer(self, data, system):
        if self.child_state != self.INITIAL:
            # We can only store an answer if the problem has not moved into the assessment phase.
            return self.out_of_sync_error(data)

        self.stored_answer = data['student_answer']
        return {'success': True}

    def get_progress(self):
        '''
        For now, just return last score / max_score
        '''
        if self._max_score > 0:
            try:
                return Progress(int(self.get_score()['score']), int(self._max_score))
            except Exception as err:
                # This is a dev_facing_error
                log.exception("Got bad progress from open ended child module. Max Score: {0}".format(self._max_score))
                return None
        return None

    def out_of_sync_error(self, data, msg=''):
        """
        return dict out-of-sync error message, and also log.
        """
        # This is a dev_facing_error
        log.warning("Open ended child state out sync. state: %r, data: %r. %s",
                    self.child_state, data, msg)
        # This is a student_facing_error
        return {'success': False,
                'error': 'The problem state got out-of-sync.  Please try reloading the page.'}

    def get_html(self):
        """
         Needs to be implemented by inheritors.  Renders the HTML that students see.
        @return:
        """
        pass

    def handle_ajax(self):
        """
        Needs to be implemented by child modules.  Handles AJAX events.
        @return:
        """
        pass

    def is_submission_correct(self, score):
        """
        Checks to see if a given score makes the answer correct.  Very naive right now (>66% is correct)
        @param score: Numeric score.
        @return: Boolean correct.
        """
        correct = False
        if (isinstance(score, (int, long, float, complex))):
            score_ratio = int(score) / float(self.max_score())
            correct = (score_ratio >= 0.66)
        return correct

    def is_last_response_correct(self):
        """
        Checks to see if the last response in the module is correct.
        @return: 'correct' if correct, otherwise 'incorrect'
        """
        score = self.get_score()['score']
        correctness = 'correct' if self.is_submission_correct(score) else 'incorrect'
        return correctness

    def upload_file_to_s3(self, file_data):
        """
        Uploads a file to S3.
        file_data: InMemoryUploadedFileObject that responds to read() and seek().
        @return: A URL corresponding to the uploaded object.
        """

        file_key = file_data.name + datetime.now(UTC).strftime(
            xqueue_interface.dateformat
        )

        file_data.seek(0)
        s3_public_url = upload_to_s3(
            file_data, file_key, self.s3_interface
        )

        return s3_public_url

    def check_for_file_and_upload(self, data):
        """
        Checks to see if a file was passed back by the student.  If so, it will be uploaded to S3.
        @param data: AJAX post dictionary containing keys student_file and valid_files_attached.
        @return: has_file_to_upload, whether or not a file was in the data dictionary,
        and image_tag, the html needed to create a link to the uploaded file.
        """
        has_file_to_upload = False
        image_tag = ""

        # Ensure that a valid file was uploaded.
        if ('valid_files_attached' in data
            and data['valid_files_attached'] in ['true', '1', True]
            and data['student_file'] is not None
            and len(data['student_file']) > 0):
                has_file_to_upload = True
                student_file = data['student_file'][0]

                # Upload the file to S3 and generate html to embed a link.
                s3_public_url = self.upload_file_to_s3(student_file)
                image_tag = self.generate_file_link_html_from_url(s3_public_url, student_file.name)

        return has_file_to_upload, image_tag

    def generate_file_link_html_from_url(self, s3_public_url, file_name):
        """
        Create an html link to a given URL.
        @param s3_public_url: URL of the file.
        @param file_name: Name of the file.
        @return: Boolean success, updated AJAX data.
        """
        image_link = """
                        <a href="{0}" target="_blank">{1}</a>
                         """.format(s3_public_url, file_name)
        return image_link

    def append_file_link_to_student_answer(self, data):
        """
        Adds a file to a student answer after uploading it to S3.
        @param data: AJAX data containing keys student_answer, valid_files_attached, and student_file.
        @return: Boolean success, and updated AJAX data dictionary.
        """

        error_message = ""

        if not self.accept_file_upload:
            # If the question does not accept file uploads, do not do anything
            return True, error_message, data

        try:
            # Try to upload the file to S3.
            has_file_to_upload, image_tag = self.check_for_file_and_upload(data)
            data['student_answer'] += image_tag
            success = True
            if not has_file_to_upload:
                # If there is no file to upload, probably the student has embedded the link in the answer text
                success, data['student_answer'] = self.check_for_url_in_text(data['student_answer'])

                # If success is False, we have not found a link, and no file was attached.
                # Show error to student.
                if success is False:
                    error_message = FILE_NOT_FOUND_IN_RESPONSE_MESSAGE

        except Exception:
            # In this case, an image was submitted by the student, but the image could not be uploaded to S3.  Likely
            # a config issue (development vs deployment).
            log.exception("Student AJAX post to combined open ended xmodule indicated that it contained a file, "
                          "but the image was not able to be uploaded to S3.  This could indicate a configuration "
                          "issue with this deployment and the S3_INTERFACE setting.")
            success = False
            error_message = ERROR_SAVING_FILE_MESSAGE

        return success, error_message, data

    def check_for_url_in_text(self, string):
        """
        Checks for urls in a string.
        @param string: Arbitrary string.
        @return: Boolean success, and the edited string.
        """
        has_link = False

        # Find all links in the string.
        links = re.findall(r'(https?://\S+)', string)
        if len(links)>0:
            has_link = True

        # Autolink by wrapping links in anchor tags.
        for link in links:
            string = re.sub(link, self.generate_file_link_html_from_url(link, link), string)

        return has_link, string

    def get_eta(self):
        if self.controller_qs:
            response = self.controller_qs.check_for_eta(self.location_string)
            try:
                response = json.loads(response)
            except:
                pass
        else:
            return ""

        success = response['success']
        if isinstance(success, basestring):
            success = (success.lower() == "true")

        if success:
            eta = controller_query_service.convert_seconds_to_human_readable(response['eta'])
            eta_string = "Please check back for your response in at most {0}.".format(eta)
        else:
            eta_string = ""

        return eta_string
