"""The board — what a pile of drag events adds up to.

Every drag is one append-only row in Board-Register.csv (see storage.BOARD_HEADER). Nothing in
here edits a row; the state of the board is derived from the rows every time, same rule as the
timesheet derives hours from taps. That is deliberate and it is the whole reason the board is worth
anything: a zone card marked done, with the men and the times already on it, IS the production
record. Nobody has to write it up afterwards, and nobody can quietly change it later.

Two altitudes, one shape:
  foreman  - names move between ZONES on his job
  admin    - names move between JOBS for tomorrow, written to Roster.csv

The diary reads the same rows back as sentences. See works_lines().
"""
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import date, datetime, timedelta

# What a formwork/reo deck actually does, in the order it does it. The foreman drags one out and
# types three words - "5m seats", "bay 3 deck" - and that is a zone. Names, not a taxonomy.
DECK = ["SET OUT", "PREP", "SHUTTER", "STEEL", "INSPECT", "POUR", "STRIP", "CLEAN"]

# Always on every board. ROAMING is the bloke doing bits and pieces; DAYWORKS is the money one -
# dragging a man onto it is the T&M evidence writing itself.
ALWAYS = ["ROAMING", "DAYWORKS"]

# How a pour progresses. One ladder, and one hold point on it.
STAGES = ["formed", "steel", "inspected", "poured", "stripped", "cleaned"]
# You do not pour what nobody has looked at. This is the only rule the board enforces against the
# foreman, and it is enforced because the consequence of getting it wrong is a demolition bill.
HOLD = {"poured": "inspected"}

ESTIMATES = ["1h", "3h", "today", "tomorrow"]

# What a chip long-press offers. Both are records, not just a tidy-up of the screen.
CHIP_ACTIONS = {"home": "sent home", "moved": "moved job", "off": "off the tools"}

PLACERS = ("on", "off", "home", "moved")


def clean_zone(name: str) -> str:
    """Three words, as typed, tidied. Uppercase because it is read at arm's length in the sun."""
    return " ".join((name or "").split())[:24].upper()


def is_t5(detail: str) -> str:
    """'t5:20260825-1512-matty and a bit' -> the id. '' if this row did not come off a Take 5."""
    d = (detail or "").strip()
    if not d.startswith("t5:"):
        return ""
    return d[3:].split(" ", 1)[0]


@dataclass
class Zone:
    name: str
    kind: str = ""                      # the deck card it came off, if any
    stage: str = ""                     # where it is up to now
    stages_done: list = field(default_factory=list)
    estimate: str = ""
    done: bool = False
    fixed: bool = False                 # ROAMING / DAYWORKS - always there, never created
    people: list = field(default_factory=list)

    @property
    def next_stage(self) -> str:
        if self.stage in STAGES and STAGES.index(self.stage) < len(STAGES) - 1:
            return STAGES[STAGES.index(self.stage) + 1]
        return "" if self.stage == STAGES[-1] else STAGES[0]

    @property
    def title(self) -> str:
        return self.kind + " — " + self.name if self.kind and self.kind != self.name else self.name


@dataclass
class Span:
    """One bloke, on one zone, between two times. The pricework line, free."""
    zone: str
    kind: str
    person: str
    start: str
    finish: str = ""
    task: str = ""

    @property
    def times(self) -> str:
        return self.start + "–" + self.finish if self.finish else self.start + "– on now"


def _hhmm(ts: str) -> str:
    return (ts or "")[11:16]


def state(rows: list[dict]) -> tuple[dict, dict]:
    """(zones by name, where each man is). Rows in file order - the file IS the order.

    A man is on at most one zone: dragging him somewhere new takes him off the old one without
    anybody having to remember to. The register still holds both rows.
    """
    zones: dict[str, Zone] = {n: Zone(name=n, fixed=True) for n in ALWAYS}
    where: dict[str, str] = {}
    for r in rows:
        zn = clean_zone(r.get("zone", ""))
        person = (r.get("person") or "").strip()
        action = (r.get("action") or "").strip().lower()
        detail = (r.get("detail") or "").strip()
        if action == "created" and zn:
            zones.setdefault(zn, Zone(name=zn))
            zones[zn].kind = detail or zones[zn].kind
            zones[zn].done = False
        elif action == "stage" and zn in zones and detail in STAGES:
            zones[zn].stage = detail
            if detail not in zones[zn].stages_done:
                zones[zn].stages_done.append(detail)
        elif action == "estimate" and zn in zones:
            zones[zn].estimate = detail
        elif action == "done" and zn in zones:
            zones[zn].done = True
        elif action in PLACERS and person:
            if action == "on" and zn:
                zones.setdefault(zn, Zone(name=zn))
                where[person] = zn
            else:
                where.pop(person, None)
    for person, zn in where.items():
        if zn in zones:
            zones[zn].people.append(person)
    return zones, where


