
import subprocess

import conf
from const import logPath, sql_nmap_scan_all
from helper import json_struc, timeNowTZ, updateState
from logger import append_line_to_file, mylog
#-------------------------------------------------------------------------------



class nmap_entry:
    def __init__(self, mac, time, port, state, service, name = '', extra = '', index = 0):
        self.mac = mac
        self.time = time
        self.port = port
        self.state = state
        self.service = service
        self.name = name
        self.extra = extra
        self.index = index
        self.hash = str(mac) + str(port)+ str(state)+ str(service)


#-------------------------------------------------------------------------------
def performNmapScan(db, devicesToScan):
    """
    run nmap scan on a list of devices
    discovers open ports and keeps track existing and new open ports
    """
    if len(devicesToScan) > 0:

        timeoutSec = conf.NMAP_TIMEOUT

        devTotal = len(devicesToScan)

        updateState(db,"Scan: Nmap")

        mylog('verbose', ['[NMAP Scan] Scan: Nmap for max ', str(timeoutSec), 's ('+ str(round(int(timeoutSec) / 60, 1)) +'min) per device'])  
        mylog('verbose', ["[NMAP Scan] Estimated max delay: ", (devTotal * int(timeoutSec)), 's ', '(', round((devTotal * int(timeoutSec))/60,1) , 'min)' ])

        devIndex = 0
        for device in devicesToScan:
            # Execute command
            output = ""
            # prepare arguments from user supplied ones
            nmapArgs = ['nmap'] + conf.NMAP_ARGS.split() + [device["dev_LastIP"]]

            progress = ' (' + str(devIndex+1) + '/' + str(devTotal) + ')'

            try:
                # try runnning a subprocess with a forced (timeout + 30 seconds)  in case the subprocess hangs
                output = subprocess.check_output (nmapArgs, universal_newlines=True,  stderr=subprocess.STDOUT, timeout=(timeoutSec + 30))
            except subprocess.CalledProcessError as e:
                # An error occured, handle it
                mylog('none', ["[NMAP Scan] " ,e.output])
                mylog('none', ["[NMAP Scan] Error - Nmap Scan - check logs", progress])            
            except subprocess.TimeoutExpired as timeErr:
                mylog('verbose', ['[NMAP Scan] Nmap TIMEOUT - the process forcefully terminated as timeout reached for ', device["dev_LastIP"], progress]) 

            if output == "": # check if the subprocess failed                    
                mylog('minimal', ['[NMAP Scan] Nmap FAIL for ', device["dev_LastIP"], progress ,' check logs for details']) 
            else: 
                mylog('verbose', ['[NMAP Scan] Nmap SUCCESS for ', device["dev_LastIP"], progress])

            devIndex += 1
            
            #  check the last run output        
            newLines = output.split('\n')

            # regular logging
            for line in newLines:
                append_line_to_file (logPath + '/pialert_nmap.log', line +'\n')                
            
            # collect ports / new Nmap Entries
            newEntriesTmp = []

            index = 0
            startCollecting = False
            duration = "" 
            for line in newLines:            
                if 'Starting Nmap' in line:
                    if len(newLines) > index+1 and 'Note: Host seems down' in newLines[index+1]:
                        break # this entry is empty
                elif 'PORT' in line and 'STATE' in line and 'SERVICE' in line:
                    startCollecting = True
                elif 'PORT' in line and 'STATE' in line and 'SERVICE' in line:    
                    startCollecting = False # end reached
                elif startCollecting and len(line.split()) == 3:                                    
                    newEntriesTmp.append(nmap_entry(device["dev_MAC"], timeNowTZ(), line.split()[0], line.split()[1], line.split()[2], device["dev_Name"]))
                elif 'Nmap done' in line:
                    duration = line.split('scanned in ')[1]            
            index += 1
            mylog('verbose', ['[NMAP Scan] Ports found by NMAP: ', len(newEntriesTmp)])
            process_discovered_ports(db, device, newEntriesTmp)
        #end for loop



