#!/usr/bin/env python3
"""
souleyez.engine.result_handler - Auto-parse job results
"""
import os
import logging
from typing import Optional, Dict, Any
from .job_status import STATUS_DONE, STATUS_NO_RESULTS, STATUS_WARNING, STATUS_ERROR

logger = logging.getLogger(__name__)


def handle_job_result(job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """
    Process completed job and parse results into database.

    Args:
        job: Job dict from background system

    Returns:
        Parse results or None if not applicable

    Note:
        Auto-chaining is handled separately in background.py
        to avoid duplicate job creation.
    """
    tool = job.get('tool', '').lower()
    log_path = job.get('log')
    status = job.get('status')

    # Some tools return non-zero exit codes even on success
    # Parse 'done' jobs and 'error' jobs for certain tools
    # gobuster: exits 1 for wildcard detection (but we can still parse and retry)
    tools_with_nonzero_success = ['gobuster', 'msf_auxiliary', 'msfconsole']

    if status == 'done':
        # Always parse successful jobs
        pass
    elif status == 'error' and tool in tools_with_nonzero_success:
        # Parse error jobs for tools that can succeed with non-zero exit codes
        pass
    else:
        # Skip other error/failed jobs
        return None

    if not log_path or not os.path.exists(log_path):
        return None

    # Get engagement ID from job or fall back to current engagement
    engagement_id = job.get('engagement_id')

    if not engagement_id:
        try:
            from souleyez.storage.engagements import EngagementManager
            em = EngagementManager()
            engagement = em.get_current()

            if not engagement:
                return None

            engagement_id = engagement['id']
        except Exception:
            return None

    # Route to appropriate parser
    parse_result = None
    if tool == 'nmap' or tool == 'ard':
        # ARD plugin uses nmap under the hood
        parse_result = parse_nmap_job(engagement_id, log_path, job)
    elif tool == 'nuclei':
        parse_result = parse_nuclei_job(engagement_id, log_path, job)
    elif tool == 'theharvester':
        parse_result = parse_theharvester_job(engagement_id, log_path, job)
    elif tool == 'gobuster':
        parse_result = parse_gobuster_job(engagement_id, log_path, job)
    elif tool == 'crackmapexec':
        parse_result = parse_crackmapexec_job(engagement_id, log_path, job)
    elif tool == 'enum4linux':
        parse_result = parse_enum4linux_job(engagement_id, log_path, job)
    elif tool == 'msf_auxiliary':
        parse_result = parse_msf_auxiliary_job(engagement_id, log_path, job)
    elif tool == 'msf_exploit':
        parse_result = parse_msf_exploit_job(engagement_id, log_path, job)
    elif tool == 'sqlmap':
        parse_result = parse_sqlmap_job(engagement_id, log_path, job)
    elif tool == 'smbmap':
        parse_result = parse_smbmap_job(engagement_id, log_path, job)
    elif tool == 'whois':
        parse_result = parse_whois_job(engagement_id, log_path, job)
    elif tool == 'dnsrecon':
        parse_result = parse_dnsrecon_job(engagement_id, log_path, job)

    elif tool == 'wpscan':
        parse_result = parse_wpscan_job(engagement_id, log_path, job)
    elif tool == 'hydra':
        parse_result = parse_hydra_job(engagement_id, log_path, job)
    elif tool == 'ffuf':
        parse_result = parse_ffuf_job(engagement_id, log_path, job)
    elif tool == 'searchsploit':
        parse_result = parse_searchsploit_job(engagement_id, log_path, job)
    elif tool in ['impacket-secretsdump', 'impacket-getnpusers', 'impacket-psexec', 'impacket-smbclient']:
        parse_result = parse_impacket_job(engagement_id, log_path, job)
    elif tool == 'responder':
        parse_result = parse_responder_job(engagement_id, log_path, job)
    elif tool == 'bloodhound':
        parse_result = parse_bloodhound_job(engagement_id, log_path, job)
    elif tool == 'nikto':
        parse_result = parse_nikto_job(engagement_id, log_path, job)
    elif tool == 'dalfox':
        parse_result = parse_dalfox_job(engagement_id, log_path, job)

    # NOTE: Auto-chaining is now handled in background.py after parsing completes
    # This avoids duplicate job creation and gives better control over timing

    return parse_result


def reparse_job(job_id: int) -> Dict[str, Any]:
    """
    Re-parse an existing job using updated parser logic.

    This is useful when parsers are updated and you want to
    re-extract data from existing job logs without re-running the job.

    Args:
        job_id: The job ID to re-parse

    Returns:
        Dict with 'success', 'message', and optionally 'parse_result'
    """
    from .background import get_job, _read_jobs, _write_jobs

    # Get the job
    job = get_job(job_id)
    if not job:
        return {'success': False, 'message': f'Job {job_id} not found'}

    log_path = job.get('log')
    if not log_path or not os.path.exists(log_path):
        return {'success': False, 'message': f'Log file not found: {log_path}'}

    tool = job.get('tool', '').lower()
    if not tool:
        return {'success': False, 'message': 'Job has no tool specified'}

    # Get engagement ID
    engagement_id = job.get('engagement_id')
    if not engagement_id:
        try:
            from souleyez.storage.engagements import EngagementManager
            em = EngagementManager()
            engagement = em.get_current()
            if engagement:
                engagement_id = engagement['id']
        except Exception:
            pass

    if not engagement_id:
        return {'success': False, 'message': 'Could not determine engagement ID'}

    # Route to appropriate parser (same logic as handle_job_result)
    parse_result = None
    try:
        if tool == 'nmap' or tool == 'ard':
            # ARD plugin uses nmap under the hood
            parse_result = parse_nmap_job(engagement_id, log_path, job)
        elif tool == 'nuclei':
            parse_result = parse_nuclei_job(engagement_id, log_path, job)
        elif tool == 'theharvester':
            parse_result = parse_theharvester_job(engagement_id, log_path, job)
        elif tool == 'gobuster':
            parse_result = parse_gobuster_job(engagement_id, log_path, job)
        elif tool == 'crackmapexec':
            parse_result = parse_crackmapexec_job(engagement_id, log_path, job)
        elif tool == 'enum4linux':
            parse_result = parse_enum4linux_job(engagement_id, log_path, job)
        elif tool == 'msf_auxiliary':
            parse_result = parse_msf_auxiliary_job(engagement_id, log_path, job)
        elif tool == 'msf_exploit':
            parse_result = parse_msf_exploit_job(engagement_id, log_path, job)
        elif tool == 'sqlmap':
            parse_result = parse_sqlmap_job(engagement_id, log_path, job)
        elif tool == 'smbmap':
            parse_result = parse_smbmap_job(engagement_id, log_path, job)
        elif tool == 'whois':
            parse_result = parse_whois_job(engagement_id, log_path, job)
        elif tool == 'dnsrecon':
            parse_result = parse_dnsrecon_job(engagement_id, log_path, job)
        elif tool == 'wpscan':
            parse_result = parse_wpscan_job(engagement_id, log_path, job)
        elif tool == 'hydra':
            parse_result = parse_hydra_job(engagement_id, log_path, job)
        elif tool == 'ffuf':
            parse_result = parse_ffuf_job(engagement_id, log_path, job)
        elif tool == 'searchsploit':
            parse_result = parse_searchsploit_job(engagement_id, log_path, job)
        elif tool in ['impacket-secretsdump', 'impacket-getnpusers', 'impacket-psexec', 'impacket-smbclient']:
            parse_result = parse_impacket_job(engagement_id, log_path, job)
        elif tool == 'responder':
            parse_result = parse_responder_job(engagement_id, log_path, job)
        elif tool == 'bloodhound':
            parse_result = parse_bloodhound_job(engagement_id, log_path, job)
        elif tool == 'nikto':
            parse_result = parse_nikto_job(engagement_id, log_path, job)
        elif tool == 'dalfox':
            parse_result = parse_dalfox_job(engagement_id, log_path, job)
        else:
            return {'success': False, 'message': f'No parser available for tool: {tool}'}

    except Exception as e:
        return {'success': False, 'message': f'Parse error: {str(e)}'}

    if not parse_result:
        return {'success': False, 'message': 'Parser returned no results'}

    if 'error' in parse_result:
        return {'success': False, 'message': f"Parse error: {parse_result['error']}"}

    # Update job status in jobs.json
    new_status = parse_result.get('status')
    if new_status:
        jobs = _read_jobs()
        for j in jobs:
            if j.get('id') == job_id:
                old_status = j.get('status')
                j['status'] = new_status
                j['reparsed'] = True
                logger.info(f"Job {job_id} status updated: {old_status} -> {new_status}")
                break
        _write_jobs(jobs)

    return {
        'success': True,
        'message': f"Job {job_id} re-parsed successfully",
        'old_status': job.get('status'),
        'new_status': new_status,
        'parse_result': parse_result
    }


def reparse_jobs_by_tool(tool: str, limit: int = 100) -> Dict[str, Any]:
    """
    Re-parse all jobs for a specific tool.

    Args:
        tool: Tool name (e.g., 'enum4linux', 'msf_auxiliary')
        limit: Maximum number of jobs to reparse

    Returns:
        Dict with summary of results
    """
    from .background import list_jobs

    jobs = list_jobs(limit=limit)
    tool_jobs = [j for j in jobs if j.get('tool', '').lower() == tool.lower()]

    results = {
        'tool': tool,
        'total': len(tool_jobs),
        'success': 0,
        'failed': 0,
        'updated': [],
        'errors': []
    }

    for job in tool_jobs:
        job_id = job.get('id')
        old_status = job.get('status')

        result = reparse_job(job_id)

        if result.get('success'):
            results['success'] += 1
            new_status = result.get('new_status')
            if old_status != new_status:
                results['updated'].append({
                    'id': job_id,
                    'old': old_status,
                    'new': new_status
                })
        else:
            results['failed'] += 1
            results['errors'].append({
                'id': job_id,
                'error': result.get('message')
            })

    return results


def parse_nmap_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse nmap job results."""
    try:
        from souleyez.parsers.nmap_parser import parse_nmap_log
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.findings import FindingsManager
        from souleyez.core.cve_matcher import CVEMatcher

        # Parse the log file
        parsed = parse_nmap_log(log_path)

        if 'error' in parsed:
            return {'error': parsed['error']}

        # Import into database
        hm = HostManager()
        result = hm.import_nmap_results(engagement_id, parsed)

        # Check for CVEs and common issues
        fm = FindingsManager()
        cve_matcher = CVEMatcher()
        findings_added = 0

        # First, store any script-detected vulnerabilities (from --script vuln)
        for vuln in parsed.get('vulnerabilities', []):
            host_ip = vuln.get('host_ip')
            if not host_ip:
                continue

            # Find host ID
            host = hm.get_host_by_ip(engagement_id, host_ip)
            if not host:
                continue

            host_id = host['id']

            # Determine severity from CVSS score
            cvss = vuln.get('cvss_score')
            if cvss:
                if cvss >= 9.0:
                    severity = 'critical'
                elif cvss >= 7.0:
                    severity = 'high'
                elif cvss >= 4.0:
                    severity = 'medium'
                else:
                    severity = 'low'
            else:
                # Default based on script type
                severity = 'high' if vuln.get('state') == 'VULNERABLE' else 'medium'

            # Build references string from CVE IDs
            cve_ids = vuln.get('cve_ids', [])
            refs = None
            if cve_ids:
                cve_refs = [f"https://nvd.nist.gov/vuln/detail/{cve}" for cve in cve_ids[:3]]
                refs = ', '.join(cve_refs)
            elif vuln.get('references'):
                refs = ', '.join(vuln.get('references', [])[:3])

            # Build description with CVSS and CVE info
            description = vuln.get('description', f"Detected by nmap script: {vuln.get('script')}")
            if cvss:
                description += f"\n\nCVSS Score: {cvss}"
            if cve_ids:
                description += f"\nCVE IDs: {', '.join(cve_ids[:5])}"

            # Build title - include CVE if available
            title = vuln.get('title', vuln.get('script', 'Unknown Vulnerability'))
            if cve_ids and cve_ids[0] not in title:
                title = f"{cve_ids[0]}: {title}"

            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=title,
                finding_type='vulnerability',
                severity=severity,
                description=description,
                port=vuln.get('port'),
                tool='nmap',
                refs=refs,
                evidence=f"Host: {vuln.get('host_ip', 'unknown')}:{vuln.get('port', 'N/A')}\nScript: {vuln.get('script', 'nmap')}"
            )
            findings_added += 1

        # Then check for inferred CVEs based on service versions
        for host_data in parsed.get('hosts', []):
            if host_data.get('status') == 'up':
                # Find host ID
                host = hm.get_host_by_ip(engagement_id, host_data.get('ip'))
                if not host:
                    continue

                host_id = host['id']

                # Check each service for CVEs and common issues
                for svc in host_data.get('services', []):
                    service_info = {
                        'service_name': svc.get('service', ''),
                        'version': svc.get('version', ''),
                        'port': svc.get('port'),
                        'protocol': svc.get('protocol', 'tcp')
                    }

                    # Also check database for stored version if not in parsed data
                    if not service_info['version']:
                        services = hm.get_host_services(host_id)
                        for stored_svc in services:
                            if stored_svc['port'] == svc.get('port'):
                                service_info['version'] = stored_svc.get('service_version', '')
                                break

                    # Check for CVEs
                    cve_findings = cve_matcher.parse_nmap_service(service_info)
                    for finding in cve_findings:
                        fm.add_finding(
                            engagement_id=engagement_id,
                            host_id=host_id,
                            title=finding['title'],
                            finding_type='vulnerability',
                            severity=finding['severity'],
                            description=finding['description'],
                            port=finding.get('port'),
                            tool='nmap',
                            refs=f"https://nvd.nist.gov/vuln/detail/{finding.get('cve_id')}"
                        )
                        findings_added += 1

                    # Check for common issues
                    issue_findings = cve_matcher.scan_for_common_issues(service_info)
                    for finding in issue_findings:
                        fm.add_finding(
                            engagement_id=engagement_id,
                            host_id=host_id,
                            title=finding['title'],
                            finding_type='misconfiguration',
                            severity=finding['severity'],
                            description=finding['description'],
                            port=finding.get('port'),
                            tool='nmap'
                        )
                        findings_added += 1

        # Build host details list for summary
        host_details = []
        for host_data in parsed.get('hosts', []):
            if host_data.get('status') == 'up':
                services = host_data.get('services', [])
                service_count = len(services)

                # Get top ports for detailed scans
                top_ports = []
                for svc in services[:5]:  # Top 5 ports
                    port = svc.get('port')
                    service_name = svc.get('service', 'unknown')
                    top_ports.append(f"{port}/{service_name}")

                host_details.append({
                    'ip': host_data.get('ip'),
                    'hostname': host_data.get('hostname'),
                    'os': host_data.get('os'),
                    'service_count': service_count,
                    'top_ports': top_ports
                })

        # Determine scan type based on job args
        args = job.get('args', [])
        is_discovery = '-sn' in args or '--discovery' in args
        is_full_scan = any(x in args for x in ['-sV', '-O', '-A', '-p1-65535'])

        # Collect all services for tool chaining
        all_services = []
        for host_data in parsed.get('hosts', []):
            if host_data.get('status') == 'up':
                for svc in host_data.get('services', []):
                    all_services.append({
                        'ip': host_data.get('ip'),
                        'port': svc.get('port'),
                        'protocol': svc.get('protocol', 'tcp'),
                        'state': svc.get('state', 'open'),
                        'service_name': svc.get('service', ''),
                        'version': svc.get('version', '')
                    })

        # Determine status based on results
        hosts_up = len([h for h in parsed.get('hosts', []) if h.get('status') == 'up'])
        if hosts_up > 0:
            status = STATUS_DONE  # Found hosts
        else:
            status = STATUS_NO_RESULTS  # No hosts up

        return {
            'tool': 'nmap',
            'status': status,
            'hosts_added': result['hosts_added'],
            'services_added': result['services_added'],
            'findings_added': findings_added,
            'host_details': host_details,
            'is_discovery': is_discovery,
            'is_full_scan': is_full_scan,
            'services': all_services,  # For tool chaining
            'hosts': parsed.get('hosts', [])  # For tool chaining
        }
    except Exception as e:
        return {'error': str(e)}


def parse_theharvester_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse theHarvester job results."""
    try:
        from souleyez.parsers.theharvester_parser import parse_theharvester_output, get_osint_stats
        from souleyez.storage.osint import OsintManager
        from souleyez.storage.hosts import HostManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse theHarvester output
        target = job.get('target', '')
        parsed = parse_theharvester_output(log_content, target)

        # Store OSINT data
        om = OsintManager()
        osint_added = 0

        # Add emails
        if parsed['emails']:
            count = om.bulk_add_osint_data(engagement_id, 'email', parsed['emails'], 'theHarvester', target)
            osint_added += count

        # Add hosts/subdomains
        if parsed['hosts']:
            count = om.bulk_add_osint_data(engagement_id, 'host', parsed['hosts'], 'theHarvester', target)
            osint_added += count

        # Add IPs
        if parsed['ips']:
            count = om.bulk_add_osint_data(engagement_id, 'ip', parsed['ips'], 'theHarvester', target)
            osint_added += count

        # Add URLs
        if parsed['urls']:
            count = om.bulk_add_osint_data(engagement_id, 'url', parsed['urls'], 'theHarvester', target)
            osint_added += count

        # Add ASNs
        if parsed['asns']:
            count = om.bulk_add_osint_data(engagement_id, 'asn', parsed['asns'], 'theHarvester', target)
            osint_added += count

        # Also add discovered IPs and hosts to the hosts table if they look valid
        hm = HostManager()
        hosts_added = 0

        for ip in parsed['ips']:
            try:
                # Try to add IP as a host with 'discovered' status
                host_id = hm.add_or_update_host(engagement_id, {
                    'ip': ip,
                    'status': 'discovered'
                })
                hosts_added += 1
                from souleyez.log_config import get_logger
                logger = get_logger(__name__)
                logger.info(f"Added IP {ip} to hosts table (host_id={host_id})")
            except Exception as e:
                # Log the error but continue processing other IPs
                from souleyez.log_config import get_logger
                logger = get_logger(__name__)
                logger.warning(f"Failed to add IP {ip} to hosts: {e}")

        stats = get_osint_stats(parsed)

        # Determine status based on results
        total_osint_found = len(parsed['emails']) + len(parsed['hosts']) + len(parsed['ips']) + len(parsed['urls'])
        if total_osint_found > 0:
            status = STATUS_DONE  # Found OSINT data
        else:
            status = STATUS_NO_RESULTS  # No data found

        return {
            'tool': 'theHarvester',
            'status': status,
            'osint_added': osint_added,
            'hosts_added': hosts_added,
            'stats': stats,
            'domains': [target] if target else [],  # For auto-chaining to whois/dnsrecon
            'target': target,  # Original target domain
            'urls': parsed['urls'],  # All URLs - processed by tool_chaining special handler
            'ips': parsed['ips'],  # All IPs for nmap auto-chaining
        }
    except Exception as e:
        return {'error': str(e)}


def _matches_path_pattern(path: str, pattern: str) -> bool:
    """
    Check if a path matches a pattern using smart matching rules.

    For most patterns: substring match (e.g., 'phpmyadmin' in '/phpmyadmin/index.php')
    For 'api' and 'rest': exact path segment match only
        - Matches: /api, /api/, /api/users, /rest, /rest/products
        - Does NOT match: /apis, /restore, /restaurants, /api.js

    Args:
        path: URL path (e.g., '/api/users' or '/restore')
        pattern: Pattern to match (e.g., 'api' or 'rest')

    Returns:
        True if the path matches the pattern
    """
    # Patterns that require exact path segment matching (not substring)
    EXACT_SEGMENT_PATTERNS = {'api', 'rest'}

    if pattern in EXACT_SEGMENT_PATTERNS:
        # Split path into segments and check for exact match
        # /api → ['', 'api'] → 'api' in segments
        # /api/users → ['', 'api', 'users'] → 'api' in segments
        # /apis → ['', 'apis'] → 'api' NOT in segments
        # /restore → ['', 'restore'] → 'rest' NOT in segments
        segments = path.rstrip('/').split('/')
        return pattern in segments
    else:
        # Default: substring match
        return pattern in path


def _create_findings_for_sensitive_paths(engagement_id: int, host_id: int, paths: list, job: Optional[Dict[str, Any]] = None) -> list:
    """
    Analyze discovered paths and create findings for sensitive/interesting ones.

    Args:
        engagement_id: Engagement ID
        host_id: Host ID where paths were discovered
        paths: List of path dictionaries from gobuster
        job: Optional job dict for metadata (includes deduplication info)

    Returns:
        List of created findings (with id, title, severity, etc.)
    """
    from souleyez.storage.findings import FindingsManager
    from souleyez.storage.hosts import HostManager

    # Define sensitive path patterns with severity and descriptions
    SENSITIVE_PATTERNS = {
        # Database admin interfaces (Critical/High)
        'phpmyadmin': {
            'severity': 'high',
            'title': 'Database Admin Interface: phpMyAdmin',
            'description': 'phpMyAdmin interface exposed. This provides direct database access and is a high-value target. Check for default credentials and known CVEs.'
        },
        'adminer': {
            'severity': 'high',
            'title': 'Database Admin Interface: Adminer',
            'description': 'Adminer database management tool exposed. Provides access to multiple database types.'
        },
        'mysql': {
            'severity': 'medium',
            'title': 'MySQL Related Path',
            'description': 'MySQL related endpoint discovered. May provide database information or access.'
        },

        # Admin panels (High)
        'admin': {
            'severity': 'high',
            'title': 'Admin Panel Discovered',
            'description': 'Administrative interface found. May allow privileged access if credentials are compromised.'
        },
        'administrator': {
            'severity': 'high',
            'title': 'Administrator Panel',
            'description': 'Administrator interface discovered. Target for credential attacks and privilege escalation.'
        },
        'cpanel': {
            'severity': 'high',
            'title': 'cPanel Interface',
            'description': 'cPanel control panel exposed. Provides server-wide administration capabilities.'
        },
        'plesk': {
            'severity': 'high',
            'title': 'Plesk Control Panel',
            'description': 'Plesk web hosting control panel discovered.'
        },
        'console': {
            'severity': 'high',
            'title': 'Console Interface',
            'description': 'Administrative console interface found. May provide system-level access.'
        },
        'dashboard': {
            'severity': 'medium',
            'title': 'Dashboard Interface',
            'description': 'Dashboard interface discovered. May contain sensitive information or administrative functions.'
        },
        'manager': {
            'severity': 'medium',
            'title': 'Management Interface',
            'description': 'Management interface found. Review for administrative capabilities.'
        },

        # Backup/Config (High/Medium)
        'backup': {
            'severity': 'high',
            'title': 'Backup Directory',
            'description': 'Backup directory discovered. May contain database dumps, source code, or sensitive data.'
        },
        'backups': {
            'severity': 'high',
            'title': 'Backups Directory',
            'description': 'Backups directory found. Check for downloadable backup files.'
        },
        'config': {
            'severity': 'medium',
            'title': 'Configuration Directory',
            'description': 'Configuration directory discovered. May expose configuration files with credentials.'
        },
        'configuration': {
            'severity': 'medium',
            'title': 'Configuration Path',
            'description': 'Configuration path found. Review for exposed settings and credentials.'
        },
        '.env': {
            'severity': 'critical',
            'title': 'Environment File Exposed',
            'description': 'Environment configuration file (.env) is accessible. Likely contains database credentials, API keys, and secrets.'
        },
        '.git': {
            'severity': 'critical',
            'title': 'Git Repository Exposed',
            'description': 'Git repository (.git) directory is accessible. Can be downloaded to recover source code and commit history.'
        },
        '.svn': {
            'severity': 'critical',
            'title': 'SVN Repository Exposed',
            'description': 'Subversion (.svn) directory exposed. May allow source code recovery.'
        },

        # Upload directories (Medium)
        'upload': {
            'severity': 'medium',
            'title': 'Upload Directory',
            'description': 'File upload directory discovered. Check for upload functionality and file execution capabilities.'
        },
        'uploads': {
            'severity': 'medium',
            'title': 'Uploads Directory',
            'description': 'Uploads directory found. May allow file upload and potential code execution.'
        },
        'files': {
            'severity': 'low',
            'title': 'Files Directory',
            'description': 'Files directory discovered. May contain user-uploaded or system files.'
        },

        # Development/Testing (Medium)
        'test': {
            'severity': 'medium',
            'title': 'Test Environment',
            'description': 'Test environment or directory found. Often has weaker security than production.'
        },
        'testing': {
            'severity': 'medium',
            'title': 'Testing Directory',
            'description': 'Testing directory discovered. May have debug features enabled.'
        },
        'dev': {
            'severity': 'medium',
            'title': 'Development Environment',
            'description': 'Development environment found. Typically has less security controls than production.'
        },
        'development': {
            'severity': 'medium',
            'title': 'Development Directory',
            'description': 'Development directory discovered. May expose debug information or development tools.'
        },
        'staging': {
            'severity': 'medium',
            'title': 'Staging Environment',
            'description': 'Staging environment found. May mirror production with weaker access controls.'
        },
        'debug': {
            'severity': 'medium',
            'title': 'Debug Interface',
            'description': 'Debug interface or directory discovered. May expose sensitive debugging information.'
        },

        # WordPress (Medium)
        'wp-admin': {
            'severity': 'medium',
            'title': 'WordPress Admin Panel (wp-admin)',
            'description': 'WordPress admin login page found. Target for brute-force attacks.'
        },
        'wp-login': {
            'severity': 'medium',
            'title': 'WordPress Login (wp-login)',
            'description': 'WordPress login page discovered.'
        },
        'wp-content': {
            'severity': 'low',
            'title': 'WordPress Installation (wp-content)',
            'description': 'WordPress installation detected. Run WPScan for detailed enumeration.'
        },
        'xmlrpc': {
            'severity': 'medium',
            'title': 'WordPress XML-RPC (xmlrpc.php)',
            'description': 'WordPress XML-RPC interface exposed. Can be used for brute-force attacks and DDoS amplification.'
        },
        'wp-json': {
            'severity': 'medium',
            'title': 'WordPress REST API (wp-json)',
            'description': 'WordPress REST API endpoint. May expose user enumeration via /wp-json/wp/v2/users.'
        },

        # API endpoints (Low/Info)
        'api': {
            'severity': 'low',
            'title': 'API Endpoint',
            'description': 'API endpoint discovered. Review for authentication requirements and exposed functionality.'
        },
        'rest': {
            'severity': 'low',
            'title': 'REST API Endpoint',
            'description': 'REST API endpoint discovered. Test for authentication bypass and injection vulnerabilities.'
        },
        'swagger': {
            'severity': 'medium',
            'title': 'Swagger API Documentation',
            'description': 'Swagger API documentation interface found. Exposes API structure and endpoints.'
        },
        'graphql': {
            'severity': 'low',
            'title': 'GraphQL Endpoint',
            'description': 'GraphQL endpoint discovered. Test for introspection and injection vulnerabilities.'
        },

        # Vulnerable Web Applications (High)
        'juice': {
            'severity': 'high',
            'title': 'OWASP Juice Shop Detected',
            'description': 'OWASP Juice Shop vulnerable web application found. This is an intentionally insecure application for training purposes.'
        },
        'juice-shop': {
            'severity': 'high',
            'title': 'OWASP Juice Shop Detected',
            'description': 'OWASP Juice Shop vulnerable web application found. This is an intentionally insecure application for training purposes.'
        },
        'dvwa': {
            'severity': 'high',
            'title': 'Damn Vulnerable Web App (DVWA) Detected',
            'description': 'DVWA intentionally vulnerable application found. Contains multiple security vulnerabilities for testing.'
        },
        'webgoat': {
            'severity': 'high',
            'title': 'OWASP WebGoat Detected',
            'description': 'OWASP WebGoat training application found. Contains intentional security flaws for educational purposes.'
        },
        'mutillidae': {
            'severity': 'high',
            'title': 'Mutillidae Vulnerable App Detected',
            'description': 'Mutillidae intentionally vulnerable application found. Contains OWASP Top 10 vulnerabilities.'
        },
        'bwapp': {
            'severity': 'high',
            'title': 'bWAPP Detected',
            'description': 'buggy Web Application (bWAPP) found. Contains over 100 web vulnerabilities for testing.'
        },
        'hackazon': {
            'severity': 'high',
            'title': 'Hackazon Vulnerable App Detected',
            'description': 'Hackazon vulnerable e-commerce application found. Contains realistic web vulnerabilities.'
        }
    }

    fm = FindingsManager()
    hm = HostManager()

    # Get host info for finding description
    host_info = hm.get_host(host_id)
    host_ip = host_info.get('ip_address', 'Unknown') if host_info else 'Unknown'

    findings_created = 0
    created_findings = []  # List of finding dicts for auto-chaining

    for path_dict in paths:
        url = path_dict.get('url', '')
        status_code = path_dict.get('status_code', 0)

        # Only create findings for accessible paths (2xx, 3xx, 401, 403, 5xx)
        # 403 is interesting because it confirms the path exists even if forbidden
        # 5xx errors indicate the path exists but has server-side issues (valuable for testing)
        if status_code not in [200, 201, 301, 302, 303, 307, 308, 401, 403, 500, 502, 503]:
            continue

        # Extract path from URL
        from urllib.parse import urlparse
        parsed = urlparse(url)
        path = parsed.path.lower()

        # Check each sensitive pattern
        for pattern, info in SENSITIVE_PATTERNS.items():
            if _matches_path_pattern(path, pattern):
                # Create finding
                description = f"{info['description']}\n\nURL: {url}\nStatus: {status_code}\nHost: {host_ip}"

                # Add deduplication attribution if this scan covered multiple IPs
                if job:
                    metadata = job.get('metadata', {})
                    associated_ips = metadata.get('associated_ips', [])
                    if associated_ips and len(associated_ips) > 1:
                        representative_ip = metadata.get('representative_ip', host_ip)
                        domain = metadata.get('domain_context', '')
                        description += f"\n\n[Web Target Deduplication] This finding was discovered on {representative_ip}"
                        description += f" (representative IP for {len(associated_ips)} IPs serving"
                        if domain:
                            description += f" {domain}"
                        description += f"). All affected IPs: {', '.join(associated_ips)}"

                finding_data = {
                    'title': info['title'],
                    'description': description,
                    'severity': info['severity'],
                    'tool': 'gobuster',
                    'host_id': host_id,
                    'job_id': job.get('id') if job else None
                }

                # Check if finding already exists (avoid duplicates)
                existing_findings = fm.list_findings(engagement_id)
                duplicate = False
                for existing in existing_findings:
                    if (existing.get('title') == finding_data['title']
                        and existing.get('host_id') == host_id
                            and url in existing.get('description', '')):
                        duplicate = True
                        break

                if not duplicate:
                    finding_id = fm.add_finding(
                        engagement_id=engagement_id,
                        title=finding_data['title'],
                        finding_type='web_discovery',
                        severity=finding_data['severity'],
                        description=finding_data.get('description'),
                        host_id=finding_data.get('host_id'),
                        tool=finding_data.get('tool')
                    )
                    findings_created += 1
                    
                    # Add to created_findings list for auto-chaining
                    created_findings.append({
                        'id': finding_id,
                        'title': finding_data['title'],
                        'severity': finding_data['severity'],
                        'tool': 'gobuster',
                        'url': url  # Include full URL for finding-based auto-chains
                    })

                # Only match first pattern to avoid duplicate findings
                break

    return created_findings


def parse_gobuster_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse gobuster job results."""
    try:
        from souleyez.parsers.gobuster_parser import parse_gobuster_output, get_paths_stats
        from souleyez.storage.web_paths import WebPathsManager
        from souleyez.storage.hosts import HostManager
        from urllib.parse import urlparse

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse gobuster output
        target = job.get('target', '')
        parsed = parse_gobuster_output(log_content, target)

        # Get or create host from target URL
        hm = HostManager()
        host_id = None

        if parsed['target_url']:
            parsed_url = urlparse(parsed['target_url'])
            hostname = parsed_url.hostname

            if hostname:
                # Try to find existing host by hostname
                hosts = hm.list_hosts(engagement_id)
                for host in hosts:
                    if host.get('hostname') == hostname or host.get('ip_address') == hostname:
                        host_id = host['id']
                        break

                # Create host if not found
                if not host_id:
                    # Try to determine if it's an IP or hostname
                    import re
                    is_ip = re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', hostname)

                    if is_ip:
                        host_id = hm.add_or_update_host(engagement_id, {
                            'ip': hostname,
                            'status': 'up'
                        })
                    else:
                        # It's a hostname - we need an IP, so skip host creation for now
                        # Just store paths without host_id (will need to fix schema)
                        pass

        # Store web paths
        wpm = WebPathsManager()
        paths_added = 0
        created_findings = []  # Initialize here

        if host_id and parsed['paths']:
            paths_added = wpm.bulk_add_web_paths(host_id, parsed['paths'])

            # Check for sensitive paths and create findings
            created_findings = _create_findings_for_sensitive_paths(engagement_id, host_id, parsed['paths'], job)

        stats = get_paths_stats(parsed)

        # Extract PHP files for auto-chaining SQLMap with --crawl
        # Include 200 (accessible), 401 (auth-required), 403 (forbidden) - all can have parameters
        # Exclude 404 (doesn't exist) and 5xx (server error)
        # Store as tuples (url, status_code) to enable prioritization in tool_chaining.py
        php_files = [
            (path.get('url'), path.get('status_code')) for path in parsed['paths']
            if path.get('url', '').endswith('.php') and path.get('status_code') in [200, 401, 403]
        ]

        # Extract ASP/ASPX files for auto-chaining SQLMap (same logic as PHP)
        asp_files = [
            (path.get('url'), path.get('status_code')) for path in parsed['paths']
            if (path.get('url', '').lower().endswith('.asp') or path.get('url', '').lower().endswith('.aspx'))
            and path.get('status_code') in [200, 401, 403]
        ]

        # Extract high-value directories (redirects) for auto-chaining
        # These are vulnerable web apps that should be crawled by sqlmap
        high_value_dir_keywords = [
            'mutillidae', 'dvwa', 'bwapp', 'webgoat', 'phpmyadmin',
            'juice', 'juice-shop',  # OWASP Juice Shop
            'hackazon', 'pentesterlab', 'vulnhub',  # Other vulnerable apps
            'api', 'rest', 'graphql',  # Modern API endpoints
            'drupal', 'wordpress', 'joomla', 'moodle', 'magento',
            'phpbb', 'opencart', 'prestashop', 'zen-cart', 'oscommerce'
        ]

        high_value_dirs = []
        for path in parsed['paths']:
            # Look for 301 redirects to directories
            if path.get('status_code') == 301 and path.get('redirect'):
                url = path.get('url', '').lower()
                redirect = path.get('redirect', '').lower()

                # Check if URL or redirect contains high-value keywords
                if any(keyword in url or keyword in redirect for keyword in high_value_dir_keywords):
                    # Use the redirect target (with trailing slash)
                    high_value_dirs.append(path.get('redirect'))
                    logger.info(f"Detected high-value directory: {path.get('redirect')}")

        logger.info(f"Gobuster result summary: {len(php_files)} PHP files, {len(asp_files)} ASP files, {len(high_value_dirs)} high-value directories")

        # Check for wildcard response (for auto-retry)
        wildcard_detected = False
        exclude_length = None

        if "the server returns a status code that matches" in log_content:
            wildcard_detected = True
            # Extract response length: "=> 403 (Length: 1434)"
            import re
            length_match = re.search(r'\(Length: (\d+)\)', log_content)
            if length_match:
                exclude_length = length_match.group(1)
                logger.info(f"Gobuster wildcard detected: Length {exclude_length}b")

        # Determine status based on results
        if wildcard_detected:
            # Wildcard detected - warning status (triggers auto-retry)
            status = STATUS_WARNING
        elif stats['total'] > 0:
            # Found paths - done
            status = STATUS_DONE
        else:
            # No paths found - no_results
            status = STATUS_NO_RESULTS

        result = {
            'tool': 'gobuster',
            'status': status,
            'paths_added': paths_added,
            'total_paths': stats['total'],
            'paths_found': stats['total'],  # For dashboard display
            'redirects_found': stats.get('redirects', 0),
            'by_status': stats['by_status'],
            'target_url': parsed.get('target_url'),
            'findings': created_findings,  # Include findings for auto-chaining
            'php_files': php_files,  # For SQLMap --crawl auto-chaining
            'asp_files': asp_files,  # For SQLMap --crawl auto-chaining (ASP/ASPX)
            'high_value_dirs': high_value_dirs  # High-value directories to crawl
        }

        # Add wildcard info if detected
        if wildcard_detected:
            result['wildcard_detected'] = True
            if exclude_length:
                result['exclude_length'] = exclude_length

        return result
    except Exception as e:
        return {'error': str(e)}


def parse_sqlmap_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse sqlmap job results."""
    try:
        from souleyez.parsers.sqlmap_parser import parse_sqlmap_output, get_sqli_stats
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.sqlmap_data import SQLMapDataManager
        from urllib.parse import urlparse

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse sqlmap output
        target = job.get('target', '')
        parsed = parse_sqlmap_output(log_content, target)

        # Get or create host from target URL
        hm = HostManager()
        host_id = None

        if parsed['target_url']:
            parsed_url = urlparse(parsed['target_url'])
            hostname = parsed_url.hostname

            if hostname:
                # Try to find existing host
                import re
                is_ip = re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', hostname)

                if is_ip:
                    host = hm.get_host_by_ip(engagement_id, hostname)
                    if host:
                        host_id = host['id']
                    else:
                        host_id = hm.add_or_update_host(engagement_id, {
                            'ip': hostname,
                            'status': 'up'
                        })
                else:
                    # Try to match by hostname (check both hostname field and ip field)
                    hosts = hm.list_hosts(engagement_id)
                    for h in hosts:
                        # Match against ip_address, ip (normalized), or hostname fields
                        if (h.get('hostname') == hostname or 
                            h.get('ip') == hostname or 
                            h.get('ip_address') == hostname):
                            host_id = h['id']
                            break
                    
                    # If host not found, try to resolve hostname to IP and create
                    if not host_id:
                        try:
                            import socket
                            ip_address = socket.gethostbyname(hostname)
                            host_id = hm.add_or_update_host(engagement_id, {
                                'ip': ip_address,
                                'hostname': hostname,
                                'status': 'up'
                            })
                        except (socket.gaierror, socket.herror):
                            # Cannot resolve hostname, skip exploitation findings
                            pass

        # Create virtual HTTP/HTTPS service for web targets
        if host_id and parsed['target_url']:
            parsed_url = urlparse(parsed['target_url'])
            port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)

            # Check if service already exists
            existing_services = hm.get_host_services(host_id)
            service_exists = any(s.get('port') == port for s in existing_services)

            if not service_exists:
                # Create virtual HTTP/HTTPS service entry
                hm.add_service(host_id, {
                    'port': port,
                    'protocol': 'tcp',
                    'service_name': parsed_url.scheme,  # 'http' or 'https'
                    'state': 'open'
                })

        # Extract port from URL for finding association
        target_port = None
        if parsed['target_url']:
            parsed_url = urlparse(parsed['target_url'])
            target_port = parsed_url.port or (443 if parsed_url.scheme == 'https' else 80)

        # Store vulnerabilities as findings
        fm = FindingsManager()
        findings_added = 0
        credentials_added = 0  # Track credentials extracted from dumps

        for vuln in parsed['vulnerabilities']:
            # Determine severity
            vuln_type = vuln.get('vuln_type', 'unknown')
            if vuln_type == 'sqli' and vuln.get('injectable'):
                severity = 'critical'
                finding_type = 'sql_injection'
                title = f"SQL Injection in parameter '{vuln['parameter']}'"
            elif vuln_type == 'xss':
                severity = vuln.get('severity', 'medium')
                finding_type = 'xss'
                title = f"Possible XSS in parameter '{vuln['parameter']}'"
            elif vuln_type == 'file_inclusion':
                severity = vuln.get('severity', 'high')
                finding_type = 'file_inclusion'
                title = f"Possible File Inclusion in parameter '{vuln['parameter']}'"
            else:
                severity = 'medium'
                finding_type = 'web_vulnerability'
                title = f"Vulnerability in parameter '{vuln['parameter']}'"

            # Create description
            description = vuln.get('description', '')
            if vuln.get('technique'):
                description += f"\nTechnique: {vuln['technique']}"
            if vuln.get('dbms'):
                description += f"\nDBMS: {vuln['dbms']}"

            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                port=target_port,
                title=title,
                finding_type=finding_type,
                severity=severity,
                description=description,
                tool='sqlmap',
                path=vuln.get('url')
            )
            findings_added += 1
        
        # === EXPLOITATION FINDINGS ===
        # Track progressive exploitation levels beyond just detecting the vulnerability
        
        # Level 2: Database Enumeration (Exploitation Confirmed)
        databases = parsed.get('databases', [])
        if databases and host_id:
            dbms_info = parsed.get('dbms', 'Unknown')
            db_count = len(databases)
            
            # Limit database list in description (show first 10)
            db_list = databases[:10]
            db_list_str = ', '.join(db_list)
            if len(databases) > 10:
                db_list_str += f' ... and {len(databases) - 10} more'
            
            description = f"SQL injection was successfully exploited to enumerate {db_count} database(s).\n\n"
            description += f"DBMS: {dbms_info}\n"
            description += f"Databases: {db_list_str}\n\n"
            description += "This confirms the SQL injection vulnerability is exploitable and provides access to database structure."
            
            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                port=target_port,
                title=f"SQL Injection Exploited - {db_count} Database(s) Enumerated",
                finding_type='sql_injection_exploitation',
                severity='critical',
                description=description,
                tool='sqlmap',
                path=parsed.get('target_url')
            )
            findings_added += 1
        
        # Level 3: Table Enumeration (Deeper Exploitation)
        tables = parsed.get('tables', {})
        if tables and host_id:
            total_tables = sum(len(table_list) for table_list in tables.values())
            
            if total_tables > 0:
                description = f"SQL injection was exploited to enumerate {total_tables} table(s) across {len(tables)} database(s).\n\n"
                
                # List tables by database (limit to first 5 databases, first 10 tables each)
                for idx, (db_name, table_list) in enumerate(list(tables.items())[:5]):
                    description += f"\nDatabase '{db_name}' ({len(table_list)} tables):\n"
                    shown_tables = table_list[:10]
                    description += "  - " + "\n  - ".join(shown_tables)
                    if len(table_list) > 10:
                        description += f"\n  ... and {len(table_list) - 10} more tables"
                
                if len(tables) > 5:
                    remaining_dbs = len(tables) - 5
                    remaining_tables = sum(len(table_list) for db_name, table_list in list(tables.items())[5:])
                    description += f"\n\n... and {remaining_tables} more tables in {remaining_dbs} other database(s)"
                
                description += "\n\nTable structure reveals potential sensitive data locations and attack surface."
                
                fm.add_finding(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    port=target_port,
                    title=f"SQL Injection Exploited - {total_tables} Table(s) Enumerated",
                    finding_type='sql_injection_exploitation',
                    severity='high',
                    description=description,
                    tool='sqlmap',
                    path=parsed.get('target_url')
                )
                findings_added += 1
        
        # Level 4: Data Dumped (CRITICAL - Actual Data Breach!)
        dumped_data = parsed.get('dumped_data', {})
        if dumped_data and host_id:
            total_rows = sum(data.get('row_count', 0) for data in dumped_data.values())
            total_tables_dumped = len(dumped_data)
            
            description = f"🚨 DATA BREACH CONFIRMED 🚨\n\n"
            description += f"SQL injection was exploited to dump actual data from {total_tables_dumped} table(s).\n"
            description += f"Total rows exfiltrated: {total_rows}\n\n"
            
            # List dumped tables with row counts (limit to first 10)
            description += "Tables dumped:\n"
            for idx, (table_key, data_info) in enumerate(list(dumped_data.items())[:10]):
                row_count = data_info.get('row_count', 0)
                columns = data_info.get('columns', [])
                csv_path = data_info.get('csv_path')
                
                description += f"\n  • {table_key} ({row_count} rows)\n"
                if columns:
                    col_list = ', '.join(columns[:5])
                    if len(columns) > 5:
                        col_list += f' ... and {len(columns) - 5} more columns'
                    description += f"    Columns: {col_list}\n"
                if csv_path:
                    description += f"    CSV: {csv_path}\n"
            
            if len(dumped_data) > 10:
                remaining = len(dumped_data) - 10
                description += f"\n  ... and {remaining} more table(s) dumped"
            
            description += "\n\n⚠️ IMMEDIATE ACTION REQUIRED ⚠️\n"
            description += "- Review dumped data for PII/PCI/PHI exposure\n"
            description += "- Notify security team and stakeholders\n"
            description += "- Consider breach notification requirements\n"
            description += "- Patch SQL injection vulnerability immediately\n"
            description += "- Implement WAF/IDS rules\n"
            description += "- Review database access logs"
            
            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                port=target_port,
                title=f"🚨 SQL Injection Data Breach - {total_rows} Row(s) Exfiltrated",
                finding_type='data_breach',
                severity='critical',
                description=description,
                tool='sqlmap',
                path=parsed.get('target_url')
            )
            findings_added += 1
            
            # Extract credentials from dumped data
            credentials_added = _extract_credentials_from_dump(
                dumped_data=dumped_data,
                engagement_id=engagement_id,
                host_id=host_id,
                job_id=job.get('id')
            )
            
            if credentials_added > 0:
                from souleyez.log_config import get_logger
                logger = get_logger(__name__)
                logger.info(f"SQLMap: Extracted {credentials_added} credentials from dumped tables")
        
        # Store SQLMap database discoveries
        # Store if we have host_id AND (databases OR tables OR dumped data)
        has_data_to_store = (parsed.get('databases') or 
                            parsed.get('tables') or 
                            parsed.get('dumped_data'))
        
        if host_id and has_data_to_store:
            sdm = SQLMapDataManager()
            dbms_type = parsed.get('dbms', 'Unknown')
            
            # Store databases
            db_ids = {}
            for db_name in parsed.get('databases', []):
                db_id = sdm.add_database(engagement_id, host_id, db_name, dbms_type)
                if db_id:
                    db_ids[db_name] = db_id
            
            # Store tables
            table_ids = {}
            for db_table_key, table_list in parsed.get('tables', {}).items():
                # db_table_key might be "database" or include more context
                # If we have multiple databases, tables dict might be keyed by database name
                for table_name in table_list:
                    # Try to get database ID from db_ids
                    db_id = db_ids.get(db_table_key)
                    if not db_id and db_ids:
                        # Fallback: use first database if only one exists
                        db_id = list(db_ids.values())[0]
                    
                    if db_id:
                        table_id = sdm.add_table(db_id, table_name)
                        if table_id:
                            full_key = f"{db_table_key}.{table_name}"
                            table_ids[full_key] = table_id
            
            # Store columns
            for table_key, column_list in parsed.get('columns', {}).items():
                table_id = table_ids.get(table_key)
                if table_id:
                    columns = [{'name': col} for col in column_list]
                    sdm.add_columns(table_id, columns)
            
            # Store dumped data (extract db/table from key if not already stored)
            for data_key, dump_info in parsed.get('dumped_data', {}).items():
                table_id = table_ids.get(data_key)
                
                # If table not already stored, extract db.table from key and create them
                if not table_id and '.' in data_key:
                    db_name, table_name = data_key.rsplit('.', 1)
                    
                    # Get or create database
                    db_id = db_ids.get(db_name)
                    if not db_id:
                        db_id = sdm.add_database(engagement_id, host_id, db_name, dbms_type)
                        if db_id:
                            db_ids[db_name] = db_id
                    
                    # Create table
                    if db_id:
                        row_count = dump_info.get('row_count', len(dump_info.get('rows', [])))
                        table_id = sdm.add_table(db_id, table_name, row_count)
                        if table_id:
                            table_ids[data_key] = table_id
                            
                            # Store columns from dumped data
                            if dump_info.get('columns'):
                                columns = [{'name': col} for col in dump_info['columns']]
                                sdm.add_columns(table_id, columns)
                
                # Store the dumped data
                if table_id:
                    sdm.add_dumped_data(
                        table_id,
                        dump_info.get('rows', []),
                        dump_info.get('csv_path')
                    )

        stats = get_sqli_stats(parsed)

        # Determine status based on results
        if stats['sqli_confirmed'] or stats['xss_possible'] or stats['fi_possible']:
            status = STATUS_DONE  # Found injection vulnerabilities
        else:
            status = STATUS_NO_RESULTS  # No injections found

        return {
            'tool': 'sqlmap',
            'status': status,
            'findings_added': findings_added,
            'sqli_confirmed': stats['sqli_confirmed'],
            'xss_possible': stats['xss_possible'],
            'fi_possible': stats['fi_possible'],
            'urls_tested': stats['urls_tested'],
            'databases': parsed.get('databases', []),
            'tables': parsed.get('tables', {}),
            'columns': parsed.get('columns', {}),
            'dumped_tables': stats.get('dumped_tables', 0),
            'dumped_rows': stats.get('dumped_rows', 0),
            'dumped_data': parsed.get('dumped_data', {}),  # Full dump data for display

            # NEW: Add chaining flags
            'dbms': parsed.get('dbms'),  # Database type (sqlite, mysql, etc.)
            'sql_injection_confirmed': parsed.get('sql_injection_confirmed', False),
            'injectable_parameter': parsed.get('injectable_parameter', ''),
            'injectable_url': parsed.get('injectable_url', target),  # FIX: Use correct key
            'injectable_post_data': parsed.get('injectable_post_data', ''),  # For POST injections
            'injectable_method': parsed.get('injectable_method', 'GET'),  # GET or POST
            'all_injection_points': parsed.get('all_injection_points', []),  # For fallback
            'databases_enumerated': len(parsed.get('databases', [])) > 0,
            'tables_enumerated': len(parsed.get('tables', {})) > 0,
            'columns_enumerated': len(parsed.get('columns', {})) > 0,

            # NEW: Post-exploitation flags for advanced chaining
            'is_dba': parsed.get('is_dba', False),
            'privileges': parsed.get('privileges', []),
            'current_user': parsed.get('current_user'),
            'file_read_success': parsed.get('file_read_success', False),
            'os_command_success': parsed.get('os_command_success', False),

            # Credentials flag for cross-tool chaining (hydra, etc.)
            'credentials_dumped': credentials_added > 0,
            'credentials_count': credentials_added,
        }
    except Exception as e:
        return {'error': str(e)}


