#!/usr/bin/env python3
"""
GDPR Compliance Checker

Scans codebases, configurations, and data handling patterns for potential
GDPR compliance issues. Identifies personal data processing, consent gaps,
and documentation requirements.

Usage:
    python gdpr_compliance_checker.py /path/to/project
    python gdpr_compliance_checker.py . --json
    python gdpr_compliance_checker.py /path/to/project --output report.json
"""

import argparse
import json
import os
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple


# Personal data patterns to detect
PERSONAL_DATA_PATTERNS = {
    "email": {
        "pattern": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
        "category": "contact_data",
        "gdpr_article": "Art. 4(1)",
        "risk": "medium"
    },
    "ip_address": {
        "pattern": r"\b(?:\d{1,3}\.){3}\d{1,3}\b",
        "category": "online_identifier",
        "gdpr_article": "Art. 4(1), Recital 30",
        "risk": "medium"
    },
    "phone_number": {
        "pattern": r"(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}",
        "category": "contact_data",
        "gdpr_article": "Art. 4(1)",
        "risk": "medium"
    },
    "credit_card": {
        "pattern": r"\b(?:\d{4}[-\s]?){3}\d{4}\b",
        "category": "financial_data",
        "gdpr_article": "Art. 4(1)",
        "risk": "high"
    },
    "iban": {
        "pattern": r"\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}(?:[A-Z0-9]?){0,16}\b",
        "category": "financial_data",
        "gdpr_article": "Art. 4(1)",
        "risk": "high"
    },
    "german_id": {
        "pattern": r"\b[A-Z0-9]{9}\b",
        "category": "government_id",
        "gdpr_article": "Art. 4(1)",
        "risk": "high"
    },
    "date_of_birth": {
        "pattern": r"\b(?:birth|dob|geboren|geburtsdatum)\b",
        "category": "demographic_data",
        "gdpr_article": "Art. 4(1)",
        "risk": "medium"
    },
    "health_data": {
        "pattern": r"\b(?:diagnosis|treatment|medication|patient|medical|health|symptom|disease)\b",
        "category": "special_category",
        "gdpr_article": "Art. 9(1)",
        "risk": "critical"
    },
    "biometric": {
        "pattern": r"\b(?:fingerprint|facial|retina|biometric|voice_print)\b",
        "category": "special_category",
        "gdpr_article": "Art. 9(1)",
        "risk": "critical"
    },
    "religion": {
        "pattern": r"\b(?:religion|religious|faith|church|mosque|synagogue)\b",
        "category": "special_category",
        "gdpr_article": "Art. 9(1)",
        "risk": "critical"
    }
}

# Code patterns indicating GDPR concerns
CODE_PATTERNS = {
    "logging_personal_data": {
        "pattern": r"(?:log|print|console)\s*\.\s*(?:info|debug|warn|error)\s*\([^)]*(?:email|user|name|address|phone)",
        "issue": "Potential logging of personal data",
        "gdpr_article": "Art. 5(1)(c) - Data minimization",
        "recommendation": "Review logging to ensure personal data is not logged or is properly pseudonymized",
        "severity": "high"
    },
    "missing_consent": {
        "pattern": r"(?:track|analytics|marketing|cookie)(?!.*consent)",
        "issue": "Tracking without apparent consent mechanism",
        "gdpr_article": "Art. 6(1)(a) - Consent",
        "recommendation": "Implement consent management before tracking",
        "severity": "high"
    },
    "hardcoded_retention": {
        "pattern": r"(?:retention|expire|ttl|lifetime)\s*[=:]\s*(?:null|undefined|0|never|forever)",
        "issue": "Indefinite data retention detected",
        "gdpr_article": "Art. 5(1)(e) - Storage limitation",
        "recommendation": "Define and implement data retention periods",
        "severity": "medium"
    },
    "third_party_transfer": {
        "pattern": r"(?:api|http|fetch|request)\s*\.\s*(?:post|put|send)\s*\([^)]*(?:user|personal|data)",
        "issue": "Potential third-party data transfer",
        "gdpr_article": "Art. 28 - Processor requirements",
        "recommendation": "Ensure Data Processing Agreement exists with third parties",
        "severity": "medium"
    },
    "encryption_missing": {
        "pattern": r"(?:password|secret|token|key)\s*[=:]\s*['\"][^'\"]+['\"]",
        "issue": "Potentially unencrypted sensitive data",
        "gdpr_article": "Art. 32(1)(a) - Encryption",
        "recommendation": "Encrypt sensitive data at rest and in transit",
        "severity": "critical"
    },
    "no_deletion": {
        "pattern": r"(?:delete|remove|erase).*(?:disabled|false|TODO|FIXME)",
        "issue": "Data deletion may be disabled or incomplete",
        "gdpr_article": "Art. 17 - Right to erasure",
        "recommendation": "Implement complete data deletion functionality",
        "severity": "high"
    }
}

