#!/usr/bin/env python3

###############################################################################
# vulnerability_report_parser.py
#
# Parse vulnerability reports and format them for Claude
#
# Supports:
#  - npm audit (JSON)
#  - safety (JSON)
#  - composer audit (JSON)
#  - cargo audit (JSON)
#  - Generic CVSS reports
#
# Usage:
#   ./vulnerability_report_parser.py --input report.json
#   ./vulnerability_report_parser.py --input npm-audit.json --format markdown
#   ./vulnerability_report_parser.py --input report.json --severity high
#
# Exit Codes:
#   0 - Parsing successful
#   1 - Error
#   2 - Invalid arguments
###############################################################################

import argparse
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from enum import Enum


class Severity(Enum):
    """Vulnerability severity levels."""
    CRITICAL = 4
    HIGH = 3
    MODERATE = 2
    LOW = 1
    INFO = 0

    @classmethod
    def from_string(cls, value: str) -> "Severity":
        """Convert string to Severity enum.

        Args:
            value: Severity level string

        Returns:
            Severity enum value
        """
        value_lower = value.lower()
        for member in cls:
            if member.name.lower() == value_lower:
                return member
        raise ValueError(f"Unknown severity level: {value}")


class Vulnerability:
    """Represents a single vulnerability."""

    def __init__(
        self,
        name: str,
        version: str,
        severity: Severity,
        cve: Optional[str] = None,
        description: Optional[str] = None,
        remediation: Optional[str] = None,
        affected_package: Optional[str] = None,
        fixed_in: Optional[str] = None,
    ):
        """Initialize a vulnerability.

        Args:
            name: Package name
            version: Affected version
            severity: Severity level
            cve: CVE identifier
            description: Vulnerability description
            remediation: How to fix
            affected_package: Full package identifier
            fixed_in: Version where it's fixed
        """
        self.name = name
        self.version = version
        self.severity = severity
        self.cve = cve
        self.description = description
        self.remediation = remediation
        self.affected_package = affected_package
        self.fixed_in = fixed_in

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary.

        Returns:
            Dictionary representation
        """
        return {
            "name": self.name,
            "version": self.version,
            "severity": self.severity.name,
            "cve": self.cve,
            "description": self.description,
            "remediation": self.remediation,
            "affected_package": self.affected_package,
            "fixed_in": self.fixed_in,
        }


class VulnerabilityReport:
    """Parses and formats vulnerability reports."""

    def __init__(self, min_severity: Severity = Severity.LOW, verbose: bool = False):
        """Initialize the report parser.

        Args:
            min_severity: Minimum severity to include
            verbose: Enable verbose output
        """
        self.min_severity = min_severity
        self.verbose = verbose
        self.vulnerabilities: List[Vulnerability] = []

    def log(self, message: str) -> None:
        """Log a message if verbose mode is enabled.

        Args:
            message: Message to log
        """
        if self.verbose:
            print(f"[DEBUG] {message}", file=sys.stderr)

    def error(self, message: str) -> None:
        """Log an error message.

        Args:
            message: Error message
        """
        print(f"ERROR: {message}", file=sys.stderr)

    def parse_npm_audit(self, data: Dict[str, Any]) -> None:
        """Parse npm audit JSON report.

        Args:
            data: npm audit JSON data
        """
        self.log("Parsing npm audit report")

        vulnerabilities = data.get("vulnerabilities", {})

        for package_name, package_data in vulnerabilities.items():
            if not isinstance(package_data, dict):
                continue

            # Get severity info
            via = package_data.get("via", [])
            if isinstance(via, str):
                via = [via]

            for vuln in via:
                if isinstance(vuln, str):
                    # String reference - skip
                    continue

                if isinstance(vuln, dict):
                    severity_str = vuln.get("severity", "low")
                    severity = Severity.from_string(severity_str)

                    if severity.value < self.min_severity.value:
                        continue

                    vuln_obj = Vulnerability(
                        name=package_name,
                        version=package_data.get("version", "unknown"),
                        severity=severity,
                        cve=vuln.get("cve"),
                        description=vuln.get("title") or vuln.get("summary"),
                        affected_package=vuln.get("package"),
                        fixed_in=vuln.get("patched_versions"),
                    )

                    self.vulnerabilities.append(vuln_obj)

    def parse_safety(self, data: List[Dict[str, Any]]) -> None:
        """Parse safety JSON report.

        Args:
            data: safety JSON data
        """
        self.log("Parsing safety report")

        for vuln in data:
            if not isinstance(vuln, dict):
                continue

            # Determine severity from CVSS score
            cvss_score = vuln.get("cvss_score")
            if cvss_score:
                if cvss_score >= 9.0:
                    severity = Severity.CRITICAL
                elif cvss_score >= 7.0:
                    severity = Severity.HIGH
                elif cvss_score >= 4.0:
                    severity = Severity.MODERATE
                else:
                    severity = Severity.LOW
            else:
                severity = Severity.MODERATE

            if severity.value < self.min_severity.value:
                continue

            vuln_obj = Vulnerability(
                name=vuln.get("package"),
                version=vuln.get("installed_version"),
                severity=severity,
                cve=vuln.get("vulnerability_id"),
                description=vuln.get("summary"),
                affected_package=f"{vuln.get('package')}=={vuln.get('installed_version')}",
                fixed_in=vuln.get("safe_version"),
            )

            self.vulnerabilities.append(vuln_obj)

    def parse_composer_audit(self, data: Dict[str, Any]) -> None:
        """Parse Composer audit JSON report.

        Args:
            data: Composer audit JSON data
        """
        self.log("Parsing Composer audit report")

        vulnerabilities = data.get("vulnerabilities", [])

        for vuln in vulnerabilities:
            if not isinstance(vuln, dict):
                continue

            # Determine severity
            severity_str = vuln.get("severity", "low")
            try:
                severity = Severity.from_string(severity_str)
            except ValueError:
                severity = Severity.MODERATE

            if severity.value < self.min_severity.value:
                continue

            vuln_obj = Vulnerability(
                name=vuln.get("package"),
                version=vuln.get("version"),
                severity=severity,
                cve=vuln.get("cve"),
                description=vuln.get("title") or vuln.get("summary"),
                affected_package=f"{vuln.get('package')}:{vuln.get('version')}",
                fixed_in=vuln.get("fixed_in"),
            )

            self.vulnerabilities.append(vuln_obj)

    def parse_cargo_audit(self, data: Dict[str, Any]) -> None:
        """Parse Cargo audit JSON report.

        Args:
            data: Cargo audit JSON data
        """
        self.log("Parsing Cargo audit report")

        vulnerabilities = data.get("vulnerabilities", [])

        for vuln in vulnerabilities:
            if not isinstance(vuln, dict):
                continue

            severity_str = vuln.get("severity", "low")
            try:
                severity = Severity.from_string(severity_str)
            except ValueError:
                severity = Severity.MODERATE

            if severity.value < self.min_severity.value:
                continue

            vuln_obj = Vulnerability(
                name=vuln.get("package", {}).get("name"),
                version=vuln.get("version"),
                severity=severity,
                cve=vuln.get("advisory", {}).get("id"),
                description=vuln.get("advisory", {}).get("title"),
                affected_package=vuln.get("package", {}).get("name"),
            )

            self.vulnerabilities.append(vuln_obj)

    def parse_file(self, file_path: str) -> bool:
        """Parse a vulnerability report file.

        Args:
            file_path: Path to the report file

        Returns:
            True if successful, False otherwise
        """
        try:
            with open(file_path, "r") as f:
                data = json.load(f)

            self.log(f"Loaded report from {file_path}")

            # Try to detect format and parse
            if isinstance(data, dict):
                if "vulnerabilities" in data and "metadata" in data:
                    # npm audit format
                    self.parse_npm_audit(data)
                elif "vulnerabilities" in data:
                    # Composer audit format
                    self.parse_composer_audit(data)
                else:
                    # Try generic format
                    self.log("Detected generic vulnerability format")

            elif isinstance(data, list):
                # safety or generic list format
                self.parse_safety(data)

            return True
        except json.JSONDecodeError as e:
            self.error(f"Invalid JSON: {e}")
            return False
        except Exception as e:
            self.error(f"Failed to parse report: {e}")
            return False

    def to_text(self) -> str:
        """Format report as plain text.

        Returns:
            Text formatted report
        """
        if not self.vulnerabilities:
            return "No vulnerabilities found.\n"

        lines = [
            f"Vulnerability Report - {len(self.vulnerabilities)} issue(s) found",
            "=" * 70,
            "",
        ]

        # Group by severity
        by_severity = {}
        for vuln in self.vulnerabilities:
            if vuln.severity not in by_severity:
                by_severity[vuln.severity] = []
            by_severity[vuln.severity].append(vuln)

        # Output by severity (highest first)
        for severity in sorted(by_severity.keys(), key=lambda x: x.value, reverse=True):
            vulns = by_severity[severity]
            lines.append(f"\n{severity.name} ({len(vulns)} issue(s)):")
            lines.append("-" * 70)

            for vuln in vulns:
                lines.append(f"\nPackage: {vuln.name}")
                if vuln.version:
                    lines.append(f"Version: {vuln.version}")
                if vuln.cve:
                    lines.append(f"CVE: {vuln.cve}")
                if vuln.description:
                    lines.append(f"Description: {vuln.description}")
                if vuln.fixed_in:
                    lines.append(f"Fixed in: {vuln.fixed_in}")
                if vuln.remediation:
                    lines.append(f"Remediation: {vuln.remediation}")

        lines.append("")
        return "\n".join(lines)

    def to_markdown(self) -> str:
        """Format report as Markdown.

        Returns:
            Markdown formatted report
        """
        if not self.vulnerabilities:
            return "# Vulnerability Report\n\nNo vulnerabilities found.\n"

        lines = [
            "# Vulnerability Report",
            "",
            f"**Total Issues:** {len(self.vulnerabilities)}",
            "",
        ]

        # Group by severity
        by_severity = {}
        for vuln in self.vulnerabilities:
            if vuln.severity not in by_severity:
                by_severity[vuln.severity] = []
            by_severity[vuln.severity].append(vuln)

        # Summary table
        lines.append("## Summary")
        lines.append("")
        lines.append("| Severity | Count |")
        lines.append("|----------|-------|")
        for severity in sorted(by_severity.keys(), key=lambda x: x.value, reverse=True):
            lines.append(f"| {severity.name} | {len(by_severity[severity])} |")
        lines.append("")

        # Details by severity
        lines.append("## Details")
        lines.append("")

        for severity in sorted(by_severity.keys(), key=lambda x: x.value, reverse=True):
            vulns = by_severity[severity]
            lines.append(f"### {severity.name} ({len(vulns)} issue(s))")
            lines.append("")

            for vuln in vulns:
                lines.append(f"**{vuln.name}** (v{vuln.version})")
                if vuln.cve:
                    lines.append(f"- CVE: `{vuln.cve}`")
                if vuln.description:
                    lines.append(f"- Description: {vuln.description}")
                if vuln.fixed_in:
                    lines.append(f"- Fixed in: `{vuln.fixed_in}`")
                lines.append("")

        return "\n".join(lines)

    def to_json(self) -> str:
        """Format report as JSON.

        Returns:
            JSON formatted report
        """
        output = {
            "summary": {
                "total": len(self.vulnerabilities),
                "by_severity": {
                    severity.name: sum(
                        1 for v in self.vulnerabilities if v.severity == severity
                    )
                    for severity in Severity
                },
            },
            "vulnerabilities": [v.to_dict() for v in self.vulnerabilities],
        }

        return json.dumps(output, indent=2)

    def to_csv(self) -> str:
        """Format report as CSV.

        Returns:
            CSV formatted report
        """
        lines = [
            "Package,Version,Severity,CVE,Description,Fixed In",
        ]

        for vuln in self.vulnerabilities:
            line = f'"{vuln.name}","{vuln.version}","{vuln.severity.name}","{vuln.cve or ""}","{vuln.description or ""}","{vuln.fixed_in or ""}"'
            lines.append(line)

        return "\n".join(lines) + "\n"


def main() -> int:
    """Main entry point.

    Returns:
        Exit code (0 for success, 1 for error, 2 for invalid arguments)
    """
    parser = argparse.ArgumentParser(
        description="Parse vulnerability reports and format for Claude",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s --input report.json
  %(prog)s --input npm-audit.json --format markdown
  %(prog)s --input report.json --severity high --output parsed.md
  %(prog)s --input vulnerabilities.json --format csv
""",
    )

    parser.add_argument(
        "--input",
        "-i",
        required=True,
        help="Path to vulnerability report JSON file",
    )

    parser.add_argument(
        "--format",
        "-f",
        default="text",
        choices=["text", "markdown", "json", "csv"],
        help="Output format (default: text)",
    )

    parser.add_argument(
        "--severity",
        "-s",
        default="low",
        choices=["low", "moderate", "high", "critical"],
        help="Minimum severity to include (default: low)",
    )

    parser.add_argument(
        "--output",
        "-o",
        help="Output file (default: stdout)",
    )

    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Enable verbose output",
    )

    args = parser.parse_args()

    # Create parser
    try:
        min_severity = Severity.from_string(args.severity)
    except ValueError:
        print(f"ERROR: Invalid severity level: {args.severity}", file=sys.stderr)
        return 2

    parser_obj = VulnerabilityReport(min_severity=min_severity, verbose=args.verbose)

    # Parse report
    if not parser_obj.parse_file(args.input):
        return 1

    # Format output
    if args.format == "text":
        output = parser_obj.to_text()
    elif args.format == "markdown":
        output = parser_obj.to_markdown()
    elif args.format == "json":
        output = parser_obj.to_json()
    elif args.format == "csv":
        output = parser_obj.to_csv()
    else:
        output = parser_obj.to_text()

    # Write output
    if args.output:
        try:
            with open(args.output, "w") as f:
                f.write(output)
            print(f"Saved report to: {args.output}")
        except Exception as e:
            print(f"ERROR: Failed to write output: {e}", file=sys.stderr)
            return 1
    else:
        print(output)

    return 0


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