def _extract_credentials_from_dump(dumped_data: Dict, engagement_id: int, 
                                   host_id: int, job_id: int) -> int:
    """
    Extract credentials from SQLMap dumped data.
    
    Args:
        dumped_data: Dict of {table_key: {rows, columns, row_count, csv_path}}
        engagement_id: Engagement ID
        host_id: Host ID
        job_id: Job ID for tracking
    
    Returns:
        int: Number of credentials added
    """
    from souleyez.intelligence.sensitive_tables import is_sensitive_table
    from souleyez.storage.credentials import CredentialsManager
    from souleyez.storage.hosts import HostManager
    from souleyez.log_config import get_logger
    
    logger = get_logger(__name__)
    cred_manager = CredentialsManager()
    hm = HostManager()
    
    # Get host IP for credential tracking
    host = hm.get_host(host_id)
    host_ip = host.get('ip_address', 'unknown') if host else 'unknown'
    
    credentials_added = 0
    
    for table_key, data_info in dumped_data.items():
        # Parse table name
        parts = table_key.split('.')
        table_name = parts[-1]
        db_name = parts[0] if len(parts) > 1 else 'unknown'
        
        # Check if this is a credentials table
        is_sensitive, category, priority = is_sensitive_table(table_name)
        
        if category != 'credentials':
            continue  # Only extract from credential tables
        
        rows = data_info.get('rows', [])
        columns = data_info.get('columns', [])
        
        if not rows or not columns:
            continue
        
        # Find username and password columns
        # Strategy: Exact matches first, then substring matches (with ID column blocklist)
        username_col = None
        password_col = None
        
        # Prioritized patterns: more specific first (username, uname before email)
        username_patterns = ['username', 'uname', 'user_name', 'login', 'account', 'email', 'user']
        password_patterns = ['password', 'passwd', 'pass', 'pwd', 'hash', 'pwd_hash', 'password_hash']
        
        # Blocklist for columns that are likely foreign keys or IDs
        id_columns = ['id', 'user_id', 'userid', 'account_id', 'user_fk']
        
        # Step 1: Try EXACT matches first for username (more reliable)
        for pattern in username_patterns:
            for col in columns:
                if col.lower() == pattern:
                    # Skip obvious ID columns even if they match
                    if col.lower() in id_columns:
                        continue
                    username_col = col
                    break
            if username_col:
                break
        
        # Step 2: Fall back to SUBSTRING matches only if no exact match for username
        if not username_col:
            for pattern in username_patterns:
                for col in columns:
                    if pattern in col.lower():
                        # Skip columns that are likely foreign keys
                        if col.lower() in id_columns:
                            continue
                        username_col = col
                        break
                if username_col:
                    break
        
        # Step 1: Try EXACT matches first for password (more reliable)
        for pattern in password_patterns:
            for col in columns:
                if col.lower() == pattern:
                    # Skip obvious ID columns even if they match
                    if col.lower() in id_columns:
                        continue
                    password_col = col
                    break
            if password_col:
                break
        
        # Step 2: Fall back to SUBSTRING matches only if no exact match for password
        if not password_col:
            for pattern in password_patterns:
                for col in columns:
                    if pattern in col.lower():
                        # Skip columns that are likely foreign keys
                        if col.lower() in id_columns:
                            continue
                        password_col = col
                        break
                if password_col:
                    break
        
        if not username_col or not password_col:
            logger.debug(f"Skipping {table_key}: missing username/password columns")
            continue
        
        # Extract credentials from rows
        for row in rows:
            username = str(row.get(username_col, '')).strip()
            password = str(row.get(password_col, '')).strip()

            # Handle SQLMap's <blank> placeholder for NULL/empty values
            if username in ['<blank>', 'NULL', 'None', '']:
                # Try email as fallback for username
                email = str(row.get('email', '')).strip()
                if email and email not in ['<blank>', 'NULL', 'None', '']:
                    username = email
                else:
                    continue  # No valid username found

            if not password or password in ['NULL', 'None', '', '<blank>']:
                continue
            
            # Detect hash type
            credential_type = _detect_hash_type(password)
            
            # Add to credentials database
            try:
                cred_manager.add_credential(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    username=username,
                    password=password,
                    credential_type=credential_type,
                    tool='sqlmap',
                    service='web'
                )
                credentials_added += 1
            except Exception as e:
                logger.warning(f"Failed to add credential {username}: {e}")
    
    return credentials_added


