from enum import Enum
from typing import Dict, List, Union

# Mirror of config._RISK_WEIGHTS — used to resolve conflicting findings.
_TYPE_PRIORITY: Dict[str, int] = {
    "CREDIT_CARD": 5,
    "IBAN": 5,
    "PESEL": 5,
    "BR_CPF": 5,
    "PT_NISS": 5,
    "SECRET": 5,
    "PT_NIF": 4,
    "UUID": 4,
    "EMAIL": 3,
    "PHONE": 3,
    "ADDRESS": 3,
    "DATE_OF_BIRTH": 4,
    "URL": 2,
    "LONG_NUMERIC_ID": 2,
    "ORG": 2,
    "NAME": 2,
}


class RedactionMode(str, Enum):
    # Replace with [REDACTED:TYPE]  (default)
    LABEL = "label"
    # Mask all characters with X, preserving separators for structured types
    MASK = "mask"
    # Keep last 4 digits for numeric identifiers, mask the rest
    PARTIAL = "partial"
    # Replace with session-stable pseudonyms (PERSON_1, EMAIL_1, …)
    PSEUDONYM = "pseudonym"
    # Replace with semantic role labels ([PERSON], [CARD_NUMBER], …)
    SKELETON = "skeleton"


def _mask_value(value: str, pii_type: str, mode: RedactionMode) -> str:
    if mode == RedactionMode.LABEL:
        return f"[REDACTED:{pii_type}]"

    digits_only = "".join(c for c in value if c.isdigit())

    if mode == RedactionMode.PARTIAL:
        # For numeric types show last 4 digits; for others fall back to full mask
        if pii_type in ("CREDIT_CARD", "IBAN", "PHONE", "PESEL", "BR_CPF", "PT_NIF") and len(digits_only) >= 4:
            suffix = digits_only[-4:]
            masked_len = len(value) - 4
            return "X" * masked_len + suffix
        # Non-numeric types: full label redaction
        return f"[REDACTED:{pii_type}]"

    # MASK mode: replace every alphanumeric char with X, keep separators
    return "".join(X_or_keep(c) for c in value)


def X_or_keep(c: str) -> str:
    return "X" if c.isalnum() else c


def _resolve_conflicts(
    findings: List[Dict[str, Union[int, str]]]
) -> List[Dict[str, Union[int, str]]]:
    """Resolve overlapping findings by keeping the highest-priority type.

    When two findings cover the same span, the one with higher weight in
    _TYPE_PRIORITY wins. When priority is equal, the longer span wins.
    """
    # Sort by start, then by descending priority so highest-priority comes first.
    sorted_f = sorted(
        findings,
        key=lambda f: (int(f["start"]), -_TYPE_PRIORITY.get(str(f["type"]), 1)),
    )

    resolved: List[Dict[str, Union[int, str]]] = []
    for finding in sorted_f:
        start = int(finding["start"])
        end = int(finding["end"])
        if end <= start:
            continue

        # Check if this finding overlaps with the last accepted one.
        if resolved:
            last = resolved[-1]
            last_end = int(last["end"])
            if start < last_end:
                # Overlap: keep whichever has higher priority.
                last_prio = _TYPE_PRIORITY.get(str(last["type"]), 1)
                this_prio = _TYPE_PRIORITY.get(str(finding["type"]), 1)
                if this_prio > last_prio or (
                    this_prio == last_prio and (end - start) > (last_end - int(last["start"]))
                ):
                    resolved[-1] = finding
                continue

        resolved.append(finding)

    return resolved


def apply_text_redaction(
    text: str,
    findings: List[Dict[str, Union[int, str]]],
    mode: RedactionMode = RedactionMode.LABEL,
) -> str:
    ordered = _resolve_conflicts(findings)

    out: list[str] = []
    cursor = 0
    for item in ordered:
        start = int(item["start"])
        end = int(item["end"])

        if start < cursor or end <= start:
            continue

        out.append(text[cursor:start])
        original = str(item.get("text", text[start:end]))
        out.append(_mask_value(original, str(item["type"]), mode))
        cursor = end

    out.append(text[cursor:])
    return "".join(out)
