"""
WHAT A BRAND-NEW ACCOUNT GETS — one "Personal" space, every page type, filled.

Until this existed a new user's `GET /api/spaces/` answered `[]`, and the first
tree was supposed to arrive from the Phase 12 local-data migration. That works
for the people who already have a Lifey installation and for nobody else: the
first user who signs up with nothing to migrate gets an empty sidebar, no pages,
and no route to making one that does not start with understanding what a
"Space" is.

So the server seeds, and the seed is also where the example data comes from.

-------------------------------------------------------------------------------
THE FOUR DECISIONS
-------------------------------------------------------------------------------

**One space, called "Personal".** The client's `seedSpaces()` made two —
"Private", holding one page per type, and a "Work" space with a Calendar and a
Notes page — on the argument that the sidebar should demonstrate that spaces are
plural. It demonstrates it by being a sidebar with a "New space" control in it;
what a second seeded space actually produces is a folder somebody has to decide
whether to delete. "Private" became "Personal" at the same time, which is the
name the product uses.

**One page per type, including Dashboard.** A page type with no page is a
feature the user has no way to discover — the grid in `NewPageDialog` exists to
introduce them, and it is behind a `+` on a space row. Dashboard is seeded like
the rest even though it has no rows of its own: it is the page that reads every
other one, so it is most useful on exactly the account that has just been filled
with example rows.

**Rows are the SAME set `GET /api/<x>/sample/` serves.** Not a second fixture.
Seeding and "Fill with example data" have to plant identical rows or the two
drift, and the one that drifts is the one nobody looks at — which would be this
one, since it runs once per account and is never seen again by whoever wrote it.

**Every seeded row carries `is_sample=True`**, and that flag is load-bearing
now rather than decorative. The client uses it to tell a PRISTINE page (every
row is example data) from a used one, which is what drives the keep-or-clear
prompt on the first edit and what hides "Clear all rows" once real work exists.
A seeded row with the flag missing is a row the user can never be offered a way
to clear.

-------------------------------------------------------------------------------
TWO PAGES ARRIVE WITH NOTHING, DELIBERATELY
-------------------------------------------------------------------------------

**Focus** — `samples/focus.py` returns `[]`. A focus session is a measurement
and has no definition underneath it, so seeding one means claiming the user
focused for four hours last week and then drawing that claim in a ring and a
chart. The long form is in that module.

**Habits arrive with empty logs**, which is the same rule one level down: the
habits themselves are definitions and are seeded, their history is a
measurement and is not. Every streak reads zero, which is true.

**Quick Capture is not seeded either**, and it is not a page — it is one global
inbox hanging off the user, and an inbox is a place things arrive. There is no
`samples/capture.py` and there should not be one.

-------------------------------------------------------------------------------
HOW IT RUNS
-------------------------------------------------------------------------------

From a `post_save` signal on `CustomUser` (see `lifey_api/signals.py`), on
creation only. It is **idempotent** — a user who already has a space is left
alone — so it is safe against a re-run, against a fixture that creates a user
twice, and against accounts that predate it.

It is also **not fatal**. A failure here is logged and swallowed: the account
exists and the user can sign in to an empty sidebar, which is recoverable. The
alternative is a 500 on registration that leaves them with no account at all
because their example tasks could not be written.
"""

import logging
from types import SimpleNamespace

from django.db import transaction

from .models import PAGE_TYPE_IDS, Page, Space
from .samples.bookshelf import sample_books
from .samples.budget import sample_transactions
from .samples.focus import sample_sessions
from .samples.goals import sample_goals
from .samples.habits import sample_habits
from .samples.notes import sample_notes
from .samples.tasks import sample_tasks
from .serializers.bookshelf import BookSerializer
from .serializers.budget import TransactionSerializer
from .serializers.focus import FocusSessionSerializer
from .serializers.goals import GoalSerializer
from .serializers.habits import HabitSerializer
from .serializers.notes import NoteSerializer
from .serializers.tasks import TaskSerializer

logger = logging.getLogger(__name__)

DEFAULT_SPACE_NAME = 'Personal'

# The page names the sidebar shows, mirroring `PAGE_TYPES[].label` in
# `front/src/renderer/src/app/navigation.js`. Spelled out rather than derived
# with `.title()`, so a label that stops being the capitalised id — the client
# owns these words — is a one-line change here instead of a surprise.
PAGE_NAMES = {
    'tasks': 'Tasks',
    'habits': 'Habits',
    'goals': 'Goals',
    'calendar': 'Calendar',
    'notes': 'Notes',
    'budget': 'Budget',
    'bookshelf': 'Bookshelf',
    'focus': 'Focus',
    'dashboard': 'Dashboard',
}

