"""
core/scanner.py — VaultHound v1.0.0
Core scanning engine — applies patterns to text content.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Iterator

from core.patterns import Pattern, get_patterns
from utils.models import SecretFinding, Severity
from utils.helpers import redact
from utils.logger import log


# Lines to skip — common false positives
_SKIP_LINE_PATTERNS = [
    re.compile(r"^\s*#"),           # comments
    re.compile(r"^\s*//"),          # JS/Go comments
    re.compile(r"^\s*\*"),          # block comment lines
    re.compile(r"example\.com"),    # example domains
    re.compile(r"your[_\-]"),       # placeholder text
    re.compile(r"<[A-Z_]+>"),       # template placeholders <YOUR_KEY>
    re.compile(r"\$\{[^}]+\}"),     # shell variable substitutions
    re.compile(r"\{\{[^}]+\}\}"),   # template variables {{key}}
]

# Max line length to scan — avoids scanning minified JS blobs
MAX_LINE_LENGTH = 2000


class Scanner:
    """
    Applies all patterns to text content.
    Returns a list of SecretFinding objects.
    """

    def __init__(self, patterns: list[Pattern] | None = None, min_severity: str = "low"):
        self.patterns     = patterns or get_patterns()
        self.min_severity = min_severity
        self._min_rank    = Severity.rank(min_severity)

    def scan_text(
        self,
        content:     str,
        source:      str,
        start_line:  int = 1,
    ) -> list[SecretFinding]:
        """
        Scan a block of text. Returns all findings above min_severity.

        Args:
            content    — text to scan
            source     — filename or URL (for reporting)
            start_line — line number offset (for git diff context)
        """
        findings: list[SecretFinding] = []
        seen: set[tuple[str, str]] = set()   # (pattern_name, redacted_match) dedup

        lines = content.splitlines()
        for line_idx, line in enumerate(lines):
            line_num = start_line + line_idx

            # Skip long lines (minified code)
            if len(line) > MAX_LINE_LENGTH:
                continue

            # Skip obvious comment/placeholder lines
            if any(pat.search(line) for pat in _SKIP_LINE_PATTERNS):
                continue

            for pattern in self.patterns:
                # Skip patterns below minimum severity
                if Severity.rank(pattern.severity) < self._min_rank:
                    continue

                for match in pattern.regex.finditer(line):
                    # Get the captured group if present, else full match
                    value = match.group(1) if match.lastindex else match.group(0)

                    # Run validator
                    if pattern.validator and not pattern.validator(value):
                        continue

                    # Deduplicate
                    key = (pattern.name, redact(value))
                    if key in seen:
                        continue
                    seen.add(key)

                    # Build context line (redact the secret)
                    safe_line = line.replace(value, redact(value))

                    findings.append(SecretFinding(
                        pattern_name = pattern.name,
                        severity     = pattern.severity,
                        match        = value,
                        source       = source,
                        line_number  = line_num,
                        line_content = safe_line.strip()[:200],
                        context      = pattern.context,
                        redacted     = True,
                    ))
                    log.debug(f"[{pattern.severity.upper()}] {pattern.name} @ {source}:{line_num}")

        return findings

    def scan_file(self, path: Path) -> list[SecretFinding]:
        """Scan a single file. Handles encoding errors gracefully."""
        try:
            content = path.read_text(encoding="utf-8", errors="replace")
            return self.scan_text(content, source=str(path))
        except (PermissionError, OSError) as e:
            log.warning(f"Cannot read {path}: {e}")
            return []
