"""
The wire shape of a focus session — snake_case, exactly as
`features/focus/data/api.js` spells it.

**`task` AND `task_page` KEEP THEIR PLAIN NAMES HERE**, unlike Calendar's
`task_id` and a task's `calendar_page_id`. That is not an inconsistency to
tidy: `toWire` spells them `task` and `task_page`, and the wire is the
contract. Two features naming the same idea differently is the cost of the
client having been written first, and it is cheaper than a rename that would
have to ship on both sides at once.

Three rules this serializer holds to, each of which reads as a bug if changed:

- **`focus_minutes_logged` AND `break_minutes_logged` ARE TAKEN AS SENT.** They
  are measurements. Recomputing either from the plan is wrong in two opposite
  directions on the same row — 0 for a session that was abandoned, or the full
  plan for one that ran 18 minutes of 50.
- **`paused_at` IS NOT A FIELD.** A row that arrives from the server is
  finished; a running session lives in the client.
- **The four plan numbers are CLAMPED, not rejected**, matching `clampField`.
  A plan outside the limits came from an import or an older build, and the
  client would clamp it on read anyway — so rejecting would strand a row the
  UI would have drawn.

`ended_at` may be null, and that is a real state rather than an oversight: a
session written by a client that crashed mid-run has a start and no end. It
sorts and renders as an unfinished row rather than being repaired, because the
server does not know when it stopped and guessing is how a measurement becomes
a fiction.
"""

from rest_framework import serializers

from ..models import FOCUS_LIMITS, FOCUS_MODES, FocusSession, Page, Task


class FocusSessionSerializer(serializers.ModelSerializer):
    id = serializers.CharField(read_only=True)
    page = serializers.PrimaryKeyRelatedField(queryset=Page.objects.none())
    task = serializers.PrimaryKeyRelatedField(
        queryset=Task.objects.none(), allow_null=True, required=False
    )
    task_page = serializers.PrimaryKeyRelatedField(
        queryset=Page.objects.none(), allow_null=True, required=False
    )

    class Meta:
        model = FocusSession
        fields = [
            'id',
            'page',
            'title',
            'task',
            'task_page',
            'focus_minutes',
            'break_minutes',
            'cycles',
            'mode',
            'started_at',
            'ended_at',
            'paused_ms',
            'focus_minutes_logged',
            'break_minutes_logged',
            'completed',
            'is_sample',
            'created_at',
        ]
        read_only_fields = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        request = self.context.get('request')
        if request is not None and request.user.is_authenticated:
            pages = Page.objects.filter(space__user=request.user)
            self.fields['page'].queryset = pages
            self.fields['task_page'].queryset = pages
            self.fields['task'].queryset = Task.objects.filter(
                page__space__user=request.user
            )

    def _clamp(self, field, value):
        low, high = FOCUS_LIMITS[field]
        if value is None:
            return low
        return max(low, min(high, int(value)))

    def validate_focus_minutes(self, value):
        return self._clamp('focus_minutes', value)

    def validate_break_minutes(self, value):
        # Zero is legal and is not the same as missing: a straight block with
        # no breaks is a plan somebody chose.
        return self._clamp('break_minutes', value)

    def validate_cycles(self, value):
        return self._clamp('cycles', value)

    def validate_mode(self, value):
        """
        Anything that is not the stopwatch is a timer, matching `load()`.

        **THE MODE HAS TO TRAVEL.** A stopwatch has no focus length and no cycle
        count, so a row whose mode was dropped comes back as a timer claiming a
        plan nobody set — and the log then badges it "ended early" because it
        never reached a length it never had.
        """
        return 'stopwatch' if value == 'stopwatch' else 'timer'

    def validate_paused_ms(self, value):
        if value is not None and value < 0:
            raise serializers.ValidationError('paused_ms cannot be negative.')
        return value or 0

    def validate_focus_minutes_logged(self, value):
        return self._non_negative('focus_minutes_logged', value)

    def validate_break_minutes_logged(self, value):
        return self._non_negative('break_minutes_logged', value)

    @staticmethod
    def _non_negative(name, value):
        # The ONLY check these two get. Negative credit is not a measurement
        # any clock could produce; anything else is the client's number and is
        # kept exactly as reported.
        if value is not None and value < 0:
            raise serializers.ValidationError(f'{name} cannot be negative.')
        return value or 0

    def validate(self, attrs):
        """
        An end before its own start is the one shape no reader can recover
        from — `elapsedMs` returns a negative and every total that includes the
        session is silently wrong.

        The instance supplies whatever a PATCH left out, so moving one end is
        still checked against the other.
        """
        instance = self.instance
        started = attrs.get('started_at', getattr(instance, 'started_at', None))
        ended = attrs.get('ended_at', getattr(instance, 'ended_at', None))
        if started and ended and ended < started:
            raise serializers.ValidationError(
                {'ended_at': 'A session cannot end before it starts.'}
            )
        return attrs
