import os
import json
from datetime import datetime
import re
import xml.etree.ElementTree as ET # For Nmap XML parsing

REPORTS_DIR = "reports"

def sanitize_filename(name):
    """
    Sanitiza un string para que sea un nombre de archivo/directorio válido.
    """
    return re.sub(r'[^a-zA-Z0-9_.-]', '_', name)

def create_session(target_name):
    """
    Crea la estructura de directorios para un nuevo objetivo/sesión.
    El nombre de la sesión se sanitiza y se le añade un timestamp.

    Returns:
        str: El nombre único de la sesión creada.
    """
    if not os.path.exists(REPORTS_DIR):
        os.makedirs(REPORTS_DIR)

    sanitized_target = sanitize_filename(target_name)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    session_name = f"{sanitized_target}_{timestamp}"
    session_path = os.path.join(REPORTS_DIR, session_name)
    
    if not os.path.exists(session_path):
        os.makedirs(session_path)
        
        # Crear archivo de metadatos
        metadata = {
            "target": target_name,
            "session_name": session_name,
            "creation_date": datetime.now().isoformat(),
            "tools_used": []
        }
        with open(os.path.join(session_path, "session_info.json"), "w") as f:
            json.dump(metadata, f, indent=4)
            
    return session_name

def parse_nmap_xml(xml_content):
    """
    Parses Nmap XML output and extracts key information into a structured format.
    """
    summary = {
        "hosts": []
    }
    try:
        root = ET.fromstring(xml_content)
        for host in root.findall('host'):
            host_info = {
                "addresses": [],
                "hostname": "N/A",
                "status": "N/A",
                "ports": []
            }

            # Addresses
            for addr in host.findall('address'):
                host_info["addresses"].append(addr.get('addr'))
            
            # Hostname
            hostname_elem = host.find('hostnames/hostname')
            if hostname_elem is not None:
                host_info["hostname"] = hostname_elem.get('name')
            
            # Status
            status_elem = host.find('status')
            if status_elem is not None:
                host_info["status"] = status_elem.get('state')
            
            # Ports
            for port_elem in host.findall('ports/port'):
                port_info = {
                    "portid": port_elem.get('portid'),
                    "protocol": port_elem.get('protocol'),
                    "state": "N/A",
                    "service": "N/A",
                    "version": "N/A"
                }
                
                state_elem = port_elem.find('state')
                if state_elem is not None:
                    port_info["state"] = state_elem.get('state')
                
                service_elem = port_elem.find('service')
                if service_elem is not None:
                    port_info["service"] = service_elem.get('name')
                    port_info["version"] = service_elem.get('version', 'N/A')
                
                if port_info["state"] == "open": # Only report open ports
                    host_info["ports"].append(port_info)
            
            summary["hosts"].append(host_info)
    except ET.ParseError as e:
        print(f"Error parsing Nmap XML: {e}")
        return None
    except Exception as e:
        print(f"An unexpected error occurred during Nmap XML parsing: {e}")
        return None
        
    return summary