def _detect_hash_type(password: str) -> str:
    """
    Detect hash type from password string.
    
    Args:
        password: Password or hash string
    
    Returns:
        str: Credential type ('password', 'hash:bcrypt', 'hash:md5', etc.)
    """
    import re
    
    # Check for hash prefixes
    if password.startswith('$2y$') or password.startswith('$2a$') or password.startswith('$2b$'):
        return 'hash:bcrypt'
    elif password.startswith('$1$'):
        return 'hash:md5crypt'
    elif password.startswith('$5$'):
        return 'hash:sha256crypt'
    elif password.startswith('$6$'):
        return 'hash:sha512crypt'
    elif password.startswith('$apr1$'):
        return 'hash:apr1'
    elif password.startswith('{SHA}') or password.startswith('{SSHA}'):
        return 'hash:ldap'
    
    # Check hex patterns
    if re.match(r'^[a-fA-F0-9]{32}$', password):
        return 'hash:md5'
    elif re.match(r'^[a-fA-F0-9]{40}$', password):
        return 'hash:sha1'
    elif re.match(r'^[a-fA-F0-9]{64}$', password):
        return 'hash:sha256'
    elif re.match(r'^[a-fA-F0-9]{128}$', password):
        return 'hash:sha512'
    
    # Check for NTLM hash
    if re.match(r'^[a-fA-F0-9]{32}:[a-fA-F0-9]{32}$', password):
        return 'hash:ntlm'
    
    # Likely plaintext if short and no special patterns
    if len(password) < 20 and not re.search(r'[\$\{\}]', password):
        return 'password'
    
    # Default to generic hash
    return 'hash'


