"""Comments + notifications — the Interaction Standard's first implementation
(04 local-only mode), modelled on review.bowden.works and born here for later
extraction into the shared kit.

The split that matters: COMMENTS live with the item (one thread per item, and
the workflow's own flag/reply messages post into the same thread, so there is
exactly one conversation stream). NOTIFICATIONS aggregate per person — deep
link, human context label, dedupe-keyed, never notifying the author of their
own action.

Recipient sides (plan §1: two kinds of people):
  * client side of a punchlist = users granted on it who do NOT hold act_team
  * internal side = members with the act_team permission
"""

import re

from sqlalchemy import select

from app import accounts
from app import bw_accounts as bwa
from app.models import Comment, Item, Notification, Thread, utcnow

_MENTION = re.compile(r"@([a-zA-Z0-9_.-]{2,64})")
_PREVIEW_LEN = 160


def _preview(text: str) -> str:
    return re.sub(r"\s+", " ", (text or "")).strip()[:_PREVIEW_LEN]


def thread_for_item(session, item: Item) -> Thread:
    t = session.execute(select(Thread).where(
        Thread.subject_type == "item", Thread.subject_id == item.id)).scalars().first()
    if t is None:
        t = Thread(punchlist_id=item.punchlist_id, subject_type="item", subject_id=item.id)
        session.add(t)
        session.flush()
    return t


def comments_of(session, item: Item) -> list[Comment]:
    t = session.execute(select(Thread).where(
        Thread.subject_type == "item", Thread.subject_id == item.id)).scalars().first()
    if t is None:
        return []
    return list(session.execute(
        select(Comment).where(Comment.thread_id == t.id).order_by(Comment.created_at)
    ).scalars())


# ------------------------------------------------------------------ sides

def internal_users() -> list[str]:
    return [m["username"] for m in bwa.members()
            if accounts.app_can(m["username"], accounts.PERM_ACT_TEAM)]


def client_users_of(punchlist_id: str) -> list[str]:
    out = []
    for m in bwa.members():
        u = m["username"]
        if accounts.app_can(u, accounts.PERM_ACT_TEAM):
            continue
        if m.get("grants", {}).get(punchlist_id) or m.get("all_instances"):
            out.append(u)
    return out


# ------------------------------------------------------------------ emit

def _emit(session, *, recipient: str, actor: str, kind: str, item: Item,
          body: str, dedupe: str) -> None:
    if recipient == actor:
        return                       # never notify the author of their own action
    if session.execute(select(Notification.id).where(
            Notification.dedupe_key == dedupe,
            Notification.recipient == recipient)).first():
        return
    session.add(Notification(
        recipient=recipient, actor=actor, kind=kind,
        context_label=item.title, body=_preview(body),
        path=f"/punchlists/{item.punchlist_id}?item={item.id}",
        punchlist_id=item.punchlist_id, item_id=item.id,
        dedupe_key=f"{dedupe}:{recipient}"))


def add_comment(session, item: Item, *, author: str, author_kind: str,
                body: str) -> Comment:
    """One write path for conversation — freeform comments AND the workflow's
    flag/reply messages (bridged by the action layer). Server-side mention
    parsing (review's rule: the client payload never decides who is notified),
    plus thread-participation notifications."""
    body = (body or "").strip()
    if not body:
        raise ValueError("empty comment")
    t = thread_for_item(session, item)
    c = Comment(thread_id=t.id, author=author, author_kind=author_kind, body=body)
    session.add(c)
    session.flush()

    known = {m["username"] for m in bwa.members()} | {bwa._state["owner"]}
    mentioned = {u for u in _MENTION.findall(body) if u in known}
    participants = {x.author for x in comments_of(session, item)} - {author}
    # the other side of the conversation always hears about a reply
    other_side = set(client_users_of(item.punchlist_id)
                     if author_kind == "team" else internal_users())

    for u in mentioned:
        _emit(session, recipient=u, actor=author, kind="mention", item=item,
              body=body, dedupe=f"mention:{c.id}")
    for u in (participants | other_side) - mentioned:
        _emit(session, recipient=u, actor=author, kind="reply", item=item,
              body=body, dedupe=f"reply:{c.id}")
    return c


def notify_turn(session, item: Item, *, actor: str, old_status: str) -> None:
    """The ball changed courts: tell the side whose court it landed in.
    Called after an action when the derived status flipped."""
    if item.status == old_status:
        return
    if item.status == "waiting_on_client":
        recipients, body = client_users_of(item.punchlist_id), "Your turn on this one."
    elif item.status in ("waiting_on_team", "needs_attention"):
        recipients, body = internal_users(), (
            "Needs help." if item.status == "needs_attention" else "Ready for our check.")
    elif item.status == "done":
        recipients, body = client_users_of(item.punchlist_id), "Confirmed and done."
    else:
        return
    seq_marker = f"turn:{item.id}:{item.status}:{utcnow().strftime('%Y%m%d%H%M%S')}"
    for u in recipients:
        _emit(session, recipient=u, actor=actor,
              kind=("turn" if item.status != "done" else "status"),
              item=item, body=body, dedupe=seq_marker)


# ------------------------------------------------------------------ inbox

def inbox(session, username: str, limit: int = 30) -> dict:
    rows = list(session.execute(
        select(Notification).where(Notification.recipient == username)
        .order_by(Notification.read, Notification.created_at.desc()).limit(limit)
    ).scalars())
    unread = [r for r in rows if not r.read]
    needs_you = any(r.kind in ("mention", "turn") for r in unread)
    return {
        "unread": len(unread),
        "needs_you": needs_you,
        "notifications": [{
            "id": r.id, "actor": r.actor, "kind": r.kind,
            "context_label": r.context_label, "body": r.body, "path": r.path,
            "read": r.read, "at": r.created_at.isoformat(),
        } for r in rows],
    }


def mark_read(session, username: str, ids: list[str] | None = None) -> int:
    q = select(Notification).where(Notification.recipient == username,
                                   Notification.read == False)  # noqa: E712
    if ids:
        q = q.where(Notification.id.in_(ids))
    n = 0
    for r in session.execute(q).scalars():
        r.read = True
        n += 1
    session.flush()
    return n