def log_tool_output(session_name, tool_name, content, output_format="txt"):
    """
    Guarda el output de una herramienta en su archivo de log correspondiente.
    Si el output es Nmap XML, también lo parsear y guarda un resumen JSON.
    """
    session_path = os.path.join(REPORTS_DIR, session_name)
    if not os.path.exists(session_path):
        print(f"Error: La sesión '{session_name}' no existe.")
        return

    # Sanitiza el nombre de la herramienta para usarlo como nombre de archivo
    log_filename = f"{sanitize_filename(tool_name)}.{output_format}"
    log_path = os.path.join(session_path, log_filename)

    with open(log_path, "w", encoding="utf-8") as f:
        f.write(content)
        
    # --- Nmap XML Parsing and Summary Generation ---
    if tool_name == "Nmap Scan (XML)" and output_format == "xml":
        nmap_summary = parse_nmap_xml(content)
        if nmap_summary:
            summary_filename = f"{sanitize_filename('Nmap_Scan_Summary')}.json"
            summary_path = os.path.join(session_path, summary_filename)
            with open(summary_path, "w", encoding="utf-8") as f:
                json.dump(nmap_summary, f, indent=4)
            
            # Also update metadata for the summary file
            metadata_path = os.path.join(session_path, "session_info.json")
            if os.path.exists(metadata_path):
                with open(metadata_path, "r+") as f:
                    metadata = json.load(f)
                    if "tools_output_formats" not in metadata:
                        metadata["tools_output_formats"] = {}
                    if "Nmap Scan Summary" not in metadata["tools_output_formats"]:
                        metadata["tools_output_formats"]["Nmap Scan Summary"] = []
                    if "json" not in metadata["tools_output_formats"]["Nmap Scan Summary"]:
                        metadata["tools_output_formats"]["Nmap Scan Summary"].append("json")
                    f.seek(0)
                    json.dump(metadata, f, indent=4)
                    f.truncate()
    # --- End Nmap XML Parsing ---

    # Actualizar metadatos
    metadata_path = os.path.join(session_path, "session_info.json")
    if os.path.exists(metadata_path):
        with open(metadata_path, "r+") as f:
            metadata = json.load(f)
            
            # Ensure 'tools_output_formats' exists
            if "tools_output_formats" not in metadata:
                metadata["tools_output_formats"] = {}

            # Add or update the format for the current tool
            if tool_name not in metadata["tools_output_formats"]:
                metadata["tools_output_formats"][tool_name] = []
            if output_format not in metadata["tools_output_formats"][tool_name]:
                metadata["tools_output_formats"][tool_name].append(output_format)

            f.seek(0)
            json.dump(metadata, f, indent=4)
            f.truncate()

def get_sessions():
    """
    Devuelve una lista de todos los nombres de sesiones existentes.
    """
    if not os.path.exists(REPORTS_DIR):
        return []
    return sorted([d for d in os.listdir(REPORTS_DIR) if os.path.isdir(os.path.join(REPORTS_DIR, d))], reverse=True)

def get_session_data(session_name):
    """
    Lee todos los archivos de log de una sesión y devuelve su contenido,
    organizado por herramienta y formato.
    """
    session_path = os.path.join(REPORTS_DIR, session_name)
    if not os.path.exists(session_path):
        return None, {}

    data = {} # Key: tool_name, Value: {format: content}
    metadata = {}
    
    for item in os.listdir(session_path):
        item_path = os.path.join(session_path, item)
        if os.path.isfile(item_path):
            name, ext = os.path.splitext(item)
            ext = ext[1:] # Remove the dot
            
            if item == "session_info.json":
                try:
                    with open(item_path, "r", encoding="utf-8") as f:
                        metadata = json.load(f)
                except json.JSONDecodeError:
                    print(f"Warning: session_info.json for session '{session_name}' is malformed. Initializing metadata as empty.")
                    metadata = {} # Provide empty metadata if parsing fails
                except Exception as e:
                    print(f"Error reading session_info.json for session '{session_name}': {e}. Initializing metadata as empty.")
                    metadata = {}
            elif ext in ["txt", "json", "xml", "csv"]: # Handle other relevant formats
                tool_name = sanitize_filename(name) # Ensure tool name is clean
                if tool_name not in data:
                    data[tool_name] = {}
                try:
                    with open(item_path, "r", encoding="utf-8") as f:
                        data[tool_name][ext] = f.read()
                except UnicodeDecodeError:
                    print(f"Warning: Could not decode {item} in session '{session_name}'. Attempting with latin-1.")
                    try:
                        with open(item_path, "r", encoding="latin-1") as f:
                            data[tool_name][ext] = f.read()
                    except Exception as e:
                        print(f"Error reading {item} with latin-1 in session '{session_name}': {e}. Skipping file.")
                        data[tool_name][ext] = f"[Error: No se pudo leer el archivo {item} ({e})]"
                except Exception as e:
                    print(f"Error reading {item} in session '{session_name}': {e}. Skipping file.")
                    data[tool_name][ext] = f"[Error: No se pudo leer el archivo {item} ({e})]"
                
    return metadata, data

