"""Task rules. Views only resolve the tenant; scope and assignment checks live here.

Scope of tasks.view / tasks.update / tasks.delete: `self` = tasks I created or that are assigned
to me; `organization` (or wider) = every task of the organization.
"""
from __future__ import annotations

from django.db.models import Q
from django.utils import timezone
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError

from apps.audit import services as audit
from apps.organizations.models import OrganizationMembership
from apps.tasks.models import Task
from domain.access_policy import scope_rank


def _sees_all(ctx, permission: str) -> bool:
    scope = ctx.effective.scope_of(permission)
    return scope is not None and scope_rank(scope) >= scope_rank("organization")


def visible_tasks(ctx, permission: str):
    qs = Task.objects.filter(organization=ctx.organization).select_related("assignee", "created_by")
    if _sees_all(ctx, permission):
        return qs
    user = ctx.user
    return qs.filter(Q(created_by=user) | Q(assignee=user))


def get_task(ctx, pk: int, permission: str) -> Task:
    task = visible_tasks(ctx, permission).filter(pk=pk).first()
    if task is None:
        raise NotFound()  # same answer for "missing" and "not yours"
    return task


def _assignee(ctx, user_id):
    if user_id is None:
        return None
    member = (
        OrganizationMembership.objects.filter(organization=ctx.organization, user_id=user_id, status="active")
        .select_related("user")
        .first()
    )
    if member is None:
        raise ValidationError({"assignee": "The assignee is not an active member of this organization."})
    return member.user


def _check_values(data: dict) -> None:
    if "title" in data and not str(data["title"]).strip():
        raise ValidationError({"title": "Title is required."})
    if "status" in data and data["status"] not in dict(Task.STATUSES):
        raise ValidationError({"status": "Invalid status."})
    if "priority" in data and data["priority"] not in dict(Task.PRIORITIES):
        raise ValidationError({"priority": "Invalid priority."})


def _notify_assignment(task: Task, actor, assignee) -> None:
    """Record that `assignee` was given the task; it counts as "new" until they open it."""
    task.assignee = assignee
    task.assigned_at = timezone.now()
    task.assignee_seen_at = timezone.now() if assignee is None or assignee.pk == actor.pk else None


def create_task(ctx, *, data: dict, request=None) -> Task:
    _check_values({**data, "title": data.get("title", "")})
    if data.get("status") in ("review", "done"):
        raise ValidationError({"status": "A new task cannot start as finished."})
    assignee = data.get("assignee")
    if assignee is not None and assignee != ctx.user.pk and not ctx.has("tasks.assign", "organization"):
        raise PermissionDenied("You may not assign tasks to others.", code="permission_denied")
    task = Task(
        organization=ctx.organization,
        title=data["title"].strip(),
        description=data.get("description", ""),
        status=data.get("status", "todo"),
        priority=data.get("priority", "normal"),
        due_date=data.get("due_date"),
        created_by=ctx.user,
    )
    _notify_assignment(task, ctx.user, _assignee(ctx, assignee) if assignee is not None else ctx.user)
    task.save()
    audit.record(
        actor=ctx.user, action="task.create", organization=ctx.organization,
        entity_type="task", entity_id=task.pk, after={"title": task.title}, request=request,
    )
    return task


def _can_approve(ctx) -> bool:
    return ctx.has("tasks.approve", "organization")


def update_task(ctx, task: Task, *, data: dict, request=None) -> Task:
    _check_values(data)
    before = {"status": task.status, "assignee": task.assignee_id}
    if "assignee" in data and data["assignee"] != task.assignee_id:
        if not ctx.has("tasks.assign", "organization"):
            raise PermissionDenied("You may not reassign tasks.", code="permission_denied")
        _notify_assignment(task, ctx.user, _assignee(ctx, data["assignee"]))
    if "status" in data and data["status"] != task.status:
        new = data["status"]
        if new == "done" and not _can_approve(ctx):
            raise PermissionDenied("Only an approver can mark a task as done; submit it for approval.", code="permission_denied")
        if task.status == "done" and not _can_approve(ctx):
            raise PermissionDenied("An approved task can only be reopened by an approver.", code="permission_denied")
        if new == "review" and task.status not in ("todo", "in_progress"):
            raise ValidationError({"status": "Only an open task can be submitted for approval."})
        if new != "review":
            task.review_note = ""
        if new == "done":  # an approver finishing a task directly counts as its approval
            task.reviewed_by, task.reviewed_at = ctx.user, timezone.now()
        else:  # leaving "done" (reopened) voids the earlier rating
            task.quality, task.rated_by, task.rated_at = "", None, None
    for field in ("title", "description", "status", "priority", "due_date"):
        if field in data:
            setattr(task, field, data[field].strip() if field == "title" else data[field])
    task.save()
    audit.record(
        actor=ctx.user, action="task.update", organization=ctx.organization, entity_type="task",
        entity_id=task.pk, before=before, after={"status": task.status, "assignee": task.assignee_id},
        request=request,
    )
    return task


def review_task(ctx, task: Task, *, approve: bool, note: str = "", request=None) -> Task:
    """Approver's decision on a submitted task: approve = finished, reject = still open (back in progress)."""
    if task.status != "review":
        raise ValidationError({"status": "Only a task awaiting approval can be approved or rejected."})
    task.status = "done" if approve else "in_progress"
    if not approve:
        task.quality, task.rated_by, task.rated_at = "", None, None
    task.review_note = "" if approve else (note or "").strip()[:500]
    task.reviewed_by = ctx.user
    task.reviewed_at = timezone.now()
    if not approve:  # the assignee should notice the rejection
        task.assignee_seen_at = None
    task.save()
    audit.record(
        actor=ctx.user, action="task.approve" if approve else "task.reject", organization=ctx.organization,
        entity_type="task", entity_id=task.pk, after={"note": task.review_note}, request=request,
    )
    return task


def rate_task(ctx, task: Task, *, quality: str, request=None) -> Task:
    """An approver rates the quality of a finished task (can be changed later)."""
    if task.status != "done":
        raise ValidationError({"quality": "Only a finished (approved) task can be rated."})
    if quality not in dict(Task.QUALITIES):
        raise ValidationError({"quality": "Quality must be one of: excellent, good, average, poor, terrible."})
    before = task.quality
    task.quality, task.rated_by, task.rated_at = quality, ctx.user, timezone.now()
    task.save()
    audit.record(
        actor=ctx.user, action="task.rate", organization=ctx.organization, entity_type="task",
        entity_id=task.pk, before={"quality": before}, after={"quality": quality}, request=request,
    )
    return task


def mark_seen(ctx, task: Task) -> None:
    if task.assignee_id == ctx.user.pk and task.assignee_seen_at is None:
        task.assignee_seen_at = timezone.now()
        task.save(update_fields=["assignee_seen_at"])


def summary(ctx) -> dict:
    qs = visible_tasks(ctx, "tasks.view")
    counts = {s: qs.filter(status=s).count() for s, _ in Task.STATUSES}
    return {
        **counts,
        "total": sum(counts.values()),
        "new_assigned": Task.objects.filter(
            organization=ctx.organization, assignee=ctx.user, assignee_seen_at__isnull=True
        ).count(),
    }

def delete_task(ctx, task: Task, *, request=None) -> None:
    pk = task.pk
    task.delete()
    audit.record(
        actor=ctx.user, action="task.delete", organization=ctx.organization,
        entity_type="task", entity_id=pk, request=request,
    )


def assignable_members(ctx):
    return (
        OrganizationMembership.objects.filter(organization=ctx.organization, status="active")
        .select_related("user")
        .order_by("user__name")
    )