# Configuration files to check for GDPR-relevant settings
CONFIG_PATTERNS = {
    "analytics_config": {
        "files": ["analytics.json", "gtag.js", "google-analytics.js"],
        "check": "anonymize_ip",
        "issue": "IP anonymization should be enabled for analytics",
        "gdpr_article": "Art. 5(1)(c)"
    },
    "cookie_config": {
        "files": ["cookie.config.js", "cookies.json"],
        "check": "consent_required",
        "issue": "Cookie consent should be required before non-essential cookies",
        "gdpr_article": "Art. 6(1)(a)"
    }
}

# File extensions to scan
SCANNABLE_EXTENSIONS = {
    ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".kt",
    ".go", ".rb", ".php", ".cs", ".swift", ".json", ".yaml",
    ".yml", ".xml", ".html", ".env", ".config"
}

# Files/directories to skip
SKIP_PATTERNS = {
    "node_modules", "vendor", ".git", "__pycache__", "dist",
    "build", ".venv", "venv", "env"
}


def should_skip(path: Path) -> bool:
    """Check if path should be skipped."""
    return any(skip in path.parts for skip in SKIP_PATTERNS)


def scan_file_for_patterns(
    filepath: Path,
    patterns: Dict
) -> List[Dict]:
    """Scan a file for pattern matches."""
    findings = []

    try:
        with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
            content = f.read()
            lines = content.split("\n")

        for pattern_name, pattern_info in patterns.items():
            regex = re.compile(pattern_info["pattern"], re.IGNORECASE)

            for line_num, line in enumerate(lines, 1):
                matches = regex.findall(line)
                if matches:
                    findings.append({
                        "file": str(filepath),
                        "line": line_num,
                        "pattern": pattern_name,
                        "matches": len(matches) if isinstance(matches, list) else 1,
                        **{k: v for k, v in pattern_info.items() if k != "pattern"}
                    })

    except Exception as e:
        pass  # Skip files that can't be read

    return findings


def analyze_project(project_path: Path) -> Dict:
    """Analyze project for GDPR compliance issues."""
    personal_data_findings = []
    code_issue_findings = []
    config_findings = []
    files_scanned = 0

    # Scan all relevant files
    for filepath in project_path.rglob("*"):
        if filepath.is_file() and not should_skip(filepath):
            if filepath.suffix.lower() in SCANNABLE_EXTENSIONS:
                files_scanned += 1

                # Check for personal data patterns
                personal_data_findings.extend(
                    scan_file_for_patterns(filepath, PERSONAL_DATA_PATTERNS)
                )

                # Check for code issues
                code_issue_findings.extend(
                    scan_file_for_patterns(filepath, CODE_PATTERNS)
                )

    # Check for specific config files
    for config_name, config_info in CONFIG_PATTERNS.items():
        for config_file in config_info["files"]:
            config_path = project_path / config_file
            if config_path.exists():
                try:
                    with open(config_path, "r") as f:
                        content = f.read()
                    if config_info["check"] not in content.lower():
                        config_findings.append({
                            "file": str(config_path),
                            "config": config_name,
                            "issue": config_info["issue"],
                            "gdpr_article": config_info["gdpr_article"]
                        })
                except Exception:
                    pass

    # Calculate risk scores
    critical_count = sum(1 for f in personal_data_findings if f.get("risk") == "critical")
    critical_count += sum(1 for f in code_issue_findings if f.get("severity") == "critical")

    high_count = sum(1 for f in personal_data_findings if f.get("risk") == "high")
    high_count += sum(1 for f in code_issue_findings if f.get("severity") == "high")

    medium_count = sum(1 for f in personal_data_findings if f.get("risk") == "medium")
    medium_count += sum(1 for f in code_issue_findings if f.get("severity") == "medium")

    # Determine compliance score (100 = compliant, 0 = critical issues)
    score = 100
    score -= critical_count * 20
    score -= high_count * 10
    score -= medium_count * 5
    score -= len(config_findings) * 5
    score = max(0, score)

    # Determine compliance status
    if score >= 80:
        status = "compliant"
        status_description = "Low risk - minor improvements recommended"
    elif score >= 60:
        status = "needs_attention"
        status_description = "Medium risk - action required"
    elif score >= 40:
        status = "non_compliant"
        status_description = "High risk - immediate action required"
    else:
        status = "critical"
        status_description = "Critical risk - significant GDPR violations detected"

    return {
        "summary": {
            "files_scanned": files_scanned,
            "compliance_score": score,
            "status": status,
            "status_description": status_description,
            "issue_counts": {
                "critical": critical_count,
                "high": high_count,
                "medium": medium_count,
                "config_issues": len(config_findings)
            }
        },
        "personal_data_findings": personal_data_findings[:50],  # Limit output
        "code_issues": code_issue_findings[:50],
        "config_issues": config_findings,
        "recommendations": generate_recommendations(
            personal_data_findings, code_issue_findings, config_findings
        )
    }


