"""Shared detector primitives.

Detectors operate on a plain string and yield Match objects with offsets relative
to that string. The engine wraps each Match into a Finding, attaching the file path
and the unit locator. Keeping detectors string-in / match-out lets the same detector
run per-unit (for located scan findings) or over full document text (for sanitize).
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Optional

from ..severity import severity_for
from ..models import Severity


@dataclass
class Match:
    detector: str
    type: str
    start: int
    end: int
    raw: str
    preview: str = ""
    role: Optional[str] = None
    confidence: Optional[float] = None
    detail: dict[str, Any] = field(default_factory=dict)

    @property
    def severity(self) -> Severity:
        return severity_for(self.type)


def mask_digits(raw: str, keep_last: int = 4) -> str:
    """Mask all digits except the last keep_last, preserving separators."""
    total = sum(c.isdigit() for c in raw)
    out: list[str] = []
    seen = 0
    for c in raw:
        if c.isdigit():
            seen += 1
            out.append(c if seen > total - keep_last else "X")
        else:
            out.append(c)
    return "".join(out)


def mask_email(raw: str) -> str:
    local, sep, domain = raw.partition("@")
    if not sep:
        return raw
    masked = (local[0] + "***") if local else "***"
    return f"{masked}@{domain}"


def mask_middle(raw: str, keep_ends: int = 3) -> str:
    """Generic mask that keeps the first and last few characters."""
    if len(raw) <= keep_ends * 2:
        return raw[0] + "***" if raw else "***"
    return f"{raw[:keep_ends]}...{raw[-keep_ends:]}"


def luhn_valid(digits: str) -> bool:
    """Standard Luhn checksum over a digit string."""
    nums = [int(c) for c in digits if c.isdigit()]
    if len(nums) < 2:
        return False
    total = 0
    parity = len(nums) % 2
    for i, n in enumerate(nums):
        if i % 2 == parity:
            n *= 2
            if n > 9:
                n -= 9
        total += n
    return total % 10 == 0


def aba_valid(digits: str) -> bool:
    """ABA routing-transit checksum over exactly 9 digits."""
    d = [int(c) for c in digits if c.isdigit()]
    if len(d) != 9:
        return False
    checksum = (
        3 * (d[0] + d[3] + d[6])
        + 7 * (d[1] + d[4] + d[7])
        + 1 * (d[2] + d[5] + d[8])
    )
    return checksum % 10 == 0


def dedupe_matches(matches: list[Match]) -> list[Match]:
    """Drop a match whose span is fully contained in another match of equal or
    higher severity. Keeps the broader / more severe finding when two detectors
    fire on overlapping text (e.g. a card number vs an incidental digit run)."""
    kept: list[Match] = []
    ordered = sorted(matches, key=lambda m: (m.start, -(m.end - m.start)))
    for m in ordered:
        covered = False
        for other in ordered:
            if other is m:
                continue
            if (
                other.start <= m.start
                and other.end >= m.end
                and (other.end - other.start) > (m.end - m.start)
                and other.severity.rank >= m.severity.rank
            ):
                covered = True
                break
        if not covered:
            kept.append(m)
    return kept
