@echo off
setlocal EnableExtensions DisableDelayedExpansion
cd /d "%~dp0"

:MENU
cls
echo ============================================================
echo  NETWORK SCANNER (PING + ARP + HOSTNAME LOOKUP)
echo ============================================================
echo.

set /p "CIDR=Enter CIDR (e.g. 192.168.1.0/24): "
if "%CIDR%"=="" goto MENU

set /p "TOUT=Timeout ms [100]: "
if "%TOUT%"=="" set "TOUT=100"

set /p "CNT=Pings [1]: "
if "%CNT%"=="" set "CNT=1"

REM ============================================================
REM PARSE AND VALIDATE CIDR
REM ============================================================
for /f "tokens=1,2 delims=/" %%A in ("%CIDR%") do (
    set "IP_ADDR=%%A"
    set "PREFIX=%%B"
)

if "%PREFIX%"=="" set "PREFIX=24"

REM Trim spaces from prefix
for /f "delims= " %%T in ("%PREFIX%") do set "PREFIX=%%T"

set /a PNUM=%PREFIX% 2>nul
if errorlevel 1 (
    echo [Error] Prefix must be numeric.
    pause
    goto MENU
)

if %PNUM% LSS 1 goto ERR_RANGE
if %PNUM% GTR 30 goto ERR_RANGE

for /f "tokens=1-4 delims=." %%A in ("%IP_ADDR%") do (
    set "O1=%%A"
    set "O2=%%B"
    set "O3=%%C"
    set "O4=%%D"
)

if "%O4%"=="" (
    echo [Error] Invalid IP format.
    pause
    goto MENU
)

for %%V in (%O1% %O2% %O3% %O4%) do (
    if %%V LSS 0 goto ERR_RANGE
    if %%V GTR 255 goto ERR_RANGE
)

REM ============================================================
REM CALCULATE SUBNET MASK
REM ============================================================
set "M1=0"
set "M2=0"
set "M3=0"
set "M4=0"

if %PNUM% LEQ 8 (
    set /a "M1=256-(1<<(8-%PNUM%))"
    goto MASK_DONE
)

set "M1=255"

if %PNUM% LEQ 16 (
    set /a "M2=256-(1<<(16-%PNUM%))"
    goto MASK_DONE
)

set "M2=255"

if %PNUM% LEQ 24 (
    set /a "M3=256-(1<<(24-%PNUM%))"
    goto MASK_DONE
)

set "M3=255"
set /a "M4=256-(1<<(32-%PNUM%))"

:MASK_DONE

REM ============================================================
REM CALCULATE HOST COUNT
REM ============================================================
set /a "HOSTS=(1<<(32-%PNUM%))-2"

if "%HOSTS%"=="0" (
    echo [Error] No usable hosts in this range.
    pause
    goto MENU
)

if %HOSTS% LSS 1 (
    echo [Error] Invalid host count calculated.
    pause
    goto MENU
)

REM ============================================================
REM CALCULATE NETWORK ADDRESS
REM ============================================================
set /a "N1=O1&M1"
set /a "N2=O2&M2"
set /a "N3=O3&M3"
set /a "N4=O4&M4"

set "NETWORK_ADDR=%N1%.%N2%.%N3%.%N4%/%PNUM%"

REM ============================================================
REM CALCULATE BROADCAST ADDRESS
REM Broadcast = Network + wildcard mask
REM Wildcard mask = 255 - subnet mask octet
REM ============================================================
set /a "B1=N1+(255-M1)"
set /a "B2=N2+(255-M2)"
set /a "B3=N3+(255-M3)"
set /a "B4=N4+(255-M4)"

set "BROADCAST_ADDR=%B1%.%B2%.%B3%.%B4%"

REM ============================================================
REM CALCULATE START IP
REM Start IP = Network + 1
REM ============================================================
set /a S1=N1
set /a S2=N2
set /a S3=N3
set /a S4=N4+1

if %S4% GTR 255 (
    set /a S4=0
    set /a S3+=1
)

if %S3% GTR 255 (
    set /a S3=0
    set /a S2+=1
)

if %S2% GTR 255 (
    set /a S2=0
    set /a S1+=1
)

set "START_IP=%S1%.%S2%.%S3%.%S4%"

REM ============================================================
REM CALCULATE END IP
REM End IP = Broadcast - 1
REM ============================================================
set /a E1=B1
set /a E2=B2
set /a E3=B3
set /a E4=B4-1

