"""
Detection Validator.

Correlates SoulEyez attacks with Wazuh SIEM alerts to determine
if attacks were detected by the security monitoring infrastructure.
"""

import json
import os
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional

from souleyez.storage.database import get_db
from souleyez.integrations.wazuh.config import WazuhConfig
from souleyez.integrations.wazuh.client import WazuhClient
from .attack_signatures import get_signature, DEFAULT_DETECTION_WINDOW

# Job queue file location (same as background.py)
DATA_DIR = os.path.join(os.path.expanduser("~"), ".souleyez", "data")
JOBS_FILE = os.path.join(DATA_DIR, "jobs", "jobs.json")


def _read_jobs_file() -> List[Dict[str, Any]]:
    """Read jobs from the JSON job queue file."""
    if not os.path.exists(JOBS_FILE):
        return []
    try:
        with open(JOBS_FILE, "r", encoding="utf-8") as fh:
            return json.load(fh)
    except Exception:
        return []


def _get_job_by_id(job_id: int) -> Optional[Dict[str, Any]]:
    """Get a single job by ID from the job queue."""
    jobs = _read_jobs_file()
    for job in jobs:
        if job.get('id') == job_id:
            return job
    return None


def _reconstruct_command(job: Dict[str, Any]) -> str:
    """Reconstruct the command string from job data."""
    tool = job.get('tool', '')
    target = job.get('target', '')
    args = job.get('args', [])

    # Build command string
    parts = [tool]
    if args:
        parts.extend(args)
    if target and target not in args:
        parts.append(target)

    return ' '.join(parts)


@dataclass
class DetectionResult:
    """Result of validating detection for a single job."""
    job_id: int
    status: str  # 'detected', 'not_detected', 'partial', 'offline', 'unknown'
    alerts_count: int = 0
    alerts: List[Dict[str, Any]] = field(default_factory=list)
    rule_ids: List[str] = field(default_factory=list)
    reason: str = ""
    checked_at: datetime = field(default_factory=datetime.now)


@dataclass
class EngagementDetectionSummary:
    """Summary of detection coverage for an engagement."""
    engagement_id: int
    total_attacks: int
    detected: int
    not_detected: int
    partial: int
    offline: int
    unknown: int
    coverage_percent: float
    results: List[DetectionResult] = field(default_factory=list)


