"""Pattern registry for secrets detection.

Loads provider and benign patterns from YAML, compiles regexes,
and scans text lines for matches.
"""

from __future__ import annotations

import importlib.resources
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

import yaml


@dataclass(frozen=True)
class PatternMatch:
    """A single regex match against a provider pattern."""

    pattern_id: str
    provider: str
    severity: str
    value: str
    line_number: int
    start: int
    end: int


class PatternRegistry:
    """Loads and manages provider/benign patterns for secrets scanning."""

    def __init__(
        self,
        providers_path: Optional[Path] = None,
        benign_path: Optional[Path] = None,
    ) -> None:
        providers_path = providers_path or self._default_providers_path()
        benign_path = benign_path or self._default_benign_path()

        self._providers = self._load_providers(providers_path)
        self._benign = self._load_benign(benign_path)
        self._benign_override_ids: set[str] = {p["id"] for p in self._providers if p.get("benign_override")}

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def pattern_count(self) -> int:
        return len(self._providers)

    @property
    def provider_count(self) -> int:
        return len({p["provider"] for p in self._providers})

    @property
    def providers(self) -> list[dict]:
        return self._providers

    @property
    def benign(self) -> list[dict]:
        return self._benign

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def scan_line(self, line: str, line_number: int) -> list[PatternMatch]:
        """Scan a single line of text against all provider patterns.

        Returns a list of PatternMatch for every regex hit.
        Lines longer than 4000 chars are skipped — they are minified code
        or base64 blobs that cannot contain readable secrets and would
        otherwise risk catastrophic regex backtracking.  (The longest
        known legitimate secret value is ~1200 chars for GraphCMS JWTs,
        so 4000 provides ample headroom.)
        """
        if len(line) > 4000:
            return []
        matches: list[PatternMatch] = []
        for pat in self._providers:
            for m in pat["compiled"].finditer(line):
                # Prefer capture group 1 (the actual secret value) over the
                # full match when the regex includes a context prefix.
                if m.lastindex and m.lastindex >= 1:
                    value = m.group(1)
                    start = m.start(1)
                    end = m.end(1)
                else:
                    value = m.group(0)
                    start = m.start()
                    end = m.end()
                matches.append(
                    PatternMatch(
                        pattern_id=pat["id"],
                        provider=pat["provider"],
                        severity=pat["severity"],
                        value=value,
                        line_number=line_number,
                        start=start,
                        end=end,
                    )
                )
        return matches

    def is_benign(self, value: str, pattern_id: str | None = None) -> bool:
        """Return True if *value* matches any benign pattern.

        A benign match only suppresses when it covers >= 80 % of the
        total value length.  If *pattern_id* refers to a provider
        pattern with ``benign_override: true``, benign checking is
        bypassed entirely.
        """
        if pattern_id and pattern_id in self._benign_override_ids:
            return False

        val_len = len(value)
        if val_len == 0:
            return False

        for bp in self._benign:
            m = bp["compiled"].search(value)
            if m and (m.end() - m.start()) >= val_len * 0.8:
                return True
        return False

    # ------------------------------------------------------------------
    # Loading helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _load_providers(path: Path) -> list[dict]:
        if path.is_dir():
            patterns = []
            for f in sorted(path.glob("*.yaml")):
                entries = yaml.safe_load(f.read_text(encoding="utf-8"))
                if isinstance(entries, list):
                    patterns.extend(entries)
        else:
            raw = yaml.safe_load(path.read_text(encoding="utf-8"))
            patterns = raw.get("providers", [])
        for pat in patterns:
            # Simplify redundant alternation: (?:.|\s) and (?:\s|.)
            # are equivalent to [\s\S] (match anything including newline),
            # but since scan_line operates on single lines (no newlines),
            # we can safely replace with just "." to halve NFA states
            # and prevent backtracking in the alternation group.
            regex = pat["regex"]
            regex = regex.replace("(?:.|\\s)", ".")
            regex = regex.replace("(?:\\s|.)", ".")
            pat["compiled"] = re.compile(regex)
        return patterns

    @staticmethod
    def _load_benign(path: Path) -> list[dict]:
        raw = yaml.safe_load(path.read_text(encoding="utf-8"))
        patterns = raw.get("benign", [])
        for pat in patterns:
            pat["compiled"] = re.compile(pat["regex"])
        return patterns

    # ------------------------------------------------------------------
    # Default path resolution
    # ------------------------------------------------------------------

    @staticmethod
    def _default_providers_path() -> Path:
        """Find bundled providers directory via importlib, fallback to repo root."""
        try:
            data_dir = importlib.resources.files("argus_nano.data")
            p = data_dir / "providers"
            with importlib.resources.as_file(p) as real:
                if Path(real).is_dir():
                    return Path(real)
        except (ModuleNotFoundError, FileNotFoundError, TypeError):
            pass
        # Fallback: repo root patterns/providers/
        repo = Path(__file__).resolve().parent.parent.parent
        return repo / "patterns" / "providers"

    @staticmethod
    def _default_benign_path() -> Path:
        """Find bundled benign.yaml via importlib, fallback to repo root."""
        try:
            data_dir = importlib.resources.files("argus_nano.data")
            p = data_dir / "benign.yaml"
            with importlib.resources.as_file(p) as real:
                return Path(real)
        except (ModuleNotFoundError, FileNotFoundError, TypeError):
            pass
        repo = Path(__file__).resolve().parent.parent.parent
        return repo / "patterns" / "benign.yaml"
