package browser

import (
	"fmt"
	"net"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"syscall"
	"time"
)

// ChromeLaunchOpts configura el lanzamiento de Chrome con CDP.
type ChromeLaunchOpts struct {
	// Port es el puerto de remote debugging. Por defecto 9222.
	Port int
	// UserDataDir es el directorio de perfil de Chrome. Por defecto /tmp/chrome-cdp-profile.
	// En WSL2 con chrome.exe, si el valor comienza con /tmp/ o /home/ se traduce
	// automaticamente a una ruta Windows via wslpath. Pasar una ruta Windows
	// (ej: C:\Users\...) la deja intacta.
	UserDataDir string
	// Headless activa el modo headless (--headless=new). Por defecto false.
	Headless bool
	// ChromePath es la ruta al ejecutable de Chrome. Si esta vacio, se busca automaticamente.
	ChromePath string
	// ExtraArgs permite pasar flags adicionales a Chrome.
	ExtraArgs []string
	// KeepExtensions, si es true, NO añade --disable-extensions (mantiene las
	// extensiones del perfil cargadas). Por defecto false (comportamiento actual).
	KeepExtensions bool
	// ProfileDirectory selecciona el perfil dentro del user-data-dir (--profile-directory).
	// Vacío = no se pasa el flag (Chrome usa su default o muestra el selector si hay varios perfiles).
	// Ej: "Default", "Automation".
	ProfileDirectory string
	// ReuseExisting, si es true y el puerto CDP ya responde a una conexion TCP,
	// NO lanza un Chrome nuevo: devuelve (0, nil) para que el caller reutilice el
	// navegador que ya está vivo en ese puerto. Evita acumular procesos chromium
	// duplicados (cada uno ~789 MiB RSS) cuando se llama repetidamente al mismo
	// puerto. El caller distingue el reuso por pid == 0.
	ReuseExisting bool
}

// reWindowsPath coincide con rutas absolutas de Windows (C:\... D:\... etc.).
var reWindowsPath = regexp.MustCompile(`(?i)^[A-Z]:\\`)

// isWSL2 devuelve true si el proceso corre dentro de WSL2.
// Lee /proc/version y busca "microsoft" o "WSL" (case-insensitive).
func isWSL2() bool {
	b, err := os.ReadFile("/proc/version")
	if err != nil {
		return false
	}
	lower := strings.ToLower(string(b))
	return strings.Contains(lower, "microsoft") || strings.Contains(lower, "wsl")
}

// isWindowsExe devuelve true si la ruta del ejecutable corresponde a un .exe
// (chrome.exe en Windows via WSL2: puede ser /mnt/c/... o resolverse en PATH).
func isWindowsExe(path string) bool {
	return strings.HasSuffix(strings.ToLower(path), ".exe")
}

// translateUserDataDirForWindows convierte una ruta Linux a ruta Windows via wslpath.
// Ejemplo: "/tmp/foo" -> "C:\Users\lucas\AppData\Local\Temp\foo" (depende del sistema).
// Devuelve error si wslpath no esta disponible.
func translateUserDataDirForWindows(linuxPath string) (string, error) {
	out, err := exec.Command("wslpath", "-w", linuxPath).Output()
	if err != nil {
		return "", fmt.Errorf("wslpath -w %q: %w", linuxPath, err)
	}
	return strings.TrimSpace(string(out)), nil
}

// defaultWindowsUserDataDir devuelve la ruta Windows del perfil CDP por defecto,
// usando la variable de entorno USERNAME de Windows si esta disponible.
func defaultWindowsUserDataDir() (string, error) {
	// Intentar leer el home de Windows via wslpath del home de usuario
	// Si falla, usar C:\Users\Public\fn-chrome-cdp-profile como fallback.
	user := os.Getenv("USERNAME") // Windows user via WSL env passthrough
	if user == "" {
		user = os.Getenv("USER")
	}
	if user == "" {
		user = "Public"
	}
	linuxPath := fmt.Sprintf("/mnt/c/Users/%s/AppData/Local/fn-chrome-cdp-profile", user)
	return translateUserDataDirForWindows(linuxPath)
}

