"""
Network Conversion Trainer  ─  All-in-one GUI
=============================================
• IPv4 / IPv6 / MAC / Hex / Binary converters
• 0-255 Binary ⇄ Decimal ⇄ Hex reference table
• Video tab uses a **real** Chromium / Edge WebView2 window (pywebview)
  so YouTube plays perfectly.  Falls back to the default browser if
  pywebview isn’t installed.

Recommended install in a virtual-env:
    pip install customtkinter tabulate pywebview
"""

from __future__ import annotations

import ipaddress
import re
import webbrowser
import tkinter.filedialog as fd
import tkinter.messagebox as mb
from typing import List

# ── Optional extras ─────────────────────────────────────────────
try:                        # Pretty tables
    from tabulate import tabulate
    _HAS_TABULATE = True
except ImportError:
    _HAS_TABULATE = False

try:                        # Embedded Chromium/Edge browser
    import webview          # type: ignore
    _HAS_WEBVIEW = True
except ImportError:
    _HAS_WEBVIEW = False

import customtkinter as ctk

ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")

VIDEO_URL  = "https://www.youtube.com/watch?v=zDNaUi2cjv4"
VIDEO_WIN  = "Subnetting Crash-Course"


# ───────────────────────────────────────────────────────────────
# 📐  Conversion helpers
# ───────────────────────────────────────────────────────────────
def ipv4_to_binary(ip: str) -> str:
    try:
        ipaddress.IPv4Address(ip)
    except ipaddress.AddressValueError as exc:
        raise ValueError("Invalid IPv4 address (0-255 per octet).") from exc
    octets = [int(o) for o in ip.split(".")]
    rows   = [[f"Octet {i+1}", d, f"{d:08b}"] for i, d in enumerate(octets)]
    return _tbl(rows, ["Part", "Decimal", "Binary"])


def binary_to_ipv4(bits: str) -> str:
    if not re.fullmatch(r"[01]{32}", bits):
        raise ValueError("Must be exactly 32 binary digits.")
    oct_bits = [bits[i*8:(i+1)*8] for i in range(4)]
    oct_dec  = [int(b, 2) for b in oct_bits]
    header   = f"IPv4: {'.'.join(map(str, oct_dec))}\n\n"
    rows     = list(zip([f"Octet {i+1}" for i in range(4)], oct_dec, oct_bits))
    return header + _tbl(rows, ["Part", "Decimal", "Binary"])


def ipv6_to_binary(ip: str) -> str:
    try:
        expanded = ipaddress.IPv6Address(ip).exploded
    except ipaddress.AddressValueError as exc:
        raise ValueError("Invalid IPv6 address.") from exc
    hextets = expanded.split(":")
    bits    = bin(int(expanded.replace(":", ""), 16))[2:].zfill(128)
    rows    = [[hextets[i], bits[i*16:(i+1)*16]] for i in range(8)]
    return f"Expanded: {expanded}\n\n" + _tbl(rows, ["Hex 16-bit", "Binary 16-bit"])


def mac_to_binary(mac: str) -> str:
    norm = re.sub(r"[^0-9A-Fa-f]", "", mac)
    if len(norm) != 12:
        raise ValueError("MAC must contain exactly 12 hex digits.")
    bits  = bin(int(norm, 16))[2:].zfill(48)
    pairs = [norm[i*2:(i+1)*2] for i in range(6)]
    rows  = [[pairs[i], bits[i*8:(i+1)*8]] for i in range(6)]
    return _tbl(rows, ["Hex Pair", "Binary"])


def hex_to_binary(hx: str) -> str:
    hx = hx.strip().replace(" ", "")
    if not re.fullmatch(r"[0-9A-Fa-f]+", hx):
        raise ValueError("Hex digits only; no 0x prefix/spaces.")
    bits = bin(int(hx, 16))[2:].zfill(len(hx)*4)
    rows = [[hx[i].upper(), bits[i*4:(i+1)*4]] for i in range(len(hx))]
    return _tbl(rows, ["Hex", "Binary"])