def parse_msf_auxiliary_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """Parse MSF auxiliary module job results."""
    try:
        from souleyez.parsers.msf_parser import parse_msf_log
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.credentials import CredentialsManager

        # Parse the log
        parsed = parse_msf_log(log_path)

        if 'error' in parsed:
            return {'error': parsed['error']}

        target = job.get('target', '')
        hm = HostManager()
        fm = FindingsManager()
        cm = CredentialsManager()

        services_added = 0
        findings_added = 0
        credentials_added = 0

        # Get or create host
        host = hm.get_host_by_ip(engagement_id, target)
        if not host:
            host_id = hm.add_host(engagement_id, target)
        else:
            host_id = host['id']

        # Add services if any
        for svc in parsed.get('services', []):
            hm.add_service(
                host_id=host_id,
                port=svc.get('port'),
                protocol=svc.get('protocol', 'tcp'),
                state=svc.get('state', 'open'),
                service_name=svc.get('service_name'),
                service_version=svc.get('service_version')
            )
            services_added += 1

        # Add findings
        for finding in parsed.get('findings', []):
            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=finding.get('title'),
                finding_type='credential' if 'credential' in finding.get('title', '').lower() else 'security_issue',
                severity=finding.get('severity', 'info'),
                description=finding.get('description'),
                tool='msf_auxiliary',
                port=finding.get('port')
            )
            findings_added += 1

        # Add credentials if any
        for cred in parsed.get('credentials', []):
            cm.add_credential(
                engagement_id=engagement_id,
                host_id=host_id,
                service=cred.get('service', 'unknown'),
                username=cred.get('username', ''),
                password=cred.get('password', ''),
                credential_type='password',
                tool='msf_auxiliary',
                port=cred.get('port'),
                status=cred.get('status', 'valid')
            )
            credentials_added += 1

        return {
            'tool': 'msf_auxiliary',
            'status': STATUS_DONE if (services_added > 0 or findings_added > 0 or credentials_added > 0) else STATUS_NO_RESULTS,
            'host': target,
            'services_added': services_added,
            'findings_added': findings_added,
            'credentials_added': credentials_added
        }
    except Exception as e:
        return {'error': str(e)}


