"""
Dashboard Metrics Service

Provides aggregated metrics for dashboard display.
Designed with abstraction layer for future centralization:
- Current: Queries local SQLite database
- Future: Can be swapped to query centralized metrics API

Architecture:
- DashboardMetricsProvider: Abstract interface
- LocalMetricsProvider: Current SQLite implementation
- RemoteMetricsProvider: Future centralized API (not implemented yet)
"""

from abc import ABC, abstractmethod
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta, timezone
from sqlmodel import select, func, and_
from sqlalchemy import distinct
import json
import logging

from app.database import SessionDep
from app.models import ScanRecord, ScanType

logger = logging.getLogger(__name__)


class DashboardMetricsProvider(ABC):
    """Abstract interface for dashboard metrics - enables future centralization"""
    
    @abstractmethod
    async def get_metrics(self, user_id: str, time_range: Optional[str] = None) -> Dict[str, Any]:
        """Get aggregated dashboard metrics for a user"""
        pass


class LocalMetricsProvider(DashboardMetricsProvider):
    """Local SQLite implementation - current approach"""
    
    def __init__(self, session: SessionDep):
        self.session = session
    
    async def get_metrics(self, user_id: str, time_range: Optional[str] = None) -> Dict[str, Any]:
        """Aggregate metrics from local database"""
        
        now = datetime.now(timezone.utc)
        
        # Base filter for user isolation
        base_filter = [ScanRecord.user_id == user_id]
        
        # Get total scans
        total_scans = self.session.exec(
            select(func.count(ScanRecord.id)).where(*base_filter)
        ).first() or 0
        
        # Get scans in last 24 hours
        day_ago = now - timedelta(days=1)
        scans_last_24h = self.session.exec(
            select(func.count(ScanRecord.id)).where(
                *base_filter,
                ScanRecord.timestamp >= day_ago
            )
        ).first() or 0
        
        # Get scans in last 7 days
        week_ago = now - timedelta(days=7)
        scans_last_7d = self.session.exec(
            select(func.count(ScanRecord.id)).where(
                *base_filter,
                ScanRecord.timestamp >= week_ago
            )
        ).first() or 0
        
        # Get all scans for detailed analysis
        all_scans = self.session.exec(
            select(ScanRecord).where(*base_filter).order_by(ScanRecord.timestamp.desc())
        ).all()
        
        # Aggregate findings and risk scores
        total_findings = 0
        critical_findings = 0
        high_findings = 0
        medium_findings = 0
        low_findings = 0
        pqc_findings = 0
        general_findings = 0
        risk_scores = []
        
        # Previous period comparison (for trends)
        prev_period_start = week_ago - timedelta(days=7)
        prev_scans = self.session.exec(
            select(func.count(ScanRecord.id)).where(
                *base_filter,
                ScanRecord.timestamp >= prev_period_start,
                ScanRecord.timestamp < week_ago
            )
        ).first() or 0
        
        # Analyzer-specific metrics
        analyzer_metrics = {
            'code': {'scans': 0, 'findings': 0, 'last_scan': None},
            'binary': {'scans': 0, 'findings': 0, 'last_scan': None},
            'certificate': {'scans': 0, 'findings': 0, 'last_scan': None},
            'network': {'scans': 0, 'findings': 0, 'last_scan': None},
        }
        
        # Risk distribution
        risk_distribution = {
            'critical': 0,
            'high': 0,
            'medium': 0,
            'low': 0,
            'clean': 0
        }
        
        # Track which scans we've counted for clean files to avoid double-counting
        counted_clean_scans = set()
        
        for scan in all_scans:
            scan_data = scan.scan_data
            if isinstance(scan_data, str):
                try:
                    scan_data = json.loads(scan_data)
                except:
                    continue
            
            if not isinstance(scan_data, dict):
                continue
            
            # Determine analyzer type
            scan_type = scan.scan_type.value if hasattr(scan.scan_type, 'value') else str(scan.scan_type)
            # Map scan types to analyzer categories
            if scan_type == 'code_analysis':
                analyzer_type = 'code'
            elif scan_type == 'quantum':
                analyzer_type = 'certificate'
            elif scan_type == 'port':
                analyzer_type = 'network'
            else:
                analyzer_type = scan_type  # binary, etc.
            
            if analyzer_type in analyzer_metrics:
                analyzer_metrics[analyzer_type]['scans'] += 1
                if not analyzer_metrics[analyzer_type]['last_scan']:
                    analyzer_metrics[analyzer_type]['last_scan'] = scan.timestamp.isoformat()
            
            # For code scans, count clean files from clean_files_count
            if scan_type == 'code_analysis':
                clean_count = scan_data.get('clean_files_count', 0)
                if clean_count > 0:
                    risk_distribution['clean'] += clean_count
                    counted_clean_scans.add(scan.id)
            
            # Extract findings count
            findings_count = 0
            if 'findings_count' in scan_data:
                findings_count = scan_data.get('findings_count', 0)
            elif 'enriched_findings' in scan_data:
                findings_count = len(scan_data.get('enriched_findings', []))
            elif 'data' in scan_data:
                data = scan_data.get('data', {})
                if isinstance(data, dict):
                    # Binary scans: count YARA matches
                    yara = data.get('yara_analysis', {})
                    if isinstance(yara, dict):
                        findings_count = len(yara.get('match_details', []))
                    
                    # Certificate scans: data.certificate_analysis (not data.data)
                    if scan_type == 'quantum':
                        cert_analysis = data.get('certificate_analysis', {})
                        if isinstance(cert_analysis, dict) and cert_analysis.get('shor_vulnerable'):
                            findings_count = 1
                    
                    # Network scans: data.data.openPorts
                    if scan_type == 'port':
                        net_data = data.get('data', {})
                        open_ports = net_data.get('openPorts', []) if isinstance(net_data, dict) else []
                        findings_count = len(open_ports)
            
            total_findings += findings_count
            
            if analyzer_type in analyzer_metrics:
                analyzer_metrics[analyzer_type]['findings'] += findings_count
            
            # Extract severity breakdown and PQC classification
            if 'enriched_findings' in scan_data:
                for finding in scan_data.get('enriched_findings', []):
                    if isinstance(finding, dict):
                        severity = (finding.get('extra', {}).get('severity') or finding.get('severity') or 'INFO').upper()
                        if severity in ('ERROR', 'CRITICAL'):
                            critical_findings += 1
                        elif severity in ('WARNING', 'HIGH'):
                            high_findings += 1
                        elif severity == 'MEDIUM':
                            medium_findings += 1
                        else:
                            low_findings += 1
                        
                        # Check if PQC-related (use metadata flag primarily)
                        metadata = finding.get('extra', {}).get('metadata', {}) or finding.get('metadata', {})
                        is_pqc = metadata.get('quantum_vulnerable', False)
                        
                        # Fallback: check message only if metadata doesn't have the flag
                        if not is_pqc:
                            message = finding.get('extra', {}).get('message', '') or finding.get('message', '')
                            rule_id = finding.get('check_id', '') or finding.get('rule_id', '')
                            # Only count as PQC if explicitly quantum-related (not just any crypto)
                            is_pqc = 'quantum' in message.lower() or 'quantum' in rule_id.lower()
                        
                        if is_pqc:
                            pqc_findings += 1
                        else:
                            general_findings += 1
            
            # Certificate and network scans are always PQC-related
            elif scan_type in ('quantum', 'port'):
                if findings_count > 0:
                    pqc_findings += findings_count
            
            # Extract risk score
            risk_score = None
            if 'data' in scan_data:
                data = scan_data.get('data', {})
                if isinstance(data, dict):
                    risk_assessment = data.get('quantum_risk_assessment', {})
                    if isinstance(risk_assessment, dict):
                        risk_score = risk_assessment.get('quantum_risk_score')

            # Fallback: derive risk score from finding severities when no stored score exists
            # (code_analysis and port scans don't store quantum_risk_assessment)
            if risk_score is None and findings_count > 0:
                _sev_scores = {'CRITICAL': 85, 'ERROR': 85, 'HIGH': 65, 'WARNING': 65, 'MEDIUM': 45, 'LOW': 25}
                _max_score = 0
                if 'enriched_findings' in scan_data:
                    for _f in scan_data.get('enriched_findings', []):
                        _sev = ((_f.get('extra', {}) or {}).get('severity') or _f.get('severity') or 'LOW').upper()
                        _max_score = max(_max_score, _sev_scores.get(_sev, 25))
                elif findings_count > 0:
                    # Network/cert scans without enriched_findings: default to HIGH (open ports / cert issues)
                    _max_score = 65
                if _max_score > 0:
                    risk_score = _max_score

            if risk_score is not None:
                risk_scores.append(risk_score)

                # Categorize by risk level
                if risk_score >= 75:
                    risk_distribution['critical'] += 1
                elif risk_score >= 50:
                    risk_distribution['high'] += 1
                elif risk_score >= 25:
                    risk_distribution['medium'] += 1
                elif risk_score > 0:
                    risk_distribution['low'] += 1
                else:
                    if scan.id not in counted_clean_scans:
                        risk_distribution['clean'] += 1
                        counted_clean_scans.add(scan.id)
            elif findings_count == 0 and scan.id not in counted_clean_scans:
                # Only count as 1 clean scan if not already counted via clean_files_count
                risk_distribution['clean'] += 1
                counted_clean_scans.add(scan.id)
        
        # Calculate average risk score
        avg_risk_score = sum(risk_scores) / len(risk_scores) if risk_scores else 0
        
        # Get recent scans (last 10)
        recent_scans = []
        for scan in all_scans[:10]:
            scan_data = scan.scan_data
            if isinstance(scan_data, str):
                try:
                    scan_data = json.loads(scan_data)
                except:
                    scan_data = {}
            
            findings_count = 0
            risk_score = None
            
            if isinstance(scan_data, dict):
                findings_count = scan_data.get('findings_count', 0)
                if findings_count == 0 and 'enriched_findings' in scan_data:
                    findings_count = len(scan_data.get('enriched_findings', []))
                if findings_count == 0 and 'data' in scan_data:
                    data = scan_data.get('data', {})
                    if isinstance(data, dict):
                        # Binary scans
                        yara = data.get('yara_analysis', {})
                        if isinstance(yara, dict):
                            findings_count = len(yara.get('match_details', []))
                        
                        # Certificate scans
                        scan_type_str = scan.scan_type.value if hasattr(scan.scan_type, 'value') else str(scan.scan_type)
                        if scan_type_str == 'quantum':
                            cert_data = data.get('data', {}) if 'data' in data else data
                            cert_analysis = cert_data.get('certificate_analysis', {})
                            if isinstance(cert_analysis, dict):
                                if cert_analysis.get('shor_vulnerable') or cert_analysis.get('grover_vulnerable'):
                                    findings_count = 1
                        
                        # Certificate scans: data.certificate_analysis
                        if scan_type_str == 'quantum':
                            cert_analysis = data.get('certificate_analysis', {})
                            if isinstance(cert_analysis, dict) and cert_analysis.get('shor_vulnerable'):
                                findings_count = 1
                        
                        # Network scans: data.data.openPorts
                        if scan_type_str == 'port':
                            net_data = data.get('data', {})
                            open_ports = net_data.get('openPorts', []) if isinstance(net_data, dict) else []
                            findings_count = len(open_ports)
                        
                        # Risk assessment
                        risk_assessment = data.get('quantum_risk_assessment', {})
                        if isinstance(risk_assessment, dict):
                            risk_score = risk_assessment.get('quantum_risk_score')

            # Fallback: derive risk score from severities when no stored score exists
            if risk_score is None and findings_count > 0:
                _sev_scores = {'CRITICAL': 85, 'ERROR': 85, 'HIGH': 65, 'WARNING': 65, 'MEDIUM': 45, 'LOW': 25}
                _max_score = 0
                if isinstance(scan_data, dict) and 'enriched_findings' in scan_data:
                    for _f in scan_data.get('enriched_findings', []):
                        _sev = ((_f.get('extra', {}) or {}).get('severity') or _f.get('severity') or 'LOW').upper()
                        _max_score = max(_max_score, _sev_scores.get(_sev, 25))
                elif findings_count > 0:
                    _max_score = 65
                if _max_score > 0:
                    risk_score = _max_score

            scan_type = scan.scan_type.value if hasattr(scan.scan_type, 'value') else str(scan.scan_type)
            # Map scan types to analyzer categories
            if scan_type == 'code_analysis':
                analyzer_type = 'code'
            elif scan_type == 'quantum':
                analyzer_type = 'certificate'
            elif scan_type == 'port':
                analyzer_type = 'network'
            else:
                analyzer_type = scan_type  # binary, etc.
            
            recent_scans.append({
                'id': scan.id,
                'type': analyzer_type,
                'target': scan.target,
                'timestamp': scan.timestamp.isoformat(),
                'risk_score': risk_score,
                'findings_count': findings_count
            })
        
        # Calculate trends
        scan_trend = ((scans_last_7d - prev_scans) / prev_scans * 100) if prev_scans > 0 else 0
        
        return {
            'total_scans': total_scans,
            'scans_last_24h': scans_last_24h,
            'scans_last_7d': scans_last_7d,
            'total_findings': total_findings,
            'critical_findings': critical_findings,
            'high_findings': high_findings,
            'medium_findings': medium_findings,
            'low_findings': low_findings,
            'pqc_findings': pqc_findings,
            'general_findings': general_findings,
            'avg_risk_score': round(avg_risk_score, 1),
            'analyzers': analyzer_metrics,
            'recent_scans': recent_scans,
            'risk_distribution': risk_distribution,
            'trends': {
                'scans_7d_change': round(scan_trend, 1),
                'prev_period_scans': prev_scans
            }
        }


# Future: RemoteMetricsProvider for centralized server
# class RemoteMetricsProvider(DashboardMetricsProvider):
#     """Centralized API implementation - future approach"""
#     
#     def __init__(self, api_url: str, api_key: str):
#         self.api_url = api_url
#         self.api_key = api_key
#     
#     async def get_metrics(self, user_id: str, time_range: Optional[str] = None) -> Dict[str, Any]:
#         """Fetch metrics from centralized server"""
#         # Implementation will query centralized metrics API
#         # This allows aggregating data across multiple Rivet instances
#         pass


def get_metrics_provider(session: SessionDep) -> DashboardMetricsProvider:
    """Factory function - returns appropriate provider based on configuration
    
    Current: Always returns LocalMetricsProvider
    Future: Check config/env to determine if centralized mode is enabled
    """
    # TODO: Check environment variable CENTRALIZED_METRICS_ENABLED
    # if os.getenv('CENTRALIZED_METRICS_ENABLED') == 'true':
    #     return RemoteMetricsProvider(
    #         api_url=os.getenv('METRICS_API_URL'),
    #         api_key=os.getenv('METRICS_API_KEY')
    #     )
    
    return LocalMetricsProvider(session)