def generate_recommendations(
    personal_data: List[Dict],
    code_issues: List[Dict],
    config_issues: List[Dict]
) -> List[Dict]:
    """Generate prioritized recommendations."""
    recommendations = []
    seen_issues = set()

    # Critical issues first
    for finding in code_issues:
        if finding.get("severity") == "critical":
            issue_key = finding.get("issue", "")
            if issue_key not in seen_issues:
                recommendations.append({
                    "priority": "P0",
                    "issue": finding.get("issue"),
                    "gdpr_article": finding.get("gdpr_article"),
                    "action": finding.get("recommendation"),
                    "affected_files": [finding.get("file")]
                })
                seen_issues.add(issue_key)

    # Special category data
    special_category_files = set()
    for finding in personal_data:
        if finding.get("category") == "special_category":
            special_category_files.add(finding.get("file"))

    if special_category_files:
        recommendations.append({
            "priority": "P0",
            "issue": "Special category personal data (Art. 9) detected",
            "gdpr_article": "Art. 9(1)",
            "action": "Ensure explicit consent or other Art. 9(2) legal basis exists",
            "affected_files": list(special_category_files)[:5]
        })

    # High priority issues
    for finding in code_issues:
        if finding.get("severity") == "high":
            issue_key = finding.get("issue", "")
            if issue_key not in seen_issues:
                recommendations.append({
                    "priority": "P1",
                    "issue": finding.get("issue"),
                    "gdpr_article": finding.get("gdpr_article"),
                    "action": finding.get("recommendation"),
                    "affected_files": [finding.get("file")]
                })
                seen_issues.add(issue_key)

    # Config issues
    for finding in config_issues:
        recommendations.append({
            "priority": "P1",
            "issue": finding.get("issue"),
            "gdpr_article": finding.get("gdpr_article"),
            "action": f"Update configuration in {finding.get('file')}",
            "affected_files": [finding.get("file")]
        })

    return recommendations[:15]


def print_report(analysis: Dict) -> None:
    """Print human-readable report."""
    summary = analysis["summary"]

    print("=" * 60)
    print("GDPR COMPLIANCE ASSESSMENT REPORT")
    print("=" * 60)
    print()
    print(f"Compliance Score: {summary['compliance_score']}/100")
    print(f"Status: {summary['status'].upper()}")
    print(f"Assessment: {summary['status_description']}")
    print(f"Files Scanned: {summary['files_scanned']}")
    print()

    counts = summary["issue_counts"]
    print("--- ISSUE SUMMARY ---")
    print(f"  Critical: {counts['critical']}")
    print(f"  High: {counts['high']}")
    print(f"  Medium: {counts['medium']}")
    print(f"  Config Issues: {counts['config_issues']}")
    print()

    if analysis["recommendations"]:
        print("--- PRIORITIZED RECOMMENDATIONS ---")
        for i, rec in enumerate(analysis["recommendations"][:10], 1):
            print(f"\n{i}. [{rec['priority']}] {rec['issue']}")
            print(f"   GDPR Article: {rec['gdpr_article']}")
            print(f"   Action: {rec['action']}")

    print()
    print("=" * 60)
    print("Note: This is an automated assessment. Manual review by a")
    print("qualified Data Protection Officer is recommended.")
    print("=" * 60)


def main():
    parser = argparse.ArgumentParser(
        description="Scan project for GDPR compliance issues"
    )
    parser.add_argument(
        "project_path",
        nargs="?",
        default=".",
        help="Path to project directory (default: current directory)"
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output in JSON format"
    )
    parser.add_argument(
        "--output", "-o",
        help="Write output to file"
    )

    args = parser.parse_args()

    project_path = Path(args.project_path).resolve()
    if not project_path.exists():
        print(f"Error: Path does not exist: {project_path}", file=sys.stderr)
        sys.exit(1)

    analysis = analyze_project(project_path)

    if args.json:
        output = json.dumps(analysis, indent=2)
        if args.output:
            with open(args.output, "w") as f:
                f.write(output)
            print(f"Report written to {args.output}")
        else:
            print(output)
    else:
        print_report(analysis)
        if args.output:
            with open(args.output, "w") as f:
                json.dump(analysis, f, indent=2)
            print(f"\nDetailed JSON report written to {args.output}")


if __name__ == "__main__":
    main()
