"""
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
import re

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

logger = logging.getLogger(__name__)


def _extract_analyzer_findings_and_risk(scan_type: str, data: Dict[str, Any]) -> tuple:
    """Extract (findings_count, risk_score) for scan types whose real, already-computed
    numbers live nested under scan_data['data'] rather than at a path the generic
    extraction logic checks.

    - quantum (certificate scans): one certificate per scan, at data.certificate_analysis
    - port (network scans): one certificate per scanned TLS service, at
      data.data.certificates (keyed by port string) -- aggregate across all services

    Both already carry their own quantum_risk_score and recommendations list computed
    by the unified risk engine. Without this, callers fall back to a flat
    severity-derived approximation that ignores the real score entirely.
    """
    if scan_type == 'quantum':
        cert_analysis = data.get('certificate_analysis', {})
        if not isinstance(cert_analysis, dict):
            return 0, None
        findings_count = len(cert_analysis.get('recommendations') or [])
        return findings_count, cert_analysis.get('quantum_risk_score')

    if scan_type == 'port':
        net_data = data.get('data', {})
        certs = net_data.get('certificates', {}) if isinstance(net_data, dict) else {}
        if isinstance(certs, dict):
            cert_list = list(certs.values())
        elif isinstance(certs, list):
            cert_list = certs
        else:
            cert_list = []
        cert_list = [c for c in cert_list if isinstance(c, dict)]
        if not cert_list:
            open_ports = net_data.get('openPorts', []) if isinstance(net_data, dict) else []
            return len(open_ports), None
        findings_count = sum(len(c.get('recommendations') or []) for c in cert_list)
        scores = [c['quantum_risk_score'] for c in cert_list if c.get('quantum_risk_score') is not None]
        return findings_count, (max(scores) if scores else None)

    return 0, None


def _parse_time_range_cutoff(time_range: Optional[str], now: datetime) -> Optional[datetime]:
    """Parse a time_range string like '24h', '7d', '30d', '90d' into a cutoff
    datetime. Returns None (no filtering, i.e. all-time) if time_range is
    absent or doesn't match the expected shape.
    """
    if not time_range:
        return None
    match = re.fullmatch(r"(\d+)([hd])", time_range.strip().lower())
    if not match:
        return None
    amount, unit = int(match.group(1)), match.group(2)
    delta = timedelta(hours=amount) if unit == "h" else timedelta(days=amount)
    return now - delta


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, plus the requested time window (if
        # any) -- previously time_range was accepted but never actually
        # applied anywhere, so every call silently returned all-time data
        # regardless of what was requested.
        base_filter = [ScanRecord.user_id == user_id]
        cutoff = _parse_time_range_cutoff(time_range, now)
        if cutoff:
            base_filter.append(ScanRecord.timestamp >= cutoff)

        # 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
            _analyzer_risk_score = None
            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):
                    if scan_type in ('quantum', 'port'):
                        findings_count, _analyzer_risk_score = _extract_analyzer_findings_and_risk(scan_type, data)
                    else:
                        # Binary scans: count YARA matches
                        yara = data.get('yara_analysis', {})
                        if isinstance(yara, dict):
                            findings_count = len(yara.get('match_details', []))
            
            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

            # 1. Code analysis scans: use stored quantum_risk_score
            if risk_score is None and 'quantum_risk_score' in scan_data:
                stored = scan_data.get('quantum_risk_score')
                if stored is not None:
                    risk_score = stored

            # 2. Certificate/network scans: computed above alongside findings_count
            if risk_score is None and _analyzer_risk_score is not None:
                risk_score = _analyzer_risk_score

            # 3. Binary scans: use quantum_risk_assessment
            if risk_score is None and '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')

            # 4. Fallback: derive from severity for older records without stored score
            if risk_score is None and findings_count > 0:
                _sev_scores = {'CRITICAL': 90, 'ERROR': 90, 'HIGH': 70, 'WARNING': 70, 'MEDIUM': 45, 'LOW': 20}
                _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, 20))
                elif findings_count > 0:
                    _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
            scan_type_str = scan.scan_type.value if hasattr(scan.scan_type, 'value') else str(scan.scan_type)

            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):
                        if scan_type_str in ('quantum', 'port'):
                            findings_count, risk_score = _extract_analyzer_findings_and_risk(scan_type_str, data)
                        else:
                            # Binary scans
                            yara = data.get('yara_analysis', {})
                            if isinstance(yara, dict):
                                findings_count = len(yara.get('match_details', []))

                        # Risk assessment (binary scans)
                        if risk_score is None:
                            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

            # Map scan types to analyzer categories
            if scan_type_str == 'code_analysis':
                analyzer_type = 'code'
            elif scan_type_str == 'quantum':
                analyzer_type = 'certificate'
            elif scan_type_str == 'port':
                analyzer_type = 'network'
            else:
                analyzer_type = scan_type_str  # 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)