def parse_msf_exploit_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """Parse MSF exploit module job results."""
    try:
        import re
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.findings import FindingsManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            content = f.read()

        target = job.get('target', '')
        hm = HostManager()
        fm = FindingsManager()

        findings_added = 0
        exploit_success = False

        # Get or create host
        host = hm.get_host_by_ip(engagement_id, target)
        if not host:
            host_id = hm.add_host(engagement_id, target)
        else:
            host_id = host['id']

        # Extract exploit name from header or args
        exploit_match = re.search(r'Exploit:\s*(.+)$', content, re.MULTILINE)
        exploit_name = exploit_match.group(1).strip() if exploit_match else 'unknown'

        # Check for successful exploitation indicators
        # Common success patterns in MSF output
        success_patterns = [
            r'\[\*\]\s+Command shell session \d+ opened',
            r'\[\*\]\s+Meterpreter session \d+ opened',
            r'\[\+\]\s+\d+\.\d+\.\d+\.\d+:\d+\s+-\s+Session \d+ created',
            r'\[\+\].*shell.*opened',
            r'\[\+\].*session.*created',
            r'Exploit completed.*session',
            r'\[\*\]\s+Starting the payload handler',
            r'\[\*\]\s+Sending stage',
        ]

        for pattern in success_patterns:
            if re.search(pattern, content, re.IGNORECASE):
                exploit_success = True
                break

        # Also check for explicit failure indicators
        failure_patterns = [
            r'Exploit completed, but no session was created',
            r'\[-\].*failed',
            r'\[-\].*Exploit aborted',
            r'No session was created',
        ]

        explicit_failure = False
        for pattern in failure_patterns:
            if re.search(pattern, content, re.IGNORECASE):
                explicit_failure = True
                break

        # Create finding based on result
        if exploit_success:
            # Extract session info if available
            session_match = re.search(r'session (\d+) (opened|created)', content, re.IGNORECASE)
            session_info = f" (Session {session_match.group(1)})" if session_match else ""

            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=f'Exploit Successful: {exploit_name.split("/")[-1]}',
                finding_type='vulnerability',
                severity='critical',
                description=f'Successfully exploited {target} using {exploit_name}.{session_info}\n\nThis confirms the vulnerability is exploitable.',
                tool='msf_exploit'
            )
            findings_added += 1
        elif not explicit_failure:
            # Check for [+] lines that might indicate partial success
            plus_lines = re.findall(r'\[\+\]\s+(.+)', content)
            if plus_lines:
                for line in plus_lines[:3]:  # Limit to first 3 findings
                    fm.add_finding(
                        engagement_id=engagement_id,
                        host_id=host_id,
                        title=f'{exploit_name.split("/")[-1]}: {line[:60]}',
                        finding_type='security_issue',
                        severity='medium',
                        description=f'Exploit output: {line}',
                        tool='msf_exploit'
                    )
                    findings_added += 1

        return {
            'tool': 'msf_exploit',
            'status': STATUS_DONE if (exploit_success or findings_added > 0) else STATUS_NO_RESULTS,
            'host': target,
            'exploit': exploit_name,
            'success': exploit_success,
            'findings_added': findings_added
        }
    except Exception as e:
        return {'error': str(e)}


