"""Relationship-pattern detector.

Detects PII by recognising *links* between person-like tokens and sensitive
attribute keywords rather than purely structural patterns.  This catches
constructions that structural/regex detectors miss, such as:

  EN:  "John's email is alice@example.com"
       "the SSN of Maria Santos is 123-45-6789"
  PT:  "o CPF de João Silva é 123.456.789-09"
  ES:  "el DNI de Carlos García es X1234567A"
  PL:  "PESEL Jana Kowalskiego to 44051401358"

The detector emits NAME entities for the person-like token and also
promotes confidence of nearby already-detected PII (overlap with
existing findings is resolved by the standard conflict resolution).

Strategy
--------
1. Build a regex of possessive or prepositional link patterns per language.
2. For each match, extract the person-name candidate and the attribute keyword.
3. Map the attribute keyword to a PII entity type.
4. Emit the person-name span as a NAME finding.
5. Also emit an attribute span for the following value if not already covered
   by a higher-priority detector (the caller handles dedup via conflict resolution).
"""
import re
from typing import Dict, List, Union

from upf.config import UPFConfig

# ---------------------------------------------------------------------------
# Person-like token: one or two capitalised words (first/last name)
# Uses [ \t] to avoid crossing newline boundaries.
# ---------------------------------------------------------------------------
_PERSON_TOKEN = (
    r"([A-ZÀ-Ý][a-zà-ÿ]+(?:-[A-ZÀ-Ý][a-zà-ÿ]+)?"
    r"(?:[ \t]+[A-ZÀ-Ý][a-zà-ÿ]+(?:-[A-ZÀ-Ý][a-zà-ÿ]+)?)?)"
)

# ---------------------------------------------------------------------------
# Sensitive attribute keywords mapped to entity types
# (used to identify what follows the person reference)
# ---------------------------------------------------------------------------
_ATTR_MAP: List[tuple] = [
    # EN
    (r"e-?mail(?:\s+(?:address|is|:))?", "EMAIL"),
    (r"phone(?:\s+(?:number|is|:))?", "PHONE"),
    (r"(?:mobile|cell)(?:\s+(?:number|is|:))?", "PHONE"),
    (r"ssn(?:\s+is)?", "PESEL"),
    (r"social\s+security(?:\s+number)?(?:\s+is)?", "PESEL"),
    (r"(?:credit\s+)?card(?:\s+number)?(?:\s+is)?", "CREDIT_CARD"),
    (r"iban(?:\s+is)?", "IBAN"),
    (r"address(?:\s+is)?", "ADDRESS"),
    (r"passport(?:\s+(?:number|is))?", "PESEL"),
    # PT
    (r"e-?mail", "EMAIL"),
    (r"telefone(?:\s+(?:é|is))?", "PHONE"),
    (r"cpf(?:\s+(?:é|is))?", "BR_CPF"),
    (r"nif(?:\s+(?:é|is))?", "PT_NIF"),
    (r"niss(?:\s+(?:é|is))?", "PT_NISS"),
    # ES
    (r"correo(?:\s+electrónico)?(?:\s+es)?", "EMAIL"),
    (r"teléfono(?:\s+es)?", "PHONE"),
    (r"dni(?:\s+es)?", "PESEL"),
    # PL
    (r"pesel(?:\s+to)?", "PESEL"),
    (r"telefon(?:\s+to)?", "PHONE"),
]

# Possessive / prepositional link patterns:
#   EN: "John's X is …", "the X of John …"
#   PT/ES: "o X de João …", "el X de …"
#   PL: "X Jana …" (genitive, approximate)
_ATTR_PATTERN = "|".join(f"(?:{p})" for p, _ in _ATTR_MAP)
_ATTR_TYPE_MAP = [(re.compile(p, re.IGNORECASE), t) for p, t in _ATTR_MAP]

# Possessive: "John's email", "Maria's phone number"
# Uses [ \t]+ to avoid crossing newlines.
_POSSESSIVE_RE = re.compile(
    rf"{_PERSON_TOKEN}'s[ \t]+(?:{_ATTR_PATTERN})",
    re.IGNORECASE | re.UNICODE,
)

# Prepositional "of/de/do/del": "the email of John Smith", "o CPF de João"
# Uses [ \t]+ to avoid crossing newlines.
_OF_PREP_RE = re.compile(
    rf"(?:the[ \t]+)?(?:{_ATTR_PATTERN})[ \t]+(?:of|de|do|da|del|di)[ \t]+{_PERSON_TOKEN}",
    re.IGNORECASE | re.UNICODE,
)

# Genitive-style: "PESEL Jana Kowalskiego" (attribute then person, no connector)
# Uses [ \t]+ instead of \s+ so the match does not cross newline boundaries.
_GENITIVE_RE = re.compile(
    rf"(?:{_ATTR_PATTERN})[ \t]+{_PERSON_TOKEN}",
    re.IGNORECASE | re.UNICODE,
)


def _add(findings, seen, start, end, text, etype):
    if (start, end) not in seen and end > start:
        seen.add((start, end))
        findings.append({"type": etype, "start": start, "end": end, "text": text})


def detect_relationship_entities(
    text: str, config: UPFConfig
) -> List[Dict[str, Union[int, str]]]:
    """Detect person–attribute relationships and emit NAME spans for the person tokens."""
    if not config.redact_names:
        return []

    findings: List[Dict[str, Union[int, str]]] = []
    seen: set = set()

    # Possessive: "John's email"
    for m in _POSSESSIVE_RE.finditer(text):
        person = m.group(1)
        p_start = m.start(1)
        p_end = p_start + len(person)
        _add(findings, seen, p_start, p_end, person, "NAME")

    # Prepositional: "the email of John Smith"
    for m in _OF_PREP_RE.finditer(text):
        # person is the last capture group
        person = m.group(m.lastindex)
        p_start = m.start(m.lastindex)
        p_end = p_start + len(person)
        _add(findings, seen, p_start, p_end, person, "NAME")

    # Genitive: "PESEL Jana"
    for m in _GENITIVE_RE.finditer(text):
        person = m.group(m.lastindex)
        p_start = m.start(m.lastindex)
        p_end = p_start + len(person)
        _add(findings, seen, p_start, p_end, person, "NAME")

    findings.sort(key=lambda f: (int(f["start"]), int(f["end"])))
    return findings
