# (c) cavaliba.com - data - views_task.py

"""
DataTask browsing UI: navbar dropdown of active/recent tasks, a filterable
list, a detail view with progress/ETA, and a stop action.

No permission gates the browsing views: every logged-in user sees their own
task backlog (empty for most, since producing a task needs an import/export/
pipeline permission in the first place). start_view's auth_required=True still
redirects anonymous callers to the login page, and visitor=False keeps
visitors out. p_task_all widens every view to all owners.

Stopping follows the same shape (5.4.0): your own task needs no permission -
the import/export progress modals' Abort button never asked for one, and a
hidden modal must not leave you unable to stop what you started - while
stopping somebody else's needs p_task_manage (plus p_task_all to see it at
all). The API keeps requiring p_task_manage either way, see api/task.py.

Method inventory:

Helpers: _json_items, _attachment_url, _can_stop

Views: task_navbar_partial, task_list, task_stop, task_detail, task_errors
"""

import json

from django.http import Http404, HttpResponse, HttpResponseNotAllowed
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone

from app_data.aaa import start_view
from app_data.models import DataTask
from app_data.task_manager import abort_task
from app_data.task_ui import own_task
from app_home.log import WARNING, log2


def _json_items(data):
    """Return list of (key, value_str) for a JSON dict, or None if not a dict.

    A list of scalars renders one value per line rather than as a JSON array:
    an import task's "errors" is such a list, and the bracket/quote noise of
    json.dumps() makes a long one hard to read. Lists holding dicts or lists
    keep the JSON dump, which is the only readable form for them."""

    if not isinstance(data, dict):
        return None
    items = []
    for k, v in data.items():
        if k == "aaa" and isinstance(v, dict):
            v = {ik: iv for ik, iv in v.items() if ik != "perms"}
        if isinstance(v, list) and not any(isinstance(entry, (dict, list)) for entry in v):
            items.append((k, "\n".join(str(entry) for entry in v)))
        elif isinstance(v, (dict, list)):
            items.append((k, json.dumps(v, indent=2, ensure_ascii=False)))
        else:
            items.append((k, str(v) if v is not None else "-"))
    return items


def _can_stop(task, aaa):
    """Whether this caller may stop this task: their own always, somebody
    else's only with p_task_manage.

    Read by the list and detail templates as well as by task_stop itself - a
    Stop button that is only refused once clicked reads as a broken page, which
    is exactly what it did before 5.4.0 (rendered on task state alone, then
    bounced by the view's permission check)."""

    if task.owner_id == aaa.get("username", ""):
        return True
    return "p_task_manage" in aaa.get("perms", [])


NAVBAR_ACTIVE_STATES = ["QUEUED", "RUNNING"]
NAVBAR_TERMINAL_STATES = ["DONE", "FAILED", "ABORTED"]
NAVBAR_RECENT_HOURS = 24
NAVBAR_RECENT_MAX = 5


# ------------------------------------------------------------------
# HTMX partial — navbar dropdown fragment
# GET /data/private/tasks/navbar/
# ------------------------------------------------------------------


def task_navbar_partial(request):
    """HTMX partial: navbar dropdown of the caller's (or, with p_task_all,
    everyone's) active tasks plus recently-finished tasks from the last
    NAVBAR_RECENT_HOURS."""

    context = start_view(
        request,
        app="data",
        view="task_navbar",
        noauthz="app_home:private",
    )
    if context["redirect"]:
        return HttpResponse("")

    aaa = context["aaa"]
    username = aaa.get("username", "")

    if "p_task_all" in aaa["perms"]:
        active_tasks = list(
            DataTask.objects.filter(state__in=NAVBAR_ACTIVE_STATES).order_by("-created_at")
        )
    else:
        active_tasks = list(
            DataTask.objects.filter(owner_id=username, state__in=NAVBAR_ACTIVE_STATES).order_by(
                "-created_at"
            )
        )

    cutoff = timezone.now() - timezone.timedelta(hours=NAVBAR_RECENT_HOURS)

    if "p_task_all" in aaa["perms"]:
        recent_tasks = list(
            DataTask.objects.filter(
                state__in=NAVBAR_TERMINAL_STATES,
                finished_at__gte=cutoff,
            ).order_by("-finished_at")[:NAVBAR_RECENT_MAX]
        )
    else:
        recent_tasks = list(
            DataTask.objects.filter(
                owner_id=username,
                state__in=NAVBAR_TERMINAL_STATES,
                finished_at__gte=cutoff,
            ).order_by("-finished_at")[:NAVBAR_RECENT_MAX]
        )

    context["active_tasks"] = active_tasks
    context["recent_tasks"] = recent_tasks

    return render(request, "app_data/tasks/navbar_partial.html", context)