def parse_smbmap_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse smbmap job results."""
    try:
        from souleyez.parsers.smbmap_parser import parse_smbmap_output, extract_findings
        from souleyez.storage.smb_shares import SMBSharesManager
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse smbmap output
        target = job.get('target', '')
        parsed = parse_smbmap_output(log_content, target)

        # Get or create host from target
        hm = HostManager()
        host_id = None

        if parsed['target']:
            # Try to find existing host
            import re
            is_ip = re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', parsed['target'])

            if is_ip:
                host = hm.get_host_by_ip(engagement_id, parsed['target'])
                if host:
                    host_id = host['id']
                else:
                    # Create host
                    host_id = hm.add_or_update_host(engagement_id, {
                        'ip': parsed['target'],
                        'status': 'up'
                    })

        if not host_id:
            return {'error': 'Could not determine target host'}

        # Store SMB shares
        smm = SMBSharesManager()
        shares_added = 0
        files_added = 0

        for share in parsed['shares']:
            share_id = smm.add_share(host_id, share)
            shares_added += 1

            # Add files if any
            share_files = [f for f in parsed['files'] if f.get('share') == share['name']]
            for file_data in share_files:
                smm.add_file(share_id, file_data)
                files_added += 1

        # Extract and store findings
        fm = FindingsManager()
        findings_added = 0

        findings = extract_findings(parsed)
        for finding in findings:
            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                finding_type='smb_share',
                severity=finding.get('severity'),
                title=finding.get('title'),
                description=finding.get('description'),
                evidence=finding.get('evidence'),
                tool='smbmap'
            )
            findings_added += 1

        return {
            'tool': 'smbmap',
            'host': parsed['target'],
            'connection_status': parsed.get('status', 'Unknown'),  # SMB connection status
            'status': STATUS_DONE if (shares_added > 0 or findings_added > 0) else STATUS_NO_RESULTS,  # Job status
            'shares_added': shares_added,
            'files_added': files_added,
            'findings_added': findings_added
        }
    except Exception as e:
        return {'error': str(e)}


def parse_whois_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse WHOIS job results."""
    try:
        from souleyez.parsers.whois_parser import parse_whois_output, map_to_osint_data, extract_emails
        from souleyez.storage.osint import OsintManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse WHOIS output
        target = job.get('target', '')
        parsed = parse_whois_output(log_content, target)

        # Store OSINT data
        om = OsintManager()
        osint_record = map_to_osint_data(parsed, engagement_id)
        om.add_osint_data(
            engagement_id,
            osint_record['data_type'],
            osint_record['target'],  # This is the 'value' parameter
            source=osint_record['source'],
            target=target,
            summary=osint_record['summary'],
            content=osint_record['content'],
            metadata=osint_record['metadata']
        )

        # Extract emails and add separately for better querying
        emails = extract_emails(parsed)
        emails_added = 0
        if emails:
            emails_added = om.bulk_add_osint_data(engagement_id, 'email', emails, 'whois', target)

        return {
            'tool': 'whois',
            'status': STATUS_DONE if (parsed.get('registrar') or parsed.get('nameservers')) else STATUS_NO_RESULTS,
            'domain': parsed.get('domain', target),
            'registrar': parsed.get('registrar'),
            'created': parsed.get('dates', {}).get('created'),
            'expires': parsed.get('dates', {}).get('expires'),
            'emails_found': len(emails),
            'nameservers': len(parsed.get('nameservers', [])),
            'osint_records_added': 1,
            'emails_added': emails_added
        }
    except Exception as e:
        return {'error': str(e)}


def parse_dnsrecon_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse dnsrecon job results."""
    try:
        from souleyez.parsers.dnsrecon_parser import parse_dnsrecon_output
        from souleyez.storage.osint import OsintManager
        from souleyez.storage.hosts import HostManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse dnsrecon output
        target = job.get('target', '')
        parsed = parse_dnsrecon_output(log_content, target)

        # Store OSINT data
        om = OsintManager()
        osint_added = 0

        # Add nameservers
        if parsed['nameservers']:
            count = om.bulk_add_osint_data(engagement_id, 'nameserver', parsed['nameservers'], 'dnsrecon', target)
            osint_added += count

        # Add mail servers
        if parsed['mail_servers']:
            count = om.bulk_add_osint_data(engagement_id, 'mail_server', parsed['mail_servers'], 'dnsrecon', target)
            osint_added += count

        # Add TXT records
        if parsed['txt_records']:
            # Limit TXT record length
            txt_records = [txt[:500] for txt in parsed['txt_records']]
            count = om.bulk_add_osint_data(engagement_id, 'txt_record', txt_records, 'dnsrecon', target)
            osint_added += count

        # Add subdomains/hosts
        if parsed['subdomains']:
            count = om.bulk_add_osint_data(engagement_id, 'host', parsed['subdomains'], 'dnsrecon', target)
            osint_added += count

        # Also add discovered hosts to the hosts table
        hm = HostManager()
        hosts_added = 0

        for host_data in parsed.get('hosts', []):
            try:
                hostname = host_data.get('hostname', '')
                ip = host_data.get('ip', '')

                if ip and hostname:
                    hm.add_or_update_host(engagement_id, {
                        'ip': ip,
                        'hostname': hostname,
                        'status': 'up',
                        'notes': f'Discovered by dnsrecon for domain: {target}'
                    })
                    hosts_added += 1
            except Exception:
                pass  # Skip if invalid

        return {
            'tool': 'dnsrecon',
            'domain': parsed.get('target_domain', target),
            'hosts_found': len(parsed.get('hosts', [])),
            'nameservers': len(parsed.get('nameservers', [])),
            'mail_servers': len(parsed.get('mail_servers', [])),
            'txt_records': len(parsed.get('txt_records', [])),
            'subdomains': len(parsed.get('subdomains', [])),
            'osint_records_added': osint_added,
            'hosts_added': hosts_added,
            'status': STATUS_DONE if (osint_added > 0 or hosts_added > 0) else STATUS_NO_RESULTS
        }
    except Exception as e:
        return {'error': str(e)}


def parse_wpscan_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse WPScan job results."""
    try:
        from souleyez.parsers.wpscan_parser import parse_wpscan_output
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse WPScan output
        target = job.get('target', '')
        parsed = parse_wpscan_output(log_content, target)

        # Get or create host
        hm = HostManager()
        host = hm.get_host_by_ip(engagement_id, target)
        if not host:
            host = hm.add_or_update_host(engagement_id, {'ip': target, 'status': 'up'})
        host_id = host['id']

        # Add findings
        fm = FindingsManager()
        findings_added = 0

        # WordPress version vulnerabilities
        for vuln in parsed.get('vulnerabilities', []):
            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=vuln['title'],
                finding_type='vulnerability',
                severity=vuln['severity'],
                description=vuln['description'],
                tool='wpscan',
                refs=', '.join(vuln.get('references', []))
            )
            findings_added += 1

        # Plugin vulnerabilities
        for plugin_name, plugin_data in parsed.get('plugins', {}).items():
            for vuln in plugin_data.get('vulnerabilities', []):
                fm.add_finding(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    title=f"WordPress Plugin: {plugin_name} - {vuln['title']}",
                    finding_type='vulnerability',
                    severity=vuln['severity'],
                    description=vuln['description'],
                    tool='wpscan',
                    refs=', '.join(vuln.get('references', []))
                )
                findings_added += 1

        # Theme vulnerabilities
        for theme_name, theme_data in parsed.get('themes', {}).items():
            for vuln in theme_data.get('vulnerabilities', []):
                fm.add_finding(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    title=f"WordPress Theme: {theme_name} - {vuln['title']}",
                    finding_type='vulnerability',
                    severity=vuln['severity'],
                    description=vuln['description'],
                    tool='wpscan',
                    refs=', '.join(vuln.get('references', []))
                )
                findings_added += 1

        return {
            'tool': 'wpscan',
            'status': STATUS_DONE if findings_added > 0 else STATUS_NO_RESULTS,
            'target': target,
            'wp_version': parsed.get('version'),
            'plugins_found': len(parsed.get('plugins', {})),
            'themes_found': len(parsed.get('themes', {})),
            'users_found': len(parsed.get('users', [])),
            'findings_added': findings_added,
            'users': parsed.get('users', [])  # For hydra chaining
        }
    except Exception as e:
        return {'error': str(e)}