// chromePathsLinux lista los binarios Linux-nativos de Chrome en orden de preferencia.
// Las rutas absolutas a los binarios REALES van primero: saltan el wrapper
// /usr/bin/chromium (un script que inyecta los flags de /etc/chromium.d/*, p.ej.
// --user-data-dir y --remote-debugging-port globales que pisarian el aislamiento
// del navegador del agente). Si no existen, se cae a los nombres de PATH — que
// pueden resolver al wrapper, en cuyo caso el aislamiento depende de que nuestros
// flags vayan al final (Chrome usa el ultimo --user-data-dir duplicado).
var chromePathsLinux = []string{
	"/usr/lib/chromium/chromium",
	"/usr/lib/chromium-browser/chromium-browser",
	"chromium",
	"chromium-browser",
	"google-chrome",
	"google-chrome-stable",
	"brave-browser",
}

// chromePathsWSL lista los ejecutables de Chrome para WSL2 (Windows .exe primero).
var chromePathsWSL = []string{
	"chrome.exe",
	"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
	"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
	"/mnt/c/Users/Public/Desktop/chrome.exe",
	// binarios Linux como ultimo recurso en WSL
	"chromium",
	"chromium-browser",
	"google-chrome",
	"google-chrome-stable",
}

// findChrome localiza el ejecutable de Chrome en el sistema.
// En Linux nativo busca primero binarios Linux; en WSL2 busca primero chrome.exe.
func findChrome() (string, error) {
	var paths []string
	if isWSL2() {
		paths = chromePathsWSL
	} else {
		// Linux nativo: primero binarios nativos, despues .exe como ultimo recurso
		paths = append(chromePathsLinux,
			"chrome.exe",
			"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
			"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
		)
	}
	for _, p := range paths {
		if path, err := exec.LookPath(p); err == nil {
			return path, nil
		}
		if _, err := os.Stat(p); err == nil {
			return p, nil
		}
	}
	return "", fmt.Errorf("chrome: ejecutable no encontrado en PATH ni en rutas conocidas")
}

// dialCDP intenta una conexion TCP unica al puerto CDP. Devuelve true si el
// puerto acepta la conexion (hay algo escuchando), false en caso contrario.
// host vacio usa "127.0.0.1".
func dialCDP(host string, port int, timeout time.Duration) bool {
	if host == "" {
		host = "127.0.0.1"
	}
	addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
	conn, err := net.DialTimeout("tcp", addr, timeout)
	if err != nil {
		return false
	}
	conn.Close()
	return true
}

// cdpPortResponds indica si ya hay un proceso escuchando el puerto CDP en
// 127.0.0.1. Es un sondeo TCP unico con timeout corto, usado por ChromeLaunch
// (opts.ReuseExisting) para no relanzar un Chrome duplicado cuando el puerto ya
// tiene uno vivo.
func cdpPortResponds(port int) bool {
	return dialCDP("127.0.0.1", port, 300*time.Millisecond)
}

// waitCDPReady espera hasta que el puerto CDP responda conexiones TCP.
// host puede estar vacio (usa "127.0.0.1").
func waitCDPReady(host string, port int, timeout time.Duration) error {
	if host == "" {
		host = "127.0.0.1"
	}
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if dialCDP(host, port, 200*time.Millisecond) {
			return nil
		}
		time.Sleep(200 * time.Millisecond)
	}
	return fmt.Errorf("chrome: puerto CDP %s no disponible despues de %s",
		net.JoinHostPort(host, fmt.Sprintf("%d", port)), timeout)
}

