# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Build Reproducibility Scanner.

Scans build configurations for reproducibility and determinism issues:
- Dockerfiles
- Build scripts
- Package manager configurations
- CI/CD build steps
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from enum import Enum
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


class BuildFileType(Enum):
    """Supported build file types."""

    DOCKERFILE = "dockerfile"
    DOCKER_COMPOSE = "docker_compose"
    MAKEFILE = "makefile"
    PACKAGE_JSON = "package_json"
    REQUIREMENTS_TXT = "requirements_txt"
    PYPROJECT_TOML = "pyproject_toml"
    GO_MOD = "go_mod"
    CARGO_TOML = "cargo_toml"
    GEMFILE = "gemfile"
    BUILD_GRADLE = "build_gradle"
    POM_XML = "pom_xml"
    UNKNOWN = "unknown"


class BuildIssueSeverity(Enum):
    """Build issue severity levels."""

    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
    INFO = "info"


class BuildIssueCategory(Enum):
    """Build issue categories."""

    REPRODUCIBILITY = "reproducibility"
    CACHING = "caching"
    SECURITY = "security"
    BEST_PRACTICE = "best_practice"
    PERFORMANCE = "performance"
    MAINTENANCE = "maintenance"


@dataclass
class BuildRule:
    """A build reproducibility rule definition."""

    id: str
    name: str
    description: str
    pattern: str
    severity: BuildIssueSeverity
    category: BuildIssueCategory
    file_types: list[BuildFileType]
    remediation: str
    flags: str = ""  # "i" for case-insensitive, "m" for multiline

    def __post_init__(self) -> None:
        """Compile the regex pattern."""
        regex_flags = 0
        if "i" in self.flags:
            regex_flags |= re.IGNORECASE
        if "m" in self.flags:
            regex_flags |= re.MULTILINE
        if "s" in self.flags:
            regex_flags |= re.DOTALL
        self._compiled = re.compile(self.pattern, regex_flags)

    def check(self, content: str) -> list[dict]:
        """Check content against the rule.

        Returns list of matches with line numbers.
        """
        matches = []
        lines = content.split("\n")

        for line_no, line in enumerate(lines, start=1):
            for match in self._compiled.finditer(line):
                matches.append(
                    {
                        "line_no": line_no,
                        "column_start": match.start() + 1,
                        "column_end": match.end() + 1,
                        "matched_text": match.group(),
                        "line_content": line.strip(),
                    }
                )

        return matches


# =============================================================================
# Build Reproducibility Rules
# =============================================================================