def binary_to_hex(bits: str) -> str:
    if not re.fullmatch(r"[01]{4,128}", bits) or len(bits) % 4:
        raise ValueError("Binary length 4-128, multiple of 4.")
    hx   = hex(int(bits, 2))[2:].upper()
    rows = [[hx[i], bits[i*4:(i+1)*4]] for i in range(len(hx))]
    return f"Hex: {hx}\n\n" + _tbl(rows, ["Hex", "Binary"])


def full_table_0_255() -> str:
    rows = [[f"{i:08b}", i, f"{i:02X}"] for i in range(256)]
    return _tbl(rows, ["Binary (8-bit)", "Decimal", "Hex"])


# ───────────────────────────────────────────────────────────────
# 🗄️  Table helper
# ───────────────────────────────────────────────────────────────
def _tbl(data: List[List[str]], headers: List[str]) -> str:
    if _HAS_TABULATE:
        return tabulate(data, headers=headers, tablefmt="fancy_grid")
    w   = [max(len(str(r[c])) for r in data+[headers]) for c in range(len(headers))]
    sep = "─".join("".ljust(x, "─") for x in w)
    out = [" | ".join(headers[i].ljust(w[i]) for i in range(len(headers))), sep]
    out += [" | ".join(str(r[i]).ljust(w[i]) for i in range(len(r))) for r in data]
    return "\n".join(out)


# ───────────────────────────────────────────────────────────────
# 🔧  Config dicts
# ───────────────────────────────────────────────────────────────
_CONV_FUNCS = {
    "Decimal IPv4 → Binary": ipv4_to_binary,
    "Binary (32-bit) → IPv4": binary_to_ipv4,
    "IPv6 → Binary":          ipv6_to_binary,
    "MAC → Binary":           mac_to_binary,
    "Hex → Binary":           hex_to_binary,
    "Binary → Hex":           binary_to_hex,
}
_HINT = {
    "Decimal IPv4 → Binary": "e.g. 192.168.1.1",
    "Binary (32-bit) → IPv4": "32 bits 0/1",
    "IPv6 → Binary":          "fd00::1",
    "MAC → Binary":           "00:1A:2B:3C:4D:5E",
    "Hex → Binary":           "1A3C",
    "Binary → Hex":           "11011010",
}
_INS = {
    "Decimal IPv4 → Binary": "Enter dotted-quad IPv4 address.",
    "Binary (32-bit) → IPv4": "Provide 32 binary digits.",
    "IPv6 → Binary":          "Standard IPv6 address ( :: ok ).",
    "MAC → Binary":           "48-bit MAC; separators optional.",
    "Hex → Binary":           "Hex digits only.",
    "Binary → Hex":           "Binary length multiple of 4.",
}


# ───────────────────────────────────────────────────────────────
# 🖼️  GUI Components
# ───────────────────────────────────────────────────────────────
class ConversionTab(ctk.CTkFrame):
    def __init__(self, master, label: str):
        super().__init__(master)
        self.func = _CONV_FUNCS[label]
        self.last: str | None = None

        self.entry = ctk.CTkEntry(self, placeholder_text=_HINT[label])
        self.entry.pack(fill="x", padx=10, pady=(10, 6))

        ctk.CTkLabel(
            self, text=_INS[label], font=("TkDefaultFont", 10),
            wraplength=600, anchor="w", justify="left"
        ).pack(fill="x", padx=10, pady=(0, 8))

        bf = ctk.CTkFrame(self, fg_color="transparent")
        bf.pack(fill="x", padx=10, pady=4)

        ctk.CTkButton(bf, text="Convert", command=self._convert).pack(side="left", padx=(0, 6))
        self.save_btn = ctk.CTkButton(bf, text="Save", state="disabled", command=self._save)
        self.save_btn.pack(side="left")

        self.out = ctk.CTkTextbox(self, wrap="none", height=220)
        self.out.configure(state="disabled")
        self.out.pack(fill="both", expand=True, padx=10, pady=(4, 10))

    def _display(self, txt: str):
        self.out.configure(state="normal")
        self.out.delete("1.0", "end")
        self.out.insert("end", txt)
        self.out.configure(state="disabled")

    def _convert(self):
        raw = self.entry.get().strip()
        if not raw:
            mb.showwarning("Input required", "Enter a value to convert.")
            return
        try:
            res = self.func(raw)
        except ValueError as e:
            mb.showerror("Invalid input", str(e))
            self._display(str(e))
            self.save_btn.configure(state="disabled")
            return
        self.last = res
        self._display(res)
        self.save_btn.configure(state="normal")

    def _save(self):
        if not self.last:
            return
        path = fd.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
        )
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(self.last)
            mb.showinfo("Saved", f"Results saved to {path}")
        except OSError as exc:
            mb.showerror("Save failed", str(exc))


