#!/usr/bin/env python3
"""
DevSecOps Pipeline Security Maturity Assessor
Main Flask application for assessing CI/CD pipeline security maturity.

Author: Mitchele Jebet
License: MIT
"""

from flask import Flask, request, jsonify, render_template, flash, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager, login_required, current_user
from datetime import datetime, timezone
import os
import json
import yaml
import requests
from typing import Dict, List, Any
import logging
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

# Initialize Flask app
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-key-change-in-production')
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///devsecops_assessor.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Initialize extensions
db = SQLAlchemy(app)
migrate = Migrate(app, db)

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Import models after db initialization
from models import User, Organization, Assessment, Subscription, Invoice

# Initialize authentication (this likely registers the auth_bp internally)
from auth import init_auth
init_auth(app)

# Import and register billing blueprint only
from billing import billing_bp
app.register_blueprint(billing_bp, url_prefix='/billing')

# Security Maturity Framework
class SecurityMaturityFramework:
    """Core framework for assessing DevSecOps maturity"""

    CATEGORIES = {
        'source_code_security': {
            'name': 'Source Code Security',
            'weight': 0.20,
            'checks': [
                'dependency_scanning',
                'secret_detection',
                'sast_tools',
                'code_quality_gates'
            ]
        },
        'build_security': {
            'name': 'Build Security',
            'weight': 0.15,
            'checks': [
                'secure_build_environment',
                'artifact_signing',
                'build_reproducibility',
                'supply_chain_security'
            ]
        },
        'deployment_security': {
            'name': 'Deployment Security',
            'weight': 0.20,
            'checks': [
                'infrastructure_as_code',
                'container_scanning',
                'deployment_approval_gates',
                'environment_isolation'
            ]
        },
        'runtime_security': {
            'name': 'Runtime Security',
            'weight': 0.15,
            'checks': [
                'monitoring_logging',
                'incident_response',
                'vulnerability_management',
                'security_testing'
            ]
        },
        'compliance_governance': {
            'name': 'Compliance & Governance',
            'weight': 0.20,
            'checks': [
                'policy_as_code',
                'audit_trails',
                'compliance_reporting',
                'access_controls'
            ]
        },
        'culture_training': {
            'name': 'Culture & Training',
            'weight': 0.10,
            'checks': [
                'security_champions',
                'training_programs',
                'security_reviews',
                'knowledge_sharing'
            ]
        }
    }

    MATURITY_LEVELS = {
        0: {'name': 'Initial', 'description': 'Ad-hoc security practices, reactive approach'},
        1: {'name': 'Developing', 'description': 'Basic security tools implemented'},
        2: {'name': 'Defined', 'description': 'Documented security processes and standards'},
        3: {'name': 'Managed', 'description': 'Metrics-driven security improvements'},
        4: {'name': 'Optimized', 'description': 'Continuous security optimization and innovation'}
    }

    @classmethod
    def calculate_maturity_level(cls, score: float) -> tuple:
        """Calculate maturity level based on score"""
        if score >= 90:
            level = 4
        elif score >= 75:
            level = 3
        elif score >= 60:
            level = 2
        elif score >= 40:
            level = 1
        else:
            level = 0

        return level, cls.MATURITY_LEVELS[level]

