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

The field list is short and two entries carry all of the reasoning.

**`log` IS THE ONE FIELD A PARTIAL PATCH MUST NOT WIPE.** The client is careful
about this — `updateHabit` filters `undefined` out of its patch body precisely
because "for `log` that means a year of history" — and the server's half of the
bargain is that a PATCH which does not mention `log` leaves it alone, which
`partial=True` already guarantees. What is written here is the other half: when
`log` IS sent, it is validated key by key and value by value, because a single
bad key is a day of somebody's history that reads as a different day.

**THE 92-DAY WINDOW IS ENFORCED HERE, ON WRITE.** `HISTORY_DAYS` matches
`features/habits/csv.js`, whose export already covers exactly that window — a
server that kept more would hold days its own export cannot produce. It is a
trim rather than a rejection: an import or an older client sending a longer
history is not a bad request, and refusing the whole write would lose the
ninety days that ARE in range along with the ones that are not. **This is
lossy, deliberately, and it is the one place in the app where the server
discards data the client sent.** It is the beta's stated limit, and the
settings UI says so.

WHAT IS FOLDED RATHER THAN REJECTED, and it follows the rule Goals set: fold
where the client already falls through to a default, reject where the value
could not have come from the UI.

- An unknown `schedule.kind` FOLDS to `daily`, because `scheduleOf` does — "an
  imported file or a row written by a later build must still draw, and every
  day is the reading that never hides a day from the user".
- An empty `days` set FOLDS to the working week, because a `weekdays` habit
  with no days is permanently off, which is a habit you cannot log.
- A malformed day KEY is rejected. There is no sensible fold: guessing which
  day somebody meant is worse than saying the write failed.
"""

import re
from datetime import date, timedelta

from rest_framework import serializers

from ..models import DEFAULT_SCHEDULE, HISTORY_DAYS, SCHEDULE_KINDS, Habit, Page

# A local wall-clock day, and nothing else. Anchored, so a timestamp that
# happens to start with a date does not pass — a key with a clock on it is a
# day that sorts and compares differently from every other key in the map.
DAY_RE = re.compile(r'^\d{4}-\d{2}-\d{2}$')


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

    class Meta:
        model = Habit
        fields = [
            'id',
            'page',
            'title',
            'notes',
            'icon',
            'color',
            'tags',
            'schedule',
            'target',
            'unit',
            'log',
            'archived',
            'properties',
            '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:
            self.fields['page'].queryset = Page.objects.filter(space__user=request.user)

    def validate_schedule(self, value):
        """
        `{kind, days, timesPerWeek}`, with every field defaulted — the server
        side of `scheduleOf`.

        All three are stored even when only one is meaningful for the kind, and
        that is not redundancy: switching a habit from "3× a week" to "Mon, Wed,
        Fri" and back has to return the days the user picked, not the default
        set. The client keeps all three for the same reason.
        """
        if not isinstance(value, dict):
            raise serializers.ValidationError('schedule is {kind, days, timesPerWeek}.')

        kind = value.get('kind')
        if kind not in SCHEDULE_KINDS:
            kind = DEFAULT_SCHEDULE['kind']

        days = value.get('days')
        if not isinstance(days, list) or not days:
            days = list(DEFAULT_SCHEDULE['days'])
        else:
            if any(not isinstance(d, int) or isinstance(d, bool) or not 0 <= d <= 6 for d in days):
                raise serializers.ValidationError(
                    'schedule.days are weekday numbers, Sunday 0 to Saturday 6.'
                )
            days = sorted(set(days))

        try:
            times = round(float(value.get('timesPerWeek', DEFAULT_SCHEDULE['timesPerWeek'])))
        except (TypeError, ValueError):
            times = DEFAULT_SCHEDULE['timesPerWeek']
        times = max(1, min(7, times))

        return {'kind': kind, 'days': days, 'timesPerWeek': times}

    def validate_target(self, value):
        # At least one, because a target of zero is a habit that is done before
        # it is started — `targetOf` clamps it the same way.
        if value is None:
            return 1
        return max(1, int(value))

    def validate_log(self, value):
        """
        `{'yyyy-MM-dd': count}`, trimmed to the 92-day window.

        A ZERO DELETES THE KEY rather than being stored, matching `withLog`:
        "the two mean the same thing to every reader here, and one of them
        grows the record by a day every time somebody unticks something".
        """
        if not isinstance(value, dict):
            raise serializers.ValidationError('log is a map of day to count.')

        floor = (date.today() - timedelta(days=HISTORY_DAYS - 1)).isoformat()
        trimmed = {}
        for day, count in value.items():
            if not DAY_RE.match(str(day)):
                raise serializers.ValidationError(f'{day} is not a yyyy-MM-dd day.')
            if isinstance(count, bool) or not isinstance(count, (int, float)):
                raise serializers.ValidationError(f'{day} must carry a number.')
            # Outside the window the app keeps, so there is nowhere for it to
            # live — the same line `csv.js` takes on import.
            if day < floor:
                continue
            count = round(count)
            if count > 0:
                trimmed[day] = count
        return trimmed

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