# ------------------------------------------------------------------
# list task
# no perm needed : own tasks only
# perm p_task_all : show all tasks
# GET  /data/private/tasks/
# ------------------------------------------------------------------


def task_list(request):
    """Filterable (state, search) list of up to 200 DataTask rows, restricted to
    the caller's own tasks unless p_task_all."""

    context = start_view(
        request,
        app="data",
        view="task_list",
        noauthz="app_home:private",
    )
    if context["redirect"]:
        return redirect(context["redirect"])

    aaa = context["aaa"]
    username = aaa.get("username", "")

    state_filter = request.GET.get("state", "").strip().upper()
    search = request.GET.get("search", "").strip()

    if "p_task_all" in aaa["perms"]:
        qs = DataTask.objects.all().order_by("-created_at")
    else:
        qs = DataTask.objects.filter(owner_id=username).order_by("-created_at")

    if state_filter:
        qs = qs.filter(state=state_filter)

    if search:
        from django.db.models import Q

        qs = qs.filter(
            Q(name__icontains=search)
            | Q(owner_id__icontains=search)
            | Q(owner_type__icontains=search)
        )

    tasks = qs[:200]

    log2.access("task.list", extra={"state": state_filter, "search": search})

    # a download link per row, so a finished export is one click away from the
    # navbar Tasks dropdown without opening its detail page; can_stop decides
    # whether the row offers a Stop button at all
    for task in tasks:
        task.attachment_url = _attachment_url(task, aaa)
        task.can_stop = _can_stop(task, aaa)

    context["tasks"] = tasks
    context["state_filter"] = state_filter
    context["search"] = search
    context["states"] = ["", "QUEUED", "RUNNING", "DONE", "FAILED", "ABORTED"]

    return render(request, "app_data/tasks/list.html", context)


# ------------------------------------------------------------------
# Stop task
# POST /data/private/tasks/<handle>/stop/  (stop button)
# no perm needed   : own task only
# perm p_task_manage : required for another user's task
# ------------------------------------------------------------------


def task_stop(request, handle):
    """POST to abort a DataTask by handle, then redirect to ?next= or the task list.

    Your own task needs no permission (see the module docstring); somebody
    else's needs p_task_manage, and own_task() already 404s on a task this
    caller cannot even see."""

    context = start_view(
        request,
        app="data",
        view="task_stop",
        noauthz="app_home:private",
    )
    if context["redirect"]:
        return redirect(context["redirect"])

    aaa = context["aaa"]

    task = own_task(handle, aaa)
    if not task:
        raise Http404

    if request.method == "POST":
        if not _can_stop(task, aaa):
            log2.deny("task.stop", reason="not_owner", target=("task", handle))
            return redirect("app_home:private")
        if abort_task(handle, aaa=aaa):
            log2.ok("task.stop", target=("task", handle))
        else:
            log2.fail("task.stop", reason="not_running", target=("task", handle), level=WARNING)

    next_url = request.POST.get("next") or request.GET.get("next") or ""
    if next_url:
        return redirect(next_url)
    return redirect("app_data:task_list")