class TableTab(ctk.CTkFrame):
    def __init__(self, master):
        super().__init__(master)
        self.last: str | None = None

        ctk.CTkLabel(
            self, text="Generate the 0-255 Binary | Decimal | Hex table.",
            font=("TkDefaultFont", 10), wraplength=600,
            anchor="w", justify="left"
        ).pack(fill="x", padx=10, pady=(10, 6))

        bf = ctk.CTkFrame(self, fg_color="transparent")
        bf.pack(fill="x", padx=10, pady=4)

        ctk.CTkButton(bf, text="Generate", command=self._gen).pack(side="left", padx=(0, 6))
        self.save_btn = ctk.CTkButton(bf, text="Save", state="disabled", command=self._save)
        self.save_btn.pack(side="left")

        self.out = ctk.CTkTextbox(self, wrap="none", height=300)
        self.out.configure(state="disabled")
        self.out.pack(fill="both", expand=True, padx=10, pady=(4, 10))

    def _gen(self):
        self.last = full_table_0_255()
        self._display(self.last)
        self.save_btn.configure(state="normal")

    def _display(self, txt: str):
        self.out.configure(state="normal")
        self.out.delete("1.0", "end")
        self.out.insert("end", txt)
        self.out.configure(state="disabled")

    def _save(self):
        if not self.last:
            return
        path = fd.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
        )
        if not path:
            return
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(self.last)
            mb.showinfo("Saved", f"Table saved to {path}")
        except OSError as exc:
            mb.showerror("Save failed", str(exc))


class VideoTab(ctk.CTkFrame):
    """Launches a full Chromium/Edge window via pywebview."""
    def __init__(self, master):
        super().__init__(master)

        ctk.CTkLabel(
            self,
            text="Subnetting crash-course video.",
            font=("TkDefaultFont", 10),
            wraplength=600, anchor="w", justify="left"
        ).pack(fill="x", padx=10, pady=(10, 6))

        ctk.CTkButton(
            self, text="Play Video", command=self._play
        ).pack(pady=(0, 20))

        ctk.CTkLabel(
            self,
            text=("Requires 'pywebview' (Edge WebView2 / Chromium).\n"
                  "If unavailable, your default browser will be used."),
            font=("TkDefaultFont", 9), anchor="w", justify="center"
        ).pack()

    def _play(self):
        if _HAS_WEBVIEW:
            webview.create_window(VIDEO_WIN, VIDEO_URL, width=960, height=540)
            webview.start()
        else:
            webbrowser.open_new(VIDEO_URL)


# ───────────────────────────────────────────────────────────────
# 🚀  Main Application
# ───────────────────────────────────────────────────────────────
class NetworkConverterApp(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Network Conversion Trainer")
        self.minsize(820, 620)

        tabs = ctk.CTkTabview(self)
        tabs.pack(fill="both", expand=True, padx=10, pady=10)

        for lbl in _CONV_FUNCS:
            page = tabs.add(lbl)
            ConversionTab(page, lbl).pack(fill="both", expand=True)

        TableTab(tabs.add("0-255 Table")).pack(fill="both", expand=True)
        VideoTab(tabs.add("Video")).pack(fill="both", expand=True)


if __name__ == "__main__":
    NetworkConverterApp().mainloop()
