import tkinter as tk
from tkinter import ttk, messagebox
import socket
import subprocess
import threading
import ipaddress
import platform
import re
from datetime import datetime


class App13:
    """ネットワークスキャナー"""

    # MAC アドレス（aa-bb-cc-dd-ee-ff / aa:bb:cc:dd:ee:ff の両形式）
    MAC_RE = re.compile(r"([0-9A-Fa-f]{2}([-:])[0-9A-Fa-f]{2}(?:\2[0-9A-Fa-f]{2}){4})")

    def __init__(self, root):
        self.root = root
        self.root.title("ネットワークスキャナー")
        self.root.geometry("820x580")
        self.root.configure(bg="#f8f9fc")
        self.scanning = False
        self._build_ui()
        self._detect_local_ip()

    def _build_ui(self):
        title_frame = tk.Frame(self.root, bg="#e65100", pady=10)
        title_frame.pack(fill=tk.X)
        tk.Label(title_frame, text="🔍 ネットワークスキャナー",
                 font=("Noto Sans JP", 15, "bold"),
                 bg="#e65100", fg="white").pack(side=tk.LEFT, padx=12)

        # スキャン設定
        cfg_frame = ttk.LabelFrame(self.root, text="スキャン設定", padding=10)
        cfg_frame.pack(fill=tk.X, padx=8, pady=6)

        tk.Label(cfg_frame, text="IPレンジ:").grid(row=0, column=0, sticky="w")
        self.range_var = tk.StringVar(value="192.168.1.0/24")
        ttk.Entry(cfg_frame, textvariable=self.range_var,
                  width=22, font=("Courier New", 11)).grid(
            row=0, column=1, padx=8, sticky="w")

        tk.Label(cfg_frame, text="タイムアウト(ms):").grid(
            row=0, column=2, sticky="w", padx=(16, 4))
        self.timeout_var = tk.IntVar(value=300)
        ttk.Spinbox(cfg_frame, from_=50, to=2000,
                    textvariable=self.timeout_var, width=6).grid(row=0, column=3)

        tk.Label(cfg_frame, text="スレッド数:").grid(
            row=0, column=4, sticky="w", padx=(16, 4))
        self.threads_var = tk.IntVar(value=50)
        ttk.Spinbox(cfg_frame, from_=1, to=200,
                    textvariable=self.threads_var, width=6).grid(row=0, column=5)

        self.port_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(cfg_frame, text="ポートスキャン",
                        variable=self.port_var).grid(row=0, column=6, padx=16)

        self.scan_btn = ttk.Button(cfg_frame, text="▶ スキャン開始",
                                   command=self._toggle_scan)
        self.scan_btn.grid(row=0, column=7, padx=8)

        # プログレス
        self.progress_var = tk.IntVar(value=0)
        self.progress = ttk.Progressbar(self.root, variable=self.progress_var,
                                         maximum=100)
        self.progress.pack(fill=tk.X, padx=8)

        # 結果テーブル
        result_frame = ttk.LabelFrame(self.root, text="スキャン結果", padding=4)
        result_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=6)
        cols = ("ip", "mac", "hostname", "ping", "ports", "status")
        self.tree = ttk.Treeview(result_frame, columns=cols,
                                  show="headings", selectmode="browse")
        for c, h, w in [("ip", "IPアドレス", 120), ("mac", "MACアドレス", 140),
                         ("hostname", "ホスト名", 160),
                         ("ping", "応答(ms)", 70), ("ports", "開放ポート", 170),
                         ("status", "状態", 70)]:
            self.tree.heading(c, text=h)
            self.tree.column(c, width=w, minwidth=40)
        v_sb = ttk.Scrollbar(result_frame, command=self.tree.yview)
        self.tree.configure(yscrollcommand=v_sb.set)
        v_sb.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.tag_configure("online", foreground="#27ae60")
        self.tree.tag_configure("offline", foreground="#bbb")

        # 下部: ローカル情報 + ツール
        bottom = tk.Frame(self.root, bg="#f8f9fc")
        bottom.pack(fill=tk.X, padx=8, pady=4)
        self.local_info_var = tk.StringVar(value="")
        tk.Label(bottom, textvariable=self.local_info_var,
                 bg="#f8f9fc", font=("Courier New", 10),
                 fg="#555").pack(side=tk.LEFT)

        tool_frame = tk.Frame(bottom, bg="#f8f9fc")
        tool_frame.pack(side=tk.RIGHT)
        tk.Label(tool_frame, text="ツール: ", bg="#f8f9fc").pack(side=tk.LEFT)
        self.tool_ip_var = tk.StringVar()
        ttk.Entry(tool_frame, textvariable=self.tool_ip_var, width=16).pack(side=tk.LEFT, padx=4)
        for text, cmd in [("Ping", self._ping_single),
                           ("ホスト名解決", self._resolve_host),
                           ("Traceroute", self._traceroute)]:
            ttk.Button(tool_frame, text=text, command=cmd).pack(side=tk.LEFT, padx=2)

        self.status_var = tk.StringVar(value="スキャン待機中")
        tk.Label(self.root, textvariable=self.status_var,
                 bg="#dde", font=("Arial", 9), anchor="w", padx=8
                 ).pack(fill=tk.X, side=tk.BOTTOM)

    def _detect_local_ip(self):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            # デフォルトレンジをセット
            parts = local_ip.rsplit(".", 1)
            self.range_var.set(f"{parts[0]}.0/24")
            hostname = socket.gethostname()
            self.local_info_var.set(
                f"ホスト: {hostname}  ローカルIP: {local_ip}")
            self.tool_ip_var.set(local_ip)
        except Exception:
            pass

    def _toggle_scan(self):
        if self.scanning:
            self.scanning = False
            self.scan_btn.config(text="▶ スキャン開始")
        else:
            self._start_scan()

    def _start_scan(self):
        try:
            network = ipaddress.ip_network(self.range_var.get().strip(),
                                           strict=False)
        except ValueError as e:
            messagebox.showerror("エラー", f"無効なIPレンジ: {e}")
            return
        self.tree.delete(*self.tree.get_children())
        self.scanning = True
        self.scan_btn.config(text="⏹ 停止")
        hosts = list(network.hosts())
        self.progress_var.set(0)
        threading.Thread(target=self._scan_range,
                         args=(hosts,), daemon=True).start()

    def _scan_range(self, hosts):
        total = len(hosts)
        found = 0
        semaphore = threading.Semaphore(self.threads_var.get())
        results = []
        lock = threading.Lock()

        def scan_host(ip):
            nonlocal found
            with semaphore:
                if not self.scanning:
                    return
                ip_str = str(ip)
                online, ms = self._ping(ip_str)
                if online:
                    mac = self._get_mac(ip_str)
                    hostname = self._get_hostname(ip_str)
                    ports = self._scan_ports(ip_str) if self.port_var.get() else ""
                    with lock:
                        found += 1
                        results.append((ip_str, mac, hostname, ms, ports))
                        self.root.after(0, self._add_result,
                                        ip_str, mac, hostname, ms, ports)

        threads = []
        for i, ip in enumerate(hosts):
            if not self.scanning:
                break
            t = threading.Thread(target=scan_host, args=(ip,), daemon=True)
            t.start()
            threads.append(t)
            pct = int((i + 1) / total * 100)
            self.root.after(0, self.progress_var.set, pct)
            self.root.after(0, self.status_var.set,
                            f"スキャン中: {i+1}/{total}  発見: {found}")

        for t in threads:
            t.join(timeout=2)

        self.root.after(0, self._scan_done, found)

    def _ping(self, ip):
        """pingを実行して(alive, ms)を返す"""
        timeout_s = self.timeout_var.get() / 1000
        try:
            param = "-n" if platform.system().lower() == "windows" else "-c"
            cmd = ["ping", param, "1", "-w" if platform.system().lower() == "windows"
                   else "-W", str(self.timeout_var.get()), ip]
            import time
            t0 = time.time()
            result = subprocess.run(cmd, capture_output=True,
                                     timeout=timeout_s + 1)
            ms = int((time.time() - t0) * 1000)
            alive = result.returncode == 0
            return alive, ms
        except Exception:
            # socketで接続チェック
            try:
                import time
                t0 = time.time()
                s = socket.create_connection((ip, 80),
                                              timeout=timeout_s)
                s.close()
                ms = int((time.time() - t0) * 1000)
                return True, ms
            except Exception:
                return False, 0

    def _get_mac(self, ip):
        """ARP テーブルから MAC アドレスを引く（ping 直後なら載っている）。取れなければ "-" を返す"""
        if platform.system().lower() == "windows":
            cmds = [["arp", "-a", ip]]
        else:
            cmds = [["ip", "neigh", "show", ip], ["arp", "-a", ip]]
        for cmd in cmds:
            try:
                out = subprocess.run(cmd, capture_output=True, text=True,
                                     encoding="cp932" if platform.system() == "Windows" else "utf-8",
                                     errors="replace", timeout=3).stdout
            except Exception:
                continue        # コマンドが無い / タイムアウト → 次の手段へ
            mac = self._parse_mac(out, ip)
            if mac != "-":
                return mac
        return "-"

    @staticmethod
    def _parse_mac(output, ip):
        """arp -a / ip neigh の出力テキストから ip の行の MAC を取り出す（無ければ "-"）"""
        # 192.168.1.1 と 192.168.1.10 を区別するため前後が数字・ドットでない行だけ見る
        ip_pat = re.compile(r"(?<![\d.])" + re.escape(ip) + r"(?![\d.])")
        for line in output.splitlines():
            if not ip_pat.search(line):
                continue
            m = App13.MAC_RE.search(line)
            if m:
                return m.group(1).replace("-", ":").lower()
        return "-"

    def _get_hostname(self, ip):
        try:
            return socket.gethostbyaddr(ip)[0]
        except Exception:
            return ""

    def _scan_ports(self, ip):
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443,
                        445, 3306, 3389, 5432, 8080, 8443]
        open_ports = []
        for port in common_ports:
            try:
                s = socket.create_connection((ip, port), timeout=0.3)
                s.close()
                open_ports.append(port)
            except Exception:
                pass
        return ", ".join(str(p) for p in open_ports)

    def _add_result(self, ip, mac, hostname, ms, ports):
        self.tree.insert("", "end",
                         values=(ip, mac, hostname, f"{ms} ms", ports, "オンライン"),
                         tags=("online",))

    def _scan_done(self, found):
        self.scanning = False
        self.scan_btn.config(text="▶ スキャン開始")
        self.progress_var.set(100)
        self.status_var.set(
            f"スキャン完了  {found} 台発見  "
            f"({datetime.now().strftime('%H:%M:%S')})")

    def _ping_single(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        threading.Thread(target=self._do_ping_single,
                         args=(ip,), daemon=True).start()

    def _do_ping_single(self, ip):
        alive, ms = self._ping(ip)
        msg = f"{ip}: {'応答あり' if alive else '応答なし'} ({ms} ms)"
        self.root.after(0, lambda: messagebox.showinfo("Ping結果", msg))

    def _resolve_host(self):
        target = self.tool_ip_var.get().strip()
        if not target:
            return
        try:
            result = socket.gethostbyaddr(target)
            messagebox.showinfo("ホスト名解決",
                                f"ホスト名: {result[0]}\nエイリアス: {result[1]}\nIP: {result[2]}")
        except Exception as e:
            messagebox.showinfo("ホスト名解決", f"解決できませんでした: {e}")

    def _traceroute(self):
        ip = self.tool_ip_var.get().strip()
        if not ip:
            return
        win = tk.Toplevel(self.root)
        win.title(f"Traceroute — {ip}")
        win.geometry("500x300")
        txt = tk.Text(win, font=("Courier New", 10), bg="#1e1e1e",
                      fg="#ccc")
        txt.pack(fill=tk.BOTH, expand=True)

        def run():
            cmd = (["tracert", ip] if platform.system().lower() == "windows"
                   else ["traceroute", ip])
            try:
                proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT,
                                         text=True,
                                         encoding="cp932" if platform.system() == "Windows" else "utf-8",
                                         errors="replace")
                for line in proc.stdout:
                    win.after(0, txt.insert, tk.END, line)
            except Exception as e:
                win.after(0, txt.insert, tk.END, f"エラー: {e}")

        threading.Thread(target=run, daemon=True).start()


if __name__ == "__main__":
    root = tk.Tk()
    app = App13(root)
    root.mainloop()
