"""Поиск секретов в тексте.

Используется дважды: при вычистке брифа в фазе подготовки и как страж перед коммитом.
Значение секрета никогда не печатается — только класс, имя переменной и место.
"""

from __future__ import annotations

import re
from .store import STATE_DIR

# (имя класса, шаблон, предлагаемое имя переменной)
PATTERNS: list[tuple[str, re.Pattern[str], str]] = [
    ("приватный ключ", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), "PRIVATE_KEY"),
    ("ключ OpenAI", re.compile(r"\bsk-[A-Za-z0-9_-]{20,}"), "OPENAI_API_KEY"),
    ("ключ Anthropic", re.compile(r"\bsk-ant-[A-Za-z0-9_-]{20,}"), "ANTHROPIC_API_KEY"),
    ("ключ Stripe", re.compile(r"\bsk_live_[A-Za-z0-9]{16,}"), "STRIPE_SECRET_KEY"),
    ("токен GitHub", re.compile(r"\b(ghp_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})"), "GITHUB_TOKEN"),
    ("ключ AWS", re.compile(r"\b(AKIA|ASIA)[A-Z0-9]{16}\b"), "AWS_ACCESS_KEY_ID"),
    ("ключ Google", re.compile(r"\b(AIza[A-Za-z0-9_-]{30,}|ya29\.[A-Za-z0-9_-]{20,})"), "GOOGLE_API_KEY"),
    ("токен Slack", re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{10,}"), "SLACK_TOKEN"),
    ("токен Telegram-бота", re.compile(r"\b\d{8,10}:[A-Za-z0-9_-]{35}\b"), "BOT_TOKEN"),
    ("JWT", re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"), "JWT"),
    (
        "пароль в строке подключения",
        re.compile(r"\b[a-z][a-z0-9+.-]*://[^\s:/@]+:[^\s:/@]{3,}@[^\s/]+"),
        "DATABASE_URL",
    ),
]

GENERIC = re.compile(
    r"(?i)\b(api[_-]?key|secret|token|password|passwd|пароль|токен|ключ|доступ)\b"
    r"\s*[:=]\s*[\"']?([A-Za-z0-9+/=_-]{32,})[\"']?"
)

SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build", STATE_DIR}
SKIP_SUFFIXES = {".lock", ".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".bin", ".so", ".dylib"}

PLACEHOLDER = re.compile(r"^(\[СЕКРЕТ:|<|\$\{|xxx|placeholder|your[_-]|changeme|example)", re.I)


class Finding:
    __slots__ = ("kind", "var", "file", "line", "preview")

    def __init__(self, kind: str, var: str, file: str, line: int, matched: str):
        self.kind = kind
        self.var = var
        self.file = file
        self.line = line
        self.preview = self._mask(matched)

    @staticmethod
    def _mask(value: str) -> str:
        value = value.strip()
        if len(value) <= 10:
            return "*" * len(value)
        return f"{value[:4]}…{value[-2:]} ({len(value)} симв.)"

    def __str__(self) -> str:
        where = f"{self.file}:{self.line}" if self.file else f"строка {self.line}"
        return f"{where} — {self.kind}, похоже на {self.var}: {self.preview}"


def scan_text(text: str, filename: str = "") -> list[Finding]:
    found: list[Finding] = []
    for lineno, line in enumerate(text.splitlines(), 1):
        if len(line) > 4000:
            continue
        for kind, pattern, var in PATTERNS:
            m = pattern.search(line)
            if m and not PLACEHOLDER.match(m.group(0)):
                found.append(Finding(kind, var, filename, lineno, m.group(0)))
        m = GENERIC.search(line)
        if m and not PLACEHOLDER.match(m.group(2)):
            if not any(f.line == lineno and f.file == filename for f in found):
                found.append(Finding("похоже на секрет", m.group(1).upper(), filename, lineno, m.group(2)))
    return found


def scan_files(paths: list[Path], root: Path | None = None) -> list[Finding]:
    found: list[Finding] = []
    for path in paths:
        if not path.is_file():
            continue
        if path.suffix.lower() in SKIP_SUFFIXES:
            continue
        if any(part in SKIP_DIRS for part in path.parts):
            continue
        try:
            text = path.read_text(encoding="utf-8")
        except (UnicodeDecodeError, OSError):
            continue
        name = str(path.relative_to(root)) if root and root in path.parents else str(path)
        found.extend(scan_text(text, name))
    return found


def redact(text: str) -> tuple[str, list[Finding]]:
    """Заменяет найденные значения на [СЕКРЕТ:ИМЯ]. Возвращает текст и находки."""
    findings = scan_text(text)
    out = text
    for kind, pattern, var in PATTERNS:
        out = pattern.sub(f"[СЕКРЕТ:{var}]", out)
    out = GENERIC.sub(lambda m: f"{m.group(1)}=[СЕКРЕТ:{m.group(1).upper()}]", out)
    return out, findings
