#!/usr/bin/env python

import os
import argparse
import sys
import subprocess

# Register NetAlertX directories
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])

from plugin_helper import Plugin_Objects  # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger, append_line_to_file  # noqa: E402 [flake8 lint suppression]
from utils.datetime_utils import timeNowDB  # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value   # noqa: E402 [flake8 lint suppression]
from const import logPath  # noqa: E402 [flake8 lint suppression]
import conf  # noqa: E402 [flake8 lint suppression]
from pytz import timezone  # noqa: E402 [flake8 lint suppression]

# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value('TIMEZONE'))

# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))

pluginName = 'NMAP'

LOG_PATH = logPath + '/plugins'
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')

# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)


# -------------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(
        description='Scan ports of devices specified by IP addresses'
    )

    # Accept ANY key=value pairs
    parser.add_argument('params', nargs='+', help="key=value style params")

    raw = parser.parse_args()

    try:
        args = parse_kv_args(raw.params)
    except ValueError as e:
        mylog('error', [f"[{pluginName}] Argument error: {e}"])
        sys.exit(1)

    # Required keys
    required = ['ips', 'macs']
    for key in required:
        if key not in args:
            mylog('error', [f"[{pluginName}] Missing required parameter: {key}"])
            sys.exit(1)

    # Parse lists
    ip_list = safe_split_list(args['ips'], "ips")
    mac_list = safe_split_list(args['macs'], "macs")

    if len(ip_list) != len(mac_list):
        mylog('error', [
            f"[{pluginName}] Mismatch: {len(ip_list)} IPs but {len(mac_list)} MACs"
        ])
        sys.exit(1)

    # Optional
    timeout = int(args.get("timeout", get_setting_value("NMAP_RUN_TIMEOUT")))

    NMAP_ARGS = get_setting_value("NMAP_ARGS")

    mylog('debug', [f'[{pluginName}] Parsed IPs: {ip_list}'])
    mylog('debug', [f'[{pluginName}] Parsed MACs: {mac_list}'])
    mylog('debug', [f'[{pluginName}] Timeout: {timeout}'])
    mylog('debug', [f'[{pluginName}] NMAP_ARGS: {NMAP_ARGS}'])

    entries = performNmapScan(
        ip_list,
        mac_list,
        timeout,
        NMAP_ARGS
    )

    mylog('verbose', [f'[{pluginName}] Total number of ports found by NMAP: ', len(entries)])

    for entry in entries:

        plugin_objects.add_object(
            primaryId   = entry.mac,    # MAC (Device Name)
            secondaryId = entry.port,   # IP Address (always 0.0.0.0)
            watched1    = entry.state,  # Device Name
            watched2    = entry.service,
            watched3    = entry.ip + ":" + entry.port,
            watched4    = "",
            extra       = entry.extra,
            foreignKey  = entry.mac
        )

    plugin_objects.write_result_file()


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


# -------------------------------------------------------------------------------
def parse_kv_args(raw_args):
    """
    Converts ['ips=a,b,c', 'macs=x,y,z', 'timeout=5'] to a dict.
    Ignores unknown keys.
    """
    parsed = {}

    for item in raw_args:
        if '=' not in item:
            mylog('none', [f"[{pluginName}] Scan: Invalid parameter (missing '='): {item}"])

        key, value = item.split('=', 1)

        if key in parsed:
            mylog('none', [f"[{pluginName}] Scan: Duplicate parameter supplied: {key}"])

        parsed[key] = value

    return parsed


# -------------------------------------------------------------------------------
def safe_split_list(value, keyname):
    """Split comma list safely and ensure no empty items."""
    items = [x.strip() for x in value.split(',') if x.strip()]
    if not items:
        mylog('none', [f"[{pluginName}] Scan: {keyname} list is empty or invalid"])
    return items


# -------------------------------------------------------------------------------
def performNmapScan(deviceIPs, deviceMACs, timeoutSec, args):
    """
    run nmap scan on a list of devices
    discovers open ports and keeps track existing and new open ports
    """

    # collect ports / new Nmap Entries
    newEntriesTmp = []

    if len(deviceIPs) > 0:

        devTotal = len(deviceIPs)

        mylog('verbose', [f'[{pluginName}] 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 ip in deviceIPs:
            # Execute command
            output = ""
            # prepare arguments from user supplied ones
            nmapArgs = ['nmap'] + args.split() + [ip]

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

            try:
                # try runnning a subprocess with a forced (timeout)  in case the subprocess hangs
                output = subprocess.check_output(
                    nmapArgs,
                    universal_newlines=True,
                    stderr=subprocess.STDOUT,
                    timeout=(float(timeoutSec))
                )
            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:
                mylog('verbose', [f'[{pluginName}] Nmap TIMEOUT - the process forcefully terminated as timeout reached for ', ip, progress])

            if output == "":  # check if the subprocess failed
                mylog('minimal', [f'[{pluginName}] Nmap FAIL for ', ip, progress, ' check logs for details'])
            else:
                mylog('verbose', [f'[{pluginName}] Nmap SUCCESS for ', ip, progress])

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

            # regular logging
            for line in newLines:
                append_line_to_file(logPath + '/app_nmap.log', line + '\n')

            index = 0
            startCollecting = False
            duration = ""
            newPortsPerDevice = 0
            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(ip, deviceMACs[devIndex], timeNowDB(), line.split()[0], line.split()[1], line.split()[2]))
                    newPortsPerDevice += 1
                elif 'Nmap done' in line:
                    duration = line.split('scanned in ')[1]

            mylog('verbose', [f'[{pluginName}] {newPortsPerDevice} ports found on {deviceMACs[devIndex]} after {duration}'])

            index += 1
            devIndex += 1

        return newEntriesTmp


# ===============================================================================
# BEGIN
# ===============================================================================
if __name__ == '__main__':
    main()