if %E4% LSS 0 (
    set /a E4=255
    set /a E3-=1
)

if %E3% LSS 0 (
    set /a E3=255
    set /a E2-=1
)

if %E2% LSS 0 (
    set /a E2=255
    set /a E1-=1
)

set "END_IP=%E1%.%E2%.%E3%.%E4%"

REM ============================================================
REM PREPARE OUTPUT FILE
REM ============================================================
set "SAFE_IP=%IP_ADDR:.=_%"
set "SAFE_IP=%SAFE_IP:/=_%"
set "OUT_FILE=ping_%SAFE_IP%_p%PNUM%.txt"

call :GET_TIME_CS START_CS
set "START_STAMP=%DATE% %TIME%"

echo.
echo Network:    %NETWORK_ADDR%
echo Start IP:   %START_IP%
echo End IP:     %END_IP%
echo Broadcast:  %BROADCAST_ADDR%
echo Hosts:      %HOSTS%
echo Timeout:    %TOUT% ms
echo Pings:      %CNT%
echo File:       %OUT_FILE%
echo.
echo Scanning... Please wait.
echo.

REM ============================================================
REM CREATE OUTPUT FILE ONLY IF IT DOES NOT EXIST
REM Existing file is used as an accumulated inventory.
REM ============================================================
if not exist "%OUT_FILE%" (
    > "%OUT_FILE%" (
        echo ============================================================
        echo NETWORK SCANNER ACCUMULATED REPORT
        echo ============================================================
        echo.
        echo File created;%START_STAMP%
        echo Initial CIDR;%CIDR%
        echo Initial network;%NETWORK_ADDR%
        echo Initial start IP;%START_IP%
        echo Initial end IP;%END_IP%
        echo Initial broadcast IP;%BROADCAST_ADDR%
        echo Broadcast excluded;Yes
        echo Hostname lookup;nslookup, nslookup PTR, nbtstat -A, ping -a
        echo.
        echo ------------------------------------------------------------
        echo UNIQUE HOST RECORDS
        echo ------------------------------------------------------------
        echo IP;MAC;HOSTNAME;HOSTNAME_SOURCE
    )
)

REM ============================================================
REM INIT SCAN COUNTERS
REM Start scan exactly from calculated Start IP.
REM Broadcast IP is not included.
REM ============================================================
set /a IP1=S1
set /a IP2=S2
set /a IP3=S3
set /a IP4=S4

set /a CUR=0
set /a ALIVE=0
set /a NEW_RECORDS=0
set /a SKIPPED_RECORDS=0
set /a UPDATED_RECORDS=0
set /a PE=HOSTS/50

if %PE% LSS 1 set PE=1

REM ============================================================
REM SCAN LOOP
REM ============================================================
setlocal EnableDelayedExpansion

