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

Three fields behave differently from the rest:

- **`position` IS ACCEPTED AS SENT AND NEVER RECOMPUTED.** It is a bookmark,
  not an accumulation. A back-filled shelf has a position and no sessions at
  all, and a server that "corrected" it to the sum of the sessions would reset
  every one of those books to page zero.
- **`has_cover` IS A BOOLEAN.** The bytes are in the renderer's local storage.
  See the model.
- **`ebook_url` IS RESTRICTED TO http AND https.** The UI renders it as a link
  and the main process passes opened URLs to `shell.openExternal`, which will
  launch a `file:` path or another application's registered scheme. The field
  is free text on screen, so this is the only place the check exists.

AN UNKNOWN `status` IS FOLDED, NOT REJECTED — `statusMeta` resolves a retired
id through `LEGACY_STATUSES` and falls back to `tbr`, so a row from an older
build or an import already renders. What is folded is the retired spelling;
anything else is rejected, because a status the client can never produce means
something is wrong rather than old. `dnf` becomes `abandoned` and `done`
becomes `read`, which matters: the fallback would report a finished book as
never begun.
"""

from rest_framework import serializers

from ..models import BOOK_STATUSES, BOOK_UNITS, LEGACY_BOOK_STATUSES, Book, Page


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

    class Meta:
        model = Book
        fields = [
            'id',
            'page',
            'title',
            'author',
            'publisher',
            'status',
            'unit',
            'length',
            'position',
            'genres',
            'rating',
            'ebook_url',
            'notes',
            'date_started',
            'date_finished',
            'sessions',
            'quotes',
            'quote_target',
            'has_cover',
            'color',
            '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_status(self, value):
        value = LEGACY_BOOK_STATUSES.get(value, value)
        if value not in BOOK_STATUSES:
            raise serializers.ValidationError(f'Unknown status: {value}.')
        return value

    def validate_unit(self, value):
        # Folded rather than rejected, matching `unitMeta`, which falls back to
        # `pages` for an id it does not know — so an older row is simply a
        # paperback rather than a row that will not load.
        return value if value in BOOK_UNITS else 'pages'

    def validate_rating(self, value):
        # Null is a real value and means "not rated", which is not the same as
        # nought stars. Five is the scale the UI draws.
        if value is not None and not 1 <= value <= 5:
            raise serializers.ValidationError('A rating is 1 to 5, or null.')
        return value

    def validate_length(self, value):
        if value is not None and value < 0:
            raise serializers.ValidationError('A length cannot be negative.')
        return value

    def validate_position(self, value):
        # NOT clamped to `length`, and not compared against it. The client
        # clamps when it LOGS a session, which is a gesture; a stored position
        # past a length that was later corrected downwards is data, and
        # `progressOf` already caps the bar it draws at 1.
        if value is not None and value < 0:
            raise serializers.ValidationError('A bookmark cannot be negative.')
        return value

    def validate_ebook_url(self, value):
        """
        http or https only. See the module docstring — this URL reaches
        `shell.openExternal`.
        """
        if value and not value.lower().startswith(('http://', 'https://')):
            raise serializers.ValidationError('An e-book link must be http or https.')
        return value or None

    def validate_sessions(self, value):
        """
        `[{id, date, amount}]`. The amount is what the reader SAID they read,
        and it is not reconciled against anything — `withSession` deliberately
        keeps the number that was typed even when the bookmark it moves is
        clamped, "because that is what the user said and the history should not
        be quietly edited to make the arithmetic tidy".
        """
        if not isinstance(value, list):
            raise serializers.ValidationError('sessions must be a list.')
        for item in value:
            if not isinstance(item, dict) or 'id' not in item or 'date' not in item:
                raise serializers.ValidationError('Each session needs an id and a date.')
            amount = item.get('amount')
            if isinstance(amount, bool) or not isinstance(amount, (int, float)) or amount < 0:
                raise serializers.ValidationError('A session amount is a number, zero or more.')
        return value

    def validate_quotes(self, value):
        """
        `[{id, text, page, sent, createdAt}]`.

        **`sent` has to survive**: it records that the quotation has been
        written into a note somewhere, and losing it re-sends every quote the
        next time a destination is chosen. It is not required on the way in,
        because a quote typed just now has not been sent and the client omits
        the field on some paths — a missing flag reads as False, which is the
        safe direction.
        """
        if not isinstance(value, list):
            raise serializers.ValidationError('quotes must be a list.')
        for item in value:
            if not isinstance(item, dict) or 'id' not in item or 'text' not in item:
                raise serializers.ValidationError('Each quote needs an id and its text.')
        return value

    def validate_quote_target(self, value):
        if value is None:
            return None
        if not isinstance(value, dict) or 'pageId' not in value or 'noteId' not in value:
            raise serializers.ValidationError('quote_target is {pageId, noteId} or null.')
        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