def spans(rows: list[dict], zones: dict) -> list[Span]:
    """Every stint on the board today, closed off by whatever moved the man next."""
    open_at: dict[str, tuple[str, str, str]] = {}     # person -> (zone, started, task)
    out: list[Span] = []

    def close(person: str, at: str):
        got = open_at.pop(person, None)
        if got:
            zn, started, task = got
            out.append(Span(zn, (zones.get(zn) or Zone(zn)).kind, person, started, at, task))

    for r in rows:
        action = (r.get("action") or "").strip().lower()
        if action not in PLACERS:
            continue
        person = (r.get("person") or "").strip()
        if not person:
            continue
        ts = _hhmm(r.get("timestamp", ""))
        close(person, ts)
        if action == "on":
            zn = clean_zone(r.get("zone", ""))
            detail = (r.get("detail") or "").strip()
            task = detail.split(" ", 1)[1] if is_t5(detail) and " " in detail else detail
            open_at[person] = (zn, ts, task)
    for person, got in open_at.items():
        zn, started, task = got
        out.append(Span(zn, (zones.get(zn) or Zone(zn)).kind, person, started, "", task))
    return out


def works_lines(rows: list[dict]) -> list[str]:
    """The WORKS section of the diary, written by the board while the foreman was doing something else.

    'STEEL - 5M SEATS - Matty Bell, 07:04-11:15'. One line per stint, then what moved on each zone.
    """
    zones, _ = state(rows)
    lines = []
    by_zone: dict[str, list] = {}
    for s in spans(rows, zones):
        by_zone.setdefault(s.zone, []).append(s)
    for zn, ss in by_zone.items():
        z = zones.get(zn) or Zone(zn)
        head = z.kind + " — " + zn if z.kind and z.kind != zn else zn
        for s in ss:
            task = " (" + s.task + ")" if s.task else ""
            lines.append(head + " — " + s.person + ", " + s.times + task)
    for r in rows:
        action = (r.get("action") or "").strip().lower()
        zn = clean_zone(r.get("zone", ""))
        when = _hhmm(r.get("timestamp", ""))
        who = (r.get("by") or "").strip()
        person = (r.get("person") or "").strip()
        detail = (r.get("detail") or "").strip()
        if action == "stage":
            lines.append(zn + " — " + detail + " " + when + (" (" + who + ")" if who else ""))
        elif action == "done":
            lines.append(zn + " — finished " + when + (" (" + who + ")" if who else ""))
        elif action == "home":
            lines.append(person + " sent home " + when + (" — " + detail if detail else ""))
        elif action == "moved":
            lines.append(person + " moved to " + detail + " " + when)
    return lines


# How the hold point reads out loud. "needs inspected first" is not English on a phone screen.
HOLD_WORDS = {"inspected": "inspection"}


def hold_block(zone: Zone, target: str) -> str:
    """'' = go ahead. Anything else is the words that go on the foreman's screen."""
    need = HOLD.get(target)
    if need and need not in zone.stages_done:
        return "needs " + HOLD_WORDS.get(need, need) + " first"
    return ""


# --------------------------------------------------------------------------- roster
#
# Tomorrow, not today. The board is written of an evening and the answer it produces is "who is
# where in the morning" - which is exactly the message somebody already types into WhatsApp, and
# that habit is the hook. If the message does not come off the board, the board is dead in a week.


def next_working_day(today: date) -> date:
    """Tomorrow, unless tomorrow is a Sunday. Saturdays are worked and always have been."""
    nxt = today + timedelta(days=1)
    if nxt.weekday() == 6:
        nxt += timedelta(days=1)
    return nxt


def roster_for(rows: list[dict], day: str, person: str) -> str:
    """Where this bloke is on that date. Last row wins - the file is a history, this is the state."""
    key = " ".join((person or "").split()).lower()
    job = ""
    for r in rows:
        if (r.get("date") or "").strip() == day and " ".join((r.get("person") or "").split()).lower() == key:
            job = (r.get("job") or "").strip()
    return job


def roster_by_job(rows: list[dict], day: str) -> dict:
    latest: dict[str, str] = {}
    order: list[str] = []
    for r in rows:
        if (r.get("date") or "").strip() != day:
            continue
        person = " ".join((r.get("person") or "").split())
        if not person:
            continue
        if person not in order:
            order.append(person)
        latest[person] = (r.get("job") or "").strip()
    out: dict[str, list] = {}
    for person in order:
        out.setdefault(latest[person], []).append(person)
    return out


def roster_message(day: date, jobs: list, by_job: dict) -> str:
    """The WhatsApp message, tradie-typed. Copied out by hand - nothing here sends anything."""
    lines = [day.strftime("%a %d/%m").upper(), ""]
    got = False
    for j in jobs:
        names = by_job.get(j["key"], [])
        if not names:
            continue
        got = True
        lines.append((j["label"] + " " + j["key"]).strip())
        lines += sorted(names, key=str.lower)
        lines.append("")
    if not got:
        return lines[0] + "\n\nNobody rostered yet."
    return "\n".join(lines).rstrip() + "\n\nAny problems ring me."


def row(now: datetime, job: str, zone: str, person: str, action: str, detail: str, by: str) -> dict:
    return {"timestamp": now.isoformat(timespec="seconds"), "job": job, "zone": zone,
            "person": person, "action": action, "detail": detail, "by": by}
