"""Blind-review PII scrubbing for reviewer-facing application context.

PRD §5.1 FRFA-AM020 / FRFA-AM025 require that when `GrantCall.blind_review`
is enabled, reviewers never see the applicant's name, email, institution, or
anything that could correlate back to the applicant. Historically this was
enforced only by replacing the display reference with `anonymized_code` in
reviewer templates. That is sufficient today because the reviewer template
(`grants/review_submit.html`) does not render proposal text or uploaded
documents — but any future change that adds proposal text or file previews to
the reviewer UI MUST route the context through `blinded_application_context`
below to preserve the invariant.

The helper scrubs free-text (proposal body, reviewer-visible summaries) and
replaces document display names with generic labels. File bytes themselves
still contain the applicant's embedded metadata (PDF `/Author`, docx core.xml)
— stripping those is tracked as a follow-up in the P0 plan and is out of
scope for this helper.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Iterable

from apps.rims.grants.models import Application


def _tokenize_pii(application: Application) -> list[str]:
    """Collect the user-identifying strings we know about for this application.

    Order matters: longer strings are listed first so we substitute them before
    their substrings — e.g. "Jane Q. Doe" must be replaced before "Doe".
    """
    applicant = application.applicant
    tokens: list[str] = []
    full_name = (applicant.get_full_name() or "").strip()
    if full_name and full_name != applicant.email:
        tokens.append(full_name)
    first = (applicant.first_name or "").strip()
    last = (applicant.last_name or "").strip()
    if first:
        tokens.append(first)
    if last:
        tokens.append(last)
    if applicant.email:
        tokens.append(applicant.email)
    institution = getattr(application, "institution", None)
    if institution and institution.name:
        tokens.append(institution.name)
    # De-duplicate while preserving "longest first" ordering.
    seen: set[str] = set()
    ordered: list[str] = []
    for t in sorted(tokens, key=len, reverse=True):
        key = t.lower()
        if key in seen or len(t) < 2:
            continue
        seen.add(key)
        ordered.append(t)
    return ordered


def scrub_author_tokens(text: str, tokens: Iterable[str], *, replacement: str = "[redacted]") -> str:
    """Replace each token in *text* (case-insensitive, whole-or-partial match)
    with *replacement*. Preserves surrounding punctuation."""
    if not text:
        return text
    scrubbed = text
    for token in tokens:
        if not token:
            continue
        # re.escape so special chars in names/emails (".", "+") do not break the regex.
        pattern = re.compile(re.escape(token), flags=re.IGNORECASE)
        scrubbed = pattern.sub(replacement, scrubbed)
    return scrubbed


@dataclass(frozen=True)
class BlindedApplicationContext:
    """Minimal view of an application suitable for a reviewer when blind_review
    is enabled. All applicant-identifying fields are either replaced with
    `[redacted]` or reduced to `anonymized_code`."""

    reference: str
    proposal: str
    documents: list[tuple[str, object]]  # (display_label, original_document)


def blinded_application_context(application: Application) -> BlindedApplicationContext:
    """Return a reviewer-safe context object for *application*.

    Callers must treat the returned object as the only legitimate source of
    application content to render to reviewers when `call.blind_review` is
    true. Passing unblinded fields from `application` directly into a reviewer
    template is a defect.
    """
    tokens = _tokenize_pii(application)
    scrubbed_proposal = scrub_author_tokens(application.proposal or "", tokens)
    documents = [
        (f"Document {idx}", doc)
        for idx, doc in enumerate(application.documents.all().order_by("pk"), start=1)
    ]
    return BlindedApplicationContext(
        reference=application.anonymized_code or "ANON",
        proposal=scrubbed_proposal,
        documents=documents,
    )
