import csv
import datetime
from collections import defaultdict

from django.contrib import messages
from django.http.response import FileResponse
from django.shortcuts import redirect, render
from django.utils import timezone
from django.views.decorators.http import require_GET, require_http_methods, require_POST

from authentication.admin_authentication import (authenticate_admin,
    authenticate_researcher_study_access, forest_enabled)
from constants.data_access_api_constants import CHUNK_FIELDS
from constants.forest_constants import ForestTaskStatus, ForestTree
from database.data_access_models import ChunkRegistry
from database.study_models import Study
from database.tableau_api_models import ForestTask
from database.user_models import Participant
from forms.django_forms import CreateTasksForm
from libs.http_utils import easy_url
from libs.internal_types import ParticipantQuerySet, ResearcherRequest
from libs.streaming_zip import zip_generator
from libs.utils.date_utils import daterange
from middleware.abort_middleware import abort
from serializers.forest_serializers import ForestTaskCsvSerializer, ForestTaskSerializer


@require_GET
@authenticate_researcher_study_access
@forest_enabled
def analysis_progress(request: ResearcherRequest, study_id=None):
    study: Study = Study.objects.get(pk=study_id)
    participants: ParticipantQuerySet = Participant.objects.filter(study=study_id)
    
    # generate chart of study analysis progress logs
    trackers = ForestTask.objects.filter(participant__in=participants).order_by("created_on")
    
    start_date = (study.get_earliest_data_time_bin() or study.created_on).date()
    end_date = (study.get_latest_data_time_bin() or timezone.now()).date()
    
    # this code simultaneously builds up the chart of most recent forest results for date ranges
    # by participant and tree, and tracks the metadata
    params = dict()
    results = defaultdict(lambda: "--")
    tracker: ForestTask
    for tracker in trackers:
        for date in daterange(tracker.data_date_start, tracker.data_date_end, inclusive=True):
            results[(tracker.participant_id, tracker.forest_tree, date)] = tracker.status
            if tracker.status == tracker.status.success:
                params[(tracker.participant_id, tracker.forest_tree, date)] = tracker.forest_param_id
            else:
                params[(tracker.participant_id, tracker.forest_tree, date)] = None
    
    # generate the date range for charting
    dates = list(daterange(start_date, end_date, inclusive=True))
    
    chart = []
    for participant in participants:
        for tree in ForestTree.values():
            row = [participant.patient_id, tree] + \
                [results[(participant.id, tree, date)] for date in dates]
            chart.append(row)
    
    # ensure that within each tree, only a single set of param values are used (only the most recent runs
    # are considered, and unsuccessful runs are assumed to invalidate old runs, clearing params)
    params_conflict = False
    for tree in set([k[1] for k in params.keys()]):
        if len(set([m for k, m in params.items() if m is not None and k[1] == tree])) > 1:
            params_conflict = True
            break
    
    return render(
        request,
        'forest/analysis_progress.html',
        context=dict(
            study=study,
            chart_columns=["participant", "tree"] + dates,
            status_choices=ForestTaskStatus,
            params_conflict=params_conflict,
            start_date=start_date,
            end_date=end_date,
            chart=chart  # this uses the jinja safe filter and should never involve user input
        )
    )


@require_http_methods(['GET', 'POST'])
@authenticate_admin
@forest_enabled
def create_tasks(request: ResearcherRequest, study_id=None):
    # Only a SITE admin can queue forest tasks
    if not request.session_researcher.site_admin:
        return abort(403)
    try:
        study = Study.objects.get(pk=study_id)
    except Study.DoesNotExist:
        return abort(404)
    
    # FIXME: remove this double endpoint pattern, it is bad.
    if request.method == "GET":
        return render_create_tasks(request, study)
    form = CreateTasksForm(data=request.POST, study=study)
    
    if not form.is_valid():
        error_messages = [
            f'"{field}": {message}'
            for field, messages in form.errors.items()
            for message in messages
        ]
        error_messages_string = "\n".join(error_messages)
        messages.warning(request, f"Errors:\n\n{error_messages_string}")
        return render_create_tasks(request, study)
    
    form.save()
    messages.success(request, "Forest tasks successfully queued!")
    return redirect(easy_url("forest_pages.task_log", study_id=study_id))


