"""Fake UniVerse DB fixture: Class Attendance Register.

Generates realistic-looking records that mimic what you'd find in a
UniVerse file called ATTENDANCE.

UniVerse stores records as dynamic arrays delimited by system marks:
    - AM  (Attribute Mark)  chr(254)  separates fields/attributes
    - VM  (Value Mark)      chr(253)  separates multi-values within a field
    - SVM (SubValue Mark)   chr(252)  separates sub-values within a value

The data dictionary describes the structure of each field.  UniVerse supports
two primary dictionary styles which can be mixed in the same DICT file:

    Prime style  (D-type descriptors)
    Pick  style  (A/S-type descriptors)

Both are represented here so callers can test against either format.

Attribute layout for ATTENDANCE records:
    Attr 1 : STUDENT_ID   (e.g. "STU-0042")
    Attr 2 : STUDENT_NAME (e.g. "Thandi Mokoena")
    Attr 3 : CLASS        (e.g. "CS201")
    Attr 4 : DATE         (internal date integer, conv D4/)
    Attr 5 : STATUS       (P=Present, A=Absent, L=Late, E=Excused)
    Attr 6 : TIME_IN      (e.g. "08:02")
    Attr 7 : NOTES        (free text, may be multi-valued)

Keys are formatted as: ATT-{class}-{date}-{student_seq}
    e.g. ATT-CS201-20260310-0042
"""

from __future__ import annotations

import random
from dataclasses import dataclass
from typing import Any


# ---------------------------------------------------------------------------
# UniVerse system delimiters
# ---------------------------------------------------------------------------

AM = chr(254)    # Attribute Mark  — separates fields
VM = chr(253)    # Value Mark      — separates multi-values within a field
SVM = chr(252)   # SubValue Mark   — separates sub-values within a value


# ---------------------------------------------------------------------------
# Data dictionary definitions (both styles)
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class PrimeDictEntry:
    """Prime-style (D-type) dictionary descriptor.

    Layout of a D-type dict record (attributes):
        <0>  D
        <1>  field_no        — attribute position in the data record
        <2>  conversion      — conversion code (e.g. D4/, MT, "")
        <3>  heading         — column heading for reports
        <4>  format          — display format (width + justification)
        <5>  sm              — S=singlevalue, M=multivalue
    """

    field_no: int
    conversion: str
    heading: str
    fmt: str
    sm: str  # "S" or "M"

    def to_dynamic_array(self) -> str:
        return AM.join([
            "D",
            str(self.field_no),
            self.conversion,
            self.heading,
            self.fmt,
            self.sm,
        ])


@dataclass(frozen=True)
class PickDictEntry:
    """Pick-style (A-type) dictionary descriptor.

    Layout of an A-type dict record (attributes):
        <0>  A
        <1>  field_no        — attribute position in the data record
        <2>  conversion      — conversion code (e.g. D4/, MT, "")
        <3>  heading         — column heading (may contain VM for multi-line)
        <4>  format          — display format (width + justification)
        <5>  association      — association name (or "")
    """

    field_no: int
    conversion: str
    heading: str
    fmt: str
    association: str

    def to_dynamic_array(self) -> str:
        return AM.join([
            "A",
            str(self.field_no),
            self.conversion,
            self.heading,
            self.fmt,
            self.association,
        ])


# Prime-style dictionary for ATTENDANCE file.
PRIME_DICT: dict[str, PrimeDictEntry] = {
    "STUDENT_ID":   PrimeDictEntry(field_no=1, conversion="",    heading="Student ID",   fmt="10L", sm="S"),
    "STUDENT_NAME": PrimeDictEntry(field_no=2, conversion="",    heading="Student Name", fmt="25L", sm="S"),
    "CLASS":        PrimeDictEntry(field_no=3, conversion="",    heading="Class",        fmt="8L",  sm="S"),
    "DATE":         PrimeDictEntry(field_no=4, conversion="D4/", heading="Date",         fmt="10L", sm="S"),
    "STATUS":       PrimeDictEntry(field_no=5, conversion="",    heading="Status",       fmt="1L",  sm="S"),
    "TIME_IN":      PrimeDictEntry(field_no=6, conversion="",    heading="Time In",      fmt="5L",  sm="S"),
    "NOTES":        PrimeDictEntry(field_no=7, conversion="",    heading="Notes",        fmt="30L", sm="M"),
}

