```python
#!/usr/bin/env python3
"""Secure ML Container Deployment Validation Script.

This script helps teams perform a practical pre-release review for containerized
machine learning model deployments.

It checks for:
- model artifact presence and optional checksum validation
- dependency pinning in common requirement files
- Dockerfile security hygiene and runtime hardening signals
- basic container image metadata validation from a local manifest file
- a simple release readiness report

The script is intentionally vendor-neutral and does not contact external services.
It is designed to assist with local validation in CI/CD pipelines or developer workstations.

Usage examples:
    python secure_ml_deploy_check.py --model model.pkl --requirements requirements.txt --dockerfile Dockerfile
    python secure_ml_deploy_check.py --model model.pkl --checksum-file model.sha256 --dockerfile Dockerfile --image-manifest image-manifest.json
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Tuple


PINNED_SPECIFIERS = ("==", "===")
UNSAFE_DOCKERFILE_PATTERNS = {
    r"^\s*FROM\s+.*:latest\s*$": "Base image uses 'latest' tag",
    r"^\s*USER\s+root\s*$": "Container runs as root",
    r"^\s*RUN\s+.*apt-get\s+install(?!.*--no-install-recommends).*$": "APT install without --no-install-recommends",
    r"^\s*ADD\s+": "Prefer COPY over ADD unless ADD semantics are required",
    r"^\s*COPY\s+.*\s+/root/.*$": "Files copied into root home directory",
}


@dataclass
class CheckResult:
    name: str
    passed: bool
    details: List[str] = field(default_factory=list)


@dataclass
class Report:
    checks: List[CheckResult] = field(default_factory=list)

    def add(self, result: CheckResult) -> None:
        self.checks.append(result)

    def passed(self) -> bool:
        return all(check.passed for check in self.checks)

    def print(self) -> None:
        print("\nSecure ML Container Deployment Validation Report")
        print("=" * 52)
        for check in self.checks:
            status = "PASS" if check.passed else "FAIL"
            print(f"\n[{status}] {check.name}")
            for detail in check.details:
                print(f"  - {detail}")

        print("\nSummary")
        print("-" * 7)
        print(f"Checks passed: {sum(1 for c in self.checks if c.passed)} / {len(self.checks)}")
        print(f"Release readiness: {'READY' if self.passed() else 'NOT READY'}")


def read_text_file(path: Path) -> str:
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")
    if not path.is_file():
        raise IsADirectoryError(f"Expected a file, got: {path}")
    return path.read_text(encoding="utf-8", errors="replace")


def compute_sha256(path: Path) -> str:
    hasher = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b""):
            hasher.update(chunk)
    return hasher.hexdigest()


def validate_model_artifact(model_path: Path, checksum_file: Optional[Path]) -> CheckResult:
    details = []
    if not model_path.exists():
        return CheckResult("Model artifact exists", False, [f"Missing model artifact: {model_path}"])

    if not model_path.is_file():
        return CheckResult("Model artifact exists", False, [f"Model path is not a file: {model_path}"])

    details.append(f"Found model artifact: {model_path}")

    if checksum_file is not None:
        if not checksum_file.exists():
            return CheckResult("Model artifact checksum", False, [f"Missing checksum file: {checksum_file}"])

        checksum_text = read_text_file(checksum_file).strip()
        expected = checksum_text.split()[0]
        actual = compute_sha256(model_path)
        details.append(f"Expected SHA-256: {expected}")
        details.append(f"Actual SHA-256:   {actual}")
        if expected.lower() != actual.lower():
            return CheckResult("Model artifact checksum", False, details + ["Checksum mismatch"])

        details.append("Checksum matches")
        return CheckResult("Model artifact checksum", True, details)

    details.append("No checksum file provided; artifact integrity not fully verified")
    return CheckResult("Model artifact exists", True, details)


def validate_requirements_file(requirements_path: Optional[Path]) -> CheckResult:
    if requirements_path is None:
        return CheckResult(
            "Dependency pinning",
            False,
            ["No requirements file provided; cannot verify dependency pinning"],
        )

    if not requirements_path.exists():
        return CheckResult("Dependency pinning", False, [f"Missing requirements file: {requirements_path}"])

    lines = read_text_file(requirements_path).splitlines()
    issues = []
    checked = 0

    for raw in lines:
        line = raw.strip()
        if not line or line.startswith("#") or line.startswith("-"):
            continue
        if "@" in line and "https://" in line:
            issues.append(f"Direct URL dependency found: {line}")
        if any(spec in line for spec in PINNED_SPECIFIERS):
            checked += 1
        else:
            # Skip local paths and editable installs, but flag typical unpinned packages.
            if re.match(r"^[A-Za-z0-9_.-]+(?:\[.*\])?(?:\s*[<>=!~]=?.*)?$", line):
                issues.append(f"Unpinned or loosely pinned dependency: {line}")
            checked += 1

    passed = len(issues) == 0
    details = [f"Validated file: {requirements_path}", f"Dependency entries reviewed: {checked}"]
    details.extend(issues)
    if passed:
        details.append("Dependency pinning looks acceptable")
    return CheckResult("Dependency pinning", passed, details)


def validate_dockerfile(dockerfile_path: Optional[Path]) -> CheckResult:
    if dockerfile_path is None:
        return CheckResult("Dockerfile hardening", False, ["No Dockerfile provided; cannot assess runtime hardening"])

    if not dockerfile_path.exists():
        return CheckResult("Dockerfile hardening", False, [f"Missing Dockerfile: {dockerfile_path}"])

    text = read_text_file(dockerfile_path)
    details = [f"Validated file: {dockerfile_path}"]
    issues = []

    for pattern, message in UNSAFE_DOCKERFILE_PATTERNS.items():
        if re.search(pattern, text, flags=re.MULTILINE):
            issues.append(message)

    if re.search(r"^\s*USER\s+(?!root\b)[A-Za-z0-9_-]+\s*$", text, flags=re.MULTILINE) is None:
        issues.append("No non-root USER instruction detected")

    if "read-only" not in text.lower():
        details.append("Note: read-only filesystem is typically configured at runtime, not in Dockerfile")

    passed = len(issues) == 0
    details.extend(issues)
    if passed:
        details.append("Dockerfile shows basic hardening signals")
    return CheckResult("Dockerfile hardening", passed, details)


def validate_image_manifest(image_manifest_path: Optional[Path]) -> CheckResult:
    if image_manifest_path is None:
        return CheckResult(
            "Image identity",
            False,
            ["No image manifest provided; cannot verify image metadata or signature status"],
        )

    if not image_manifest_path.exists():
        return CheckResult("Image identity", False, [f"Missing image manifest: {image_manifest_path}"])

    try:
        data = json.loads(read_text_file(image_manifest_path))
    except json.JSONDecodeError as exc:
        return CheckResult("Image identity", False, [f"Invalid JSON manifest: {exc}"])

    details = [f"Validated file: {image_manifest_path}"]
    issues = []

    image_ref = data.get("image")
    digest = data.get("digest")
    signature = data.get("signature_verified")
    provenance = data.get("provenance_verified")

    if not image_ref:
        issues.append("Missing image reference")
    else:
        details.append(f"Image: {image_ref}")
        if ":latest" in str(image_ref):
            issues.append("Image reference uses mutable 'latest' tag")

    if not digest:
        issues.append("Missing image digest")
    else:
        details.append(f"Digest: {digest}")

    if signature is not True:
        issues.append("Signature verification not confirmed")
    if provenance is not True:
        issues.append("Provenance verification not confirmed")

    passed = len(issues) == 0
    details.extend(issues)
    if passed:
        details.append("Image identity and verification flags look acceptable")
    return CheckResult("Image identity", passed, details)


def validate_model_behavior(behavior_report_path: Optional[Path]) -> CheckResult:
    if behavior_report_path is None:
        return CheckResult(
            "Behavior validation",
            False,
            ["No behavior report provided; cannot confirm expected-input or output sanity checks"],
        )

    if not behavior_report_path.exists():
        return CheckResult("Behavior validation", False, [f"Missing behavior report: {behavior_report_path}"])

    try:
        data = json.loads(read_text_file(behavior_report_path))
    except json.JSONDecodeError as exc:
        return CheckResult("Behavior validation", False, [f"Invalid JSON behavior report: {exc}"])

    details = [f"Validated file: {behavior_report_path}"]
    issues = []

    expected_input_tests = data.get("expected_input_tests", False)
    schema_checks = data.get("schema_checks", False)
    output_sanity_checks = data.get("output_sanity_checks", False)

    for name, value in (
        ("expected_input_tests", expected_input_tests),
        ("schema_checks", schema_checks),
        ("output_sanity_checks", output_sanity_checks),
    ):
        details.append(f"{name}: {value}")
        if value is not True:
            issues.append(f"{name} not confirmed")

    passed = len(issues) == 0
    details.extend(issues)
    if passed:
        details.append("Behavior validation checks are confirmed")
    return CheckResult("Behavior validation", passed, details)


def build_report(args: argparse.Namespace) -> Report:
    report = Report()
    report.add(validate_model_artifact(args.model, args.checksum_file))
    report.add(validate_requirements_file(args.requirements))
    report.add(validate_dockerfile(args.dockerfile))
    report.add(validate_image_manifest(args.image_manifest))
    report.add(validate_model_behavior(args.behavior_report))
    return report


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Validate security and readiness controls for a containerized ML model deployment.",
    )
    parser.add_argument("--model", type=Path, required=True, help="Path to the model artifact file")
    parser.add_argument("--checksum-file", type=Path, default=None, help="Optional path to a SHA-256 checksum file")
    parser.add_argument("--requirements", type=Path, default=None, help="Optional path to a pinned requirements file")
    parser.add_argument("--dockerfile", type=Path, default=None, help="Optional path to the Dockerfile")
    parser.add_argument("--image-manifest", type=Path, default=None, help="Optional JSON file with image identity metadata")
    parser.add_argument(
        "--behavior-report",
        type=Path,
        default=None,
        help="Optional JSON file with behavior validation results",
    )
    parser.add_argument(
        "--fail-on-warning",
        action="store_true",
        help="Treat missing optional inputs as failures instead of warnings",
    )
    return parser.parse_args()


def main() -> int:
    args = parse_args()

    report = build_report(args)
    report.print()

    if args.fail_on_warning:
        missing_optional = any(
            check.details and any("No " in detail or "not provided" in detail for detail in check.details)
            for check in report.checks
        )
        if missing_optional and report.passed():
            print("\nOptional inputs missing and --fail-on-warning set.")
            return 2

    return 0 if report.passed() else 1


if __name__ == "__main__":
    sys.exit(main())
```