"""PIIScanner and PIILabel enum for meshulash-guard SDK.

PIIScanner detects and acts on Personally Identifiable Information (PII),
Protected Health Information (PHI), Payment Card Industry (PCI) data,
secrets/credentials, and technical identifiers.

Bundle shortcuts (PIILabel.PII, PIILabel.PHI, etc.) are expanded SDK-side
into explicit labels in required.ner and required.regex because the server's
process_sdk() does NOT expand bundles — it reads only required.ner and
required.regex directly.

Per-label action overrides produce multiple guardline specs via
to_guardline_specs(), since the server only supports one action per guardline.
"""

from __future__ import annotations

from collections.abc import Sequence

from ..enums import Action, Condition, _StrValueMixin
from .base import BaseScanner

# ---------------------------------------------------------------------------
# Bundle constants
# ---------------------------------------------------------------------------

_BUNDLE_NAMES: frozenset[str] = frozenset({"PII", "PHI", "PCI", "SECRETS", "TECH"})
_ALL_SENTINEL: str = "__ALL__"

_BUNDLE_LABELS: dict[str, list[str]] = {
    "PII": [
        "PERSON_NAME", "NATIONAL_ID", "DRIVER_LICENSE", "USERNAME",
        "TAX_NUMBER", "ACCOUNT_ID", "BUSINESS_ID",
        "EMAIL_ADDRESS", "PHONE_NUMBER", "STREET_ADDRESS", "CITY",
        "COUNTY", "STATE", "COUNTRY", "POSTAL_CODE", "IP_ADDRESS",
        "MAC_ADDRESS", "URL",
        "CREDIT_CARD", "IBAN", "BANK_ACCOUNT", "ROUTING_NUMBER",
        "CRYPTO_ADDRESS",
    ],
    "PHI": ["PERSON_NAME"],
    "PCI": ["CREDIT_CARD", "IBAN", "BANK_ACCOUNT", "ROUTING_NUMBER"],
    "SECRETS": [
        "PASSWORD", "SECRET_KEY", "ACCESS_TOKEN", "PRIVATE_KEY",
        "CONNECTION_STRING", "AWS_ACCESS_KEY", "AWS_SECRET_KEY",
        "GCP_API_KEY", "GCP_SERVICE_ACCOUNT", "OPENAI_API_KEY",
        "AZURE_API_KEY", "STRIPE_SECRET_KEY", "STRIPE_PUBLISHABLE_KEY",
        "SLACK_TOKEN", "GITHUB_TOKEN", "TWILIO_API_KEY",
        "SENDGRID_API_KEY", "HEROKU_API_KEY", "API_KEY", "TOKEN",
    ],
    "TECH": ["USER_AGENT", "AWS_ARN"],
}


# ---------------------------------------------------------------------------
# PIILabel enum
# ---------------------------------------------------------------------------