class DetectionValidator:
    """Correlates SoulEyez attacks with Wazuh alerts."""

    def __init__(self, engagement_id: int):
        """
        Initialize validator for an engagement.

        Args:
            engagement_id: The engagement to validate
        """
        self.engagement_id = engagement_id
        self._client: Optional[WazuhClient] = None

    def _get_client(self) -> Optional[WazuhClient]:
        """Get Wazuh client for the engagement."""
        if self._client:
            return self._client

        config = WazuhConfig.get_config(self.engagement_id)
        if not config or not config.get('enabled'):
            return None

        self._client = WazuhClient(
            api_url=config['api_url'],
            username=config['api_user'],
            password=config['api_password'],
            verify_ssl=config.get('verify_ssl', False),
            indexer_url=config.get('indexer_url'),
            indexer_user=config.get('indexer_user'),
            indexer_password=config.get('indexer_password')
        )
        return self._client

    def validate_job(self, job_id: int) -> DetectionResult:
        """
        Check if a completed job triggered Wazuh alerts.

        Args:
            job_id: The job to validate (from job queue)

        Returns:
            DetectionResult with status and matched alerts
        """
        import re

        # Get job details from job queue (jobs.json)
        job = _get_job_by_id(job_id)
        if not job:
            return DetectionResult(
                job_id=job_id,
                status='unknown',
                reason="Job not found in queue"
            )

        # Verify engagement matches
        if job.get('engagement_id') != self.engagement_id:
            return DetectionResult(
                job_id=job_id,
                status='unknown',
                reason="Job belongs to different engagement"
            )

        job_tool = job.get('tool') or 'unknown'
        job_command = _reconstruct_command(job)
        # Use started_at or finished_at for execution time
        executed_at = job.get('started_at') or job.get('finished_at') or job.get('created_at')
        # Job is successful if status is 'done'
        success = job.get('status') == 'done'

        # Extract target IP from command (common patterns)
        target_ip = None
        ip_pattern = r'\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b'
        ip_matches = re.findall(ip_pattern, job_command)
        if ip_matches:
            # Filter out common non-target IPs
            for ip in ip_matches:
                if not ip.startswith('127.') and not ip.startswith('0.'):
                    target_ip = ip
                    break

        # Check if job was successful
        if not success:
            return DetectionResult(
                job_id=job_id,
                status='unknown',
                reason=f"Job did not complete successfully"
            )

        # Get attack signature
        signature = get_signature(job_tool)

        # Check if offline tool (no network detection expected)
        if signature.get('offline'):
            result = DetectionResult(
                job_id=job_id,
                status='offline',
                reason=f"{job_tool} is an offline tool, no network detection expected"
            )
            self._save_result(result, job_id)
            return result

        # Check if Wazuh is configured
        client = self._get_client()
        if not client:
            return DetectionResult(
                job_id=job_id,
                status='unknown',
                reason="Wazuh not configured for this engagement"
            )

        # Parse timestamp
        try:
            if isinstance(executed_at, str):
                exec_time = datetime.fromisoformat(executed_at.replace('Z', '+00:00').replace(' ', 'T'))
            else:
                exec_time = executed_at or datetime.now()
        except Exception:
            exec_time = datetime.now() - timedelta(hours=1)

        # Query window: 5 minutes before execution to detection_window after
        detection_window = signature.get('detection_window_seconds', DEFAULT_DETECTION_WINDOW)
        query_start = exec_time - timedelta(minutes=5)
        query_end = exec_time + timedelta(seconds=detection_window)

        # Query Wazuh for alerts
        try:
            alerts = client.get_alerts(
                start_time=query_start,
                end_time=query_end,
                agent_ip=target_ip,
                rule_ids=signature.get('wazuh_rules'),
                limit=100
            )
        except Exception as e:
            return DetectionResult(
                job_id=job_id,
                status='unknown',
                reason=f"Error querying Wazuh: {str(e)}"
            )

        # Also search by patterns if no rule matches
        if not alerts and signature.get('search_patterns'):
            for pattern in signature['search_patterns'][:3]:  # Limit searches
                try:
                    pattern_alerts = client.get_alerts(
                        start_time=query_start,
                        end_time=query_end,
                        search_text=pattern,
                        limit=50
                    )
                    alerts.extend(pattern_alerts)
                except Exception:
                    continue

        # Deduplicate alerts
        seen_ids = set()
        unique_alerts = []
        for alert in alerts:
            alert_id = alert.get('id') or alert.get('_id') or str(alert.get('timestamp', ''))
            if alert_id not in seen_ids:
                seen_ids.add(alert_id)
                unique_alerts.append(alert)

        # Determine detection status
        if unique_alerts:
            rule_ids = list(set(
                str(a.get('rule', {}).get('id', 'unknown'))
                for a in unique_alerts
            ))
            result = DetectionResult(
                job_id=job_id,
                status='detected',
                alerts_count=len(unique_alerts),
                alerts=unique_alerts[:20],  # Store first 20
                rule_ids=rule_ids,
                reason=f"Found {len(unique_alerts)} related alert(s)"
            )
        else:
            result = DetectionResult(
                job_id=job_id,
                status='not_detected',
                alerts_count=0,
                reason=f"No alerts found for {job_tool} attack on {target_ip}"
            )

        # Save result to database
        self._save_result(result, job_id)
        return result

    def _save_result(self, result: DetectionResult, job_id: int):
        """Save detection result to database."""
        import re

        # Get job info from job queue
        job = _get_job_by_id(job_id)
        if not job:
            return

        # Extract target IP from command or use job target
        target_ip = job.get('target')
        command = _reconstruct_command(job)

        # Try to extract more specific IP from command
        ip_pattern = r'\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b'
        ip_matches = re.findall(ip_pattern, command)
        if ip_matches:
            for ip in ip_matches:
                if not ip.startswith('127.') and not ip.startswith('0.'):
                    target_ip = ip
                    break

        # Get timestamps
        attack_start = job.get('started_at') or job.get('created_at')
        attack_end = job.get('finished_at') or attack_start

        db = get_db()
        conn = db.get_connection()
        cursor = conn.cursor()

        # Delete existing result for this job (if any)
        cursor.execute("DELETE FROM detection_results WHERE job_id = ?", (job_id,))

        # Insert new result
        cursor.execute("""
            INSERT INTO detection_results
            (job_id, engagement_id, attack_type, target_ip, attack_start, attack_end,
             detection_status, alerts_count, wazuh_alerts_json, rule_ids, checked_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            job_id,
            self.engagement_id,
            job.get('tool'),  # attack_type (tool)
            target_ip,
            attack_start,
            attack_end,
            result.status,
            result.alerts_count,
            json.dumps(result.alerts[:20]) if result.alerts else None,
            ','.join(result.rule_ids) if result.rule_ids else None,
            datetime.now().isoformat()
        ))
        conn.commit()

    def validate_engagement(self) -> EngagementDetectionSummary:
        """
        Get detection coverage for the entire engagement.

        Returns:
            Summary with counts and individual results
        """
        # Get all completed jobs for this engagement from job queue
        all_jobs = _read_jobs_file()
        completed_jobs = [
            job for job in all_jobs
            if job.get('engagement_id') == self.engagement_id
            and job.get('status') == 'done'
        ]

        # Sort by finished_at descending
        completed_jobs.sort(
            key=lambda j: j.get('finished_at') or j.get('started_at') or '',
            reverse=True
        )

        job_ids = [job.get('id') for job in completed_jobs if job.get('id') is not None]

        if not job_ids:
            return EngagementDetectionSummary(
                engagement_id=self.engagement_id,
                total_attacks=0,
                detected=0,
                not_detected=0,
                partial=0,
                offline=0,
                unknown=0,
                coverage_percent=0.0
            )

        # Validate each job
        results = []
        for job_id in job_ids:
            result = self.validate_job(job_id)
            results.append(result)

        # Calculate summary
        detected = sum(1 for r in results if r.status == 'detected')
        not_detected = sum(1 for r in results if r.status == 'not_detected')
        partial = sum(1 for r in results if r.status == 'partial')
        offline = sum(1 for r in results if r.status == 'offline')
        unknown = sum(1 for r in results if r.status == 'unknown')

        # Calculate coverage (excluding offline and unknown)
        countable = detected + not_detected + partial
        coverage = (detected / countable * 100) if countable > 0 else 0.0

        return EngagementDetectionSummary(
            engagement_id=self.engagement_id,
            total_attacks=len(results),
            detected=detected,
            not_detected=not_detected,
            partial=partial,
            offline=offline,
            unknown=unknown,
            coverage_percent=round(coverage, 1),
            results=results
        )

    def get_detection_gaps(self) -> List[DetectionResult]:
        """Get list of attacks that were NOT detected."""
        summary = self.validate_engagement()
        return [r for r in summary.results if r.status == 'not_detected']
