"""Privacy tokenizer pre-layer.

Performs a single fast pass over text to emit typed *candidate spans* that
downstream detectors can use as hints, without running full detection logic
themselves.  This allows:

- Detectors to skip unrelated portions of the text.
- Shared span state so the pipeline avoids re-scanning the same characters
  multiple times.
- A typed candidate API that skeleton/pseudonym modes can align with.

Candidate types
---------------
``EMAIL_CANDIDATE``      — something that looks like an email address
``PHONE_CANDIDATE``      — a sequence of digits that resembles a phone number
``ALPHANUM_TOKEN``       — a mixed alphanumeric token (possible secret/ID)
``NUMERIC_SEQUENCE``     — a run of digits (possible ID, card, IBAN)
``CAPITALIZED_PHRASE``   — one or more consecutively capitalised words (possible name/org)
``URL_CANDIDATE``        — starts with http/https/www

These are *hints*, not final findings.  Each downstream detector decides
whether to accept, reject, or upgrade a candidate span.
"""
from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Dict, Iterator, List, Union


@dataclass(frozen=True)
class CandidateSpan:
    """A typed candidate span produced by the tokenizer."""

    candidate_type: str   # one of the candidate type constants below
    start: int
    end: int
    text: str

    def as_finding(self, entity_type: str) -> Dict[str, Union[int, str]]:
        """Convert to a finding dict compatible with the detector pipeline."""
        return {"type": entity_type, "start": self.start, "end": self.end, "text": self.text}


# Candidate type constants
EMAIL_CANDIDATE = "EMAIL_CANDIDATE"
PHONE_CANDIDATE = "PHONE_CANDIDATE"
ALPHANUM_TOKEN = "ALPHANUM_TOKEN"
NUMERIC_SEQUENCE = "NUMERIC_SEQUENCE"
CAPITALIZED_PHRASE = "CAPITALIZED_PHRASE"
URL_CANDIDATE = "URL_CANDIDATE"

# ---------------------------------------------------------------------------
# Tokenizer patterns (ordered by specificity — most specific first)
# ---------------------------------------------------------------------------

_PATTERNS: List[tuple] = [
    # URL candidate (before email so http://user@host isn't split)
    (URL_CANDIDATE, re.compile(
        r"(?:https?|ftp|sftp|ftps|ssh|git|www)\S+", re.IGNORECASE
    )),
    # Email candidate
    (EMAIL_CANDIDATE, re.compile(
        r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b"
    )),
    # Phone candidate: digit run with optional + prefix and separators
    (PHONE_CANDIDATE, re.compile(
        r"(?<![A-Za-z0-9])\+?\d(?:[\d().\-\s]{5,}\d)(?![A-Za-z0-9])"
    )),
    # Numeric sequence: pure digit runs ≥4 chars
    (NUMERIC_SEQUENCE, re.compile(r"\b\d{4,}\b")),
    # Capitalised phrase: 1-4 consecutive Title-Case words
    (CAPITALIZED_PHRASE, re.compile(
        r"\b(?:[A-ZÁÀÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝ][a-záàâãäåæçèéêëìíîïðñòóôõöøùúûüý]+"
        r"(?:-[A-Z][a-z]+)?)"
        r"(?:\s+[A-ZÁÀÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝ][a-záàâãäåæçèéêëìíîïðñòóôõöøùúûüý]+"
        r"(?:-[A-Z][a-z]+)?){0,3}\b"
    )),
    # Mixed alphanumeric token: possible secret / API key
    (ALPHANUM_TOKEN, re.compile(r"[A-Za-z0-9_\-]{16,}")),
]


def tokenize(text: str) -> List[CandidateSpan]:
    """Return candidate spans for *text*, ordered by start position.

    Overlapping candidates from different pattern types are both emitted;
    resolution happens downstream in the detector/conflict layer.
    """
    candidates: List[CandidateSpan] = []
    seen: set = set()

    for ctype, pattern in _PATTERNS:
        for m in pattern.finditer(text):
            key = (m.start(), m.end())
            if key in seen:
                continue
            seen.add(key)
            candidates.append(CandidateSpan(
                candidate_type=ctype,
                start=m.start(),
                end=m.end(),
                text=m.group(),
            ))

    candidates.sort(key=lambda c: (c.start, c.end))
    return candidates


def candidates_by_type(text: str, *types: str) -> List[CandidateSpan]:
    """Return only candidates matching one of the given *types*."""
    return [c for c in tokenize(text) if c.candidate_type in types]


def window_around(
    text: str, spans: List[CandidateSpan], context_chars: int = 120
) -> Iterator[str]:
    """Yield text windows of *context_chars* around each span (for ML models)."""
    for span in spans:
        start = max(0, span.start - context_chars)
        end = min(len(text), span.end + context_chars)
        yield text[start:end]