class PIILabel(_StrValueMixin):
    """Labels for PII detection.

    Canonical labels map exactly to the server's label registry.
    Bundle shortcuts (PII, PHI, PCI, SECRETS, TECH) are expanded SDK-side.
    ALL is a wildcard that expands all bundles.

    Total: 53 members (47 canonical + 5 bundles + 1 ALL sentinel).
    """

    # --- Identity (7) ---
    PERSON_NAME = "PERSON_NAME"
    NATIONAL_ID = "NATIONAL_ID"
    DRIVER_LICENSE = "DRIVER_LICENSE"
    USERNAME = "USERNAME"
    TAX_NUMBER = "TAX_NUMBER"
    ACCOUNT_ID = "ACCOUNT_ID"
    BUSINESS_ID = "BUSINESS_ID"

    # --- Contact (11) ---
    EMAIL_ADDRESS = "EMAIL_ADDRESS"
    PHONE_NUMBER = "PHONE_NUMBER"
    STREET_ADDRESS = "STREET_ADDRESS"
    CITY = "CITY"
    COUNTY = "COUNTY"
    STATE = "STATE"
    COUNTRY = "COUNTRY"
    POSTAL_CODE = "POSTAL_CODE"
    IP_ADDRESS = "IP_ADDRESS"
    MAC_ADDRESS = "MAC_ADDRESS"
    URL = "URL"

    # --- Financial (5) ---
    CREDIT_CARD = "CREDIT_CARD"
    IBAN = "IBAN"
    BANK_ACCOUNT = "BANK_ACCOUNT"
    ROUTING_NUMBER = "ROUTING_NUMBER"
    CRYPTO_ADDRESS = "CRYPTO_ADDRESS"

    # --- Credentials (20) ---
    PASSWORD = "PASSWORD"
    SECRET_KEY = "SECRET_KEY"
    ACCESS_TOKEN = "ACCESS_TOKEN"
    PRIVATE_KEY = "PRIVATE_KEY"
    CONNECTION_STRING = "CONNECTION_STRING"
    AWS_ACCESS_KEY = "AWS_ACCESS_KEY"
    AWS_SECRET_KEY = "AWS_SECRET_KEY"
    GCP_API_KEY = "GCP_API_KEY"
    GCP_SERVICE_ACCOUNT = "GCP_SERVICE_ACCOUNT"
    OPENAI_API_KEY = "OPENAI_API_KEY"
    AZURE_API_KEY = "AZURE_API_KEY"
    STRIPE_SECRET_KEY = "STRIPE_SECRET_KEY"
    STRIPE_PUBLISHABLE_KEY = "STRIPE_PUBLISHABLE_KEY"
    SLACK_TOKEN = "SLACK_TOKEN"
    GITHUB_TOKEN = "GITHUB_TOKEN"
    TWILIO_API_KEY = "TWILIO_API_KEY"
    SENDGRID_API_KEY = "SENDGRID_API_KEY"
    HEROKU_API_KEY = "HEROKU_API_KEY"
    API_KEY = "API_KEY"
    TOKEN = "TOKEN"

    # --- Technical (2) ---
    USER_AGENT = "USER_AGENT"
    AWS_ARN = "AWS_ARN"

    # --- Temporal (1) ---
    DATE_TIME = "DATE_TIME"

    # --- Organizational (1) ---
    ORGANIZATION = "ORGANIZATION"

    # --- Bundle shortcuts (5) ---
    PII = "PII"
    PHI = "PHI"
    PCI = "PCI"
    SECRETS = "SECRETS"
    TECH = "TECH"

    # --- Wildcard (1) ---
    ALL = "__ALL__"


# ---------------------------------------------------------------------------
# Helper: expand labels to concrete canonical label strings
# ---------------------------------------------------------------------------

def _expand_labels(labels: Sequence[PIILabel]) -> list[str]:
    """Expand a sequence of PIILabel members into canonical label strings.

    - ALL sentinel: expands all 5 bundles' labels (deduplicated).
    - Bundle shortcuts (PII, PHI, PCI, SECRETS, TECH): expand to their labels.
    - Canonical labels: returned as-is.

    Returns a deduplicated list preserving insertion order.
    """
    seen: set[str] = set()
    result: list[str] = []

    def _add(label_str: str) -> None:
        if label_str not in seen:
            seen.add(label_str)
            result.append(label_str)

    for label in labels:
        raw = label.value
        if raw == _ALL_SENTINEL:
            # Expand every bundle
            for bundle_labels in _BUNDLE_LABELS.values():
                for lbl in bundle_labels:
                    _add(lbl)
        elif raw in _BUNDLE_NAMES:
            for lbl in _BUNDLE_LABELS[raw]:
                _add(lbl)
        else:
            _add(raw)

    return result


def _build_spec(
    labels: Sequence[PIILabel],
    action: Action,
    condition: Condition,
    threshold: float | None,
    allowlist: list[str] | None,
) -> dict:
    """Build a single guardline spec dict from a set of labels."""
    expanded = _expand_labels(labels)

    # Determine which bundles are represented (for forward-compat bundles field)
    raw_values = {label.value for label in labels}
    if _ALL_SENTINEL in raw_values:
        bundles = list(_BUNDLE_NAMES)
    else:
        bundles = [v for v in raw_values if v in _BUNDLE_NAMES]

    spec: dict = {
        "name": "PIIScanner",
        "action": str(action),
        "condition": str(condition),
        "types": {
            "ner": bool(expanded),
            "regex": bool(expanded),
            "tc": False,
        },
        "required": {
            "ner": list(expanded),
            "regex": list(expanded),
            "tc": [],
        },
        "bundles": bundles,
    }

    if threshold is not None:
        spec["threshold"] = threshold

    if allowlist:
        spec["allowlist"] = allowlist

    return spec