def parse_hydra_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse Hydra job results."""
    try:
        from souleyez.parsers.hydra_parser import parse_hydra_output
        from souleyez.storage.credentials import CredentialsManager
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.findings import FindingsManager

        # Read the log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        # Parse Hydra output
        target = job.get('target', '')
        parsed = parse_hydra_output(log_content, target)

        # Add credentials for each host found in results
        cm = CredentialsManager()
        hm = HostManager()
        creds_added = 0
        usernames_added = 0
        hosts_affected = set()

        # Get target host for username-only entries
        target_host = parsed.get('target_host', target)
        # Extract IP from URL if needed
        if '://' in str(target_host):
            from urllib.parse import urlparse
            parsed_url = urlparse(target_host)
            target_host = parsed_url.hostname or target_host

        for cred in parsed.get('credentials', []):
            # Get actual host from credential (Hydra reports it in each success line)
            actual_host = cred.get('host', target_host)

            # Skip if still contains multi-target string
            if not actual_host or ' ' in str(actual_host):
                continue

            hosts_affected.add(actual_host)

            # Get or create host for this specific IP
            host = hm.get_host_by_ip(engagement_id, actual_host)
            if not host:
                host = hm.add_or_update_host(engagement_id, {'ip': actual_host, 'status': 'up'})
            host_id = host['id']

            # Add credential (CredentialsManager should handle duplicate detection)
            cm.add_credential(
                engagement_id=engagement_id,
                host_id=host_id,
                username=cred['username'],
                password=cred['password'],
                service=cred.get('service', parsed.get('service', 'unknown')),
                port=cred.get('port', parsed.get('port')),
                credential_type='password',
                tool='hydra',
                status='valid'
            )
            creds_added += 1

        # Handle username-only enumeration results (valid username, password unknown)
        for username in parsed.get('usernames', []):
            # Get target host
            actual_host = target_host
            if not actual_host or ' ' in str(actual_host):
                continue

            hosts_affected.add(actual_host)

            # Get or create host
            host = hm.get_host_by_ip(engagement_id, actual_host)
            if not host:
                host = hm.add_or_update_host(engagement_id, {'ip': actual_host, 'status': 'up'})
            host_id = host['id']

            # Add username-only credential (password unknown)
            cm.add_credential(
                engagement_id=engagement_id,
                host_id=host_id,
                username=username,
                password='',  # Unknown password
                service=parsed.get('service', 'http-post-form'),
                port=parsed.get('port'),
                credential_type='username',  # Mark as username-only
                tool='hydra',
                status='username_valid'  # Username validated, password unknown
            )
            usernames_added += 1

        # Create findings for discovered credentials/usernames
        fm = FindingsManager()
        findings_added = 0

        # Finding for valid credentials (high severity)
        if parsed.get('credentials'):
            cred_list = parsed['credentials']
            usernames_str = ', '.join([c['username'] for c in cred_list])
            service = parsed.get('service', 'unknown')
            port = parsed.get('port', '')

            # Get host_id for first affected host
            first_host = list(hosts_affected)[0] if hosts_affected else target_host
            host = hm.get_host_by_ip(engagement_id, first_host)
            finding_host_id = host['id'] if host else None

            fm.add_finding(
                engagement_id=engagement_id,
                title=f"Valid Credentials Found - {service.upper()}",
                finding_type='credential',
                severity='high',
                description=f"Hydra brute-force attack discovered {len(cred_list)} valid credential(s) on {service}:{port}.\n\n"
                           f"Affected usernames: {usernames_str}\n\n"
                           f"These credentials allow direct access to the service.",
                host_id=finding_host_id,
                tool='hydra'
            )
            findings_added += 1

        # Finding for username enumeration (medium severity)
        if parsed.get('usernames'):
            username_list = parsed['usernames']
            usernames_str = ', '.join(username_list)
            service = parsed.get('service', 'http-post-form')
            port = parsed.get('port', 80)

            # Get host_id for finding
            first_host = list(hosts_affected)[0] if hosts_affected else target_host
            host = hm.get_host_by_ip(engagement_id, first_host)
            finding_host_id = host['id'] if host else None

            fm.add_finding(
                engagement_id=engagement_id,
                title="Username Enumeration - Valid Usernames Discovered",
                finding_type='enumeration',
                severity='medium',
                description=f"Username enumeration via {service}:{port} revealed {len(username_list)} valid username(s).\n\n"
                           f"Valid usernames: {usernames_str}\n\n"
                           f"The application differentiates between valid and invalid usernames in error messages, "
                           f"allowing attackers to enumerate valid accounts. These usernames can be targeted for "
                           f"password attacks.",
                host_id=finding_host_id,
                tool='hydra'
            )
            findings_added += 1

        # Determine status based on results
        if len(parsed.get('credentials', [])) > 0:
            status = STATUS_DONE  # Found valid credentials
        elif len(parsed.get('usernames', [])) > 0:
            status = STATUS_DONE  # Found valid usernames (partial success is still a result)
        else:
            status = STATUS_NO_RESULTS  # No valid credentials or usernames found

        return {
            'tool': 'hydra',
            'status': status,
            'target': target,
            'hosts_affected': list(hosts_affected),
            'service': parsed.get('service'),
            'port': parsed.get('port'),
            'credentials_found': len(parsed.get('credentials', [])),
            'credentials_added': creds_added,
            'usernames_found': len(parsed.get('usernames', [])),
            'usernames': parsed.get('usernames', []),  # Include actual list for auto-chaining
            'usernames_added': usernames_added,
            'findings_added': findings_added,
            'attempts': parsed.get('attempts', 0)
        }
    except Exception as e:
        return {'error': str(e)}


def parse_nuclei_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse nuclei job results."""
    try:
        from souleyez.parsers.nuclei_parser import parse_nuclei
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager

        target = job.get('target', '')
        parsed = parse_nuclei(log_path, target)

        if 'error' in parsed:
            return parsed

        # Get or create host
        hm = HostManager()
        host_id = None

        # Extract host from target URL
        from urllib.parse import urlparse
        parsed_url = urlparse(target)
        target_host = parsed_url.hostname or target

        if target_host:
            host_id = hm.add_or_update_host(engagement_id, {
                'ip': target_host,
                'status': 'up'
            })

        # Store findings
        fm = FindingsManager()
        findings_added = 0

        for finding in parsed.get('findings', []):
            severity = finding.get('severity', 'info')
            
            # Map nuclei severity to souleyez severity (1-5)
            severity_map = {
                'critical': 5,
                'high': 4,
                'medium': 3,
                'low': 2,
                'info': 1
            }
            
            title = finding.get('name', 'Unknown Vulnerability')
            description = finding.get('description', '')
            
            # Add CVE and reference info to description
            if finding.get('cve_id'):
                description += f"\n\nCVE: {finding['cve_id']}"
            if finding.get('cvss_score'):
                description += f"\nCVSS Score: {finding['cvss_score']}"
            if finding.get('references'):
                description += f"\n\nReferences:\n" + "\n".join(finding['references'])

            # Add deduplication attribution if this scan covered multiple IPs
            metadata = job.get('metadata', {})
            associated_ips = metadata.get('associated_ips', [])
            if associated_ips and len(associated_ips) > 1:
                representative_ip = metadata.get('representative_ip', target_host)
                domain = metadata.get('domain_context', '')
                description += f"\n\n[Web Target Deduplication] This finding was discovered on {representative_ip}"
                description += f" (representative IP for {len(associated_ips)} IPs serving"
                if domain:
                    description += f" {domain}"
                description += f"). All affected IPs: {', '.join(associated_ips)}"

            # Build evidence with matched_at, cve_id, and template_id
            evidence = f"Template: {finding.get('template_id', 'unknown')}"
            if finding.get('matched_at'):
                evidence += f"\nMatched at: {finding.get('matched_at')}"
            if finding.get('cve_id'):
                evidence += f"\nCVE: {finding.get('cve_id')}"
            
            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=title,
                description=description,
                severity=severity,  # Use string severity from nuclei
                finding_type='vulnerability',
                tool='nuclei',
                evidence=evidence
            )
            findings_added += 1

        # Determine status based on results
        if parsed.get('findings_count', 0) > 0:
            status = STATUS_DONE  # Found vulnerabilities
        else:
            status = STATUS_NO_RESULTS  # No vulnerabilities found

        return {
            'tool': 'nuclei',
            'status': status,
            'target': target,
            'findings_count': parsed.get('findings_count', 0),
            'findings_added': findings_added,
            'critical': parsed.get('critical', 0),
            'high': parsed.get('high', 0),
            'medium': parsed.get('medium', 0),
            'low': parsed.get('low', 0),
            'info': parsed.get('info', 0)
        }
    except Exception as e:
        return {'error': str(e)}




def parse_enum4linux_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse enum4linux job results."""
    try:
        from souleyez.parsers.enum4linux_parser import parse_enum4linux_output, get_smb_stats, categorize_share
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager

        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            log_content = f.read()

        target = job.get('target', '')
        parsed = parse_enum4linux_output(log_content, target)

        hm = HostManager()
        host_id = None

        if parsed['target']:
            import re
            is_ip = re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', parsed['target'])
            if is_ip:
                host = hm.get_host_by_ip(engagement_id, parsed['target'])
                if host:
                    host_id = host['id']
                else:
                    host_id = hm.add_or_update_host(engagement_id, {
                        'ip': parsed['target'],
                        'status': 'up'
                    })

        from souleyez.storage.credentials import CredentialsManager
        cm = CredentialsManager()
        credentials_added = 0

        for username in parsed['users']:
            cm.add_credential(
                engagement_id=engagement_id,
                host_id=host_id,
                username=username,
                password='',
                credential_type='smb',
                service='smb',
                port=445,
                tool='enum4linux'
            )
            credentials_added += 1

        fm = FindingsManager()
        findings_added = 0

        for share in parsed['shares']:
            category = categorize_share(share)
            if category == 'open':
                severity = 'high'
            elif category == 'readable':
                severity = 'medium'
            elif category == 'restricted':
                severity = 'low'
            else:
                severity = 'info'

            share_name = share['name']
            share_type = share.get('type', 'Unknown')
            mapping = share.get('mapping', 'N/A')

            if mapping == 'OK':
                listing = share.get('listing', 'N/A')
                writing = share.get('writing', 'N/A')
                access_desc = f"Mapping={mapping}, Listing={listing}, Writing={writing}"
            else:
                access_desc = f"Access denied (Mapping={mapping})"

            title = f"SMB Share: {share_name} ({share_type})"
            description = f"Share: {share_name}\nType: {share_type}\nComment: {share.get('comment', 'N/A')}\nAccess: {access_desc}"

            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=title,
                finding_type='smb_share',
                severity=severity,
                description=description,
                tool='enum4linux',
                port=445
            )
            findings_added += 1

        stats = get_smb_stats(parsed)

        # Extract domains for auto-chaining
        # Filter out common workgroup names - these aren't AD domains
        domains = []
        workgroup = stats.get('workgroup')
        common_workgroups = {'WORKGROUP', 'MYGROUP', 'MSHOME', 'HOME'}
        if workgroup and workgroup.upper() not in common_workgroups:
            domains.append({
                'domain': workgroup,
                'ip': parsed['target']
            })

        # Determine status: done if we found any results (shares, users, or findings)
        has_results = (
            findings_added > 0 or
            credentials_added > 0 or
            len(parsed['users']) > 0 or
            stats['total_shares'] > 0
        )

        return {
            'tool': 'enum4linux',
            'status': STATUS_DONE if has_results else STATUS_NO_RESULTS,
            'findings_added': findings_added,
            'credentials_added': credentials_added,
            'users_found': len(parsed['users']),
            'shares_found': stats['total_shares'],
            'accessible_shares': stats['accessible_shares'],
            'writable_shares': stats['writable_shares'],
            'workgroup': stats.get('workgroup'),
            'domains': domains  # For auto-chaining to GetNPUsers and other AD tools
        }
    except Exception as e:
        return {'error': str(e)}


def parse_crackmapexec_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse CrackMapExec job results."""
    try:
        from souleyez.parsers.crackmapexec_parser import parse_crackmapexec
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.credentials import CredentialsManager
        
        target = job.get('target', '')
        parsed = parse_crackmapexec(log_path, target)

        if 'error' in parsed:
            return parsed

        hm = HostManager()
        cm = CredentialsManager()
        
        # Store hosts
        for host in parsed['findings'].get('hosts', []):
            hm.add_or_update_host(engagement_id, {
                'ip': host['ip'],
                'hostname': host.get('hostname'),
                'domain': host.get('domain'),
                'os': host.get('os'),
                'status': 'up'
            })

        # Store credentials
        creds_added = 0
        for cred in parsed['findings'].get('credentials', []):
            host = hm.get_host_by_ip(engagement_id, target)
            if host:
                cm.add_credential(
                    engagement_id=engagement_id,
                    host_id=host['id'],
                    username=cred['username'],
                    password=cred['password'],
                    service='smb',
                    credential_type='password',
                    tool='crackmapexec',
                    status='valid'
                )
                creds_added += 1

        return {
            'tool': 'crackmapexec',
            'status': STATUS_DONE if (len(parsed['findings'].get('hosts', [])) > 0 or creds_added > 0) else STATUS_NO_RESULTS,
            'target': target,
            'hosts_found': parsed.get('hosts_found', 0),
            'shares_found': parsed.get('shares_found', 0),
            'users_found': parsed.get('users_found', 0),
            'credentials_added': creds_added,
            'vulnerabilities_found': parsed.get('vulnerabilities_found', 0),
            'domains': parsed.get('domains', [])  # For auto-chaining to GetNPUsers and other AD tools
        }
    except Exception as e:
        return {'error': str(e)}