@require_GET
@authenticate_researcher_study_access
@forest_enabled
def task_log(request: ResearcherRequest, study_id=None):
    study = Study.objects.get(pk=study_id)
    forest_tasks = ForestTask.objects.filter(participant__study_id=study_id).order_by("-created_on")
    return render(
        request,
        "forest/task_log.html",
        context=dict(
            study=study,
            is_site_admin=request.session_researcher.site_admin,
            status_choices=ForestTaskStatus,
            forest_log=ForestTaskSerializer(forest_tasks, many=True).data,
        )
    )


@require_GET
@authenticate_admin
def download_task_log(request: ResearcherRequest):
    forest_tasks = ForestTask.objects.order_by("created_on")
    return FileResponse(
        stream_forest_task_log_csv(forest_tasks),
        content_type="text/csv",
        filename=f"forest_task_log_{timezone.now().isoformat()}.csv",
        as_attachment=True,
    )


@require_POST
@authenticate_admin
@forest_enabled
def cancel_task(request: ResearcherRequest, study_id, forest_task_external_id):
    if not request.session_researcher.site_admin:
        return abort(403)
        
    number_updated = \
        ForestTask.objects.filter(
            external_id=forest_task_external_id, status=ForestTaskStatus.queued
        ).update(
            status=ForestTaskStatus.cancelled,
            stacktrace=f"Canceled by {request.session_researcher.username} on {datetime.date.today()}",
        )
    
    if number_updated > 0:
        messages.success(request, "Forest task successfully cancelled.")
    else:
        messages.warning(request, "Sorry, we were unable to find or cancel this Forest task.")
    
    return redirect(easy_url("forest_pages.task_log", study_id=study_id))


@require_GET
@authenticate_admin
@forest_enabled
def download_task_data(request: ResearcherRequest, study_id, forest_task_external_id):
    try:
        tracker: ForestTask = ForestTask.objects.get(
            external_id=forest_task_external_id, participant__study_id=study_id
        )
    except ForestTask.DoesNotExist:
        return abort(404)
    
    chunks = ChunkRegistry.objects.filter(participant=tracker.participant).values(*CHUNK_FIELDS)
    f = FileResponse(
        zip_generator(chunks),
        content_type="zip",
        as_attachment=True,
        filename=f"{tracker.get_slug()}.zip",
    )
    f.set_headers(None)
    return f


def stream_forest_task_log_csv(forest_tasks):
    buffer = CSVBuffer()
    writer = csv.DictWriter(buffer, fieldnames=ForestTaskCsvSerializer.Meta.fields)
    writer.writeheader()
    yield buffer.read()
    
    for forest_task in forest_tasks:
        writer.writerow(ForestTaskCsvSerializer(forest_task).data)
        yield buffer.read()


def render_create_tasks(request: ResearcherRequest, study: Study):
    participants = Participant.objects.filter(study=study)
    try:
        start_date = ChunkRegistry.objects.filter(participant__in=participants).earliest("time_bin")
        end_date = ChunkRegistry.objects.filter(participant__in=participants).latest("time_bin")
        start_date = start_date.time_bin.date()
        end_date = end_date.time_bin.date()
    except ChunkRegistry.DoesNotExist:
        start_date = study.created_on.date()
        end_date = timezone.now().date()
    return render(
        request,
        "forest/create_tasks.html",
        context=dict(
            study=study,
            participants=list(
                study.participants.order_by("patient_id").values_list("patient_id", flat=True)
            ),
            trees=ForestTree.choices(),
            start_date=start_date.strftime('%Y-%m-%d'),
            end_date=end_date.strftime('%Y-%m-%d')
        )
    )


class CSVBuffer:
    line = ""
    
    def read(self):
        return self.line
    
    def write(self, line):
        self.line = line