# ---------------------------------------------------------------------------
# PIIScanner class
# ---------------------------------------------------------------------------

class PIIScanner(BaseScanner):
    """Scanner for PII, PHI, PCI, secrets, and technical identifiers.

    Args:
        labels: One or more PIILabel members. Use PIILabel.ALL to scan
            everything. Use bundle shortcuts (PIILabel.PII, PIILabel.PHI,
            etc.) to scan a category. Cannot be empty.
        action: Default action to take when a label is detected.
        condition: Gating condition (ANY, ALL, K_OF, CONTEXTUAL).
        overrides: Per-label action overrides. Labels in this dict use their
            specified action instead of the default ``action``. When overrides
            are present, ``to_guardline_specs()`` returns multiple specs (one
            per distinct action group).
        threshold: Optional confidence threshold (0.0–1.0).
        allowlist: Optional list of values to allow through even if detected.
    """

    def __init__(
        self,
        labels: Sequence[PIILabel],
        action: Action = Action.REPLACE,
        condition: Condition = Condition.ANY,
        overrides: dict[PIILabel, Action] | None = None,
        threshold: float | None = None,
        allowlist: list[str] | None = None,
    ) -> None:
        if not labels:
            raise ValueError(
                "PIIScanner requires at least one label. "
                "Use PIILabel.ALL to scan everything."
            )
        self._labels = list(labels)
        self._action = action
        self._condition = condition
        self._overrides = overrides or {}
        self._threshold = threshold
        self._allowlist = allowlist

    def to_guardline_spec(self) -> dict:
        """Return a single guardline spec for the default action labels.

        When overrides are present, this returns the spec for labels that
        are NOT in overrides (i.e., the default action group). Use
        ``to_guardline_specs()`` to get all specs including override groups.
        """
        if self._overrides:
            # Return only the labels NOT in overrides
            override_keys = set(self._overrides.keys())
            default_labels = [lbl for lbl in self._labels if lbl not in override_keys]
            if not default_labels:
                # All labels have overrides — return an empty-ish spec for the
                # default action (to_guardline_specs() is the preferred path)
                return _build_spec(
                    self._labels[:1],  # Placeholder — caller should use to_guardline_specs()
                    self._action,
                    self._condition,
                    self._threshold,
                    self._allowlist,
                )
            return _build_spec(
                default_labels,
                self._action,
                self._condition,
                self._threshold,
                self._allowlist,
            )
        return _build_spec(
            self._labels,
            self._action,
            self._condition,
            self._threshold,
            self._allowlist,
        )

    def to_guardline_specs(self) -> dict[str, dict]:
        """Return all guardline specs, splitting overrides into separate entries.

        If no overrides are set, returns ``{"main": self.to_guardline_spec()}``.

        When overrides are set, groups labels by their effective action:
        - Labels NOT in overrides → default action → ``"main"`` key.
        - Labels in overrides → grouped by action → ``"override_0"``, etc.

        Returns:
            A dict of ``{key: guardline_spec}``.
        """
        if not self._overrides:
            return {"main": self.to_guardline_spec()}

        # Group labels by effective action
        # action -> list of labels
        action_groups: dict[str, list[PIILabel]] = {}

        for label in self._labels:
            effective_action = self._overrides.get(label, self._action)
            key = str(effective_action)
            action_groups.setdefault(key, []).append(label)

        result: dict[str, dict] = {}
        override_idx = 0
        default_action_key = str(self._action)

        for action_str, group_labels in action_groups.items():
            spec = _build_spec(
                group_labels,
                Action(action_str),
                self._condition,
                self._threshold,
                self._allowlist,
            )
            if action_str == default_action_key and "main" not in result:
                result["main"] = spec
            else:
                result[f"override_{override_idx}"] = spec
                override_idx += 1

        # Ensure "main" always exists (even if all labels had overrides)
        if "main" not in result:
            # Pick the first group and rename it to "main"
            first_key = next(iter(result))
            result["main"] = result.pop(first_key)
            # Renumber remaining override keys
            renumbered: dict[str, dict] = {"main": result["main"]}
            idx = 0
            for k, v in result.items():
                if k != "main":
                    renumbered[f"override_{idx}"] = v
                    idx += 1
            result = renumbered

        return result
