import customtkinter as ctk
import base64
import hashlib
import urllib.parse
import requests
from bs4 import BeautifulSoup
import threading
import socket
from concurrent.futures import ThreadPoolExecutor

class CustomUtilsFrame(ctk.CTkFrame):
    def __init__(self, master, heading_font=None, text_font=None):
        super().__init__(master)
        self.heading_font = heading_font if heading_font else ctk.CTkFont(size=18, weight="bold")
        self.text_font = text_font if text_font else ctk.CTkFont(size=12)
        self.monospace_font = ctk.CTkFont(family="Fira Code", size=12) # For output textboxes
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(1, weight=1)

        title = ctk.CTkLabel(self, text="Utilidades Propias", font=self.heading_font)
        title.grid(row=0, column=0, padx=20, pady=(20, 10), sticky="ew")

        self.tab_view = ctk.CTkTabview(self)
        self.tab_view.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")

        self.tab_view.add("Codificador / Hasher")
        self.tab_view.add("Extractor de Enlaces")
        self.tab_view.add("Verificador de Puertos")

        # Configurar cada pestaña
        self.setup_encoder_tab()
        self.setup_link_extractor_tab()
        self.setup_port_checker_tab()

    # ---- PESTAÑA 1: CODIFICADOR / HASHER ----
    def setup_encoder_tab(self):
        tab = self.tab_view.tab("Codificador / Hasher")
        tab.grid_columnconfigure(0, weight=1)
        tab.grid_columnconfigure(1, weight=1)
        tab.grid_rowconfigure(1, weight=1)

        # ---- Controles ----
        controls_frame = ctk.CTkFrame(tab)
        controls_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="ew")

        self.encode_option = ctk.StringVar(value="b64_encode")
        options = {
            "Base64 Encode": "b64_encode", "Base64 Decode": "b64_decode",
            "URL Encode": "url_encode", "URL Decode": "url_decode",
            "MD5": "md5", "SHA1": "sha1", "SHA256": "sha256"
        }
        
        row, col = 0, 0
        for text, value in options.items():
            rb = ctk.CTkRadioButton(controls_frame, text=text, variable=self.encode_option, value=value)
            rb.grid(row=row, column=col, padx=10, pady=5, sticky="w")
            col += 1
            if col > 3:
                col = 0
                row += 1
        
        # ---- Input / Output ----
        self.encoder_input = ctk.CTkTextbox(tab, height=150, font=self.text_font)
        self.encoder_input.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
        self.encoder_input.insert("1.0", "Escribe o pega tu texto aquí...")

        self.encoder_output = ctk.CTkTextbox(tab, height=150, state="disabled", font=self.text_font)
        self.encoder_output.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")

        # ---- Botón ----
        convert_button = ctk.CTkButton(tab, text="Convertir", command=self.perform_conversion)
        convert_button.grid(row=2, column=0, columnspan=2, padx=10, pady=10)

    def perform_conversion(self):
        text = self.encoder_input.get("1.0", "end-1c")
        option = self.encode_option.get()
        result = ""
        try:
            if option == "b64_encode":
                result = base64.b64encode(text.encode('utf-8')).decode('utf-8')
            elif option == "b64_decode":
                result = base64.b64decode(text.encode('utf-8')).decode('utf-8')
            elif option == "url_encode":
                result = urllib.parse.quote(text)
            elif option == "url_decode":
                result = urllib.parse.unquote(text)
            elif option == "md5":
                result = hashlib.md5(text.encode('utf-8')).hexdigest()
            elif option == "sha1":
                result = hashlib.sha1(text.encode('utf-8')).hexdigest()
            elif option == "sha256":
                result = hashlib.sha256(text.encode('utf-8')).hexdigest()
        except Exception as e:
            result = f"Error: {e}"

        self.encoder_output.configure(state="normal")
        self.encoder_output.delete("1.0", "end")
        self.encoder_output.insert("1.0", result)
        self.encoder_output.configure(state="disabled")

    # ---- PESTAÑA 2: EXTRACTOR DE ENLACES ----
    def setup_link_extractor_tab(self):
        tab = self.tab_view.tab("Extractor de Enlaces")
        tab.grid_columnconfigure(0, weight=1)
        tab.grid_rowconfigure(2, weight=1)

        # ---- Controles ----
        controls_frame = ctk.CTkFrame(tab)
        controls_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
        controls_frame.grid_columnconfigure(0, weight=1)

        self.link_url_entry = ctk.CTkEntry(controls_frame, placeholder_text="Introduce la URL (ej: http://example.com)")
        self.link_url_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="ew")

        self.link_extract_button = ctk.CTkButton(controls_frame, text="Extraer Enlaces", command=self.start_link_extraction)
        self.link_extract_button.grid(row=0, column=1, padx=(5, 10), pady=10)

        # ---- Output ----
        self.link_output = ctk.CTkTextbox(tab, state="disabled", font=self.text_font)
        self.link_output.grid(row=2, column=0, padx=10, pady=10, sticky="nsew")

    def start_link_extraction(self):
        url = self.link_url_entry.get()
        if not url:
            return
        
        self.link_output.configure(state="normal")
        self.link_output.delete("1.0", "end")
        self.link_output.insert("1.0", f"Extrayendo enlaces de {url}...\n\n")
        self.link_output.configure(state="disabled")
        self.link_extract_button.configure(state="disabled", text="Extrayendo...")

        threading.Thread(target=self.run_link_extraction, args=(url,)).start()

    def run_link_extraction(self, url):
        try:
            headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}
            response = requests.get(url, headers=headers, timeout=10)
            response.raise_for_status()
            soup = BeautifulSoup(response.text, 'html.parser')
            links = {a.get('href') for a in soup.find_all('a', href=True)}
            
            output_text = "\n".join(sorted(links))
        except Exception as e:
            output_text = f"Error al extraer enlaces: {e}"

        self.link_output.after(0, self.update_link_output, output_text)

    def update_link_output(self, text):
        self.link_output.configure(state="normal")
        self.link_output.insert("end", text)
        self.link_output.configure(state="disabled")
        self.link_extract_button.configure(state="normal", text="Extraer Enlaces")
        
    # ---- PESTAÑA 3: VERIFICADOR DE PUERTOS ----
    def setup_port_checker_tab(self):
        tab = self.tab_view.tab("Verificador de Puertos")
        tab.grid_columnconfigure(0, weight=1)
        tab.grid_rowconfigure(2, weight=1)
        
        # ---- Controles ----
        controls_frame = ctk.CTkFrame(tab)
        controls_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
        controls_frame.grid_columnconfigure(0, weight=1)

        self.port_target_entry = ctk.CTkEntry(controls_frame, placeholder_text="IP del Objetivo")
        self.port_target_entry.grid(row=0, column=0, padx=10, pady=10, sticky="ew")

        self.port_list_entry = ctk.CTkEntry(controls_frame, placeholder_text="Puertos (separados por coma, ej: 80,443,8080)")
        self.port_list_entry.grid(row=1, column=0, padx=10, pady=10, sticky="ew")

        self.port_scan_button = ctk.CTkButton(controls_frame, text="Verificar Puertos", command=self.start_port_check)
        self.port_scan_button.grid(row=2, column=0, padx=10, pady=10)

        # ---- Output ----
        self.port_output = ctk.CTkTextbox(tab, state="disabled", font=self.text_font)
        self.port_output.grid(row=2, column=0, padx=10, pady=10, sticky="nsew")

    def start_port_check(self):
        target = self.port_target_entry.get()
        ports_str = self.port_list_entry.get()
        if not target or not ports_str:
            return
        
        try:
            ports = [int(p.strip()) for p in ports_str.split(',')]
        except ValueError:
            self.port_output.configure(state="normal")
            self.port_output.delete("1.0", "end")
            self.port_output.insert("1.0", "Error: La lista de puertos contiene caracteres no válidos.")
            self.port_output.configure(state="disabled")
            return
            
        self.port_output.configure(state="normal")
        self.port_output.delete("1.0", "end")
        self.port_output.insert("1.0", f"Verificando puertos en {target}...\n\n")
        self.port_output.configure(state="disabled")
        self.port_scan_button.configure(state="disabled", text="Verificando...")
        
        threading.Thread(target=self.run_port_check, args=(target, ports)).start()

    def check_port(self, target, port):
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(1)
                if s.connect_ex((target, port)) == 0:
                    return port, "Abierto"
                else:
                    return port, "Cerrado"
        except socket.gaierror:
            return port, "Error: Hostname no resuelto"
        except Exception as e:
            return port, f"Error: {e}"

    def run_port_check(self, target, ports):
        results = []
        with ThreadPoolExecutor(max_workers=50) as executor:
            futures = [executor.submit(self.check_port, target, port) for port in ports]
            for future in futures:
                results.append(future.result())
        
        # Ordenar resultados por puerto
        results.sort(key=lambda x: x[0])
        self.port_output.after(0, self.update_port_output, results)

    def update_port_output(self, results):
        self.port_output.configure(state="normal")
        for port, status in results:
            self.port_output.insert("end", f"Puerto {port}: {status}\n")
        self.port_output.configure(state="disabled")
        self.port_scan_button.configure(state="normal", text="Verificar Puertos")