def process_discovered_ports(db, device, discoveredPorts):
    """
    process ports discovered by nmap 
    compare to previosu ports
    update DB
    raise notifications
    """
    sql = db.sql # TO-DO
    # previous Nmap Entries
    oldEntries = []
    changedPortsTmp = []

    mylog('verbose', ['[NMAP Scan] Process ports found by NMAP: ', len(discoveredPorts)])
    
    if len(discoveredPorts) > 0:   

        #  get all current NMAP ports from the DB
        rows = db.read(sql_nmap_scan_all)

        for row in rows: 
            # only collect entries matching the current MAC address
            if row["MAC"] == device["dev_MAC"]:
                oldEntries.append(nmap_entry(row["MAC"], row["Time"], row["Port"], row["State"], row["Service"], device["dev_Name"], row["Extra"], row["Index"]))
                
        newEntries = []

        # Collect all entries that don't match the ones in the DB               
        for discoveredPort in discoveredPorts:

            found = False
            
            #  Check the new entry is already available in oldEntries and remove from processing if yes
            for oldEntry in oldEntries: 
                if discoveredPort.hash == oldEntry.hash:
                    found = True

            if not found:
                newEntries.append(discoveredPort)


        mylog('verbose', ['[NMAP Scan] Nmap newly discovered or changed ports: ', len(newEntries)])                

        # collect new ports, find the corresponding old entry and return for notification purposes
        # also update the DB with the new values after deleting the old ones
        if len(newEntries) > 0:
            
            # params to build the SQL query
            params = []
            indexesToDelete = ""

            # Find old entry matching the new entry hash
            for newEntry in newEntries:                   

                foundEntry = None

                for oldEntry in oldEntries:
                    if oldEntry.hash == newEntry.hash:
                        indexesToDelete = indexesToDelete + str(oldEntry.index) + ','
                        foundEntry = oldEntry                        

                columnNames = ["Name", "MAC", "Port", "State", "Service", "Extra", "NewOrOld"  ]

                # Old entry found
                if foundEntry is not None:
                    # Build params for sql query
                    params.append((newEntry.mac, newEntry.time, newEntry.port, newEntry.state, newEntry.service, oldEntry.extra))
                    # Build JSON for API and notifications
                    changedPortsTmp.append({       
                                            "Name"      : foundEntry.name, 
                                            "MAC"       : newEntry.mac, 
                                            "Port"      : newEntry.port, 
                                            "State"     : newEntry.state, 
                                            "Service"   : newEntry.service, 
                                            "Extra"     : foundEntry.extra,
                                            "NewOrOld"  : "New values"
                                        })
                    changedPortsTmp.append({ 
                                            "Name"      : foundEntry.name, 
                                            "MAC"       : foundEntry.mac, 
                                            "Port"      : foundEntry.port, 
                                            "State"     : foundEntry.state, 
                                            "Service"   : foundEntry.service, 
                                            "Extra"     : foundEntry.extra,
                                            "NewOrOld"  : "Old values"
                                        })                            
                # New entry - no matching Old entry found
                else:
                    # Build params for sql query
                    params.append((newEntry.mac, newEntry.time, newEntry.port, newEntry.state, newEntry.service, ''))
                    # Build JSON for API and notifications
                    changedPortsTmp.append({
                                            "Name"      : "New device", 
                                            "MAC"       : newEntry.mac, 
                                            "Port"      : newEntry.port, 
                                            "State"     : newEntry.state, 
                                            "Service"   : newEntry.service, 
                                            "Extra"     : "",
                                            "NewOrOld"  : "New device"                                       
                                        })

            conf.changedPorts_json_struc = json_struc({ "data" : changedPortsTmp}, columnNames)

            #  Delete old entries if available
            if len(indexesToDelete) > 0:
                sql.execute ("DELETE FROM Nmap_Scan where \"Index\" in (" + indexesToDelete[:-1] +")")
                db.commitDB()

            # Insert new values into the DB 
            sql.executemany ("""INSERT INTO Nmap_Scan ("MAC", "Time", "Port", "State", "Service", "Extra") VALUES (?, ?, ?, ?, ?, ?)""", params) 
            db.commitDB()


