import copy
from fs.errors import ResourceNotFoundError
import itertools
import json
import logging
from lxml import etree
from lxml.html import rewrite_links
from lxml.html.clean import Cleaner, autolink_html
from path import path
import os
import sys
import hashlib
import capa.xqueue_interface as xqueue_interface
import re

from pkg_resources import resource_string

from .capa_module import only_one, ComplexEncoder
from .editing_module import EditingDescriptor
from .html_checker import check_html
from progress import Progress
from .stringify import stringify_children
from .xml_module import XmlDescriptor
from xmodule.modulestore import Location
from capa.util import *
import open_ended_image_submission

from datetime import datetime

log = logging.getLogger("mitx.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


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': 'Started',
        'assessing': 'Being scored',
        'post_assessment': 'Scoring finished',
        'done': 'Problem complete',
    }

    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:
            instance_state = json.loads(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.history = instance_state.get('history', [])

        self.state = instance_state.get('state', self.INITIAL)

        self.created = instance_state.get('created', False)

        self.attempts = instance_state.get('attempts', 0)
        self.max_attempts = static_data['max_attempts']

        self.prompt = static_data['prompt']
        self.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']

        # Used for progress / grading.  Currently get credit just for
        # completion (doesn't matter if you self-assessed correct/incorrect).
        self._max_score = static_data['max_score']

        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.utcnow() > self.close_date:
            return True
        return False

    def check_if_closed(self):
        if self.closed():
            return True, {
                'success': False,
                'error': 'This problem is now closed.'
            }
        elif self.attempts > self.max_attempts:
            return True, {
                'success': False,
                'error': 'Too many attempts.'
            }
        else:
            return False, {}



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

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

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

    @staticmethod
    def sanitize_html(answer):
        try:
            answer = autolink_html(answer)
            cleaner = Cleaner(style=True, links=True, add_nofollow=False, page_structure=True, safe_attrs_only=True,
                host_whitelist=open_ended_image_submission.TRUSTED_IMAGE_DOMAINS,
                whitelist_tags=set(['embed', 'iframe', 'a', 'img']))
            clean_html = cleaner.clean_html(answer)
            clean_html = re.sub(r'</p>$', '', re.sub(r'^<p>', '', clean_html))
        except:
            clean_html = answer
        return clean_html

    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.history.append({'answer': answer})

    def record_latest_score(self, score):
        """Assumes that state is right, so we're adding a score to the latest
        history element"""
        self.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.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.state == new_state:
            return

        self.state = new_state

        if self.state == self.DONE:
            self.attempts += 1

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

        state = {
            'version': self.STATE_VERSION,
            'history': self.history,
            'state': self.state,
            'max_score': self._max_score,
            'attempts': self.attempts,
            'created': False,
        }
        return json.dumps(state)

    def _allow_reset(self):
        """Can the module be reset?"""
        return (self.state == self.DONE and self.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_progress(self):
        '''
        For now, just return last score / max_score
        '''
        if self._max_score > 0:
            try:
                return Progress(self.get_score()['score'], self._max_score)
            except Exception as err:
                log.exception("Got bad progress")
                return None
        return None

    def out_of_sync_error(self, get, msg=''):
        """
        return dict out-of-sync error message, and also log.
        """
        log.warning("Assessment module state out sync. state: %r, get: %r. %s",
            self.state, get, msg)
        return {'success': False,
                'error': 'The problem state got out-of-sync'}

    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_image_to_s3(self, image_data):
        """
        Uploads an image to S3
        Image_data: InMemoryUploadedFileObject that responds to read() and seek()
        @return:Success and a URL corresponding to the uploaded object
        """
        success = False
        s3_public_url = ""
        image_ok = False
        try:
            image_data.seek(0)
            image_ok = open_ended_image_submission.run_image_tests(image_data)
        except:
            log.exception("Could not create image and check it.")

        if image_ok:
            image_key = image_data.name + datetime.now().strftime("%Y%m%d%H%M%S")

            try:
                image_data.seek(0)
                success, s3_public_url = open_ended_image_submission.upload_to_s3(image_data, image_key)
            except:
                log.exception("Could not upload image to S3.")

        return success, image_ok, s3_public_url

    def check_for_image_and_upload(self, get_data):
        """
        Checks to see if an image was passed back in the AJAX query.  If so, it will upload it to S3
        @param get_data: AJAX get data
        @return: Success, whether or not a file was in the get dictionary,
        and the html corresponding to the uploaded image
        """
        has_file_to_upload = False
        uploaded_to_s3 = False
        image_tag = ""
        image_ok = False
        if 'can_upload_files' in get_data:
            if get_data['can_upload_files'] in ['true', '1']:
                has_file_to_upload = True
                file = get_data['student_file'][0]
                uploaded_to_s3, image_ok, s3_public_url = self.upload_image_to_s3(file)
                if uploaded_to_s3:
                    image_tag = self.generate_image_tag_from_url(s3_public_url, file.name)

        return has_file_to_upload, uploaded_to_s3, image_ok, image_tag

    def generate_image_tag_from_url(self, s3_public_url, image_name):
        """
        Makes an image tag from a given URL
        @param s3_public_url: URL of the image
        @param image_name: Name of the image
        @return: Boolean success, updated AJAX get data
        """
        image_template = """
                        <a href="{0}" target="_blank">{1}</a>
                         """.format(s3_public_url, image_name)
        return image_template

    def append_image_to_student_answer(self, get_data):
        """
        Adds an image to a student answer after uploading it to S3
        @param get_data: AJAx get data
        @return: Boolean success, updated AJAX get data
        """
        overall_success = False
        if not self.accept_file_upload:
            #If the question does not accept file uploads, do not do anything
            return True, get_data

        has_file_to_upload, uploaded_to_s3, image_ok, image_tag = self.check_for_image_and_upload(get_data)
        if uploaded_to_s3 and has_file_to_upload and image_ok:
            get_data['student_answer'] += image_tag
            overall_success = True
        elif has_file_to_upload and not uploaded_to_s3 and image_ok:
            #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).  For now, just treat this as a "success"
            log.exception("Student AJAX post to combined open ended xmodule indicated that it contained an image, "
                        "but the image was not able to be uploaded to S3.  This could indicate a config"
                        "issue with this deployment, but it could also indicate a problem with S3 or with the"
                        "student image itself.")
            overall_success = True
        elif not has_file_to_upload:
            #If there is no file to upload, probably the student has embedded the link in the answer text
            success, get_data['student_answer'] = self.check_for_url_in_text(get_data['student_answer'])
            overall_success = success

        #log.debug("Has file: {0} Uploaded: {1} Image Ok: {2}".format(has_file_to_upload, uploaded_to_s3, image_ok))

        return overall_success, get_data

    def check_for_url_in_text(self, string):
        """
        Checks for urls in a string
        @param string: Arbitrary string
        @return: Boolean success, the edited string
        """
        success = False
        links = re.findall(r'(https?://\S+)', string)
        if len(links) > 0:
            for link in links:
                success = open_ended_image_submission.run_url_tests(link)
                if not success:
                    string = re.sub(link, '', string)
                else:
                    string = re.sub(link, self.generate_image_tag_from_url(link, link), string)
                    success = True

        return success, string