# ------------------------------------------------------------------
# Task detail
# GET /data/private/tasks/<handle>/
# no perm needed   : own task only
# perm p_task_all  : required to view another user's task
# ------------------------------------------------------------------


def task_detail(request, handle):
    """Show one DataTask's progress, estimated remaining time, and formatted
    params/output; 404 if not found, redirect if the caller doesn't own it and
    lacks p_task_all."""

    context = start_view(
        request,
        app="data",
        view="task_detail",
        noauthz="app_home:private",
    )
    if context["redirect"]:
        return redirect(context["redirect"])

    aaa = context["aaa"]
    username = aaa.get("username", "")

    try:
        task = DataTask.objects.get(handle=handle)
    except DataTask.DoesNotExist:
        raise Http404 from None

    if task.owner_id != username and "p_task_all" not in aaa.get("perms", []):
        log2.deny("task.detail", reason="not_owner", target=("task", handle))
        return redirect("app_home:private")

    log2.access("task.detail", target=("task", handle))

    estimated = None
    if task.state == "RUNNING" and task.duration:
        progress = task.progress or {}
        percent = progress.get("percent", 0) if isinstance(progress, dict) else 0
        if percent and percent > 0:
            estimated = round(task.duration * (100 - percent) / percent, 1)

    context["task"] = task
    context["estimated"] = estimated
    context["output_items"] = _json_items(task.output)
    context["params_items"] = _json_items(task.params)
    context["attachment_url"] = _attachment_url(task, aaa)
    context["can_stop"] = _can_stop(task, aaa)
    return render(request, "app_data/tasks/detail.html", context)


# ------------------------------------------------------------------
# Task errors download
# GET /data/private/tasks/<handle>/errors/
# no perm needed   : own task only
# perm p_task_all  : required for another user's task
# ------------------------------------------------------------------


def task_errors(request, handle):
    """Download a task's failed-row messages as a plain text file, one per line.

    Generic on purpose: `output["errors"]` has the same shape for every producer
    (import.run, export.run), so the progress modals and the task detail page all
    link here instead of each tool growing its own download view. The modal and the
    detail page only ever show the first few errors - this is the whole list.

    Gated on ownership alone (own_task(), widened by p_task_all), like the rest of
    the task browsing UI. 404 on a task that is not the caller's, and on one with no
    errors - there is nothing to serve, and the link is not rendered in that case.
    """

    context = start_view(
        request,
        app="data",
        view="task_errors",
        noauthz="app_home:private",
    )
    if context["redirect"]:
        return redirect(context["redirect"])

    if request.method != "GET":
        return HttpResponseNotAllowed(["GET"])

    task = own_task(handle, context["aaa"])
    if not task:
        raise Http404

    errors = (task.output or {}).get("errors") or []
    if not isinstance(errors, list) or not errors:
        raise Http404

    log2.access("task.errors", count=len(errors), target=("task", handle))

    body = "\n".join(str(line) for line in errors) + "\n"
    response = HttpResponse(body, content_type="text/plain; charset=utf-8")
    response["Content-Disposition"] = f'attachment; filename="task-errors-{handle}.txt"'
    return response


def _attachment_url(task, aaa):
    """Download URL for a task's file attachment, or None.

    This is how a finished export is reached once its progress modal has been
    hidden or the page left: navbar Tasks -> the task -> this link. Without it the
    file would only be reachable by hand-building the URL.

    Exports are the only producer of a file attachment today, and their download
    view is gated on p_data_export, so the link is only offered to a caller who
    would actually be allowed through it (the view re-checks, plus task
    ownership). A future task type attaching a file needs its own branch here.
    """

    attachment = task.attachment or {}
    if not isinstance(attachment, dict):
        return None
    if attachment.get("type") != "file" or not attachment.get("fileid"):
        return None
    if task.state != "DONE":
        return None
    if "p_data_export" not in aaa.get("perms", []):
        return None

    return reverse("app_data:data_exporter_result", args=[task.handle])
