"""
SentinelScan - Port Scanner Module
Provides functionality for scanning TCP and UDP ports on a target host.
"""

import socket
import threading
import queue
import time
from typing import List, Dict

# ============================================================
# Configuration
# ============================================================

DEFAULT_TIMEOUT = 0.5
MAX_THREADS = 100

# ============================================================
# Helper Functions
# ============================================================

def banner_grab(host: str, port: int) -> str:
    """
    Attempt to grab a service banner from an open port.
    """
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(DEFAULT_TIMEOUT)
        sock.connect((host, port))
        banner = sock.recv(1024).decode(errors="ignore").strip()
        sock.close()
        if banner:
            return banner
        return "No banner"
    except Exception:
        return "Banner grab failed"


def format_result(port: int, status: str, banner: str = "") -> str:
    """
    Format the result for printing or reporting.
    """
    if banner:
        return f"[+] Port {port} {status} | Banner: {banner}"
    return f"[+] Port {port} {status}"


# ============================================================
# Worker Thread
# ============================================================

def worker(host: str, q: queue.Queue, results: List[str]):
    """
    Worker thread that scans ports from the queue.
    """
    while not q.empty():
        port = q.get()
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(DEFAULT_TIMEOUT)
            result = sock.connect_ex((host, port))
            if result == 0:
                banner = banner_grab(host, port)
                output = format_result(port, "OPEN", banner)
                print(output)
                results.append(output)
            sock.close()
        except Exception as e:
            results.append(f"[!] Error scanning port {port}: {e}")
        finally:
            q.task_done()


# ============================================================
# Main Scan Function
# ============================================================

def scan_ports(host: str, start_port: int, end_port: int) -> List[str]:
    """
    Scan ports in the given range on the target host.
    """
    print(f"[*] Starting port scan on {host} ({start_port}-{end_port})...")
    q = queue.Queue()
    results = []

    # Fill queue
    for port in range(start_port, end_port + 1):
        q.put(port)

    # Start threads
    threads = []
    for _ in range(min(MAX_THREADS, q.qsize())):
        t = threading.Thread(target=worker, args=(host, q, results))
        t.daemon = True
        threads.append(t)
        t.start()

    # Wait for completion
    q.join()

    print(f"[*] Port scan complete on {host}.")
    return results


# ============================================================
# Extended Features
# ============================================================

def scan_common_ports(host: str) -> List[str]:
    """
    Scan a predefined list of common ports.
    """
    common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443, 3306, 3389]
    results = []
    print(f"[*] Scanning common ports on {host}...")
    for port in common_ports:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(DEFAULT_TIMEOUT)
            result = sock.connect_ex((host, port))
            if result == 0:
                banner = banner_grab(host, port)
                output = format_result(port, "OPEN", banner)
                print(output)
                results.append(output)
            sock.close()
        except Exception as e:
            results.append(f"[!] Error scanning port {port}: {e}")
    return results


def scan_with_delay(host: str, start_port: int, end_port: int, delay: float = 0.1) -> List[str]:
    """
    Scan ports with a delay between attempts (stealth mode).
    """
    results = []
    print(f"[*] Stealth scan on {host} ({start_port}-{end_port}) with {delay}s delay...")
    for port in range(start_port, end_port + 1):
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(DEFAULT_TIMEOUT)
            result = sock.connect_ex((host, port))
            if result == 0:
                banner = banner_grab(host, port)
                output = format_result(port, "OPEN", banner)
                print(output)
                results.append(output)
            sock.close()
        except Exception as e:
            results.append(f"[!] Error scanning port {port}: {e}")
        time.sleep(delay)
    return results


def udp_scan(host: str, ports: List[int]) -> List[str]:
    """
    Perform a basic UDP scan.
    """
    results = []
    print(f"[*] Starting UDP scan on {host}...")
    for port in ports:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            sock.settimeout(DEFAULT_TIMEOUT)
            sock.sendto(b"", (host, port))
            try:
                data, _ = sock.recvfrom(1024)
                results.append(f"[+] UDP Port {port} OPEN (response received)")
            except socket.timeout:
                results.append(f"[?] UDP Port {port} open|filtered (no response)")
            sock.close()
        except Exception as e:
            results.append(f"[!] Error scanning UDP port {port}: {e}")
    return results


# ============================================================
# Testing Stub
# ============================================================

if __name__ == "__main__":
    target_host = "127.0.0.1"

    # TCP range scan
    results = scan_ports(target_host, 1, 100)
    for r in results:
        print(r)

    # Common ports
    common_results = scan_common_ports(target_host)
    for r in common_results:
        print(r)

    # Stealth scan
    stealth_results = scan_with_delay(target_host, 1, 50, delay=0.2)
    for r in stealth_results:
        print(r)

    # UDP scan
    udp_results = udp_scan(target_host, [53, 67, 123])
    for r in udp_results:
        print(r)