// ChromeLaunch lanza Google Chrome con remote debugging habilitado en el puerto indicado.
// Retorna el PID del proceso Chrome. Espera hasta 15s a que el puerto CDP este listo.
// Busca chrome.exe en PATH (WSL2) o en rutas conocidas de Windows.
//
// WSL2 + chrome.exe: cuando se detecta WSL2 y el ejecutable es un .exe,
//   - El UserDataDir se traduce automaticamente a ruta Windows via wslpath
//     (si esta vacio o comienza con /tmp/ o /home/).
//   - Se inyecta --remote-debugging-address=0.0.0.0 para que Chrome sea
//     alcanzable desde WSL2 via 127.0.0.1 (el WSL networking reenvía localhost).
func ChromeLaunch(opts ChromeLaunchOpts) (int, error) {
	if opts.Port == 0 {
		opts.Port = 9222
	}

	// Anti-duplicado: si el caller pide reusar y ya hay un Chrome escuchando el
	// puerto CDP, no lanzamos otro. Devolvemos pid 0 para que el caller sepa que
	// debe adjuntarse al existente en vez de registrar un proceso nuevo.
	if opts.ReuseExisting && cdpPortResponds(opts.Port) {
		return 0, nil
	}

	chromePath := opts.ChromePath
	if chromePath == "" {
		var err error
		chromePath, err = findChrome()
		if err != nil {
			return 0, err
		}
	}

	// Detectar si estamos en WSL2 lanzando un exe de Windows
	wsl2WindowsMode := isWSL2() && isWindowsExe(chromePath)

	// Resolver UserDataDir
	userDataDir := opts.UserDataDir
	if wsl2WindowsMode {
		switch {
		case userDataDir == "" ||
			strings.HasPrefix(userDataDir, "/tmp/") ||
			strings.HasPrefix(userDataDir, "/home/"):
			// Traducir a ruta Windows
			if userDataDir == "" {
				var err error
				userDataDir, err = defaultWindowsUserDataDir()
				if err != nil {
					// Fallback seguro: usar una ruta Windows fija
					userDataDir = `C:\Users\Public\fn-chrome-cdp-profile`
				}
			} else {
				translated, err := translateUserDataDirForWindows(userDataDir)
				if err != nil {
					return 0, fmt.Errorf("chrome: traducir user-data-dir para Windows: %w", err)
				}
				userDataDir = translated
			}
		case reWindowsPath.MatchString(userDataDir):
			// Ya es una ruta Windows absoluta (C:\...), dejar intacta
		default:
			// Ruta que no es ni /tmp/ ni /home/ ni Windows absoluta:
			// intentar traducir igualmente.
			translated, err := translateUserDataDirForWindows(userDataDir)
			if err == nil {
				userDataDir = translated
			}
		}
	} else if userDataDir == "" {
		userDataDir = "/tmp/chrome-cdp-profile"
	}

	args := []string{
		fmt.Sprintf("--remote-debugging-port=%d", opts.Port),
		fmt.Sprintf("--user-data-dir=%s", userDataDir),
		"--no-first-run",
		"--no-default-browser-check",
		"--disable-background-networking",
		"--disable-client-side-phishing-detection",
		"--disable-default-apps",
		"--disable-hang-monitor",
		"--disable-popup-blocking",
		"--disable-prompt-on-repost",
		"--disable-sync",
		"--disable-translate",
		"--metrics-recording-only",
		"--safebrowsing-disable-auto-update",
		"--remote-allow-origins=*",
	}
	if !opts.KeepExtensions {
		args = append(args, "--disable-extensions")
	}
	if opts.ProfileDirectory != "" {
		args = append(args, fmt.Sprintf("--profile-directory=%s", opts.ProfileDirectory))
	}

	if opts.Headless {
		args = append(args, "--headless=new", "--disable-gpu")
	}

	// En WSL2+Windows: inyectar --remote-debugging-address=0.0.0.0 si no esta ya presente
	hasBindAll := false
	for _, a := range opts.ExtraArgs {
		if a == "--remote-debugging-address=0.0.0.0" {
			hasBindAll = true
			break
		}
	}
	if wsl2WindowsMode && !hasBindAll {
		args = append(args, "--remote-debugging-address=0.0.0.0")
		hasBindAll = true
	}

	args = append(args, opts.ExtraArgs...)

	cmd := exec.Command(chromePath, args...)
	// Chrome necesita que no haya stderr/stdout bloqueados
	cmd.Stdout = nil
	cmd.Stderr = nil

	// En Linux nativo (no WSL+Windows), crear un grupo de proceso propio para que
	// el proceso sobreviva al fin del padre y para poder matar el arbol completo
	// (chromium lanza zygote, gpu-process, renderers como hijos).
	// No aplicar en WSL+Windows: chrome.exe se gestiona de forma distinta.
	if !wsl2WindowsMode {
		cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	}

	if err := cmd.Start(); err != nil {
		return 0, fmt.Errorf("chrome: arrancar proceso: %w", err)
	}

	pid := cmd.Process.Pid

	// Esperar a que el puerto CDP este listo.
	// Siempre esperamos, incluyendo el caso WSL2+Windows donde Chrome escucha en
	// 0.0.0.0 — el WSL networking reenvía localhost:9222 → Windows:9222.
	// Usamos 127.0.0.1 explicitamente para evitar resolución IPv6 en algunos entornos.
	_ = hasBindAll // ya no se usa para skipWait
	if err := waitCDPReady("127.0.0.1", opts.Port, 15*time.Second); err != nil {
		cmd.Process.Kill()
		return 0, err
	}

	return pid, nil
}