def parse_ffuf_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse ffuf job results."""
    try:
        from souleyez.parsers.ffuf_parser import parse_ffuf
        from souleyez.storage.hosts import HostManager
        from souleyez.storage.findings import FindingsManager
        
        target = job.get('target', '')
        parsed = parse_ffuf(log_path, target)

        if 'error' in parsed:
            return parsed

        # Extract base target for host tracking
        from urllib.parse import urlparse
        parsed_url = urlparse(target.replace('FUZZ', ''))
        target_host = parsed_url.hostname or target

        hm = HostManager()
        host_id = None
        
        if target_host:
            host_id = hm.add_or_update_host(engagement_id, {
                'ip': target_host,
                'status': 'up'
            })

        # Store web paths
        from souleyez.storage.web_paths import WebPathsManager
        wpm = WebPathsManager()
        paths_added = 0
        created_findings = []
        
        if host_id and parsed.get('paths'):
            paths_added = wpm.bulk_add_web_paths(host_id, parsed['paths'])
            
            # Check for sensitive paths and create findings (same as gobuster)
            created_findings = _create_findings_for_sensitive_paths(engagement_id, host_id, parsed['paths'], job)

        return {
            'tool': 'ffuf',
            'status': STATUS_DONE if parsed.get('results_found', 0) > 0 else STATUS_NO_RESULTS,
            'target': target,
            'results_found': parsed.get('results_found', 0),
            'paths_added': paths_added,
            'findings_added': len(created_findings),
            'method': parsed.get('method'),
            'parameters_found': parsed.get('paths', [])  # Enable ffuf → sqlmap chain
        }
    except Exception as e:
        return {'error': str(e)}


def parse_searchsploit_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse searchsploit job results."""
    try:
        from souleyez.parsers.searchsploit_parser import parse_searchsploit
        from souleyez.storage.exploits import add_exploit

        target = job.get('target', '')

        # Get service context if search was triggered from discovered services
        service_context = job.get('metadata', {}).get('service_context', {})
        service_id = service_context.get('service_id') if service_context else None

        # Parse the log file
        parsed = parse_searchsploit(log_path, target)

        if 'error' in parsed:
            return {'error': parsed['error']}

        # Store exploits in database
        exploits_added = 0

        for exploit in parsed.get('exploits', []):
            add_exploit(
                engagement_id=engagement_id,
                edb_id=exploit.get('edb_id', ''),
                title=exploit.get('title', ''),
                platform=exploit.get('platform', ''),
                exploit_type=exploit.get('type', ''),
                url=exploit.get('url', ''),
                date_published=exploit.get('date', ''),
                search_term=target,
                service_id=service_id  # Link to service if available
            )
            exploits_added += 1

        return {
            'tool': 'searchsploit',
            'status': STATUS_DONE if exploits_added > 0 else STATUS_NO_RESULTS,
            'target': target,
            'exploit_count': parsed.get('exploit_count', 0),
            'exploits_added': exploits_added
        }
    except Exception as e:
        return {'error': str(e)}


def parse_impacket_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse Impacket tool job results."""
    try:
        from souleyez.parsers.impacket_parser import parse_impacket
        from souleyez.storage.credentials import CredentialsManager
        from souleyez.storage.hosts import HostManager
        
        target = job.get('target', '')
        tool = job.get('tool', '')
        
        # Parse the log file
        parsed = parse_impacket(log_path, target, tool)
        
        if 'error' in parsed:
            return parsed
        
        # Get or create host
        hm = HostManager()
        host_id = None
        
        # Extract IP from target (may be in format DOMAIN/user:pass@IP)
        import re
        ip_match = re.search(r'@?(\d+\.\d+\.\d+\.\d+)', target)
        if ip_match:
            host_ip = ip_match.group(1)
            host_id = hm.add_or_update_host(engagement_id, {
                'ip': host_ip,
                'status': 'up'
            })
        
        cm = CredentialsManager()
        creds_added = 0
        hashes_added = 0
        
        # Store credentials from secretsdump
        if 'secretsdump' in tool.lower():
            # Store plaintext credentials
            for cred in parsed.get('credentials', []):
                cm.add_credential(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    username=cred.get('username'),
                    password=cred.get('password'),
                    service='windows',
                    status='valid',
                    credential_type='password',
                    tool=tool,
                    domain=cred.get('domain')
                )
                creds_added += 1
            
            # Store NTLM hashes
            for hash_data in parsed.get('hashes', []):
                cm.add_credential(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    username=hash_data.get('username'),
                    password=hash_data.get('nt_hash'),
                    service='windows',
                    status='valid',
                    credential_type='hash',
                    tool=tool,
                    notes=f"RID: {hash_data.get('rid')}, LM: {hash_data.get('lm_hash')}"
                )
                hashes_added += 1
        
        # Store AS-REP hashes from GetNPUsers
        elif 'getnpusers' in tool.lower():
            for hash_data in parsed.get('hashes', []):
                cm.add_credential(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    username=hash_data.get('username'),
                    password=hash_data.get('hash'),
                    service='kerberos',
                    status='untested',
                    credential_type='asrep_hash',
                    tool=tool,
                    domain=hash_data.get('domain'),
                    notes='Crackable with hashcat mode 18200'
                )
                hashes_added += 1
        
        # For psexec and smbclient, just record success
        success = parsed.get('success', False)

        return {
            'tool': tool,
            'target': target,
            'credentials_added': creds_added,
            'hashes_added': hashes_added,
            'success': success,
            'status': STATUS_DONE if (creds_added > 0 or hashes_added > 0 or success) else STATUS_NO_RESULTS,
            **{k: v for k, v in parsed.items() if k not in ['credentials', 'hashes', 'tickets']}
        }
    except Exception as e:
        return {'error': str(e)}


def parse_responder_job(engagement_id: int, log_path: str, job: Dict) -> Dict:
    """Parse and store Responder results."""
    try:
        from souleyez.parsers.responder_parser import parse_responder, store_responder_results
        
        target = job.get('target', '')
        parsed = parse_responder(log_path, target)
        
        job_id = job.get('id')
        store_responder_results(parsed, engagement_id, job_id)

        return {
            'tool': 'responder',
            'status': STATUS_DONE if parsed.get('credentials_captured', 0) > 0 else STATUS_NO_RESULTS,
            'interface': target,
            'credentials_captured': parsed.get('credentials_captured', 0),
            'summary': parsed.get('summary', 'No results')
        }
    except Exception as e:
        return {'error': str(e)}


def parse_bloodhound_job(engagement_id: int, log_path: str, job: Dict) -> Dict:
    """Parse and store Bloodhound results."""
    try:
        from souleyez.parsers.bloodhound_parser import parse_bloodhound, store_bloodhound_results
        
        target = job.get('target', '')
        parsed = parse_bloodhound(log_path, target)
        
        job_id = job.get('id')
        store_bloodhound_results(parsed, engagement_id, job_id)

        stats = parsed.get('statistics', {})
        total_objects = stats.get('users', 0) + stats.get('computers', 0) + stats.get('groups', 0)
        return {
            'tool': 'bloodhound',
            'status': STATUS_DONE if total_objects > 0 else STATUS_NO_RESULTS,
            'target': target,
            'users': stats.get('users', 0),
            'computers': stats.get('computers', 0),
            'groups': stats.get('groups', 0),
            'collection_path': parsed.get('collection_path', ''),
            'summary': parsed.get('summary', 'No results')
        }
    except Exception as e:
        return {'error': str(e)}


def reparse_all_sqlmap_jobs() -> Dict[str, Any]:
    """
    Re-parse all completed sqlmap jobs to refresh their parse_results.
    
    Returns:
        Dict with counts: {'reparsed': N, 'failed': N, 'skipped': N}
    """
    from souleyez.engine.background import _read_jobs, _update_job
    
    jobs = _read_jobs()
    stats = {'reparsed': 0, 'failed': 0, 'skipped': 0}
    
    for job in jobs:
        if job.get('tool', '').lower() != 'sqlmap':
            continue
        if job.get('status') not in ['done', 'no_results']:
            stats['skipped'] += 1
            continue

        try:
            parse_result = handle_job_result(job)
            if parse_result:
                _update_job(job['id'], parse_result=parse_result)
                stats['reparsed'] += 1
            else:
                stats['skipped'] += 1
        except Exception as e:
            logger.warning(f"Failed to reparse job {job['id']}: {e}")
            stats['failed'] += 1

    return stats


def parse_nikto_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse nikto web server scanner results."""
    try:
        from souleyez.parsers.nikto_parser import parse_nikto_output
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager

        target = job.get('target', '')

        # Read log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            output = f.read()

        parsed = parse_nikto_output(output, target)

        # Get or create host
        hm = HostManager()
        host_id = None

        # Use target IP from parsed output, or extract from target
        target_ip = parsed.get('target_ip') or parsed.get('target_hostname')
        if not target_ip:
            from urllib.parse import urlparse
            parsed_url = urlparse(target)
            target_ip = parsed_url.hostname or target

        if target_ip:
            host_id = hm.add_or_update_host(engagement_id, {
                'ip': target_ip,
                'hostname': parsed.get('target_hostname', ''),
                'status': 'up'
            })

        # Store findings
        fm = FindingsManager()
        findings_added = 0

        for finding in parsed.get('findings', []):
            severity = finding.get('severity', 'info')

            # Build title from OSVDB or path + description
            osvdb = finding.get('osvdb')
            path = finding.get('path', '/')
            desc = finding.get('description', '')

            if osvdb:
                title = f"Nikto: {osvdb} - {path}"
            elif path:
                title = f"Nikto: {path} - {desc[:50]}{'...' if len(desc) > 50 else ''}"
            else:
                title = f"Nikto Finding: {desc[:60]}{'...' if len(desc) > 60 else ''}"

            # Build description with server info
            description = desc
            if parsed.get('server'):
                description += f"\n\nServer: {parsed['server']}"
            if osvdb:
                description += f"\n\nReference: {osvdb}"
            if path:
                description += f"\nPath: {path}"

            # Build evidence
            evidence = f"Detected by Nikto web scanner"
            if path:
                evidence += f"\nPath: {path}"
            if osvdb:
                evidence += f"\nOSVDB: {osvdb}"

            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=title,
                description=description,
                severity=severity,
                finding_type='misconfiguration',
                tool='nikto',
                evidence=evidence
            )
            findings_added += 1

        # Determine status based on results
        if findings_added > 0:
            status = STATUS_DONE
        else:
            status = STATUS_NO_RESULTS

        return {
            'tool': 'nikto',
            'status': status,
            'target': target,
            'target_ip': parsed.get('target_ip', ''),
            'server': parsed.get('server', ''),
            'findings_count': parsed['stats']['total'],
            'findings_added': findings_added,
            'by_severity': parsed['stats']['by_severity']
        }

    except Exception as e:
        logger.error(f"Error parsing nikto job: {e}")
        return {'error': str(e)}


def parse_dalfox_job(engagement_id: int, log_path: str, job: Dict[str, Any]) -> Dict[str, Any]:
    """Parse Dalfox XSS scanner results."""
    try:
        from souleyez.parsers.dalfox_parser import parse_dalfox_output
        from souleyez.storage.findings import FindingsManager
        from souleyez.storage.hosts import HostManager

        target = job.get('target', '')

        # Read log file
        with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
            output = f.read()

        parsed = parse_dalfox_output(output, target)

        # Get or create host
        hm = HostManager()
        host_id = None

        # Extract host from target URL
        from urllib.parse import urlparse
        parsed_url = urlparse(target)
        target_host = parsed_url.hostname or target

        if target_host:
            host_id = hm.add_or_update_host(engagement_id, {
                'ip': target_host,
                'status': 'up'
            })

        # Store findings
        fm = FindingsManager()
        findings_added = 0

        for vuln in parsed.get('vulnerabilities', []):
            xss_type = vuln.get('type', 'XSS')
            url = vuln.get('url', target)
            param = vuln.get('parameter', '')
            payload = vuln.get('payload', '')

            title = f"XSS Vulnerability ({xss_type})"
            if param:
                title += f" in parameter '{param}'"

            description = f"Cross-Site Scripting ({xss_type}) vulnerability detected."
            description += f"\n\nVulnerable URL: {url}"
            if param:
                description += f"\nVulnerable Parameter: {param}"
            if payload:
                description += f"\n\nPayload:\n```\n{payload}\n```"

            # XSS is typically high severity
            severity = 'high' if xss_type in ['Reflected', 'Stored', 'DOM'] else 'medium'

            evidence = f"Detected by Dalfox XSS scanner"
            evidence += f"\nType: {xss_type}"
            if payload:
                evidence += f"\nPayload: {payload[:200]}"

            fm.add_finding(
                engagement_id=engagement_id,
                host_id=host_id,
                title=title,
                description=description,
                severity=severity,
                finding_type='vulnerability',
                tool='dalfox',
                evidence=evidence
            )
            findings_added += 1

        # Determine status
        if findings_added > 0:
            status = STATUS_DONE
        else:
            status = STATUS_NO_RESULTS

        return {
            'tool': 'dalfox',
            'status': status,
            'target': target,
            'vulnerabilities_count': len(parsed.get('vulnerabilities', [])),
            'findings_added': findings_added
        }

    except Exception as e:
        logger.error(f"Error parsing dalfox job: {e}")
        return {'error': str(e)}