class PipelineAnalyzer:
    """Analyzes CI/CD pipelines for security maturity"""

    def __init__(self):
        self.framework = SecurityMaturityFramework()

    def analyze_github_pipeline(self, repo_url: str, token: str = None) -> Dict[str, Any]:
        """Analyze GitHub Actions pipeline"""
        try:
            # Parse repository URL
            parts = repo_url.replace('https://github.com/', '').split('/')
            owner, repo = parts[0], parts[1]

            # GitHub API headers
            headers = {}
            if token:
                headers['Authorization'] = f'token {token}'

            # Get workflow files
            workflows_url = f'https://api.github.com/repos/{owner}/{repo}/contents/.github/workflows'
            response = requests.get(workflows_url, headers=headers)

            if response.status_code != 200:
                return {'error': f'Failed to access repository: {response.status_code}'}

            workflows = response.json()
            analysis_results = {}

            for workflow_file in workflows:
                if workflow_file['name'].endswith(('.yml', '.yaml')):
                    # Get workflow content
                    content_response = requests.get(workflow_file['download_url'])
                    if content_response.status_code == 200:
                        workflow_content = yaml.safe_load(content_response.text)
                        analysis_results[workflow_file['name']] = self._analyze_workflow_content(workflow_content)

            # Calculate overall scores
            return self._calculate_overall_scores(analysis_results)

        except Exception as e:
            logger.error(f"Error analyzing GitHub pipeline: {str(e)}")
            return {'error': str(e)}

    def analyze_gitlab_pipeline(self, repo_url: str, token: str = None) -> Dict[str, Any]:
        """Analyze GitLab CI pipeline"""
        try:
            # Parse repository URL
            project_path = repo_url.replace('https://gitlab.com/', '').replace('/', '%2F')

            headers = {}
            if token:
                headers['PRIVATE-TOKEN'] = token

            # Get .gitlab-ci.yml file
            file_url = f'https://gitlab.com/api/v4/projects/{project_path}/repository/files/.gitlab-ci.yml/raw?ref=main'
            response = requests.get(file_url, headers=headers)

            if response.status_code != 200:
                # Try master branch
                file_url = f'https://gitlab.com/api/v4/projects/{project_path}/repository/files/.gitlab-ci.yml/raw?ref=master'
                response = requests.get(file_url, headers=headers)

            if response.status_code != 200:
                return {'error': f'Failed to access .gitlab-ci.yml: {response.status_code}'}

            pipeline_content = yaml.safe_load(response.text)
            analysis_results = {'gitlab-ci.yml': self._analyze_workflow_content(pipeline_content)}

            return self._calculate_overall_scores(analysis_results)

        except Exception as e:
            logger.error(f"Error analyzing GitLab pipeline: {str(e)}")
            return {'error': str(e)}

    def _analyze_workflow_content(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Analyze individual workflow/pipeline content"""
        results = {}

        # Initialize category scores
        for category in self.framework.CATEGORIES:
            results[category] = {
                'score': 0,
                'max_score': 100,
                'findings': [],
                'recommendations': []
            }

        # Analyze source code security
        results['source_code_security'] = self._check_source_code_security(content)

        # Analyze build security
        results['build_security'] = self._check_build_security(content)

        # Analyze deployment security
        results['deployment_security'] = self._check_deployment_security(content)

        # Analyze runtime security
        results['runtime_security'] = self._check_runtime_security(content)

        # Analyze compliance & governance
        results['compliance_governance'] = self._check_compliance_governance(content)

        # Analyze culture & training (basic checks from pipeline)
        results['culture_training'] = self._check_culture_training(content)

        return results

    def _check_source_code_security(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Check source code security practices"""
        score = 0
        max_score = 100
        findings = []
        recommendations = []

        content_str = str(content).lower()

        # Check for dependency scanning
        dependency_tools = ['npm audit', 'yarn audit', 'pip-audit', 'safety', 'snyk', 'dependabot']
        if any(tool in content_str for tool in dependency_tools):
            score += 25
            findings.append("✅ Dependency scanning detected")
        else:
            recommendations.append("🔧 Add dependency scanning (npm audit, Snyk, etc.)")

        # Check for secret detection
        secret_tools = ['trufflesec', 'gitleaks', 'detect-secrets', 'git-secrets']
        if any(tool in content_str for tool in secret_tools):
            score += 25
            findings.append("✅ Secret detection configured")
        else:
            recommendations.append("🔧 Add secret detection tools")

        # Check for SAST tools
        sast_tools = ['sonarqube', 'codeql', 'semgrep', 'bandit', 'eslint']
        if any(tool in content_str for tool in sast_tools):
            score += 30
            findings.append("✅ Static Analysis Security Testing (SAST) detected")
        else:
            recommendations.append("🔧 Implement SAST tools")

        # Check for quality gates
        if any(gate in content_str for gate in ['quality gate', 'security gate', 'fail on', 'threshold']):
            score += 20
            findings.append("✅ Quality gates configured")
        else:
            recommendations.append("🔧 Add security quality gates")

        return {
            'score': score,
            'max_score': max_score,
            'findings': findings,
            'recommendations': recommendations
        }

    def _check_build_security(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Check build security practices"""
        score = 0
        max_score = 100
        findings = []
        recommendations = []

        content_str = str(content).lower()

        # Check for secure build environment
        if any(keyword in content_str for keyword in ['runs-on: ubuntu-latest', 'image:', 'container']):
            score += 25
            findings.append("✅ Containerized/managed build environment")
        else:
            recommendations.append("🔧 Use managed build environments")

        # Check for artifact signing
        if any(tool in content_str for tool in ['cosign', 'sigstore', 'gpg', 'sign']):
            score += 30
            findings.append("✅ Artifact signing detected")
        else:
            recommendations.append("🔧 Implement artifact signing")

        # Check for supply chain security
        if any(tool in content_str for tool in ['sbom', 'syft', 'cyclone']):
            score += 25
            findings.append("✅ Software Bill of Materials (SBOM) generation")
        else:
            recommendations.append("🔧 Generate Software Bill of Materials")

        # Check for build reproducibility
        if any(keyword in content_str for keyword in ['cache', 'lock', 'pinned']):
            score += 20
            findings.append("✅ Build reproducibility measures detected")
        else:
            recommendations.append("🔧 Implement build caching and dependency pinning")

        return {
            'score': score,
            'max_score': max_score,
            'findings': findings,
            'recommendations': recommendations
        }

    def _check_deployment_security(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Check deployment security practices"""
        score = 0
        max_score = 100
        findings = []
        recommendations = []

        content_str = str(content).lower()

        # Check for Infrastructure as Code
        if any(tool in content_str for tool in ['terraform', 'cloudformation', 'pulumi', 'ansible']):
            score += 30
            findings.append("✅ Infrastructure as Code detected")
        else:
            recommendations.append("🔧 Implement Infrastructure as Code")

        # Check for container scanning
        if any(tool in content_str for tool in ['trivy', 'clair', 'anchore', 'twistlock']):
            score += 25
            findings.append("✅ Container security scanning")
        else:
            recommendations.append("🔧 Add container security scanning")

        # Check for approval gates
        if any(keyword in content_str for keyword in ['environment', 'approval', 'manual', 'review']):
            score += 25
            findings.append("✅ Deployment approval process")
        else:
            recommendations.append("🔧 Implement deployment approvals")

        # Check for environment isolation
        if any(keyword in content_str for keyword in ['staging', 'production', 'dev', 'test']):
            score += 20
            findings.append("✅ Environment separation detected")
        else:
            recommendations.append("🔧 Implement proper environment isolation")

        return {
            'score': score,
            'max_score': max_score,
            'findings': findings,
            'recommendations': recommendations
        }

    def _check_runtime_security(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Check runtime security practices"""
        score = 0
        max_score = 100
        findings = []
        recommendations = []

        content_str = str(content).lower()

        # Check for monitoring and logging
        if any(tool in content_str for tool in ['datadog', 'newrelic', 'elastic', 'splunk', 'prometheus']):
            score += 30
            findings.append("✅ Monitoring and logging configured")
        else:
            recommendations.append("🔧 Implement comprehensive monitoring")

        # Check for security testing
        if any(tool in content_str for tool in ['zap', 'burp', 'dast', 'penetration', 'security test']):
            score += 35
            findings.append("✅ Dynamic security testing")
        else:
            recommendations.append("🔧 Add dynamic security testing (DAST)")

        # Check for vulnerability management
        if any(keyword in content_str for keyword in ['vulnerability', 'cve', 'patch', 'update']):
            score += 35
            findings.append("✅ Vulnerability management process")
        else:
            recommendations.append("🔧 Implement vulnerability management")

        return {
            'score': score,
            'max_score': max_score,
            'findings': findings,
            'recommendations': recommendations
        }

    def _check_compliance_governance(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Check compliance and governance practices"""
        score = 0
        max_score = 100
        findings = []
        recommendations = []

        content_str = str(content).lower()

        # Check for policy as code
        if any(tool in content_str for tool in ['opa', 'gatekeeper', 'policy', 'conftest']):
            score += 35
            findings.append("✅ Policy as Code implementation")
        else:
            recommendations.append("🔧 Implement Policy as Code")

        # Check for audit trails
        if any(keyword in content_str for keyword in ['audit', 'log', 'trace', 'record']):
            score += 25
            findings.append("✅ Audit trail capabilities")
        else:
            recommendations.append("🔧 Enable comprehensive audit trails")

        # Check for compliance reporting
        if any(keyword in content_str for keyword in ['report', 'compliance', 'soc', 'gdpr', 'hipaa']):
            score += 20
            findings.append("✅ Compliance reporting detected")
        else:
            recommendations.append("🔧 Add compliance reporting")

        # Check for access controls
        if any(keyword in content_str for keyword in ['rbac', 'permission', 'role', 'access']):
            score += 20
            findings.append("✅ Access control measures")
        else:
            recommendations.append("🔧 Implement proper access controls")

        return {
            'score': score,
            'max_score': max_score,
            'findings': findings,
            'recommendations': recommendations
        }

    def _check_culture_training(self, content: Dict[str, Any]) -> Dict[str, Any]:
        """Check culture and training indicators"""
        score = 50  # Base score since this is hard to detect from pipeline alone
        max_score = 100
        findings = ["ℹ️ Culture assessment requires manual review"]
        recommendations = [
            "🔧 Establish security champions program",
            "🔧 Implement security training programs",
            "🔧 Regular security design reviews"
        ]

        return {
            'score': score,
            'max_score': max_score,
            'findings': findings,
            'recommendations': recommendations
        }

    def _calculate_overall_scores(self, analysis_results: Dict[str, Any]) -> Dict[str, Any]:
        """Calculate weighted overall scores"""
        if 'error' in analysis_results:
            return analysis_results

        category_scores = {}
        overall_weighted_score = 0

        # Average scores across all workflows
        for category, config in self.framework.CATEGORIES.items():
            category_total = 0
            category_count = 0

            for workflow_name, workflow_results in analysis_results.items():
                if category in workflow_results:
                    category_total += workflow_results[category]['score']
                    category_count += 1

            if category_count > 0:
                category_avg = category_total / category_count
                category_scores[category] = category_avg
                overall_weighted_score += category_avg * config['weight']

        # Calculate maturity level
        maturity_level, maturity_info = self.framework.calculate_maturity_level(overall_weighted_score)

        return {
            'overall_score': round(overall_weighted_score, 2),
            'maturity_level': maturity_level,
            'maturity_info': maturity_info,
            'category_scores': category_scores,
            'detailed_results': analysis_results,
            'analysis_timestamp': datetime.now(timezone.utc).isoformat()
        }

# Initialize analyzer
analyzer = PipelineAnalyzer()

# Routes
@app.route('/')
def index():
    """Home page"""
    if current_user.is_authenticated:
        return redirect(url_for('dashboard'))
    return render_template('landing.html')

@app.route('/dashboard')
@login_required
def dashboard():
    """Dashboard page"""
    # Get dashboard statistics for the current user's organization
    org = current_user.organization

    # Calculate statistics
    total_assessments = Assessment.query.filter_by(organization_id=org.id).count()
    avg_score_query = db.session.query(db.func.avg(Assessment.overall_score)).filter_by(organization_id=org.id).scalar()
    avg_score = round(avg_score_query, 2) if avg_score_query else 0

    # Get category scores from recent assessments
    category_scores = []
    recent_assessment = Assessment.query.filter_by(organization_id=org.id).order_by(Assessment.created_at.desc()).first()
    if recent_assessment and recent_assessment.assessment_data:
        try:
            assessment_data = json.loads(recent_assessment.assessment_data)
            if 'category_scores' in assessment_data:
                category_scores = list(assessment_data['category_scores'].values())
        except json.JSONDecodeError:
            pass

    stats = {
        'total_assessments': total_assessments,
        'average_score': avg_score,
        'category_scores': category_scores
    }

    recent_assessments = Assessment.query.filter_by(organization_id=org.id).order_by(Assessment.created_at.desc()).limit(5).all()

    return render_template('dashboard.html', stats=stats, recent_assessments=recent_assessments)

@app.route('/assessment/new', methods=['GET', 'POST'])
@login_required
def new_assessment():
    """Create new assessment"""
    if request.method == 'POST':
        try:
            data = request.get_json() if request.is_json else request.form

            # Use current user's organization
            organization = current_user.organization

            # Validate input
            project_name = data.get('project_name', '').strip()
            repo_url = data.get('repository_url', '').strip()
            platform = data.get('platform', '').strip()
            token = data.get('token', '').strip()

            if not all([project_name, repo_url, platform]):
                return jsonify({'error': 'All fields are required'}), 400

            # Check usage limits
            if not organization.can_create_assessment():
                return jsonify({'error': 'Monthly assessment limit reached'}), 403

            # Analyze pipeline
            if platform == 'github':
                results = analyzer.analyze_github_pipeline(repo_url, token)
            elif platform == 'gitlab':
                results = analyzer.analyze_gitlab_pipeline(repo_url, token)
            else:
                return jsonify({'error': 'Unsupported platform'}), 400

            if 'error' in results:
                return jsonify({'error': results['error']}), 400

            # Create assessment record
            assessment = Assessment(
                organization_id=organization.id,
                name=project_name,
                repository_url=repo_url,
                platform=platform,
                overall_score=results['overall_score'],
                maturity_level=results['maturity_info']['name'].lower(),
                assessment_data=json.dumps(results)
            )

            db.session.add(assessment)

            # Update organization usage
            organization.monthly_assessments_used += 1

            db.session.commit()

            if request.is_json:
                return jsonify({
                    'assessment_id': assessment.id,
                    'results': results
                })
            else:
                flash('Assessment completed successfully!', 'success')
                return redirect(url_for('view_assessment', assessment_id=assessment.id))

        except Exception as e:
            logger.error(f"Error creating assessment: {str(e)}")
            db.session.rollback()
            if request.is_json:
                return jsonify({'error': str(e)}), 500
            else:
                flash(f'Error: {str(e)}', 'error')
                return redirect(url_for('new_assessment'))

    return render_template('new_assessment.html')

@app.route('/assessment/<int:assessment_id>')
@login_required
def view_assessment(assessment_id):
    """View assessment results"""
    assessment = Assessment.query.get_or_404(assessment_id)

    # Check if user has access to this assessment
    if assessment.organization_id != current_user.organization_id:
        flash('Access denied.', 'error')
        return redirect(url_for('dashboard'))

    results = json.loads(assessment.assessment_data)

    return render_template('assessment_results.html',
                         assessment=assessment,
                         results=results,
                         framework=SecurityMaturityFramework())

@app.route('/assessment/list')
@login_required
def list_assessments():
    """List all assessments for the organization"""
    assessments = Assessment.query.filter_by(organization_id=current_user.organization_id)\
                                 .order_by(Assessment.created_at.desc()).all()

    return render_template('assessment_list.html', assessments=assessments)

@app.route('/api/assessment/<int:assessment_id>')
@login_required
def api_assessment(assessment_id):
    """API endpoint for assessment data"""
    assessment = Assessment.query.get_or_404(assessment_id)

    # Check access
    if assessment.organization_id != current_user.organization_id:
        return jsonify({'error': 'Access denied'}), 403

    results = json.loads(assessment.assessment_data)

    return jsonify({
        'id': assessment.id,
        'project_name': assessment.name,
        'organization': assessment.organization.name,
        'repository_url': assessment.repository_url,
        'platform': assessment.platform,
        'overall_score': assessment.overall_score,
        'maturity_level': assessment.maturity_level,
        'created_at': assessment.created_at.isoformat(),
        'results': results
    })

@app.route('/api/organizations/assessments')
@login_required
def api_org_assessments():
    """API endpoint for organization assessments"""
    organization = current_user.organization
    assessments = Assessment.query.filter_by(organization_id=organization.id)\
                                 .order_by(Assessment.created_at.desc()).all()

    return jsonify({
        'organization': organization.name,
        'assessments': [
            {
                'id': a.id,
                'project_name': a.name,
                'overall_score': a.overall_score,
                'maturity_level': a.maturity_level,
                'created_at': a.created_at.isoformat()
            }
            for a in assessments
        ]
    })

# Error handlers
@app.errorhandler(404)
def not_found(error):
    return render_template('error.html',
                         error_code=404,
                         error_message="Page not found"), 404

@app.errorhandler(500)
def internal_error(error):
    db.session.rollback()
    return render_template('error.html',
                         error_code=500,
                         error_message="Internal server error"), 500

# Initialize database (deprecated @app.before_first_request removed)
# Tables will be created in the main block below

if __name__ == '__main__':
    # Create tables if they don't exist
    with app.app_context():
        db.create_all()

    # Run the application
    app.run(debug=True, host='0.0.0.0', port=5000)