# type_id -> (serializer, sample factory). Same shape as `copiers.py`, and for
# the same reason: a slice adds one entry, and a type missing from here is a
# page seeded EMPTY rather than one that quietly breaks the whole seed.
#
# `dashboard` is absent permanently — it has no rows of its own, every widget
# reads another feature's list. `focus` IS present and its factory returns an
# empty list, which is a different fact and is stated in that module.
#
# `calendar` IS ALSO ABSENT, and for a third reason again. `samples/calendar.py`
# still holds the example week and `views/calendar.py` still offers it through
# `sample_factory`, so "Fill with example data" works — a new Calendar page just
# does not START there. A table seeded with somebody else's rows reads as a
# demonstration; a WEEK seeded with somebody else's dentist appointment reads as
# a week you have to check, on the grid you opened to find out what you are
# doing on Thursday. The client agrees: see `startEmpty` in
# `front/src/renderer/src/features/calendar/data/mock.js`.
SEEDERS = {
    'tasks': (TaskSerializer, sample_tasks),
    'goals': (GoalSerializer, sample_goals),
    'notes': (NoteSerializer, sample_notes),
    'habits': (HabitSerializer, sample_habits),
    'budget': (TransactionSerializer, sample_transactions),
    'bookshelf': (BookSerializer, sample_books),
    'focus': (FocusSessionSerializer, sample_sessions),
}


def _context(user):
    """
    The serializers scope their foreign-key querysets off
    `context['request'].user` — that is what stops a caller filing a task on
    somebody else's page, and it is written as a queryset rather than a
    `validate_*` so a view cannot forget it (see `PageScopedViewSet`).

    Seeding has no HTTP request, so it supplies the one attribute those
    `__init__`s read. Deliberately not a `RequestFactory` request: that would
    be a more convincing fake of something that genuinely is not happening, and
    the next person to read it would go looking for the view.
    """
    return {'request': SimpleNamespace(user=user)}


def seed_workspace(user):
    """
    Give `user` their starting workspace. Returns the `Space`, or `None` if the
    user already had one.

    Idempotent on the only question that matters — does this user have any space
    at all. Not "does this user have a space called Personal": somebody who
    renamed theirs and deleted the rest must not have a second one appear.
    """
    if Space.objects.filter(user=user).exists():
        return None

    with transaction.atomic():
        space = Space.objects.create(
            user=user,
            name=DEFAULT_SPACE_NAME,
            is_default=True,
            position=0,
        )

        for position, type_id in enumerate(PAGE_TYPE_IDS):
            page = Page.objects.create(
                space=space,
                type_id=type_id,
                name=PAGE_NAMES.get(type_id, type_id.title()),
                position=position,
            )
            _seed_rows(user, page)

    return space


def _seed_rows(user, page):
    """Write one page's example rows, through that slice's own serializer."""
    entry = SEEDERS.get(page.type_id)
    if entry is None:
        return

    serializer_class, factory = entry
    rows = factory()
    if not rows:
        return

    # `id` is `sample-1`, `sample-2` — a handle for the fixture's own prose, not
    # an id. It is `read_only` on every serializer and would be ignored anyway;
    # dropping it here keeps the payload honest.
    payload = [{k: v for k, v in row.items() if k != 'id'} | {'page': page.pk} for row in rows]

    serializer = serializer_class(data=payload, many=True, context=_context(user))
    # `raise_exception` on purpose. A sample set that no longer validates against
    # its own serializer is a bug in this repo, and every slice already has a
    # test asserting exactly that — so failing loudly here means it is caught by
    # the seeding tests as well as by the sample ones.
    serializer.is_valid(raise_exception=True)
    serializer.save()


def seed_workspace_safely(user):
    """
    `seed_workspace`, with failure logged rather than raised.

    This is what the signal calls. A user whose example data could not be
    written still has an account and can still sign in; a user whose
    REGISTRATION 500ed because of it has neither, and cannot retry with the same
    address because the row may or may not exist.
    """
    try:
        return seed_workspace(user)
    except Exception:
        logger.exception('Seeding the workspace failed for user %s', getattr(user, 'pk', '?'))
        return None