def export_session_to_markdown(session_name, output_path):
    """
    Genera un único archivo Markdown resumiendo la sesión.
    """
    metadata, data = get_session_data(session_name)
    if not metadata:
        return False
        
    md_content = f"# Reporte de Pentesting para: {metadata.get('target', 'N/A')}\n\n"
    md_content += f"**Fecha de Creación:** {metadata.get('creation_date', 'N/A')}\n\n"
    md_content += "---\n\n"
    
    for tool, output in data.items():
        md_content += f"## Resultados de: {tool}\n\n"
        md_content += "```\n"
        md_content += output
        md_content += "\n```\n\n"
        md_content += "---\n\n"
        
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(md_content)
    
    return True

def export_session_to_html(session_name, output_path):
    """
    Genera un único archivo HTML resumiendo la sesión, incluyendo datos estructurados
    y un resumen parseado de Nmap si está disponible.
    """
    metadata, data = get_session_data(session_name)
    if not metadata:
        return False
    
    nmap_summary_data = None
    session_path = os.path.join(REPORTS_DIR, session_name)
    summary_filename = f"{sanitize_filename('Nmap_Scan_Summary')}.json"
    summary_path = os.path.join(session_path, summary_filename)
    if os.path.exists(summary_path):
        try:
            with open(summary_path, 'r', encoding='utf-8') as f_summary:
                nmap_summary_data = json.load(f_summary)
        except Exception as e:
            print(f"Error loading Nmap Scan Summary for HTML export: {e}")
            nmap_summary_data = None
            
    html_content = f"""
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reporte de Pentesting - {metadata.get('target', 'N/A')}</title>
    <style>
        body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }}
        .container {{ max-width: 1000px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        h1 {{ color: #0056b3; border-bottom: 2px solid #0056b3; padding-bottom: 10px; }}
        h2 {{ color: #0056b3; margin-top: 30px; }}
        h3 {{ color: #0056b3; border-bottom: 1px solid #eee; padding-bottom: 5px; margin-top: 20px; }}
        pre {{ background-color: #e9e9e9; padding: 15px; border-radius: 5px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; }}
        .json-output pre {{ background-color: #e0f7fa; border-left: 5px solid #00bcd4; }}
        .text-output pre {{ background-color: #f3e5f5; border-left: 5px solid #9c27b0; }}
        .metadata p {{ margin: 5px 0; }}
        table {{ width: 100%; border-collapse: collapse; margin-top: 10px; }}
        th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
        th {{ background-color: #f2f2f2; }}
        .nmap-summary table {{ width: 100%; border-collapse: collapse; margin-top: 15px; margin-bottom: 20px; }}
        .nmap-summary th, .nmap-summary td {{ border: 1px solid #ccc; padding: 8px; text-align: left; }}
        .nmap-summary th {{ background-color: #e0e0e0; font-weight: bold; }}
        .nmap-summary .open-port {{ color: #28a745; font-weight: bold; }}
        .nmap-summary .filtered-port {{ color: #ffc107; }}
        .nmap-summary .closed-port {{ color: #dc3545; }}
    </style>
</head>
<body>
    <div class="container">
        <h1>Reporte de Pentesting para: {metadata.get('target', 'N/A')}</h1>
        <div class="metadata">
            <p><strong>Fecha de Creación:</strong> {metadata.get('creation_date', 'N/A')}</p>
            <p><strong>Herramientas Utilizadas:</strong> {', '.join(metadata.get('tools_output_formats', {}).keys())}</p>
        </div>
        <hr>
    """
    
    if nmap_summary_data and nmap_summary_data.get("hosts"):
        html_content += "<h2>Resumen del Escaneo Nmap</h2>\n"
        for host in nmap_summary_data["hosts"]:
            html_content += f"<h3>Host: {', '.join(host.get('addresses', []))} ({host.get('hostname', 'N/A')}) - Estado: {host.get('status', 'N/A')}</h3>\n"
            if host.get("ports"):
                html_content += "<div class='nmap-summary'>\n"
                html_content += "<table>\n"
                html_content += "<thead><tr><th>Puerto</th><th>Protocolo</th><th>Estado</th><th>Servicio</th><th>Versión</th></tr></thead>\n"
                html_content += "<tbody>\n"
                for port in host["ports"]:
                    state_class = ""
                    if port.get('state') == 'open':
                        state_class = 'open-port'
                    elif port.get('state') == 'filtered':
                        state_class = 'filtered-port'
                    elif port.get('state') == 'closed':
                        state_class = 'closed-port'

                    html_content += f"""
                    <tr>
                        <td>{port.get('portid', 'N/A')}</td>
                        <td>{port.get('protocol', 'N/A')}</td>
                        <td class="{state_class}">{port.get('state', 'N/A')}</td>
                        <td>{port.get('service', 'N/A')}</td>
                        <td>{port.get('version', 'N/A')}</td>
                    </tr>
                    """
                html_content += "</tbody>\n"
                html_content += "</table>\n"
                html_content += "</div>\n"
            else:
                html_content += "<p>No se encontraron puertos abiertos para este host.</p>\n"
        html_content += "<hr>\n"

    for tool_name, outputs in data.items():
        # Skip the Nmap_Scan_Summary.json itself if it's found in data
        if tool_name == "Nmap_Scan_Summary" and "json" in outputs:
            continue
            
        html_content += f"<h2>Resultados de: {tool_name}</h2>\n"
        for output_format, content in outputs.items():
            html_content += f"<h3>Formato: {output_format.upper()}</h3>\n"
            if output_format == "json":
                try:
                    json_data = json.loads(content)
                    html_content += "<div class='json-output'>\n"
                    html_content += "<pre>" + json.dumps(json_data, indent=2) + "</pre>\n"
                    html_content += "</div>\n"
                except json.JSONDecodeError:
                    html_content += "<div class='text-output'>\n"
                    html_content += "<p>Error al parsear JSON. Mostrando como texto plano:</p>\n"
                    html_content += "<pre>" + content + "</pre>\n"
                    html_content += "</div>\n"
            else:
                html_content += "<div class='text-output'>\n"
                html_content += "<pre>" + content + "</pre>\n"
                html_content += "</div>\n"
        html_content += "<hr>\n"
        
    html_content += """
    </div>
</body>
</html>
    """
        
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(html_content)
    
    return True
# ---- Ejemplo de uso ----
if __name__ == '__main__':
    print("Creando una nueva sesión de ejemplo para 'example.com'...")
    session = create_session("example.com")
    print(f"Sesión creada: {session}")

    print("\nLogueando output de Nmap...")
    nmap_output = "Starting Nmap 7.80 ( https://nmap.org ) at 2023-10-27 10:00 EDT\n..."
    log_tool_output(session, "Nmap Scan", nmap_output)

    print("Logueando output de Gobuster...")
    gobuster_output = "/admin (Status: 301)\n/images (Status: 200)\n..."
    log_tool_output(session, "Gobuster Dir", gobuster_output)

    print("\nListando todas las sesiones:")
    sessions = get_sessions()
    print(sessions)

    print(f"\nObteniendo datos de la sesión '{session}':")
    meta, session_data = get_session_data(session)
    if meta:
        print("Metadatos:", meta)
        # print("Datos:", session_data) # Puede ser muy largo

    print(f"\nExportando la sesión '{session}' a Markdown...")
    export_path = f"./{session}.md"
    if export_session_to_markdown(session, export_path):
        print(f"Reporte exportado a: {export_path}")
