// cdp-cli — wrapper de las funciones del registry domain `browser`.
//
// Subcomandos one-shot que abren conexion CDP, ejecutan accion y salen.
// El proceso Chrome NO se mata al cerrar — sigue vivo para que el usuario
// y otros clientes CDP (incluida la extension del issue 0014) sigan
// hablando con la misma instancia. CDP soporta multiples clientes sobre
// el mismo --remote-debugging-port.
//
// Uso tipico:
//
//	cdp-cli launch --port 9222 --user-data-dir /path/to/profile
//	cdp-cli navigate --port 9222 --url https://example.com
//	cdp-cli get-html --port 9222 > page.html
//
// Issue: projects/osint_graph/apps/graph_explorer/issues/0038-browser-launch-cdp-control.md
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"time"

	"fn-registry/functions/browser"
)

const usage = `cdp-cli — control de Chrome via CDP

Subcomandos:
  launch        Lanza Chrome con --remote-debugging-port. Imprime "pid=N port=M".
  navigate      Page.navigate a la URL indicada.
  get-html      Imprime document.documentElement.outerHTML por stdout.
  screenshot    Page.captureScreenshot a archivo (--out).
  evaluate      Runtime.evaluate de la expresion (--js). Resultado por stdout.
  click         Click en selector CSS (--selector).
  type          Escribe texto en elemento activo (--text).
  wait-load     Espera document.readyState=='complete' (--timeout segundos).
  wait-element  Espera selector CSS (--selector, --timeout).
  set-cookie    Network.setCookie (--name, --value, --domain, [--path], [--http-only]).
  find-by-text  Localiza elemento por innerText (--text, [--tag], [--exact], [--case-sensitive]). Imprime selector CSS.
  click-text    find-by-text + click. Mismos flags que find-by-text.
  har-record    Captura trafico HTTP/WS durante navegacion. (--url, --out, [--settle-ms]). Output HAR 1.2 JSON.
  list-tabs     Lista pestañas/targets de la instancia. Salida JSON o tabla con --format=text.
  new-tab       Abre pestaña nueva (--url opcional). Imprime el id.
  close-tab     Cierra pestaña por id (--id).
  activate-tab  Pone pestaña en foreground (--id).

Flags globales (todos los subcomandos excepto launch):
  --port N      Puerto CDP (default 9222)
  --host H      Host CDP (default localhost)

Ejemplos:
  cdp-cli launch --port 9222 --user-data-dir /tmp/cdp-profile
  cdp-cli navigate --url https://example.com
  cdp-cli get-html > page.html
  cdp-cli screenshot --out /tmp/shot.png --full-page
  cdp-cli evaluate --js "document.title"
  cdp-cli click --selector "#submit"
  cdp-cli wait-element --selector ".result" --timeout 10
`

func main() {
	if len(os.Args) < 2 {
		fmt.Fprint(os.Stderr, usage)
		os.Exit(2)
	}
	cmd, args := os.Args[1], os.Args[2:]
	switch cmd {
	case "launch":
		cmdLaunch(args)
	case "navigate":
		cmdNavigate(args)
	case "get-html":
		cmdGetHTML(args)
	case "screenshot":
		cmdScreenshot(args)
	case "evaluate":
		cmdEvaluate(args)
	case "click":
		cmdClick(args)
	case "type":
		cmdType(args)
	case "wait-load":
		cmdWaitLoad(args)
	case "wait-element":
		cmdWaitElement(args)
	case "set-cookie":
		cmdSetCookie(args)
	case "find-by-text":
		cmdFindByText(args)
	case "click-text":
		cmdClickText(args)
	case "har-record":
		cmdHarRecord(args)
	case "list-tabs":
		cmdListTabs(args)
	case "new-tab":
		cmdNewTab(args)
	case "close-tab":
		cmdCloseTab(args)
	case "activate-tab":
		cmdActivateTab(args)
	case "-h", "--help", "help":
		fmt.Print(usage)
	default:
		fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n\n%s", cmd, usage)
		os.Exit(2)
	}
}

func dieF(format string, a ...any) {
	fmt.Fprintf(os.Stderr, "cdp-cli: "+format+"\n", a...)
	os.Exit(1)
}

