
import json
import logging

from django.http import HttpResponse

from celery.states import FAILURE, REVOKED, READY_STATES

from instructor_task.api_helper import (get_status_from_instructor_task,
                                        get_updated_instructor_task)
from instructor_task.models import PROGRESS


log = logging.getLogger(__name__)

# return status for completed tasks and tasks in progress
STATES_WITH_STATUS = [state for state in READY_STATES] + [PROGRESS]


def _get_instructor_task_status(task_id):
    """
    Returns status for a specific task.

    Written as an internal method here (rather than as a helper)
    so that get_task_completion_info() can be called without
    causing a circular dependency (since it's also called directly).
    """
    instructor_task = get_updated_instructor_task(task_id)
    status = get_status_from_instructor_task(instructor_task)
    if instructor_task is not None and instructor_task.task_state in STATES_WITH_STATUS:
        succeeded, message = get_task_completion_info(instructor_task)
        status['message'] = message
        status['succeeded'] = succeeded
    return status


def instructor_task_status(request):
    """
    View method that returns the status of a course-related task or tasks.

    Status is returned as a JSON-serialized dict, wrapped as the content of a HTTPResponse.

    The task_id can be specified to this view in one of three ways:

    * by making a request containing 'task_id' as a parameter with a single value
      Returns a dict containing status information for the specified task_id

    * by making a request containing 'task_ids' as a parameter,
      with a list of task_id values.
      Returns a dict of dicts, with the task_id as key, and the corresponding
      dict containing status information for the specified task_id

      Task_id values that are unrecognized are skipped.

    The dict with status information for a task contains the following keys:
      'message': on complete tasks, status message reporting on final progress,
          or providing exception message if failed.  For tasks in progress,
          indicates the current progress.
      'succeeded': on complete tasks or tasks in progress, boolean value indicates if the
          task outcome was successful:  did it achieve what it set out to do.
          This is in contrast with a successful task_state, which indicates that the
          task merely completed.
      'task_id': id assigned by LMS and used by celery.
      'task_state': state of task as stored in celery's result store.
      'in_progress': boolean indicating if task is still running.
      'task_progress': dict containing progress information.  This includes:
          'attempted': number of attempts made
          'updated': number of attempts that "succeeded"
          'total': number of possible subtasks to attempt
          'action_name': user-visible verb to use in status messages.  Should be past-tense.
          'duration_ms': how long the task has (or had) been running.
          'exception': name of exception class raised in failed tasks.
          'message': returned for failed and revoked tasks.
          'traceback': optional, returned if task failed and produced a traceback.

    """

    output = {}
    if 'task_id' in request.REQUEST:
        task_id = request.REQUEST['task_id']
        output = _get_instructor_task_status(task_id)
    elif 'task_ids[]' in request.REQUEST:
        tasks = request.REQUEST.getlist('task_ids[]')
        for task_id in tasks:
            task_output = _get_instructor_task_status(task_id)
            if task_output is not None:
                output[task_id] = task_output

    return HttpResponse(json.dumps(output, indent=4))


def get_task_completion_info(instructor_task):
    """
    Construct progress message from progress information in InstructorTask entry.

    Returns (boolean, message string) duple, where the boolean indicates
    whether the task completed without incident.  (It is possible for a
    task to attempt many sub-tasks, such as rescoring many students' problem
    responses, and while the task runs to completion, some of the students'
    responses could not be rescored.)

    Used for providing messages to instructor_task_status(), as well as
    external calls for providing course task submission history information.
    """
    succeeded = False

    if instructor_task.task_state not in STATES_WITH_STATUS:
        return (succeeded, "No status information available")

    # we're more surprised if there is no output for a completed task, but just warn:
    if instructor_task.task_output is None:
        log.warning("No task_output information found for instructor_task {0}".format(instructor_task.task_id))
        return (succeeded, "No status information available")

    try:
        task_output = json.loads(instructor_task.task_output)
    except ValueError:
        fmt = "No parsable task_output information found for instructor_task {0}: {1}"
        log.warning(fmt.format(instructor_task.task_id, instructor_task.task_output))
        return (succeeded, "No parsable status information available")

    if instructor_task.task_state in [FAILURE, REVOKED]:
        return (succeeded, task_output.get('message', 'No message provided'))

    if any([key not in task_output for key in ['action_name', 'attempted', 'updated', 'total']]):
        fmt = "Invalid task_output information found for instructor_task {0}: {1}"
        log.warning(fmt.format(instructor_task.task_id, instructor_task.task_output))
        return (succeeded, "No progress status information available")

    action_name = task_output['action_name']
    num_attempted = task_output['attempted']
    num_updated = task_output['updated']
    num_total = task_output['total']

    student = None
    try:
        task_input = json.loads(instructor_task.task_input)
    except ValueError:
        fmt = "No parsable task_input information found for instructor_task {0}: {1}"
        log.warning(fmt.format(instructor_task.task_id, instructor_task.task_input))
    else:
        student = task_input.get('student')

    if instructor_task.task_state == PROGRESS:
        # special message for providing progress updates:
        msg_format = "Progress: {action} {updated} of {attempted} so far"
    elif student is not None:
        if num_attempted == 0:
            msg_format = "Unable to find submission to be {action} for student '{student}'"
        elif num_updated == 0:
            msg_format = "Problem failed to be {action} for student '{student}'"
        else:
            succeeded = True
            msg_format = "Problem successfully {action} for student '{student}'"
    elif num_attempted == 0:
        msg_format = "Unable to find any students with submissions to be {action}"
    elif num_updated == 0:
        msg_format = "Problem failed to be {action} for any of {attempted} students"
    elif num_updated == num_attempted:
        succeeded = True
        msg_format = "Problem successfully {action} for {attempted} students"
    else:  # num_updated < num_attempted
        msg_format = "Problem {action} for {updated} of {attempted} students"

    if student is None and num_attempted != num_total:
        msg_format += " (out of {total})"

    # Update status in task result object itself:
    message = msg_format.format(action=action_name, updated=num_updated,
                                attempted=num_attempted, total=num_total,
                                student=student)
    return (succeeded, message)