for /L %%I in (1,1,%HOSTS%) do (
    set /a CUR+=1
    set "CIP=!IP1!.!IP2!.!IP3!.!IP4!"

    ping -n %CNT% -w %TOUT% !CIP! >nul 2>&1

    if !errorlevel! EQU 0 (
        set /a ALIVE+=1

        REM Clear values before every lookup
        set "MAC="
        set "HOSTNAME="
        set "HOSTNAME_SOURCE="

        REM ----------------------------------------------------
        REM Get MAC address from local ARP table
        REM ----------------------------------------------------
        for /f "tokens=1,2" %%A in ('arp -a') do (
            if "%%A"=="!CIP!" set "MAC=%%B"
        )

        if not defined MAC set "MAC=UNKNOWN"

        REM ----------------------------------------------------
        REM Try to get hostname by all available methods:
        REM 1. nslookup reverse DNS
        REM 2. nslookup PTR query
        REM 3. nbtstat NetBIOS lookup
        REM 4. ping -a reverse lookup
        REM ----------------------------------------------------
        call :GET_HOSTNAME "!CIP!" HOSTNAME HOSTNAME_SOURCE

        if not defined HOSTNAME set "HOSTNAME=UNKNOWN"
        if not defined HOSTNAME_SOURCE set "HOSTNAME_SOURCE=UNKNOWN"

        set "PAIR_PREFIX=!CIP!;!MAC!"
        set "FULL_RECORD=!CIP!;!MAC!;!HOSTNAME!;!HOSTNAME_SOURCE!"

        REM ----------------------------------------------------
        REM Check only IP;MAC pair.
        REM If the same IP;MAC already exists, do not duplicate it.
        REM If an existing record contains UNKNOWN and current lookup is better,
        REM update the existing line in the accumulated file.
        REM ----------------------------------------------------
        findstr /L /B /C:"!PAIR_PREFIX!" "%OUT_FILE%" >nul 2>&1

        if errorlevel 1 (
            echo(!FULL_RECORD!>> "%OUT_FILE%"
            set /a NEW_RECORDS+=1
            echo New:      !CIP! MAC: !MAC! HOSTNAME: !HOSTNAME! SOURCE: !HOSTNAME_SOURCE!
        ) else (
            set "EXISTING_HAS_UNKNOWN=0"

            findstr /L /B /C:"!PAIR_PREFIX!" "%OUT_FILE%" | findstr /I /L /C:";UNKNOWN" >nul 2>&1
            if !errorlevel! EQU 0 set "EXISTING_HAS_UNKNOWN=1"

            if /I not "!HOSTNAME_SOURCE!"=="UNKNOWN" if "!EXISTING_HAS_UNKNOWN!"=="1" (
                call :UPDATE_RECORD_BY_PREFIX "%OUT_FILE%" "!PAIR_PREFIX!" "!FULL_RECORD!" UPDATE_RESULT
                set /a UPDATED_RECORDS+=1
                echo Updated:  !CIP! MAC: !MAC! HOSTNAME: !HOSTNAME! SOURCE: !HOSTNAME_SOURCE!
            ) else (
                set /a SKIPPED_RECORDS+=1
                echo Existing: !CIP! MAC: !MAC! HOSTNAME: !HOSTNAME! SOURCE: !HOSTNAME_SOURCE!
            )
        )
    )

    set /a MOD=CUR%%PE

    if !MOD! EQU 0 (
        echo Progress: !CUR!/%HOSTS% Alive: !ALIVE! New: !NEW_RECORDS! Updated: !UPDATED_RECORDS! Existing: !SKIPPED_RECORDS!
    )

    REM --------------------------------------------------------
    REM Move to next IP address
    REM --------------------------------------------------------
    set /a IP4+=1

    if !IP4! GTR 255 (
        set /a IP4=0
        set /a IP3+=1
    )

    if !IP3! GTR 255 (
        set /a IP3=0
        set /a IP2+=1
    )

    if !IP2! GTR 255 (
        set /a IP2=0
        set /a IP1+=1
    )
)

REM Save counter values before leaving local delayed expansion scope
endlocal & (
    set "ALIVE=%ALIVE%"
    set "NEW_RECORDS=%NEW_RECORDS%"
    set "SKIPPED_RECORDS=%SKIPPED_RECORDS%"
    set "UPDATED_RECORDS=%UPDATED_RECORDS%"
)

REM ============================================================
REM CALCULATE SCAN TIME
REM ============================================================
call :GET_TIME_CS END_CS
set "END_STAMP=%DATE% %TIME%"

set /a ELAPSED_CS=END_CS-START_CS

REM Midnight rollover protection
if %ELAPSED_CS% LSS 0 set /a ELAPSED_CS+=24*60*60*100

call :FORMAT_ELAPSED %ELAPSED_CS% ELAPSED_TEXT

REM ============================================================
REM WRITE SCAN HISTORY ENTRY
REM ============================================================
>> "%OUT_FILE%" (
    echo.
    echo ------------------------------------------------------------
    echo SCAN HISTORY ENTRY
    echo ------------------------------------------------------------
    echo Started;%START_STAMP%
    echo Finished;%END_STAMP%
    echo Elapsed;%ELAPSED_TEXT%
    echo Input CIDR;%CIDR%
    echo Network;%NETWORK_ADDR%
    echo Start IP;%START_IP%
    echo End IP;%END_IP%
    echo Broadcast IP;%BROADCAST_ADDR%
    echo Broadcast excluded;Yes
    echo Timeout ms;%TOUT%
    echo Pings;%CNT%
    echo Hostname lookup;nslookup, nslookup PTR, nbtstat -A, ping -a
    echo Hosts scanned;%HOSTS%
    echo Alive hosts in this scan;%ALIVE%
    echo New records added;%NEW_RECORDS%
    echo Existing records updated;%UPDATED_RECORDS%
    echo Existing IP-MAC pairs skipped;%SKIPPED_RECORDS%
    echo ============================================================
)

echo.
echo ============================================================
echo Complete!
echo Network:                    %NETWORK_ADDR%
echo Start IP:                   %START_IP%
echo End IP:                     %END_IP%
echo Broadcast:                  %BROADCAST_ADDR%
echo Alive hosts this scan:      %ALIVE%
echo New records added:          %NEW_RECORDS%
echo Existing records updated:   %UPDATED_RECORDS%
echo Existing records skipped:   %SKIPPED_RECORDS%
echo Time:                       %ELAPSED_TEXT%
echo Results:                    %OUT_FILE%
echo ============================================================
pause
goto MENU

REM ============================================================
REM ERROR HANDLER
REM ============================================================
:ERR_RANGE
echo [Error] Value out of range. Prefix must be 1-30, octets must be 0-255.
pause
goto MENU

REM ============================================================
REM SUBROUTINE: GET HOSTNAME BY ALL AVAILABLE METHODS
REM Argument 1: IP address
REM Argument 2: output hostname variable name
REM Argument 3: output hostname source variable name
REM
REM Lookup order:
REM 1. nslookup reverse DNS
REM 2. nslookup PTR query
REM 3. nbtstat -A IP
REM 4. ping -a IP
REM ============================================================
:GET_HOSTNAME
set "%~2=UNKNOWN"
set "%~3=UNKNOWN"

set "LOOKUP_IP=%~1"
set "FOUND_NAME="

REM ------------------------------------------------------------
REM Method 1: nslookup direct reverse lookup.
REM Works if DNS server exists and has PTR/reverse records.
REM Timeout is limited to reduce scan delay.
REM ------------------------------------------------------------
for /f "tokens=1,* delims=:" %%A in ('nslookup -timeout^=1 %LOOKUP_IP% 2^>nul ^| findstr /I /B /C:"Name:"') do (
    if not defined FOUND_NAME (
        set "FOUND_NAME=%%B"
    )
)

if defined FOUND_NAME (
    call :TRIM_VALUE FOUND_NAME
    call :REMOVE_TRAILING_DOT FOUND_NAME

    if defined FOUND_NAME (
        set "%~2=%FOUND_NAME%"
        set "%~3=NSLOOKUP"
        exit /b
    )
)

REM ------------------------------------------------------------
REM Method 2: nslookup explicit PTR query.
REM This may return lines like:
REM 25.1.168.192.in-addr.arpa name = host.example.local
REM ------------------------------------------------------------
set "FOUND_NAME="

for /f "tokens=1-4 delims=." %%A in ("%LOOKUP_IP%") do (
    set "REV_PTR=%%D.%%C.%%B.%%A.in-addr.arpa"
)

for /f "tokens=1,* delims==" %%A in ('nslookup -timeout^=1 -type^=PTR %REV_PTR% 2^>nul ^| findstr /I /C:"name ="') do (
    if not defined FOUND_NAME (
        set "FOUND_NAME=%%B"
    )
)

if defined FOUND_NAME (
    call :TRIM_VALUE FOUND_NAME
    call :REMOVE_TRAILING_DOT FOUND_NAME

    if defined FOUND_NAME (
        set "%~2=%FOUND_NAME%"
        set "%~3=NSLOOKUP_PTR"
        exit /b
    )
)

REM ------------------------------------------------------------
REM Method 3: NetBIOS lookup.
REM Works if NetBIOS over TCP/IP is enabled on the remote host
REM and UDP/137 is not blocked by firewall.
REM ------------------------------------------------------------
set "FOUND_NAME="

for /f "tokens=1" %%N in ('nbtstat -A %LOOKUP_IP% 2^>nul ^| findstr /I /C:"<00>" ^| findstr /I /C:"UNIQUE"') do (
    if not defined FOUND_NAME (
        set "FOUND_NAME=%%N"
    )
)

if defined FOUND_NAME (
    call :TRIM_VALUE FOUND_NAME

    if defined FOUND_NAME (
        set "%~2=%FOUND_NAME%"
        set "%~3=NBTSTAT"
        exit /b
    )
)

REM ------------------------------------------------------------
REM Method 4: ping -a fallback.
REM This may use Windows local name resolution mechanisms:
REM DNS, NetBIOS, cache, LLMNR/mDNS depending on OS settings.
REM ------------------------------------------------------------
set "FOUND_NAME="
set "PING_PREFIX="

for /f "tokens=1 delims=[" %%L in ('ping -a -n 1 -w 300 %LOOKUP_IP% 2^>nul ^| find "["') do (
    if not defined PING_PREFIX set "PING_PREFIX=%%L"
)

if defined PING_PREFIX (
    for %%W in (%PING_PREFIX%) do set "FOUND_NAME=%%W"
)

if defined FOUND_NAME (
    call :TRIM_VALUE FOUND_NAME
    call :REMOVE_TRAILING_DOT FOUND_NAME

    if defined FOUND_NAME (
        if /I not "%FOUND_NAME%"=="Pinging" (
            set "%~2=%FOUND_NAME%"
            set "%~3=PING_A"
            exit /b
        )
    )
)

REM ------------------------------------------------------------
REM Nothing found.
REM ------------------------------------------------------------
set "%~2=UNKNOWN"
set "%~3=UNKNOWN"

exit /b

REM ============================================================
REM SUBROUTINE: UPDATE RECORD BY IP;MAC PREFIX
REM Argument 1: file path
REM Argument 2: IP;MAC prefix
REM Argument 3: new full record
REM Argument 4: output update result variable
REM ============================================================
:UPDATE_RECORD_BY_PREFIX
setlocal EnableDelayedExpansion
set "TARGET_FILE=%~1"
set "TARGET_PREFIX=%~2"
set "NEW_LINE=%~3"
set "TMP_FILE=%TARGET_FILE%.tmp"
set "UPDATED=0"

> "!TMP_FILE!" (
    for /f "usebackq delims=" %%L in ("!TARGET_FILE!") do (
        set "LINE=%%L"

        if "!LINE:%TARGET_PREFIX%=!" NEQ "!LINE!" (
            echo(!NEW_LINE!
            set "UPDATED=1"
        ) else (
            echo(!LINE!
        )
    )
)

move /Y "!TMP_FILE!" "!TARGET_FILE!" >nul 2>&1

endlocal & set "%~4=%UPDATED%"
exit /b

REM ============================================================
REM SUBROUTINE: TRIM_VALUE
REM Removes leading spaces from variable value.
REM Argument 1: variable name to trim.
REM ============================================================
:TRIM_VALUE
set "TRIM_VAR_NAME=%~1"
call set "TRIM_VALUE=%%%TRIM_VAR_NAME%%%"

for /f "tokens=* delims= " %%T in ("%TRIM_VALUE%") do set "TRIM_VALUE=%%T"

set "%TRIM_VAR_NAME%=%TRIM_VALUE%"
exit /b

REM ============================================================
REM SUBROUTINE: REMOVE_TRAILING_DOT
REM Removes trailing dot from DNS names.
REM Argument 1: variable name.
REM ============================================================
:REMOVE_TRAILING_DOT
set "DOT_VAR_NAME=%~1"
call set "DOT_VALUE=%%%DOT_VAR_NAME%%%"

if "%DOT_VALUE:~-1%"=="." set "DOT_VALUE=%DOT_VALUE:~0,-1%"

set "%DOT_VAR_NAME%=%DOT_VALUE%"
exit /b

REM ============================================================
REM SUBROUTINE: GET CURRENT TIME IN CENTISECONDS
REM Argument 1: output variable name.
REM ============================================================
:GET_TIME_CS
set "T=%TIME: =0%"

for /f "tokens=1-4 delims=:., " %%A in ("%T%") do (
    set /a "%~1=((1%%A-100)*360000)+((1%%B-100)*6000)+((1%%C-100)*100)+(1%%D-100)"
)

exit /b

REM ============================================================
REM SUBROUTINE: FORMAT CENTISECONDS AS HH:MM:SS.CC
REM Argument 1: elapsed centiseconds.
REM Argument 2: output variable name.
REM ============================================================
:FORMAT_ELAPSED
set /a _CS=%~1
set /a _H=_CS/360000
set /a _CS=_CS%%360000
set /a _M=_CS/6000
set /a _CS=_CS%%6000
set /a _S=_CS/100
set /a _C=_CS%%100

if %_H% LSS 10 set "_H=0%_H%"
if %_M% LSS 10 set "_M=0%_M%"
if %_S% LSS 10 set "_S=0%_S%"
if %_C% LSS 10 set "_C=0%_C%"

set "%~2=%_H%:%_M%:%_S%.%_C%"
exit /b
