# retoor <retoor@molodetz.nl>
from __future__ import annotations

import re

from devplacepy.services.jobs.isslop.analysis.metrics import shannon_entropy
from devplacepy.services.jobs.isslop.analysis.signals import (
    AXIS_QUALITY,
    SEVERITY_MEDIUM,
    SEVERITY_STRONG,
    FileContext,
    Signal,
)

SECRET_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
    (re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "AWS access key"),
    (re.compile(r"\bghp_[A-Za-z0-9]{36,}\b"), "GitHub personal token"),
    (re.compile(r"\bgithub_pat_[A-Za-z0-9_]{22,}\b"), "GitHub fine-grained token"),
    (re.compile(r"\bsk_live_[A-Za-z0-9]{16,}\b"), "Stripe live key"),
    (re.compile(r"\bsk-[A-Za-z0-9]{40,}\b"), "API secret key"),
    (re.compile(r"\bAIza[0-9A-Za-z_\-]{35}\b"), "Google API key"),
    (re.compile(r"\bxox[baprs]-[A-Za-z0-9\-]{10,}\b"), "Slack token"),
    (re.compile(r"-----BEGIN[A-Z ]*PRIVATE KEY-----"), "Private key material"),
    (re.compile(r"\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp)://\w+:[^@/\s]{4,}@"), "Credentials in connection string"),
)

ENTROPY_ASSIGNMENT_PATTERN = re.compile(
    r"(secret|token|passwd|password|api[_-]?key|auth[_-]?key|private[_-]?key)\w*\s*[:=]\s*[\"']([^\"'\s]{16,})[\"']",
    re.IGNORECASE,
)
ENTROPY_THRESHOLD: float = 3.5

ALLOWLIST_MARKERS: tuple[str, ...] = (
    "example", "your_", "your-", "changeme", "change_me", "placeholder", "xxxx", "dummy",
    "sample", "<", "${", "{{", "todo", "insert", "test", "fake", "redacted", "akiaiosfodnn7example",
)
ALLOWLIST_PATH_MARKERS: tuple[str, ...] = (".example", "fixture", "mock", "sample", "/docs/", "/test", "_test", "spec.")

INJECTION_PATTERNS: tuple[tuple[re.Pattern[str], str, str], ...] = (
    (
        re.compile(r"[\"'](?:SELECT|INSERT|UPDATE|DELETE|DROP)\b[^\"']*[\"']\s*\+", re.IGNORECASE),
        "SQL built by string concatenation",
        SEVERITY_STRONG,
    ),
    (
        re.compile(r"f[\"'](?:[^\"']*\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^\"']*\{)", re.IGNORECASE),
        "SQL built with f-string interpolation",
        SEVERITY_STRONG,
    ),
    (re.compile(r"\beval\s*\(\s*[a-zA-Z_$][\w$.]*\s*[,)]"), "eval() on a variable", SEVERITY_STRONG),
    (re.compile(r"dangerouslySetInnerHTML"), "dangerouslySetInnerHTML usage", SEVERITY_STRONG),
    (re.compile(r"\.innerHTML\s*=\s*(?![\"'`][^$])"), "innerHTML assigned from dynamic value", SEVERITY_MEDIUM),
    (re.compile(r"document\.write\s*\("), "document.write usage", SEVERITY_MEDIUM),
    (re.compile(r"child_process[.\w]*\.exec(?:Sync)?\s*\(\s*[`\"'][^`\"']*[$+]"), "Shell exec with interpolation", SEVERITY_STRONG),
    (re.compile(r"os\.system\s*\(\s*f?[\"'][^\"']*[{+]"), "os.system with interpolation", SEVERITY_STRONG),
    (re.compile(r"subprocess\.\w+\([^)]*shell\s*=\s*True[^)]*[+{]"), "subprocess shell=True with dynamic input", SEVERITY_STRONG),
)


def _is_allowlisted(line: str, relative_path: str) -> bool:
    lowered = line.lower()
    if any(marker in lowered for marker in ALLOWLIST_MARKERS):
        return True
    lowered_path = relative_path.lower()
    return any(marker in lowered_path for marker in ALLOWLIST_PATH_MARKERS)


def detect_security(context: FileContext) -> list[Signal]:
    findings: list[Signal] = []
    for number, line in enumerate(context.lines, start=1):
        for pattern, title in SECRET_PATTERNS:
            if pattern.search(line) and not _is_allowlisted(line, context.relative):
                findings.append(
                    Signal("HARDCODED_SECRET", f"Hardcoded secret: {title}", SEVERITY_STRONG, AXIS_QUALITY, 5.0, number, line.strip()[:120])
                )
                break
        entropy_match = ENTROPY_ASSIGNMENT_PATTERN.search(line)
        if entropy_match and not _is_allowlisted(line, context.relative):
            candidate = entropy_match.group(2)
            if shannon_entropy(candidate) >= ENTROPY_THRESHOLD:
                findings.append(
                    Signal(
                        "HARDCODED_SECRET",
                        f"High-entropy credential assigned to '{entropy_match.group(1)}'",
                        SEVERITY_STRONG,
                        AXIS_QUALITY,
                        5.0,
                        number,
                        line.strip()[:120],
                    )
                )
        for pattern, title, severity in INJECTION_PATTERNS:
            if pattern.search(line):
                findings.append(
                    Signal("INJECTION_RISK", title, severity, AXIS_QUALITY, 5.0, number, line.strip()[:120])
                )
                break
    return findings[:12]
