"""Reading an answer to "whose document is this?".

The notification asks for a reference and a number back:

    DOC-1001 35201-1234567-1

The reference is what makes the answer usable. A self-chat has no threading, so
without it a number arriving an hour later can only be matched by guessing at
the newest or the oldest waiting document — both wrong the moment two are
waiting, and wrong here means one client's bank statement filed onto another
client's record, silently, behind a success message.

## Why a bare number IS accepted here, when identifiers.py refuses one

identifiers.py requires the word "file" before a file number, and the reasoning
is sound: a bare integer in free text is not an identifier. "Sent on 12" and "3
pages" and "2024" all contain one, and a matcher that took any number would file
a document against client 12 because somebody mentioned a date.

That reasoning is about FREE TEXT. This is not free text. A message beginning
with a document reference is an answer to a question that was asked, and the
remaining token is the answer — there is nothing else it could be.

So the relaxation is narrow and conditional, never general:

  • a reference must be present, and
  • what remains after removing it must be only the identifier — not prose
    containing one.

"DOC-1001 2124" is an answer. "DOC-1001 I think this is from 2024 sometime" is
not, and is refused rather than read as file 2024. The original protection is
untouched for every message that is not a reply.
"""

from __future__ import annotations

import re
from dataclasses import dataclass

from app.documents import identifiers

#: The reference as the notification prints it, case-insensitively.
REFERENCE = re.compile(r"\bDOC-(\d{3,10})\b", re.IGNORECASE)

#: What remains once the reference is removed, if it is only an identifier.
#:
#: Anchored at both ends on purpose — this is what keeps the relaxation above
#: from becoming "any number in any sentence".
#:
#: The tail is OPTIONAL, which matters more than it looks. The first version
#: required at least two characters, and an end-to-end test against real data
#: found the hole immediately: this practice's file numbers "run from single
#: digits into the thousands" (identifiers.py), and the very first client on the
#: install is file number 1. "DOC-1061 1" silently parsed as nothing, so the
#: reply would have been ignored and the document left waiting with no sign of
#: why. Every unit test had used a comfortable four-digit number.
BARE_IDENTIFIER = re.compile(r"^[\s:,.\-]*([0-9](?:[0-9\s\-]*[0-9A-Za-z])?)[\s.,!]*$")


@dataclass(frozen=True, slots=True)
class Reply:
    """A reply naming one document and the identifier for it."""

    reference: str
    identifier: str


def read(text: str | None) -> Reply | None:
    """The reply in this message, or None if it is not one.

    None is the common answer and not a failure: most text in a self-chat is
    somebody talking to themselves, and this module's job is to notice the rare
    message that is not.
    """
    if not text:
        return None

    match = REFERENCE.search(text)

    if match is None:
        return None

    reference = f"DOC-{match.group(1)}"
    remainder = text[: match.start()] + text[match.end() :]

    # A CNIC states what it is by its shape, so it is read wherever it sits and
    # whatever surrounds it — the same rule as everywhere else in this software.
    found = identifiers.read(remainder)

    if found.cnic:
        return Reply(reference=reference, identifier=found.cnic)

    if found.file_number:
        return Reply(reference=reference, identifier=found.file_number)

    # Nothing labelled. The narrow relaxation: a reference, and then nothing but
    # a number.
    bare = BARE_IDENTIFIER.match(remainder)

    if bare is None:
        return None

    identifier = re.sub(r"\s+", "", bare.group(1))

    return Reply(reference=reference, identifier=identifier) if identifier else None
