"""
The wire shape of an event — snake_case, exactly as
`features/calendar/data/api.js` spells it.

Four fields behave differently from the rest:

- **`task_id`, `goal_id` and `link_page_id` are foreign keys wearing the names
  the client sends.** Django cannot name a foreign key `task_id` — it would
  store `task_id_id` — so all three are declared by hand with a `source`, and
  all three are owner-scoped.
- **`start_at` and `end_at` are validated as STRINGS and stored verbatim.**
  Never parsed, never localised, never re-serialised through a date library.
  The shape is checked (`yyyy-MM-dd` when all-day, `yyyy-MM-ddTHH:mm` when not)
  because a value in neither shape is one no view can draw; the VALUE is left
  exactly as it arrived.

WHAT IS VALIDATED, AND WHAT IS DELIBERATELY NOT. The client's `normalizeEvent`
enforces a minimum length, pins a timed event inside one day unless it was
already multi-day, and drops an event out of all-day mode into the working
morning. NONE of that is repeated here. Those are rules about a GESTURE — what
a drag should mean — and a server that re-applied them would be a second
implementation of the UI's judgement, disagreeing with it the first time either
changed. What the server enforces is what no reader can recover from: a shape
nothing can parse, and an end before its own start.

`reminders` IS TIDIED RATHER THAN REJECTED — whole numbers, deduped, furthest
out first, exactly as `normalizeReminders` does it. A duplicate is a reminder
that fires twice, which is the client's own reason for tidying on every write,
and a list that arrived from an import has nobody to hand a 400 back to.
"""

import re

from rest_framework import serializers

from ..models import Event, Goal, Page, Task

# `2026-08-30` and `2026-08-30T09:00`. Anchored, so a trailing zone marker — the
# exact mistake this whole field is written to prevent — does not pass.
DAY_RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')
TIMED_RE = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$')


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

    task_id = serializers.PrimaryKeyRelatedField(
        source='task', queryset=Task.objects.none(), allow_null=True, required=False
    )
    goal_id = serializers.PrimaryKeyRelatedField(
        source='goal', queryset=Goal.objects.none(), allow_null=True, required=False
    )
    link_page_id = serializers.PrimaryKeyRelatedField(
        source='link_page', queryset=Page.objects.none(), allow_null=True, required=False
    )

    class Meta:
        model = Event
        fields = [
            'id',
            'page',
            'calendar_id',
            'title',
            'location',
            'notes',
            'all_day',
            'start_at',
            'end_at',
            'reminders',
            'task_id',
            'goal_id',
            'link_page_id',
            '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['link_page_id'].queryset = pages
            self.fields['task_id'].queryset = Task.objects.filter(
                page__space__user=request.user
            )
            self.fields['goal_id'].queryset = Goal.objects.filter(
                page__space__user=request.user
            )

    def validate_reminders(self, value):
        """
        Whole minutes, no duplicates, furthest out first — `normalizeReminders`.

        Negative offsets are dropped rather than rejected, matching the client:
        a reminder AFTER the thing it reminds you about is not a setting anybody
        chose, and `reminderAt` would happily compute a moment in the past for
        it and fire it immediately on the next tick.
        """
        if not isinstance(value, list):
            raise serializers.ValidationError('reminders must be a list of minute offsets.')
        minutes = set()
        for item in value:
            if isinstance(item, bool) or not isinstance(item, (int, float)):
                raise serializers.ValidationError('Each reminder is a number of minutes.')
            if item >= 0:
                minutes.add(int(item))
        return sorted(minutes, reverse=True)

    def validate(self, attrs):
        """
        The two wall-clock strings, checked together because neither means
        anything without `all_day`.

        On a PATCH the instance supplies whatever was not sent — a patch that
        moves only `end_at` still has to be checked against the start it is
        moving away from, and a patch that flips `all_day` alone changes what
        shape the two stored strings are required to be in.
        """
        instance = self.instance
        all_day = attrs.get('all_day', getattr(instance, 'all_day', False))
        start = attrs.get('start_at', getattr(instance, 'start_at', None))
        end = attrs.get('end_at', getattr(instance, 'end_at', None))

        pattern = DAY_RE if all_day else TIMED_RE
        shape = 'yyyy-MM-dd' if all_day else 'yyyy-MM-ddTHH:mm'
        for name, value in (('start_at', start), ('end_at', end)):
            if value is None or not pattern.match(str(value)):
                raise serializers.ValidationError(
                    {name: f'Expected a local wall-clock {shape} with no timezone.'}
                )

        # A lexical comparison, which is a chronological one for these two
        # shapes — that is most of why the format is fixed. AN ALL-DAY `end_at`
        # IS THE LAST DAY, INCLUSIVE, so equal ends are a one-day event and are
        # correct; a timed event of zero length is not, and is the drag that
        # produced nothing.
        if end < start or (not all_day and end == start):
            raise serializers.ValidationError(
                {'end_at': 'An event cannot end before it starts.'}
            )
        return attrs