BUILD_RULES: list[BuildRule] = [
    # -------------------------------------------------------------------------
    # Dockerfile - Image Tags
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-latest-tag",
        name="Using :latest Image Tag",
        description="Base image uses :latest tag which is non-deterministic",
        pattern=r"^FROM\s+[^\s:]+:latest\s*$",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Pin to specific version: FROM node:20.10.0-alpine",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-no-tag",
        name="Missing Image Tag",
        description="Base image without explicit tag defaults to :latest",
        pattern=r"^FROM\s+([a-zA-Z0-9._/-]+)\s*$",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Always specify image tag: FROM node:20.10.0",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-floating-tag",
        name="Floating Version Tag",
        description="Image tag may change over time (e.g., node:20 vs node:20.10.0)",
        pattern=r"^FROM\s+[^\s:]+:(\d+|\d+\.\d+)(?:-[a-z]+)?\s*$",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Use fully qualified version: FROM node:20.10.0-alpine",
        flags="im",
    ),
    # -------------------------------------------------------------------------
    # Dockerfile - Package Installation
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-apt-no-version",
        name="APT Install Without Version",
        description="apt-get install without pinned package versions",
        pattern=r"apt-get\s+install\s+(?!.*=)[^&\n]+",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Pin package versions: apt-get install curl=7.88.1-10",
        flags="i",
    ),
    BuildRule(
        id="build-dockerfile-apk-no-version",
        name="APK Add Without Version",
        description="apk add without pinned package versions",
        pattern=r"apk\s+add\s+(?!.*=)[^&\n]+",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Pin package versions: apk add curl=8.5.0-r0",
        flags="i",
    ),
    BuildRule(
        id="build-dockerfile-yum-no-version",
        name="YUM Install Without Version",
        description="yum install without pinned package versions",
        pattern=r"yum\s+install\s+(?!.*-)[^&\n]+",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Pin package versions: yum install curl-7.76.1",
        flags="i",
    ),
    BuildRule(
        id="build-dockerfile-pip-no-version",
        name="PIP Install Without Version in Dockerfile",
        description="pip install without version constraints",
        pattern=r"pip3?\s+install\s+(?!.*[=<>])[a-zA-Z][a-zA-Z0-9_-]*\s*$",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Pin package versions: pip install requests==2.31.0",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-npm-install-no-lock",
        name="NPM Install Without Lockfile",
        description="npm install without ci flag ignores package-lock.json",
        pattern=r"npm\s+install(?!\s+ci)",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Use npm ci for reproducible builds",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Dockerfile - Remote Resources
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-add-url",
        name="ADD With Remote URL",
        description="ADD with URL is non-deterministic; use curl/wget with checksum",
        pattern=r"^ADD\s+https?://",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Use RUN curl -o file URL && sha256sum -c checksum",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-curl-no-checksum",
        name="Curl Download Without Checksum",
        description="Downloading file without checksum verification",
        pattern=r"curl\s+.*-[oOL]\s+[^\|&]+(?!.*sha\d+sum)",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Verify downloads: curl -o file URL && sha256sum -c checksum",
        flags="i",
    ),
    BuildRule(
        id="build-dockerfile-wget-no-checksum",
        name="Wget Download Without Checksum",
        description="Downloading file without checksum verification",
        pattern=r"wget\s+[^\|&]+(?!.*sha\d+sum)",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Verify downloads: wget URL && sha256sum -c checksum",
        flags="i",
    ),
    BuildRule(
        id="build-dockerfile-git-clone-no-ref",
        name="Git Clone Without Specific Ref",
        description="git clone without specific commit/tag is non-deterministic",
        pattern=r"git\s+clone\s+(?!.*--branch|.*-b)[^\n]+(?!.*&&\s*git\s+checkout)",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Pin to specific commit: git clone URL && git checkout SHA",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Dockerfile - Build Cache
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-copy-before-deps",
        name="COPY Before Dependency Install",
        description="Copying all files before installing dependencies breaks cache",
        pattern=r"COPY\s+\.\s+[^\n]*\nRUN\s+.*(?:npm|pip|yarn|bundle)\s+install",
        severity=BuildIssueSeverity.LOW,
        category=BuildIssueCategory.CACHING,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="COPY package*.json first, then RUN npm install, then COPY .",
        flags="ims",
    ),
    BuildRule(
        id="build-dockerfile-apt-no-clean",
        name="APT Cache Not Cleaned",
        description="apt-get without cleanup increases image size",
        pattern=r"apt-get\s+install[^&]*(?!.*rm\s+-rf.*apt)",
        severity=BuildIssueSeverity.LOW,
        category=BuildIssueCategory.PERFORMANCE,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Add: && rm -rf /var/lib/apt/lists/*",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Dockerfile - Security & Best Practices
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-arg-secrets",
        name="Secrets in ARG",
        description="ARG values are visible in image history",
        pattern=r"^ARG\s+(?:PASSWORD|SECRET|TOKEN|API_KEY|PRIVATE_KEY)",
        severity=BuildIssueSeverity.CRITICAL,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Use --mount=type=secret for sensitive data",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-env-secrets",
        name="Secrets in ENV",
        description="ENV values persist in image and are visible",
        pattern=r"^ENV\s+(?:PASSWORD|SECRET|TOKEN|API_KEY|PRIVATE_KEY)\s*=",
        severity=BuildIssueSeverity.CRITICAL,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Use runtime environment variables or secrets management",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-run-as-root",
        name="Running as Root",
        description="Container runs as root without USER instruction",
        pattern=r"^(?!.*USER\s+)(?:FROM|ENTRYPOINT|CMD)",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Add USER instruction to run as non-root",
        flags="im",
    ),
    BuildRule(
        id="build-dockerfile-sudo",
        name="Using sudo in Dockerfile",
        description="sudo should not be needed in containers",
        pattern=r"\bsudo\b",
        severity=BuildIssueSeverity.LOW,
        category=BuildIssueCategory.BEST_PRACTICE,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Run commands that need root before USER instruction",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Dockerfile - Deprecated/Legacy
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-maintainer",
        name="Deprecated MAINTAINER Instruction",
        description="MAINTAINER is deprecated; use LABEL instead",
        pattern=r"^MAINTAINER\s+",
        severity=BuildIssueSeverity.INFO,
        category=BuildIssueCategory.MAINTENANCE,
        file_types=[BuildFileType.DOCKERFILE],
        remediation='Use LABEL maintainer="name <email>"',
        flags="im",
    ),
    # -------------------------------------------------------------------------
    # Docker Compose
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-compose-latest-tag",
        name="Docker Compose :latest Tag",
        description="Service image uses :latest tag",
        pattern=r"image:\s*[^\s:]+:latest",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKER_COMPOSE],
        remediation="Pin to specific version in docker-compose.yml",
    ),
    BuildRule(
        id="build-compose-no-tag",
        name="Docker Compose Missing Tag",
        description="Service image without version tag",
        pattern=r'image:\s*"?[a-zA-Z0-9._/-]+(?!:)"?\s*$',
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKER_COMPOSE],
        remediation="Specify image tag: image: nginx:1.25.3",
        flags="m",
    ),
    # -------------------------------------------------------------------------
    # Package.json / npm
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-npm-latest-version",
        name="NPM Package with :latest",
        description="Package version set to 'latest' is non-deterministic",
        pattern=r'"[^"]+"\s*:\s*"latest"',
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.PACKAGE_JSON],
        remediation="Pin to specific version or semver range",
    ),
    BuildRule(
        id="build-npm-star-version",
        name="NPM Package with Star Version",
        description="Package version '*' accepts any version",
        pattern=r'"[^"]+"\s*:\s*"\*"',
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.PACKAGE_JSON],
        remediation='Pin to specific version: "package": "^1.2.3"',
    ),
    BuildRule(
        id="build-npm-git-dependency",
        name="NPM Git Dependency",
        description="Git dependency without commit SHA is non-deterministic",
        pattern=r'"[^"]+"\s*:\s*"(?:git|github):[^#]+(?!#[a-f0-9]{40})"',
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.PACKAGE_JSON],
        remediation='Pin to commit: "pkg": "github:org/repo#sha"',
    ),
    # -------------------------------------------------------------------------
    # requirements.txt / Python
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-pip-unpinned",
        name="Unpinned Python Dependency",
        description="Python package without version constraint",
        pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*\s*$",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.REQUIREMENTS_TXT],
        remediation="Pin version: requests==2.31.0",
        flags="m",
    ),
    BuildRule(
        id="build-pip-greater-than",
        name="Python Dependency with >= Only",
        description="Lower bound only allows unpredictable upgrades",
        pattern=r"^[a-zA-Z][a-zA-Z0-9_-]*\s*>=\s*[\d.]+\s*$",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.REQUIREMENTS_TXT],
        remediation="Pin exact version: requests==2.31.0",
        flags="m",
    ),
    BuildRule(
        id="build-pip-git-dependency",
        name="Python Git Dependency",
        description="Git dependency without ref is non-deterministic",
        pattern=r"^git\+https?://[^@\n]+(?!@[a-f0-9]{40})",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.REQUIREMENTS_TXT],
        remediation="Pin to commit: git+https://github.com/org/repo@sha",
        flags="m",
    ),
    # -------------------------------------------------------------------------
    # go.mod / Go
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-go-replace-local",
        name="Go Replace with Local Path",
        description="Local path replace directive breaks reproducibility",
        pattern=r"replace\s+[^\s]+\s+=>\s+\./",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.GO_MOD],
        remediation="Remove local replace directives before release",
    ),
    # -------------------------------------------------------------------------
    # Makefile
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-makefile-curl-bash",
        name="Curl Pipe to Bash in Makefile",
        description="Downloading and executing scripts is non-deterministic",
        pattern=r"curl\s+.*\|\s*(?:ba)?sh",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.MAKEFILE, BuildFileType.DOCKERFILE],
        remediation="Download, verify checksum, then execute",
        flags="i",
    ),
    BuildRule(
        id="build-makefile-wget-bash",
        name="Wget Pipe to Bash in Makefile",
        description="Downloading and executing scripts is non-deterministic",
        pattern=r"wget\s+.*\|\s*(?:ba)?sh",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.MAKEFILE, BuildFileType.DOCKERFILE],
        remediation="Download, verify checksum, then execute",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # General Build Timestamps
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-embed-timestamp",
        name="Embedded Build Timestamp",
        description="Embedding timestamps breaks reproducibility",
        pattern=r"(?:BUILD_TIME|BUILD_DATE|TIMESTAMP)\s*[=:]",
        severity=BuildIssueSeverity.LOW,
        category=BuildIssueCategory.REPRODUCIBILITY,
        file_types=[BuildFileType.DOCKERFILE, BuildFileType.MAKEFILE],
        remediation="Use SOURCE_DATE_EPOCH for reproducible timestamps",
        flags="i",
    ),
    # -------------------------------------------------------------------------
    # Dockerfile - Build-Stage Secret Handling (Build & Deploy)
    # -------------------------------------------------------------------------
    BuildRule(
        id="build-dockerfile-copy-from-build-secrets",
        name="Multi-Stage Build Copies Potential Secrets",
        description="COPY --from copies files that may contain secrets (SSH keys, credentials)",
        pattern=r"COPY\s+--from=\S+\s+\S*(?:\.ssh|\.aws|\.gnupg|credentials|secret|password|\.env|id_rsa|\.pem|\.key)\S*",
        severity=BuildIssueSeverity.MEDIUM,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation="Use --mount=type=secret instead of copying sensitive files between stages",
        flags="i",
    ),
    BuildRule(
        id="build-dockerfile-build-arg-default-secret",
        name="Build ARG With Default Secret Value",
        description="ARG with default value for sensitive parameter leaks into image history",
        pattern=r"^ARG\s+(?:\w*(?:PASSWORD|SECRET|TOKEN|API_KEY|PRIVATE_KEY|CREDENTIAL)\w*)=\S+",
        severity=BuildIssueSeverity.HIGH,
        category=BuildIssueCategory.SECURITY,
        file_types=[BuildFileType.DOCKERFILE],
        remediation=(
            "Remove default value and pass secret at build time "
            "with --build-arg, or use --mount=type=secret"
        ),
        flags="im",
    ),
]


@dataclass
class BuildFinding:
    """A build reproducibility finding."""

    rule_id: str
    rule_name: str
    description: str
    file_path: str
    line_no: int
    column_start: int
    column_end: int
    matched_text: str
    line_content: str
    severity: BuildIssueSeverity
    category: BuildIssueCategory
    file_type: BuildFileType
    remediation: str

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "rule_id": self.rule_id,
            "rule_name": self.rule_name,
            "description": self.description,
            "file_path": self.file_path,
            "line_no": self.line_no,
            "column_start": self.column_start,
            "column_end": self.column_end,
            "matched_text": self.matched_text[:100],
            "line_content": self.line_content[:200],
            "severity": self.severity.value,
            "category": self.category.value,
            "file_type": self.file_type.value,
            "remediation": self.remediation,
        }


@dataclass
class BuildScanResult:
    """Result of a build reproducibility scan."""

    scan_id: str
    timestamp: str
    repository: str
    ref: str = ""

    # Reproducibility score (0-100)
    reproducibility_score: int = 100

    # Counts by severity
    total_issues: int = 0
    critical_count: int = 0
    high_count: int = 0
    medium_count: int = 0
    low_count: int = 0
    info_count: int = 0

    # Counts by category
    reproducibility_count: int = 0
    caching_count: int = 0
    security_count: int = 0
    best_practice_count: int = 0
    performance_count: int = 0
    maintenance_count: int = 0

    findings: list[BuildFinding] = field(default_factory=list)
    files_scanned: list[str] = field(default_factory=list)
    file_types_detected: set[BuildFileType] = field(default_factory=set)
    errors: list[str] = field(default_factory=list)

    @property
    def passed(self) -> bool:
        """Check if scan passed (no critical/high reproducibility issues)."""
        return self.critical_count == 0 and self.high_count == 0

    def _calculate_score(self) -> int:
        """Calculate reproducibility score."""
        if self.total_issues == 0:
            return 100

        # Weight by severity (focus on reproducibility impact)
        penalty = (
            self.critical_count * 20
            + self.high_count * 12
            + self.medium_count * 5
            + self.low_count * 2
            + self.info_count * 1
        )

        return max(0, 100 - penalty)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for serialization."""
        self.reproducibility_score = self._calculate_score()

        return {
            "scan_id": self.scan_id,
            "timestamp": self.timestamp,
            "repository": self.repository,
            "ref": self.ref,
            "passed": self.passed,
            "reproducibility_score": self.reproducibility_score,
            "total_issues": self.total_issues,
            "severity_counts": {
                "critical": self.critical_count,
                "high": self.high_count,
                "medium": self.medium_count,
                "low": self.low_count,
                "info": self.info_count,
            },
            "category_counts": {
                "reproducibility": self.reproducibility_count,
                "caching": self.caching_count,
                "security": self.security_count,
                "best_practice": self.best_practice_count,
                "performance": self.performance_count,
                "maintenance": self.maintenance_count,
            },
            "file_types_detected": [ft.value for ft in self.file_types_detected],
            "findings": [f.to_dict() for f in self.findings],
            "files_scanned": self.files_scanned,
            "errors": self.errors,
        }


class BuildScanner:
    """Scans build configurations for reproducibility issues."""

    # Map of file patterns to types
    FILE_PATTERNS: dict[str, BuildFileType] = {
        "Dockerfile": BuildFileType.DOCKERFILE,
        "dockerfile": BuildFileType.DOCKERFILE,
        ".dockerfile": BuildFileType.DOCKERFILE,
        "docker-compose.yml": BuildFileType.DOCKER_COMPOSE,
        "docker-compose.yaml": BuildFileType.DOCKER_COMPOSE,
        "compose.yml": BuildFileType.DOCKER_COMPOSE,
        "compose.yaml": BuildFileType.DOCKER_COMPOSE,
        "Makefile": BuildFileType.MAKEFILE,
        "makefile": BuildFileType.MAKEFILE,
        "GNUmakefile": BuildFileType.MAKEFILE,
        "package.json": BuildFileType.PACKAGE_JSON,
        "requirements.txt": BuildFileType.REQUIREMENTS_TXT,
        "pyproject.toml": BuildFileType.PYPROJECT_TOML,
        "go.mod": BuildFileType.GO_MOD,
        "Cargo.toml": BuildFileType.CARGO_TOML,
        "Gemfile": BuildFileType.GEMFILE,
        "build.gradle": BuildFileType.BUILD_GRADLE,
        "build.gradle.kts": BuildFileType.BUILD_GRADLE,
        "pom.xml": BuildFileType.POM_XML,
    }

    def __init__(self, rules: list[BuildRule] | None = None):
        """Initialize scanner.

        Args:
            rules: Custom rules to use. Defaults to built-in rules.
        """
        self._rules = rules or BUILD_RULES

    def detect_file_type(self, file_path: str) -> BuildFileType:
        """Detect build file type from file path.

        Args:
            file_path: Path to the file.

        Returns:
            Detected file type or UNKNOWN.
        """
        path = Path(file_path)
        filename = path.name

        # Check exact filename match
        if filename in self.FILE_PATTERNS:
            return self.FILE_PATTERNS[filename]

        # Check if it's a Dockerfile variant
        if filename.startswith("Dockerfile") or filename.endswith(".dockerfile"):
            return BuildFileType.DOCKERFILE

        return BuildFileType.UNKNOWN

    def is_build_file(self, file_path: str) -> bool:
        """Check if a file is a build configuration file.

        Args:
            file_path: Path to check.

        Returns:
            True if it's a build configuration file.
        """
        return self.detect_file_type(file_path) != BuildFileType.UNKNOWN

    def scan_file(
        self,
        content: str,
        file_path: str,
        file_type: BuildFileType | None = None,
    ) -> list[BuildFinding]:
        """Scan a single file for reproducibility issues.

        Args:
            content: File content.
            file_path: Path to the file.
            file_type: Build file type (auto-detected if not provided).

        Returns:
            List of findings.
        """
        # Reject oversized content to prevent resource exhaustion
        if len(content) > 10 * 1024 * 1024:  # 10 MB
            logger.warning("File too large to scan (%d bytes): %s", len(content), file_path)
            return []

        if file_type is None:
            file_type = self.detect_file_type(file_path)

        findings = []

        for rule in self._rules:
            # Check if rule applies to this file type
            if file_type not in rule.file_types:
                continue

            matches = rule.check(content)

            for match in matches:
                finding = BuildFinding(
                    rule_id=rule.id,
                    rule_name=rule.name,
                    description=rule.description,
                    file_path=file_path,
                    line_no=match["line_no"],
                    column_start=match["column_start"],
                    column_end=match["column_end"],
                    matched_text=match["matched_text"],
                    line_content=match["line_content"],
                    severity=rule.severity,
                    category=rule.category,
                    file_type=file_type,
                    remediation=rule.remediation,
                )
                findings.append(finding)

        return findings

    def scan_files(
        self,
        files: dict[str, str],
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> BuildScanResult:
        """Scan multiple files for reproducibility issues.

        Args:
            files: Dictionary mapping file path to content.
            repository: Repository name.
            ref: Git ref being scanned.
            scan_id: Optional scan ID.

        Returns:
            BuildScanResult with all findings.
        """
        if scan_id is None:
            scan_id = self._generate_scan_id()

        result = BuildScanResult(
            scan_id=scan_id,
            timestamp=datetime.now(UTC).isoformat(),
            repository=repository,
            ref=ref,
        )

        for file_path, content in files.items():
            file_type = self.detect_file_type(file_path)
            if file_type == BuildFileType.UNKNOWN:
                continue

            result.files_scanned.append(file_path)
            result.file_types_detected.add(file_type)

            try:
                findings = self.scan_file(content, file_path, file_type)

                for finding in findings:
                    result.findings.append(finding)
                    result.total_issues += 1

                    # Update severity counts
                    if finding.severity == BuildIssueSeverity.CRITICAL:
                        result.critical_count += 1
                    elif finding.severity == BuildIssueSeverity.HIGH:
                        result.high_count += 1
                    elif finding.severity == BuildIssueSeverity.MEDIUM:
                        result.medium_count += 1
                    elif finding.severity == BuildIssueSeverity.LOW:
                        result.low_count += 1
                    else:
                        result.info_count += 1

                    # Update category counts
                    if finding.category == BuildIssueCategory.REPRODUCIBILITY:
                        result.reproducibility_count += 1
                    elif finding.category == BuildIssueCategory.CACHING:
                        result.caching_count += 1
                    elif finding.category == BuildIssueCategory.SECURITY:
                        result.security_count += 1
                    elif finding.category == BuildIssueCategory.BEST_PRACTICE:
                        result.best_practice_count += 1
                    elif finding.category == BuildIssueCategory.PERFORMANCE:
                        result.performance_count += 1
                    elif finding.category == BuildIssueCategory.MAINTENANCE:
                        result.maintenance_count += 1

            except Exception as e:
                logger.warning(f"Error scanning {file_path}: {e}")
                result.errors.append(f"Error scanning {file_path}: {str(e)}")

        # Sort findings by severity
        severity_order = {
            BuildIssueSeverity.CRITICAL: 0,
            BuildIssueSeverity.HIGH: 1,
            BuildIssueSeverity.MEDIUM: 2,
            BuildIssueSeverity.LOW: 3,
            BuildIssueSeverity.INFO: 4,
        }
        result.findings.sort(key=lambda f: severity_order.get(f.severity, 5))

        return result

    def scan_directory(
        self,
        directory: str | Path,
        repository: str = "",
        ref: str = "",
        scan_id: str | None = None,
    ) -> BuildScanResult:
        """Scan a directory for build configuration files.

        Args:
            directory: Path to directory.
            repository: Repository name.
            ref: Git ref.
            scan_id: Optional scan ID.

        Returns:
            BuildScanResult with findings.
        """
        directory = Path(directory)
        files: dict[str, str] = {}

        # Scan for all known build files
        for pattern in self.FILE_PATTERNS:
            for found_file in directory.rglob(pattern):
                try:
                    files[str(found_file)] = found_file.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {found_file}: {e}")

        # Also scan for Dockerfile variants
        for dockerfile in directory.rglob("Dockerfile*"):
            if str(dockerfile) not in files:
                try:
                    files[str(dockerfile)] = dockerfile.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {dockerfile}: {e}")

        for dockerfile in directory.rglob("*.dockerfile"):
            if str(dockerfile) not in files:
                try:
                    files[str(dockerfile)] = dockerfile.read_text()
                except Exception as e:
                    logger.warning(f"Could not read {dockerfile}: {e}")

        return self.scan_files(
            files,
            repository=repository or str(directory),
            ref=ref,
            scan_id=scan_id,
        )

    def _generate_scan_id(self) -> str:
        """Generate a unique scan ID."""
        import uuid

        return f"build_scan_{uuid.uuid4().hex[:12]}"


def scan_build_reproducibility(
    files: dict[str, str],
    repository: str = "",
    ref: str = "",
) -> BuildScanResult:
    """Convenience function to scan build files for reproducibility issues.

    Args:
        files: Dictionary mapping file path to content.
        repository: Repository name.
        ref: Git ref.

    Returns:
        BuildScanResult with findings.
    """
    scanner = BuildScanner()
    return scanner.scan_files(files, repository=repository, ref=ref)