[((3696, 3733), 'django.views.decorators.http.require_http_methods', 'require_http_methods', (["['GET', 'POST']"], {}), "(['GET', 'POST'])\n", (3716, 3733), False, 'from django.views.decorators.http import require_GET, require_http_methods, require_POST\n'), ((1295, 1325), 'database.study_models.Study.objects.get', 'Study.objects.get', ([], {'pk': 'study_id'}), '(pk=study_id)\n', (1312, 1325), False, 'from database.study_models import Study\n'), ((1366, 1408), 'database.user_models.Participant.objects.filter', 'Participant.objects.filter', ([], {'study': 'study_id'}), '(study=study_id)\n', (1392, 1408), False, 'from database.user_models import Participant\n'), ((1913, 1939), 'collections.defaultdict', 'defaultdict', (["(lambda : '--')"], {}), "(lambda : '--')\n", (1924, 1939), False, 'from collections import defaultdict\n'), ((4227, 4274), 'forms.django_forms.CreateTasksForm', 'CreateTasksForm', ([], {'data': 'request.POST', 'study': 'study'}), '(data=request.POST, study=study)\n', (4242, 4274), False, 'from forms.django_forms import CreateTasksForm\n'), ((4679, 4741), 'django.contrib.messages.success', 'messages.success', (['request', '"""Forest tasks successfully queued!"""'], {}), "(request, 'Forest tasks successfully queued!')\n", (4695, 4741), False, 'from django.contrib import messages\n'), ((4954, 4984), 'database.study_models.Study.objects.get', 'Study.objects.get', ([], {'pk': 'study_id'}), '(pk=study_id)\n', (4971, 4984), False, 'from database.study_models import Study\n'), ((5507, 5548), 'database.tableau_api_models.ForestTask.objects.order_by', 'ForestTask.objects.order_by', (['"""created_on"""'], {}), "('created_on')\n", (5534, 5548), False, 'from database.tableau_api_models import ForestTask\n'), ((7323, 7393), 'csv.DictWriter', 'csv.DictWriter', (['buffer'], {'fieldnames': 'ForestTaskCsvSerializer.Meta.fields'}), '(buffer, fieldnames=ForestTaskCsvSerializer.Meta.fields)\n', (7337, 7393), False, 'import csv\n'), ((7668, 7707), 'database.user_models.Participant.objects.filter', 'Participant.objects.filter', ([], {'study': 'study'}), '(study=study)\n', (7694, 7707), False, 'from database.user_models import Participant\n'), ((2012, 2085), 'libs.utils.date_utils.daterange', 'daterange', (['tracker.data_date_start', 'tracker.data_date_end'], {'inclusive': '(True)'}), '(tracker.data_date_start, tracker.data_date_end, inclusive=True)\n', (2021, 2085), False, 'from libs.utils.date_utils import daterange\n'), ((2502, 2549), 'libs.utils.date_utils.daterange', 'daterange', (['start_date', 'end_date'], {'inclusive': '(True)'}), '(start_date, end_date, inclusive=True)\n', (2511, 2549), False, 'from libs.utils.date_utils import daterange\n'), ((2628, 2647), 'constants.forest_constants.ForestTree.values', 'ForestTree.values', ([], {}), '()\n', (2645, 2647), False, 'from constants.forest_constants import ForestTaskStatus, ForestTree\n'), ((3943, 3953), 'middleware.abort_middleware.abort', 'abort', (['(403)'], {}), '(403)\n', (3948, 3953), False, 'from middleware.abort_middleware import abort\n'), ((3979, 4009), 'database.study_models.Study.objects.get', 'Study.objects.get', ([], {'pk': 'study_id'}), '(pk=study_id)\n', (3996, 4009), False, 'from database.study_models import Study\n'), ((4538, 4604), 'django.contrib.messages.warning', 'messages.warning', (['request', 'f"""Errors:\n\n{error_messages_string}"""'], {}), '(request, f"""Errors:\n\n{error_messages_string}""")\n', (4554, 4604), False, 'from django.contrib import messages\n'), ((4762, 4814), 'libs.http_utils.easy_url', 'easy_url', (['"""forest_pages.task_log"""'], {'study_id': 'study_id'}), "('forest_pages.task_log', study_id=study_id)\n", (4770, 4814), False, 'from libs.http_utils import easy_url\n'), ((5958, 5968), 'middleware.abort_middleware.abort', 'abort', (['(403)'], {}), '(403)\n', (5963, 5968), False, 'from middleware.abort_middleware import abort\n'), ((6335, 6399), 'django.contrib.messages.success', 'messages.success', (['request', '"""Forest task successfully cancelled."""'], {}), "(request, 'Forest task successfully cancelled.')\n", (6351, 6399), False, 'from django.contrib import messages\n'), ((6418, 6508), 'django.contrib.messages.warning', 'messages.warning', (['request', '"""Sorry, we were unable to find or cancel this Forest task."""'], {}), "(request,\n    'Sorry, we were unable to find or cancel this Forest task.')\n", (6434, 6508), False, 'from django.contrib import messages\n'), ((6530, 6582), 'libs.http_utils.easy_url', 'easy_url', (['"""forest_pages.task_log"""'], {'study_id': 'study_id'}), "('forest_pages.task_log', study_id=study_id)\n", (6538, 6582), False, 'from libs.http_utils import easy_url\n'), ((6761, 6856), 'database.tableau_api_models.ForestTask.objects.get', 'ForestTask.objects.get', ([], {'external_id': 'forest_task_external_id', 'participant__study_id': 'study_id'}), '(external_id=forest_task_external_id,\n    participant__study_id=study_id)\n', (6783, 6856), False, 'from database.tableau_api_models import ForestTask\n'), ((7069, 7090), 'libs.streaming_zip.zip_generator', 'zip_generator', (['chunks'], {}), '(chunks)\n', (7082, 7090), False, 'from libs.streaming_zip import zip_generator\n'), ((1482, 1537), 'database.tableau_api_models.ForestTask.objects.filter', 'ForestTask.objects.filter', ([], {'participant__in': 'participants'}), '(participant__in=participants)\n', (1507, 1537), False, 'from database.tableau_api_models import ForestTask\n'), ((4056, 4066), 'middleware.abort_middleware.abort', 'abort', (['(404)'], {}), '(404)\n', (4061, 4066), False, 'from middleware.abort_middleware import abort\n'), ((5004, 5061), 'database.tableau_api_models.ForestTask.objects.filter', 'ForestTask.objects.filter', ([], {'participant__study_id': 'study_id'}), '(participant__study_id=study_id)\n', (5029, 5061), False, 'from database.tableau_api_models import ForestTask\n'), ((6009, 6108), 'database.tableau_api_models.ForestTask.objects.filter', 'ForestTask.objects.filter', ([], {'external_id': 'forest_task_external_id', 'status': 'ForestTaskStatus.queued'}), '(external_id=forest_task_external_id, status=\n    ForestTaskStatus.queued)\n', (6034, 6108), False, 'from database.tableau_api_models import ForestTask\n'), ((6926, 6936), 'middleware.abort_middleware.abort', 'abort', (['(404)'], {}), '(404)\n', (6931, 6936), False, 'from middleware.abort_middleware import abort\n'), ((6955, 7016), 'database.data_access_models.ChunkRegistry.objects.filter', 'ChunkRegistry.objects.filter', ([], {'participant': 'tracker.participant'}), '(participant=tracker.participant)\n', (6983, 7016), False, 'from database.data_access_models import ChunkRegistry\n'), ((1699, 1713), 'django.utils.timezone.now', 'timezone.now', ([], {}), '()\n', (1711, 1713), False, 'from django.utils import timezone\n'), ((7509, 7545), 'serializers.forest_serializers.ForestTaskCsvSerializer', 'ForestTaskCsvSerializer', (['forest_task'], {}), '(forest_task)\n', (7532, 7545), False, 'from serializers.forest_serializers import ForestTaskCsvSerializer, ForestTaskSerializer\n'), ((7738, 7796), 'database.data_access_models.ChunkRegistry.objects.filter', 'ChunkRegistry.objects.filter', ([], {'participant__in': 'participants'}), '(participant__in=participants)\n', (7766, 7796), False, 'from database.data_access_models import ChunkRegistry\n'), ((7837, 7895), 'database.data_access_models.ChunkRegistry.objects.filter', 'ChunkRegistry.objects.filter', ([], {'participant__in': 'participants'}), '(participant__in=participants)\n', (7865, 7895), False, 'from database.data_access_models import ChunkRegistry\n'), ((6260, 6281), 'datetime.date.today', 'datetime.date.today', ([], {}), '()\n', (6279, 6281), False, 'import datetime\n'), ((8110, 8124), 'django.utils.timezone.now', 'timezone.now', ([], {}), '()\n', (8122, 8124), False, 'from django.utils import timezone\n'), ((8410, 8430), 'constants.forest_constants.ForestTree.choices', 'ForestTree.choices', ([], {}), '()\n', (8428, 8430), False, 'from constants.forest_constants import ForestTaskStatus, ForestTree\n'), ((5334, 5379), 'serializers.forest_serializers.ForestTaskSerializer', 'ForestTaskSerializer', (['forest_tasks'], {'many': '(True)'}), '(forest_tasks, many=True)\n', (5354, 5379), False, 'from serializers.forest_serializers import ForestTaskCsvSerializer, ForestTaskSerializer\n'), ((5693, 5707), 'django.utils.timezone.now', 'timezone.now', ([], {}), '()\n', (5705, 5707), False, 'from django.utils import timezone\n')]