"""Masking, for the one seam where data may leave the installation (ADR-0002).

Redaction happens **server-side, before a request is constructed** — never by
instructing a model to ignore something it has already been sent. By the time a
prompt says "do not store this", the data has left.

Level 1 may be sent as-is. Level 2 may be sent masked, by the rules below.
Level 3 may not be sent at all, and is not this module's job: it is kept out by
never putting it in the payload.
"""

from __future__ import annotations

import re
from enum import IntEnum

from app.ocr.extraction import CNIC, EMAIL, IBAN_PK, MOBILE


class DataLevel(IntEnum):
    """ADR-0002's classification, as a type rather than a convention."""

    SAFE = 1
    """No personal information. May be sent to a hosted model."""

    REDACTED = 2
    """Personal, but sendable once masked."""

    RESTRICTED = 3
    """Never leaves the installation."""


class RestrictedDataError(RuntimeError):
    """Raised when Level 3 data reaches an egress path. Always a bug."""


def mask_cnic(value: str) -> str:
    """``35202-1234567-1`` → ``35202-XXXXXXX-X``.

    The province prefix survives because it is not identifying on its own and
    it lets a model reason about region; everything that identifies a person
    does not.
    """
    match = CNIC.search(value)

    if not match:
        return value

    return f"{match.group(1)}-XXXXXXX-X"


def mask_name(value: str) -> str:
    """``Muhammad Ali`` → ``M. A.``

    Initials, settled deliberately in ADR-0002. A blank would stop a model
    telling two people apart inside one workflow; a full name defeats the point
    of the level entirely.
    """
    parts = [p for p in re.split(r"\s+", value.strip()) if p]

    if not parts:
        return value

    return " ".join(f"{p[0].upper()}." for p in parts)


def mask_mobile(value: str) -> str:
    """``923001234567`` → ``92*********`` — country code only."""
    digits = re.sub(r"\D", "", value)

    if len(digits) < 4:
        return "*" * len(digits)

    return digits[:2] + "*" * (len(digits) - 2)


def mask_email(value: str) -> str:
    """``ali@example.com`` → ``a***@example.com``.

    The domain survives: it is rarely identifying and often meaningful (a firm,
    a bank). The local part is what names the person.
    """
    if "@" not in value:
        return "*" * len(value)

    local, _, domain = value.partition("@")

    if not local:
        return f"***@{domain}"

    return f"{local[0]}***@{domain}"


def mask_iban(value: str) -> str:
    """``PK36SCBL0000001123456702`` → ``PK36****************6702``.

    Country and check digits at the front, last four at the back — enough for a
    human to recognise the account they meant, not enough to pay into it.
    """
    cleaned = value.replace(" ", "")

    if len(cleaned) <= 8:
        return "*" * len(cleaned)

    return cleaned[:4] + "*" * (len(cleaned) - 8) + cleaned[-4:]


def mask_address(value: str) -> str:
    """Keeps only the last comma-separated component — usually the city."""
    parts = [p.strip() for p in value.split(",") if p.strip()]

    return parts[-1] if parts else value


def redact_text(text: str) -> str:
    """Mask every recognisable identifier in free text.

    For prose that must be summarised by a hosted model. Ordered longest-pattern
    first: an email contains no CNIC, but masking a mobile before an IBAN can
    chew through digits the IBAN pattern needed.
    """
    text = IBAN_PK.sub(lambda m: mask_iban(m.group(0)), text)
    text = CNIC.sub(lambda m: mask_cnic(m.group(0)), text)
    text = EMAIL.sub(lambda m: mask_email(m.group(0)), text)
    text = MOBILE.sub(lambda m: mask_mobile(m.group(0)), text)

    return text


# Field names that must never appear in an outbound payload. Matched on the key
# rather than the value: a key called "cnic" carrying something unrecognisable
# is still a CNIC field, and the failure should be loud.
RESTRICTED_KEYS = frozenset(
    {
        "cnic",
        "passport",
        "passport_number",
        "fbr_password",
        "iris_password",
        "password",
        "raw_text",
        "document",
        "file",
        "file_path",
        "attachment",
        "image",
    }
)


def assert_no_restricted(payload: dict[str, object], *, path: str = "payload") -> None:
    """Refuse to send anything carrying Level 3 data.

    The last gate before egress. Raising is correct: silently dropping the field
    would let a caller believe it had been sent, and silently sending it is the
    outcome this whole policy exists to prevent.
    """
    for key, value in payload.items():
        here = f"{path}.{key}"

        if key.lower() in RESTRICTED_KEYS:
            raise RestrictedDataError(
                f"{here} is Level 3 and must never leave the installation (ADR-0002)."
            )

        if isinstance(value, dict):
            assert_no_restricted(value, path=here)
        elif isinstance(value, list):
            for index, item in enumerate(value):
                if isinstance(item, dict):
                    assert_no_restricted(item, path=f"{here}[{index}]")