# Pick-style dictionary for the same file.
PICK_DICT: dict[str, PickDictEntry] = {
    "STUDENT_ID":   PickDictEntry(field_no=1, conversion="",    heading="Student ID",   fmt="10L", association=""),
    "STUDENT_NAME": PickDictEntry(field_no=2, conversion="",    heading="Student Name", fmt="25L", association=""),
    "CLASS":        PickDictEntry(field_no=3, conversion="",    heading="Class",        fmt="8L",  association=""),
    "DATE":         PickDictEntry(field_no=4, conversion="D4/", heading="Date",         fmt="10L", association=""),
    "STATUS":       PickDictEntry(field_no=5, conversion="",    heading="Status",       fmt="1L",  association=""),
    "TIME_IN":      PickDictEntry(field_no=6, conversion="",    heading="Time In",      fmt="5L",  association=""),
    "NOTES":        PickDictEntry(field_no=7, conversion="",    heading="Notes",        fmt="30L", association="NOTES_ASSOC"),
}

# Ordered list of field names matching attribute positions 1-7.
FIELD_ORDER = ["STUDENT_ID", "STUDENT_NAME", "CLASS", "DATE", "STATUS", "TIME_IN", "NOTES"]


# ---------------------------------------------------------------------------
# Internal date conversion (simplified D4/ equivalent)
# ---------------------------------------------------------------------------

_EPOCH = 0  # UniVerse internal date epoch = 31 Dec 1967

def _date_to_internal(iso_date: str) -> int:
    """Convert 'YYYY-MM-DD' to a UniVerse internal date integer.

    UniVerse dates are days since 31 Dec 1967.
    """
    from datetime import date as _date
    epoch = _date(1967, 12, 31)
    y, m, d = (int(x) for x in iso_date.split("-"))
    delta = _date(y, m, d) - epoch
    return delta.days


def _internal_to_date(internal: int) -> str:
    """Convert a UniVerse internal date integer back to 'YYYY-MM-DD'."""
    from datetime import date as _date, timedelta
    epoch = _date(1967, 12, 31)
    return str(epoch + timedelta(days=internal))


# ---------------------------------------------------------------------------
# Helpers: build and parse dynamic-array records
# ---------------------------------------------------------------------------

def record_to_dynamic_array(record: dict[str, Any]) -> str:
    """Convert a field dict to a UniVerse dynamic array (AM-delimited string).

    The DATE field is stored as an internal integer (matching D4/ conversion).
    The NOTES field may contain multi-values (VM-delimited).
    """
    parts: list[str] = []
    for field_name in FIELD_ORDER:
        value = record.get(field_name, "")
        if field_name == "DATE" and isinstance(value, str) and "-" in value:
            value = str(_date_to_internal(value))
        elif field_name == "NOTES" and isinstance(value, list):
            value = VM.join(value)
        parts.append(str(value))
    return AM.join(parts)


def dynamic_array_to_record(
    raw: str,
    *,
    style: str = "prime",
) -> dict[str, Any]:
    """Parse a UniVerse dynamic array back into a field dict.

    Args:
        raw: AM-delimited string as returned by uopy File.read().
        style: 'prime' or 'pick' — which dict definitions to use for
               conversions. Both produce the same result for this file since
               the field layout is identical.

    Returns:
        A dict with human-readable field names and converted values.
    """
    dict_defs = PRIME_DICT if style == "prime" else PICK_DICT
    attrs = raw.split(AM)
    record: dict[str, Any] = {}

    for field_name in FIELD_ORDER:
        entry = dict_defs[field_name]
        idx = entry.field_no - 1  # 0-based index
        raw_val = attrs[idx] if idx < len(attrs) else ""

        # Apply conversion.
        if entry.conversion == "D4/" and raw_val:
            try:
                raw_val = _internal_to_date(int(raw_val))
            except (ValueError, OverflowError):
                pass  # Leave as-is if it doesn't parse.

        # Handle multi-values.
        if VM in raw_val:
            raw_val = raw_val.split(VM)

        record[field_name] = raw_val

    return record

_STUDENTS = [
    ("STU-0001", "Thandi Mokoena"),
    ("STU-0002", "Sipho Ndlovu"),
    ("STU-0003", "Lerato Dlamini"),
    ("STU-0004", "Kagiso Nkosi"),
    ("STU-0005", "Nomsa Zulu"),
    ("STU-0006", "Bongani Khumalo"),
    ("STU-0007", "Zanele Mthembu"),
    ("STU-0008", "Mandla Sithole"),
    ("STU-0009", "Palesa Mashaba"),
    ("STU-0010", "Thabiso Molefe"),
    ("STU-0011", "Ayanda Cele"),
    ("STU-0012", "Lindiwe Ngcobo"),
    ("STU-0013", "Vuyo Mahlangu"),
    ("STU-0014", "Sihle Mkhize"),
    ("STU-0015", "Nompilo Zwane"),
    ("STU-0016", "Sbusiso Radebe"),
    ("STU-0017", "Thandeka Nxumalo"),
    ("STU-0018", "Lwazi Mbatha"),
    ("STU-0019", "Nozipho Shabalala"),
    ("STU-0020", "Bheki Gumede"),
]