func mustConnect(host string, port int) *browser.CDPConn {
	c, err := browser.CdpConnectHost(host, port)
	if err != nil {
		dieF("connect %s:%d: %v", host, port, err)
	}
	return c
}

func addConnFlags(fs *flag.FlagSet) (*string, *int) {
	host := fs.String("host", "localhost", "host CDP")
	port := fs.Int("port", 9222, "puerto CDP")
	return host, port
}

// stringList implementa flag.Value para flags repetibles (--extra-arg foo --extra-arg bar).
type stringList []string

func (s *stringList) String() string     { return fmt.Sprint([]string(*s)) }
func (s *stringList) Set(v string) error { *s = append(*s, v); return nil }

func cmdLaunch(args []string) {
	fs := flag.NewFlagSet("launch", flag.ExitOnError)
	port := fs.Int("port", 9222, "puerto remote-debugging")
	userDataDir := fs.String("user-data-dir", "", "directorio de profile (default /tmp/chrome-cdp-profile)")
	headless := fs.Bool("headless", false, "modo headless (--headless=new)")
	chromePath := fs.String("chrome-path", "", "ruta a chrome.exe (auto si vacio)")
	bindAddr := fs.String("bind-address", "", "valor para --remote-debugging-address (ej. 0.0.0.0 para WSL→Windows)")
	var extra stringList
	fs.Var(&extra, "extra-arg", "flag adicional pasado tal cual a chrome (repetible)")
	_ = fs.Parse(args)

	extraArgs := []string(extra)
	if *bindAddr != "" {
		extraArgs = append(extraArgs, fmt.Sprintf("--remote-debugging-address=%s", *bindAddr))
	}

	pid, err := browser.ChromeLaunch(browser.ChromeLaunchOpts{
		Port:        *port,
		UserDataDir: *userDataDir,
		Headless:    *headless,
		ChromePath:  *chromePath,
		ExtraArgs:   extraArgs,
	})
	if err != nil {
		dieF("launch: %v", err)
	}
	fmt.Printf("pid=%d port=%d\n", pid, *port)
}

