"""
Participant & project management (iMotions-class).

A study is run across many participants (participants); each participant has one or
more recorded sessions. This module is the registry that ties session files to
participants and groups, so the analysis layer can aggregate across people.

Persisted as `manifest.json` in the data directory alongside the .h5 sessions:

    {
      "study": "Ad reactions",
      "participants": [
        {"id": "R01", "name": "...", "group": "A", "meta": {...},
         "sessions": ["session_20260619_2101.h5", ...]},
        ...
      ]
    }

Pure stdlib; the app and the aggregation layer both build on it.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field, asdict
from pathlib import Path


@dataclass
class Participant:
    id: str
    name: str = ""
    group: str = ""
    meta: dict = field(default_factory=dict)
    sessions: list = field(default_factory=list)


class Project:
    """Manifest of participants + their sessions for one study/data directory."""

    def __init__(self, data_dir: str, study: str = "Study"):
        self.dir = Path(data_dir)
        self.dir.mkdir(parents=True, exist_ok=True)
        self.path = self.dir / "manifest.json"
        self.study = study
        self.participants: dict[str, Participant] = {}
        self.load()

    # --- persistence -----------------------------------------------------
    def load(self) -> "Project":
        if self.path.exists():
            d = json.load(open(self.path))
            self.study = d.get("study", self.study)
            self.participants = {r["id"]: Participant(**r) for r in d.get("participants", [])}
        return self

    def save(self) -> "Project":
        d = {"study": self.study,
             "participants": [asdict(r) for r in self.participants.values()]}
        json.dump(d, open(self.path, "w"), indent=2)
        return self

    # --- mutations -------------------------------------------------------
    def add_participant(self, id: str, name: str = "", group: str = "",
                       **meta) -> Participant:
        r = self.participants.get(id)
        if r is None:
            r = Participant(id=id, name=name or id, group=group, meta=meta)
            self.participants[id] = r
        else:
            if name:
                r.name = name
            if group:
                r.group = group
            r.meta.update(meta)
        self.save()
        return r

    def attach_session(self, participant_id: str, session_file: str, *,
                       name: str = "", group: str = "") -> Participant:
        r = self.add_participant(participant_id, name=name, group=group)
        if session_file not in r.sessions:
            r.sessions.append(session_file)
        self.save()
        return r

    # --- queries ---------------------------------------------------------
    def all(self) -> list[Participant]:
        return list(self.participants.values())

    def groups(self) -> dict[str, list[Participant]]:
        out: dict[str, list[Participant]] = {}
        for r in self.participants.values():
            out.setdefault(r.group or "(all)", []).append(r)
        return out

    def sessions(self, *, group: str | None = None,
                 participant_id: str | None = None) -> list[str]:
        """Absolute session paths, optionally filtered by group or participant."""
        files = []
        for r in self.participants.values():
            if participant_id and r.id != participant_id:
                continue
            if group and (r.group or "(all)") != group:
                continue
            files += r.sessions
        return [str(self.dir / f) for f in files]

    def to_dict(self) -> dict:
        return {"study": self.study,
                "participants": [asdict(r) for r in self.participants.values()],
                "groups": sorted({(r.group or "(all)") for r in self.participants.values()})}