_CLASSES = ["CS101", "CS201", "CS301", "MATH101", "MATH201", "ENG101"]

_STATUSES = ["P", "P", "P", "P", "P", "P", "P", "A", "L", "E"]  # ~70% present

_DATES = [
    "2026-03-02", "2026-03-03", "2026-03-04", "2026-03-05", "2026-03-06",
    "2026-03-09", "2026-03-10", "2026-03-11", "2026-03-12", "2026-03-13",
]

_TIMES_IN = {
    "P": ["07:55", "07:58", "08:00", "08:01", "08:02", "08:05"],
    "L": ["08:15", "08:22", "08:30", "08:45", "09:00"],
    "A": [""],
    "E": [""],
}

_LATE_NOTES = [
    "Traffic on N1",
    "Bus delayed",
    "Taxi broke down",
    "Alarm didn't go off",
    "",
]

_EXCUSED_NOTES = [
    "Doctor's appointment",
    "Family emergency",
    "University event",
    "Sports fixture",
]


def generate_attendance_register(
    n: int = 500,
    *,
    seed: int = 42,
) -> dict[str, str]:
    """Generate a fake attendance register with *n* records.

    Records are stored as UniVerse dynamic arrays (AM-delimited strings), just
    like they'd be returned by ``uopy.File.read()``.

    Args:
        n: Number of attendance records to generate.
        seed: Random seed for reproducibility.

    Returns:
        A dict mapping record key -> dynamic array string.

    Example::

        data = generate_attendance_register(5)
        for key, raw in data.items():
            parsed = dynamic_array_to_record(raw)
            print(key, parsed["STUDENT_NAME"], parsed["STATUS"])
    """
    rng = random.Random(seed)
    records: dict[str, str] = {}

    for i in range(n):
        student_id, student_name = rng.choice(_STUDENTS)
        class_code = rng.choice(_CLASSES)
        date_iso = rng.choice(_DATES)
        status = rng.choice(_STATUSES)

        time_in = rng.choice(_TIMES_IN[status])

        # Build notes — occasionally multi-valued to exercise VM handling.
        notes: list[str] = []
        if status == "L":
            notes.append(rng.choice(_LATE_NOTES))
            if rng.random() < 0.3:
                notes.append("Warned by lecturer")
        elif status == "E":
            notes.append(rng.choice(_EXCUSED_NOTES))
            if rng.random() < 0.2:
                notes.append("Documentation provided")

        # Convert date to internal integer (matches D4/ conversion).
        date_internal = str(_date_to_internal(date_iso))

        # Build dynamic array: attr1þattr2þ...attr7
        parts = [
            student_id,                      # attr 1
            student_name,                    # attr 2
            class_code,                      # attr 3
            date_internal,                   # attr 4
            status,                          # attr 5
            time_in,                         # attr 6
            VM.join(notes) if notes else "", # attr 7  (may be multi-valued)
        ]
        raw = AM.join(parts)

        date_compact = date_iso.replace("-", "")
        seq = student_id.split("-")[1]
        key = "ATT-%s-%s-%s" % (class_code, date_compact, seq)

        # If duplicate key (same student, class, date), append a suffix.
        if key in records:
            key = "%s-%04d" % (key, i)

        records[key] = raw

    return records


def generate_dict_records(
    n: int = 500,
    *,
    seed: int = 42,
    style: str = "prime",
) -> dict[str, dict[str, Any]]:
    """Convenience: generate *and* parse records into field dicts.

    Equivalent to calling :func:`generate_attendance_register` followed by
    :func:`dynamic_array_to_record` on every record.

    Args:
        n: Number of attendance records to generate.
        seed: Random seed for reproducibility.
        style: ``'prime'`` or ``'pick'`` dict style for parsing.

    Returns:
        A dict mapping record key -> parsed field dict.
    """
    raw_data = generate_attendance_register(n, seed=seed)
    return {
        key: dynamic_array_to_record(raw, style=style)
        for key, raw in raw_data.items()
    }
