"""
FOCUS — Phase 10, and the ONE feature in this app whose dates are instants.

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

**The endpoint is `/api/focus-sessions/`.**

1. **`started_at` AND `ended_at` ARE AWARE `DateTimeField`s, and that is
   correct here and nowhere else.** Every other date in Lifey is a DAY with no
   zone, because "due Friday" is not a moment, and Calendar's two are wall-clock
   `CharField`s because a planner's 9am is 9am. A focus session is the opposite
   of both: it is a MEASURED INTERVAL, its length is the whole point, and it is
   compared against a clock. `USE_TZ = True` is what makes these two right, and
   it is the same setting Calendar opts out of by never being parsed — the
   workspace `CLAUDE.md` states the split once so it is not re-litigated.

   The failure this prevents: a session started at 23:50 and travelled with,
   stored as local naive time, reports a NEGATIVE duration.

2. **`focus_minutes_logged` IS A REPORTED MEASUREMENT AND THE SERVER MUST NEVER
   RECOMPUTE IT.** It is elapsed focus time as measured by the client that ran
   the timer, deliberately NOT `cycles × focus_minutes` — see `creditOf`. A
   session abandoned 18 minutes into a 50-minute plan is worth 18, and a server
   that "corrected" it to 0 (not completed) or to 50 (the plan) would be wrong
   in the two opposite directions on the same row. It is a step count, not a
   derivation.

3. **THE PLAN IS DENORMALISED ONTO THE SESSION, not a foreign key to a preset.**
   `focus_minutes`, `break_minutes`, `cycles`, `mode` — four columns. A session
   records what it ACTUALLY RAN, so changing the page's default plan next week
   must not retroactively rewrite what last week's sessions say they were. Four
   values on the row is the cheapest way to make that impossible.

4. **`paused_at` IS DELIBERATELY NOT PERSISTED.** It only has meaning inside a
   running session, and a running session lives in the client
   (`FocusSessionProvider`). **A row that arrives from the server is finished.**
   `paused_ms` IS stored, because it is part of the measurement — it is how
   long the interval was not being worked.

5. **THE TASK LINK IS A PAIR**, `task` and `task_page`. Rows are fetched per
   page, so a session carrying only a task id can be counted but cannot be
   resolved to a title or opened. Same shape Calendar's event link uses.

ONE THING THE WIRE DOES NOT CARRY, written down so it is a known cost rather
than a discovery: **a stopwatch session's `breaks` list is not persisted.**
`toWire` does not send it. A stopwatch's phase table is a HISTORY of the breaks
that were taken rather than a plan, so a stored stopwatch session loses the
shape of its breaks and keeps only `break_minutes_logged` — which is all any
reader of a FINISHED session asks for. It would matter if a finished session
were ever re-opened on its own dial; nothing does that.
"""

from django.db import models

from core.models import TimeStampedModel

from .tasks import Task
from .workspace import Page

# `TIMER` and `STOPWATCH` in `features/focus/model.js`. Anything that is not
# the stopwatch is a timer, including the `undefined` every session written
# before the mode existed carries.
FOCUS_MODES = ('timer', 'stopwatch')

# `LIMITS`. Enforced by the serializer, matching `clampField`.
FOCUS_LIMITS = {
    'focus_minutes': (1, 180),
    # ZERO IS LEGAL — a straight block with no breaks is a real plan.
    'break_minutes': (0, 60),
    'cycles': (1, 12),
}


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

    # Most sessions start by pressing Start, so empty is the commonest value
    # and `describeSession` renders it. Not a placeholder.
    title = models.CharField(max_length=300, blank=True)

    # SET_NULL, like every other pointer in the app: deleting the task must not
    # delete the record that somebody sat down and worked for fifty minutes.
    task = models.ForeignKey(
        Task,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='focus_sessions',
    )
    task_page = models.ForeignKey(
        Page,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='linked_focus_sessions',
    )

    # The plan, as it actually ran. See the module docstring.
    focus_minutes = models.IntegerField(default=25)
    break_minutes = models.IntegerField(default=5)
    cycles = models.IntegerField(default=1)
    mode = models.CharField(max_length=16, default='timer')

    # REAL INSTANTS. The only two in this app.
    started_at = models.DateTimeField()
    ended_at = models.DateTimeField(null=True, blank=True)

    paused_ms = models.BigIntegerField(default=0)

    # Measurements, never derivations.
    focus_minutes_logged = models.IntegerField(default=0)
    break_minutes_logged = models.IntegerField(default=0)

    # Ran to the end of the plan, as opposed to ending early. A fact the log
    # shows; stopping early is not punished anywhere.
    completed = models.BooleanField(default=False)

    is_sample = models.BooleanField(default=False)

    class Meta:
        # Newest first — the log reads downwards from the session that just
        # ended, and "Today's focus" is the top of it.
        ordering = ['-started_at', '-id']
        indexes = [
            models.Index(fields=['page', '-started_at']),
        ]

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