func cmdNavigate(args []string) {
	fs := flag.NewFlagSet("navigate", flag.ExitOnError)
	host, port := addConnFlags(fs)
	url := fs.String("url", "", "URL destino (obligatorio)")
	wait := fs.Bool("wait-load", true, "esperar a que termine de cargar")
	timeout := fs.Int("timeout", 30, "timeout de wait-load en segundos")
	_ = fs.Parse(args)
	if *url == "" {
		dieF("--url obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpNavigate(c, *url); err != nil {
		dieF("navigate: %v", err)
	}
	if *wait {
		if err := browser.CdpWaitLoad(c, time.Duration(*timeout)*time.Second); err != nil {
			dieF("wait-load: %v", err)
		}
	}
}

func cmdGetHTML(args []string) {
	fs := flag.NewFlagSet("get-html", flag.ExitOnError)
	host, port := addConnFlags(fs)
	_ = fs.Parse(args)
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	html, err := browser.CdpGetHTML(c)
	if err != nil {
		dieF("get-html: %v", err)
	}
	fmt.Print(html)
}

func cmdScreenshot(args []string) {
	fs := flag.NewFlagSet("screenshot", flag.ExitOnError)
	host, port := addConnFlags(fs)
	out := fs.String("out", "", "archivo destino (.png o .jpg). Obligatorio.")
	fullPage := fs.Bool("full-page", false, "capturar pagina completa")
	format := fs.String("format", "png", "formato: png|jpeg")
	quality := fs.Int("quality", 80, "calidad JPEG 1-100")
	_ = fs.Parse(args)
	if *out == "" {
		dieF("--out obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	err := browser.CdpScreenshot(c, *out, browser.CdpScreenshotOpts{
		FullPage: *fullPage,
		Format:   *format,
		Quality:  *quality,
	})
	if err != nil {
		dieF("screenshot: %v", err)
	}
	fmt.Println(*out)
}

func cmdEvaluate(args []string) {
	fs := flag.NewFlagSet("evaluate", flag.ExitOnError)
	host, port := addConnFlags(fs)
	js := fs.String("js", "", "expresion JavaScript (obligatorio)")
	_ = fs.Parse(args)
	if *js == "" {
		dieF("--js obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	res, err := browser.CdpEvaluate(c, *js)
	if err != nil {
		dieF("evaluate: %v", err)
	}
	fmt.Println(res)
}

func cmdClick(args []string) {
	fs := flag.NewFlagSet("click", flag.ExitOnError)
	host, port := addConnFlags(fs)
	selector := fs.String("selector", "", "selector CSS (obligatorio)")
	_ = fs.Parse(args)
	if *selector == "" {
		dieF("--selector obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpClick(c, *selector); err != nil {
		dieF("click: %v", err)
	}
}

func cmdType(args []string) {
	fs := flag.NewFlagSet("type", flag.ExitOnError)
	host, port := addConnFlags(fs)
	text := fs.String("text", "", "texto a escribir (obligatorio)")
	_ = fs.Parse(args)
	if *text == "" {
		dieF("--text obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpTypeText(c, *text); err != nil {
		dieF("type: %v", err)
	}
}

func cmdWaitLoad(args []string) {
	fs := flag.NewFlagSet("wait-load", flag.ExitOnError)
	host, port := addConnFlags(fs)
	timeout := fs.Int("timeout", 30, "timeout en segundos")
	_ = fs.Parse(args)
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpWaitLoad(c, time.Duration(*timeout)*time.Second); err != nil {
		dieF("wait-load: %v", err)
	}
}

func cmdWaitElement(args []string) {
	fs := flag.NewFlagSet("wait-element", flag.ExitOnError)
	host, port := addConnFlags(fs)
	selector := fs.String("selector", "", "selector CSS (obligatorio)")
	timeout := fs.Int("timeout", 10, "timeout en segundos")
	_ = fs.Parse(args)
	if *selector == "" {
		dieF("--selector obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpWaitElement(c, *selector, time.Duration(*timeout)*time.Second); err != nil {
		dieF("wait-element: %v", err)
	}
}

func cmdFindByText(args []string) {
	fs := flag.NewFlagSet("find-by-text", flag.ExitOnError)
	host, port := addConnFlags(fs)
	text := fs.String("text", "", "texto a localizar (obligatorio)")
	tag := fs.String("tag", "", "filtrar por tag (button, a, ...)")
	exact := fs.Bool("exact", false, "match exacto vs substring")
	caseSensitive := fs.Bool("case-sensitive", false, "comparacion case-sensitive")
	_ = fs.Parse(args)
	if *text == "" {
		dieF("--text obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	sel, err := browser.CdpFindByText(c, *text, browser.FindByTextOpts{
		Tag: *tag, Exact: *exact, CaseSensitive: *caseSensitive,
	})
	if err != nil {
		dieF("find-by-text: %v", err)
	}
	if sel == "" {
		fmt.Fprintln(os.Stderr, "no encontrado")
		os.Exit(2)
	}
	fmt.Println(sel)
}

func cmdClickText(args []string) {
	fs := flag.NewFlagSet("click-text", flag.ExitOnError)
	host, port := addConnFlags(fs)
	text := fs.String("text", "", "texto a clickar (obligatorio)")
	tag := fs.String("tag", "", "filtrar por tag")
	exact := fs.Bool("exact", false, "match exacto")
	caseSensitive := fs.Bool("case-sensitive", false, "case-sensitive")
	_ = fs.Parse(args)
	if *text == "" {
		dieF("--text obligatorio")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpClickText(c, *text, browser.FindByTextOpts{
		Tag: *tag, Exact: *exact, CaseSensitive: *caseSensitive,
	}); err != nil {
		dieF("click-text: %v", err)
	}
}

func cmdHarRecord(args []string) {
	fs := flag.NewFlagSet("har-record", flag.ExitOnError)
	host, port := addConnFlags(fs)
	url := fs.String("url", "", "URL a navegar mientras se graba (vacio = graba sin navegar)")
	out := fs.String("out", "", "archivo destino (vacio = stdout)")
	settle := fs.Int("settle-ms", 1500, "ms a esperar tras la accion para eventos trailing")
	loadTimeout := fs.Int("load-timeout", 20, "timeout en segundos para wait-load")
	_ = fs.Parse(args)
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	har, err := browser.CdpHarRecord(c, func() error {
		if *url == "" {
			return nil
		}
		if err := browser.CdpNavigate(c, *url); err != nil {
			return err
		}
		return browser.CdpWaitLoad(c, time.Duration(*loadTimeout)*time.Second)
	}, *settle)
	if err != nil {
		fmt.Fprintln(os.Stderr, "har-record warning:", err)
	}
	if *out == "" {
		fmt.Println(har)
	} else {
		if err := os.WriteFile(*out, []byte(har), 0644); err != nil {
			dieF("har-record: write %s: %v", *out, err)
		}
		fmt.Println(*out)
	}
}

func cmdListTabs(args []string) {
	fs := flag.NewFlagSet("list-tabs", flag.ExitOnError)
	host, port := addConnFlags(fs)
	format := fs.String("format", "json", "json|text")
	onlyType := fs.String("type", "", "filtrar por type (page|iframe|service_worker|...)")
	_ = fs.Parse(args)
	tabs, err := browser.CdpListTabs(*host, *port)
	if err != nil {
		dieF("list-tabs: %v", err)
	}
	if *onlyType != "" {
		filt := tabs[:0]
		for _, t := range tabs {
			if t.Type == *onlyType {
				filt = append(filt, t)
			}
		}
		tabs = filt
	}
	if *format == "text" {
		for _, t := range tabs {
			fmt.Printf("%s\t%s\t%s\t%s\n", t.ID, t.Type, t.Title, t.URL)
		}
		return
	}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	_ = enc.Encode(tabs)
}

func cmdNewTab(args []string) {
	fs := flag.NewFlagSet("new-tab", flag.ExitOnError)
	host, port := addConnFlags(fs)
	startURL := fs.String("url", "", "URL inicial (vacio = about:blank)")
	_ = fs.Parse(args)
	tab, err := browser.CdpNewTab(*host, *port, *startURL)
	if err != nil {
		dieF("new-tab: %v", err)
	}
	fmt.Printf("id=%s url=%s ws=%s\n", tab.ID, tab.URL, tab.WebSocketDebuggerURL)
}

func cmdCloseTab(args []string) {
	fs := flag.NewFlagSet("close-tab", flag.ExitOnError)
	host, port := addConnFlags(fs)
	id := fs.String("id", "", "id de la pestaña (obligatorio)")
	_ = fs.Parse(args)
	if *id == "" {
		dieF("--id obligatorio")
	}
	if err := browser.CdpCloseTab(*host, *port, *id); err != nil {
		dieF("close-tab: %v", err)
	}
}

func cmdActivateTab(args []string) {
	fs := flag.NewFlagSet("activate-tab", flag.ExitOnError)
	host, port := addConnFlags(fs)
	id := fs.String("id", "", "id de la pestaña (obligatorio)")
	_ = fs.Parse(args)
	if *id == "" {
		dieF("--id obligatorio")
	}
	if err := browser.CdpActivateTab(*host, *port, *id); err != nil {
		dieF("activate-tab: %v", err)
	}
}

func cmdSetCookie(args []string) {
	fs := flag.NewFlagSet("set-cookie", flag.ExitOnError)
	host, port := addConnFlags(fs)
	name := fs.String("name", "", "nombre cookie (obligatorio)")
	value := fs.String("value", "", "valor cookie (obligatorio)")
	domain := fs.String("domain", "", "dominio cookie (obligatorio)")
	path := fs.String("path", "/", "path")
	httpOnly := fs.Bool("http-only", true, "marca HttpOnly")
	_ = fs.Parse(args)
	if *name == "" || *value == "" || *domain == "" {
		dieF("--name, --value y --domain obligatorios")
	}
	c := mustConnect(*host, *port)
	defer browser.CdpClose(c, 0)
	if err := browser.CdpSetCookie(c, *name, *value, *domain, *path, *httpOnly); err != nil {
		dieF("set-cookie: %v", err)
	}
}
