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

Unlike the workspace tree, this half of the client HAS a `toWire`/`fromWire`
map, and it is the contract: the names below are taken from it rather than
chosen here.

Three fields behave differently from the rest:

- **`actual_minutes` is READ-ONLY.** The client has nowhere to put an increment
  and would send a total; two sessions ending seconds apart then read the same
  "before" value and the later write silently discards the earlier one's
  minutes. `POST /api/tasks/{id}/log-time/` is the only writer, and it applies
  a delta with `F()`.
- **`completed_at` is READ-ONLY and set by the transition**, not sent. It is a
  real instant, so it is the one datetime here the server may decide.
- **`page` is writable but owner-scoped**, because `createTask` posts
  `{...fields, page: pageId}` in the BODY while the list endpoint takes
  `?page=`. Both resolve against the caller's own pages or neither does.

**`calendar_page_id` AND `calendar_event_id` ARE THE WIRE NAMES OF TWO FOREIGN
KEYS.** Phase 5 converted the columns, and a foreign key's field name in Django
cannot carry the `_id` suffix the client spells — so the two are declared by
hand with a `source`. The wire is unchanged; only the model moved.
"""

from django.utils import timezone
from rest_framework import serializers

from ..models import LEGACY_STATUSES, TASK_PRIORITIES, TASK_STATUSES, Event, Page, Task


class TaskSerializer(serializers.ModelSerializer):
    id = serializers.CharField(read_only=True)
    page = serializers.PrimaryKeyRelatedField(queryset=Page.objects.none())

    # The two calendar pointers keep the names the client already sends, over
    # fields Django has to call something else. Both are owner-scoped in
    # `__init__` for the same reason `page` is.
    calendar_page_id = serializers.PrimaryKeyRelatedField(
        source='calendar_page',
        queryset=Page.objects.none(),
        allow_null=True,
        required=False,
    )
    calendar_event_id = serializers.PrimaryKeyRelatedField(
        source='calendar_event',
        queryset=Event.objects.none(),
        allow_null=True,
        required=False,
    )

    class Meta:
        model = Task
        fields = [
            'id',
            'page',
            'title',
            'notes',
            'status',
            'priority',
            'tags',
            'due_date',
            'effort_minutes',
            'actual_minutes',
            'progress_fraction',
            'needs_focus',
            'goal',
            'subtasks',
            'reminders',
            'calendar_page_id',
            'calendar_event_id',
            'note_link',
            'recurrence',
            'blocked_by',
            'properties',
            'is_sample',
            'created_at',
            'completed_at',
        ]
        read_only_fields = ['actual_minutes', 'completed_at']

    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['calendar_page_id'].queryset = pages
            self.fields['calendar_event_id'].queryset = Event.objects.filter(
                page__space__user=request.user
            )

    def validate_status(self, value):
        # Fold a retired id the way the client's `canonicalStatus` does, rather
        # than rejecting it: a row written by an older build is data, not a bad
        # request, and rejecting it would strand whoever is upgrading.
        value = LEGACY_STATUSES.get(value, value)
        if value not in TASK_STATUSES:
            raise serializers.ValidationError(f'Unknown status: {value}.')
        return value

    def validate_priority(self, value):
        if value not in TASK_PRIORITIES:
            raise serializers.ValidationError(f'Unknown priority: {value}.')
        return value

    def validate_progress_fraction(self, value):
        # A fraction, never a percentage. Null is a value here and means
        # "derive it", which is why this only runs when something was sent.
        if value is not None and not 0 <= value <= 1:
            raise serializers.ValidationError('progress_fraction is a 0–1 fraction.')
        return value

    def validate_subtasks(self, value):
        if not isinstance(value, list):
            raise serializers.ValidationError('subtasks must be a list.')
        for item in value:
            if not isinstance(item, dict) or 'id' not in item or 'title' not in item:
                raise serializers.ValidationError('Each subtask needs an id and a title.')
        return value

    def validate_properties(self, value):
        if not isinstance(value, dict):
            raise serializers.ValidationError('properties must be an object keyed by property id.')
        return value

    # ------------------------------------------------------------ completion
    #
    # `completed_at` is a fact ABOUT THE TRANSITION, so it is stamped where the
    # transition happens rather than being sent. Clearing it when a task leaves
    # `done` matters as much as setting it: a re-opened task that kept its
    # finish time reads as finished to every consumer that asks when rather
    # than whether — the Dashboard's "done this week" counts among them.

    def create(self, validated_data):
        if validated_data.get('status') == 'done':
            validated_data['completed_at'] = timezone.now()
        return super().create(validated_data)

    def update(self, instance, validated_data):
        if 'status' in validated_data:
            was_done = instance.status == 'done'
            is_done = validated_data['status'] == 'done'
            if is_done and not was_done:
                validated_data['completed_at'] = timezone.now()
            elif was_done and not is_done:
                validated_data['completed_at'] = None
        return super().update(instance, validated_data)


class LogTimeSerializer(serializers.Serializer):
    """
    Body of `POST /api/tasks/{id}/log-time/` — a DELTA in minutes.

    Negative is allowed: a session logged against the wrong task has to be
    taken off it, and the alternative is a PATCH of the total, which is the
    exact race this endpoint exists to avoid.
    """

    minutes = serializers.IntegerField()
