"""
TASKS — the reference slice.

The contract is `front/src/renderer/src/features/tasks/data/api.js` and the
field set is `features/tasks/model.js`. Where this file disagrees with either,
this file is wrong.

Five things the model states deliberately:

1. **`due_date` is a DATE.** `asDateOnly` truncates every write in the client
   for a reason: a timestamp means two tasks due the same day sort by a clock
   nobody set.
2. **`progress_fraction` is nullable and null means "derive it"** — from the
   subtasks, from logged time against the estimate, or from nothing. A default
   of `0` makes every untouched task claim to be measured at zero percent.
3. **`subtasks` is JSON, not a table.** A subtask has no identity outside its
   task, nothing queries across them, and the operation they get most is
   reorder — which as rows is an UPDATE per row and as JSON is the list. The
   day the Dashboard wants to count subtasks across pages is the day to migrate
   it, deliberately.
4. **`properties` is one JSON bag keyed by property id.** The property set is
   per-page and user-editable; a column per property means a migration every
   time somebody adds a checkbox.
5. **`actual_minutes` is written by ONE path**, `POST /api/tasks/{id}/log-time/`,
   as an atomic `F('actual_minutes') + minutes`. It is read-only everywhere
   else — see the serializer.

THE POINTERS AT OTHER PAGES WANT TO BE REAL FKs WITH `SET_NULL`, because the
server has to enforce what the client already does: deleting the event turns
the connection off by itself, since a task claiming to be on a calendar it is
not on is the one state neither page can recover from.

**`goal` is now one of them** — Phase 4 built `Goal` and migration `0003`
converted the column, nulling any value that did not resolve to a goal. Deleting
a goal clears the tasks laddering up to it, and `Goal.task_ids` is derived from
this FK rather than stored.

**`calendar_page` and `calendar_event` are now the other two** — Phase 5 built
`Event` and migration `0004` converted both columns the way `0003` converted
`goal`. The debt Phase 3 wrote down is paid: every pointer this model holds is
enforced, and nothing here points at a row that is gone.
"""

from django.db import models

from core.models import TimeStampedModel

from .goals import Goal
from .workspace import Page

# Mirrors `STATUSES` and `PRIORITIES` in `features/tasks/model.js`. The ids are
# what is stored; the labels are the client's and are deliberately absent here.
TASK_STATUSES = ('todo', 'in_progress', 'done')
TASK_PRIORITIES = ('high', 'medium', 'low')

# Retired ids -> what they became, matching the client's `canonicalStatus`. A
# row written by an older build must not read as unstarted, which would report
# finished work as not begun.
LEGACY_STATUSES = {'in_review': 'in_progress'}


class Task(TimeStampedModel):
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='tasks')

    title = models.CharField(max_length=300, blank=True)
    notes = models.TextField(blank=True)
    status = models.CharField(max_length=16, default='todo')
    priority = models.CharField(max_length=8, default='medium')

    # Tag IDS, against the page-scoped vocabulary in that page's settings blob.
    tags = models.JSONField(default=list, blank=True)

    due_date = models.DateField(null=True, blank=True)
    effort_minutes = models.IntegerField(null=True, blank=True)
    actual_minutes = models.IntegerField(default=0)
    progress_fraction = models.FloatField(null=True, blank=True)
    needs_focus = models.BooleanField(default=False)

    # SET_NULL, not CASCADE: deleting a goal must not delete the work done
    # towards it. The task stays, and stops claiming to ladder up to something
    # that is gone.
    goal = models.ForeignKey(
        Goal,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='tasks',
    )

    # SET_NULL, like `goal`: deleting the event turns the connection off by
    # itself, because a task claiming to be on a calendar it is not on is the
    # one state neither page can recover from.
    #
    # The FIELD names lost their `_id` suffix — `calendar_page`, not
    # `calendar_page_id` — because Django appends that to a foreign key's
    # COLUMN and would otherwise store `calendar_page_id_id`. The columns are
    # therefore unchanged and so is the wire, which still spells both fields
    # `calendar_page_id` and `calendar_event_id`: the serializer maps them.
    #
    # `Event` is named as a string rather than imported, because
    # `models/calendar.py` imports this module for its own `task` FK and a
    # direct import here would close the circle.
    calendar_page = models.ForeignKey(
        Page,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='linked_tasks',
    )
    calendar_event = models.ForeignKey(
        'lifey_api.Event',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='linked_tasks',
    )

    subtasks = models.JSONField(default=list, blank=True)

    # `'yyyy-MM-ddTHH:mm'` strings, stored VERBATIM. A task's reminder is a
    # local wall-clock moment the user chose, not an instant: parsing it into a
    # datetime means flying somewhere silently moves every reminder.
    reminders = models.JSONField(default=list, blank=True)

    note_link = models.JSONField(null=True, blank=True)
    recurrence = models.JSONField(null=True, blank=True)
    blocked_by = models.JSONField(default=list, blank=True)
    properties = models.JSONField(default=dict, blank=True)

    is_sample = models.BooleanField(default=False)

    # A real instant, unlike `due_date`. Set by the server when a task becomes
    # done and cleared when it stops being done — the client never sends it.
    completed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['id']
        indexes = [
            models.Index(fields=['page', 'status']),
            models.Index(fields=['page', 'due_date']),
        ]

    def __str__(self):
        return self.title or f'(untitled task {self.pk})'
