package browser

import (
	"encoding/json"
	"fmt"
	"strings"
	"sync"
	"time"
)

// ConsoleEntry es una entrada del log de consola/diagnostico capturada via CDP
// durante una ventana temporal. Type clasifica el origen:
//   - "log"/"info"/"warn"/"error"/"debug" — Runtime.consoleAPICalled (console.*)
//   - "exception" — Runtime.exceptionThrown (errores JS no capturados)
//   - el level de Log.entryAdded ("verbose"/"info"/"warning"/"error") para
//     avisos del propio navegador (network, security, deprecaciones...)
type ConsoleEntry struct {
	Type      string  `json:"type"`      // log|info|warn|warning|error|debug|exception|verbose
	Text      string  `json:"text"`      // mensaje legible (args concatenados / descripcion + stack)
	URL       string  `json:"url"`       // URL del script o recurso, si Chrome lo informa
	Line      int     `json:"line"`      // numero de linea (1-based), 0 si desconocido
	Timestamp float64 `json:"timestamp"` // CDP timestamp (monotonic seconds) o wall time
}

// consoleCollectDefaultMax es el tope de entradas por defecto cuando el caller
// pasa maxEntries <= 0. Acota la salida en paginas verbosas (setInterval ruidoso,
// SPA que loguea sin parar) para no devolver cientos de entradas y reventar el
// output del tool.
const consoleCollectDefaultMax = 200

// CdpCollectConsole habilita los dominios Runtime y Log en la conexion, se
// suscribe a los eventos de consola/excepcion/log del navegador y acumula todo
// lo que ocurra durante `durationMs` milisegundos, hasta un maximo de
// `maxEntries` entradas. Es un SNAPSHOT temporal: captura solo lo emitido dentro
// de la ventana, no el historico previo de la pagina. Si durationMs <= 0 usa
// 1500ms por defecto; si maxEntries <= 0 usa 200 por defecto.
//
// Dos defensas contra el backlog de una conexion del pool que lleva rato abierta
// con Runtime habilitado (donde Runtime.enable flushea consoleAPICalled rezagados
// con timestamps antiguos, y un setInterval verboso puede inundar):
//   - Filtro por timestamp: se captura `startMs` (wall time, ms epoch) JUSTO antes
//     de habilitar los dominios y solo se acumulan eventos cuyo timestamp sea >=
//     startMs. Los eventos `consoleAPICalled`/`exceptionThrown`/`Log.entryAdded`
//     traen `timestamp` en ms epoch, asi que los rezagados del flush (anteriores
//     a startMs) se descartan. Eventos sin timestamp (0) se aceptan: no hay forma
//     de fecharlos y casi siempre son nuevos.
//   - Cap por cantidad: alcanzado `maxEntries` se dejan de acumular entradas, pero
//     la funcion NO corta la ventana — sigue durmiendo hasta `durationMs` para no
//     dejar los dominios CDP en estado raro (handlers a medio drenar). Las entradas
//     posteriores al cap simplemente se descartan; el flag de truncamiento se
//     refleja como una ConsoleEntry final de Type "_truncated".
//
// Eventos capturados y como se mapean a ConsoleEntry.Type:
//   - Runtime.consoleAPICalled  -> el `type` del evento (log/info/warning/error/...)
//   - Runtime.exceptionThrown   -> "exception" (texto = descripcion + stack)
//   - Log.entryAdded            -> el `level` del entry (warning/error del browser)
//
// Robusta ante silencio: si no llega ningun evento devuelve un slice vacio
// (no nil, no error). La conexion debe estar abierta; la funcion no la cierra.
func CdpCollectConsole(c *CDPConn, durationMs int, maxEntries int) ([]ConsoleEntry, error) {
	if c == nil {
		return nil, fmt.Errorf("cdp collect console: conexion nula")
	}
	if durationMs <= 0 {
		durationMs = 1500
	}
	if maxEntries <= 0 {
		maxEntries = consoleCollectDefaultMax
	}

	// startMs marca el inicio de la ventana en ms epoch (mismo dominio que el
	// `timestamp` de los eventos CDP). Eventos anteriores = backlog -> se descartan.
	startMs := float64(time.Now().UnixMilli())

	var (
		mu        sync.Mutex
		entries   = make([]ConsoleEntry, 0, 16)
		truncated bool
	)

	// add intenta acumular una entrada respetando el filtro por timestamp y el cap.
	// Devuelve sin hacer nada si la entrada es backlog o si ya se alcanzo el tope.
	add := func(e ConsoleEntry) {
		// Descartar backlog: eventos fechados antes del inicio de la ventana.
		// Timestamp 0 (sin fecha) se acepta — no se puede clasificar como viejo.
		if e.Timestamp != 0 && e.Timestamp < startMs {
			return
		}
		mu.Lock()
		if len(entries) >= maxEntries {
			truncated = true
			mu.Unlock()
			return
		}
		entries = append(entries, e)
		mu.Unlock()
	}

	// Helpers para extraer campos de map[string]any sin pelearse con cast.
	str := func(m map[string]any, k string) string {
		if v, ok := m[k]; ok {
			if s, ok := v.(string); ok {
				return s
			}
		}
		return ""
	}
	num := func(m map[string]any, k string) float64 {
		if v, ok := m[k]; ok {
			if f, ok := v.(float64); ok {
				return f
			}
		}
		return 0
	}

	// argToText convierte un RemoteObject de Runtime a una representacion legible.
	// Para primitivas usa `value`; para objetos sin value cae a `description` o
	// `unserializableValue`; ultimo recurso, el `type`.
	argToText := func(arg map[string]any) string {
		if v, ok := arg["value"]; ok && v != nil {
			if s, ok := v.(string); ok {
				return s
			}
			// objetos/arrays serializados por valor -> JSON real.
			if b, err := json.Marshal(v); err == nil {
				return string(b)
			}
			return fmt.Sprintf("%v", v)
		}
		if d := str(arg, "description"); d != "" {
			return d
		}
		if u := str(arg, "unserializableValue"); u != "" {
			return u
		}
		return str(arg, "type")
	}

	// --- Runtime.consoleAPICalled: console.log / info / warn / error / ... ---
	cancel1 := c.OnEvent("Runtime.consoleAPICalled", func(_ string, p map[string]any) {
		entry := ConsoleEntry{
			Type:      str(p, "type"),
			Timestamp: num(p, "timestamp"),
		}
		// Concatenar los args a un texto legible separado por espacios.
		if rawArgs, ok := p["args"].([]any); ok {
			parts := make([]string, 0, len(rawArgs))
			for _, ra := range rawArgs {
				if am, ok := ra.(map[string]any); ok {
					parts = append(parts, argToText(am))
				}
			}
			entry.Text = strings.Join(parts, " ")
		}
		// stackTrace -> primer frame para URL/linea.
		if st, ok := p["stackTrace"].(map[string]any); ok {
			if frames, ok := st["callFrames"].([]any); ok && len(frames) > 0 {
				if f0, ok := frames[0].(map[string]any); ok {
					entry.URL = str(f0, "url")
					// lineNumber es 0-based en CDP; +1 para ser 1-based legible.
					if ln := int(num(f0, "lineNumber")); ln >= 0 {
						entry.Line = ln + 1
					}
				}
			}
		}
		add(entry)
	})
	defer cancel1()

	// --- Runtime.exceptionThrown: errores JS no capturados ---
	cancel2 := c.OnEvent("Runtime.exceptionThrown", func(_ string, p map[string]any) {
		entry := ConsoleEntry{
			Type:      "exception",
			Timestamp: num(p, "timestamp"),
		}
		ed, _ := p["exceptionDetails"].(map[string]any)
		if ed != nil {
			// Texto base de la excepcion.
			text := str(ed, "text")
			// Si hay un objeto de excepcion con descripcion (stack completo), preferirlo.
			if exc, ok := ed["exception"].(map[string]any); ok {
				if desc := str(exc, "description"); desc != "" {
					if text != "" && !strings.Contains(desc, text) {
						text = text + ": " + desc
					} else {
						text = desc
					}
				}
			}
			entry.Text = text
			entry.URL = str(ed, "url")
			// lineNumber 0-based -> 1-based.
			if ln := int(num(ed, "lineNumber")); ln >= 0 {
				entry.Line = ln + 1
			}
			// stackTrace top frame como respaldo de URL/linea.
			if entry.URL == "" {
				if st, ok := ed["stackTrace"].(map[string]any); ok {
					if frames, ok := st["callFrames"].([]any); ok && len(frames) > 0 {
						if f0, ok := frames[0].(map[string]any); ok {
							entry.URL = str(f0, "url")
							if entry.Line == 0 {
								if ln := int(num(f0, "lineNumber")); ln >= 0 {
									entry.Line = ln + 1
								}
							}
						}
					}
				}
			}
		}
		if entry.Text == "" {
			entry.Text = "uncaught exception"
		}
		add(entry)
	})
	defer cancel2()

	// --- Log.entryAdded: avisos del propio navegador (network, security...) ---
	cancel3 := c.OnEvent("Log.entryAdded", func(_ string, p map[string]any) {
		le, _ := p["entry"].(map[string]any)
		if le == nil {
			return
		}
		// Log.entryAdded reporta `timestamp` en segundos epoch (a diferencia de
		// consoleAPICalled/exceptionThrown que lo dan en ms). Normalizar a ms para
		// que el filtro por startMs compare en el mismo dominio. Heurística: si el
		// valor parece segundos (varios órdenes por debajo de un ms epoch actual),
		// multiplicar por 1000.
		ts := num(le, "timestamp")
		if ts > 0 && ts < startMs/100 {
			ts *= 1000
		}
		entry := ConsoleEntry{
			Type:      str(le, "level"), // verbose|info|warning|error
			Text:      str(le, "text"),
			URL:       str(le, "url"),
			Line:      int(num(le, "lineNumber")),
			Timestamp: ts,
		}
		add(entry)
	})
	defer cancel3()

	// Habilitar dominios. Runtime.enable provoca un flush de consoleAPICalled
	// rezagados; Log.enable abre el stream de avisos del navegador.
	if _, err := c.sendCDP("Runtime.enable", nil); err != nil {
		return nil, fmt.Errorf("cdp collect console: Runtime.enable: %w", err)
	}
	if _, err := c.sendCDP("Log.enable", nil); err != nil {
		// Log.enable puede no estar disponible en algunos targets; no es fatal,
		// seguimos capturando Runtime.*. Deshabilitar Runtime no hace falta.
		_ = err
	}
	// No deshabilitamos Runtime al salir: otras funciones (ej. cdp_pick_element_js)
	// dependen de consoleAPICalled. Solo cerramos Log que abrimos aqui.
	defer c.sendCDP("Log.disable", nil)

	// Ventana de captura. No hacemos early-return al alcanzar el cap: seguimos
	// durmiendo la ventana completa para no dejar los dominios CDP a medio drenar.
	time.Sleep(time.Duration(durationMs) * time.Millisecond)

	mu.Lock()
	out := make([]ConsoleEntry, len(entries))
	copy(out, entries)
	wasTruncated := truncated
	mu.Unlock()

	// Senal de truncamiento limpia: una entrada final que el caller puede detectar
	// por Type == "_truncated" sin cambiar la forma del slice.
	if wasTruncated {
		out = append(out, ConsoleEntry{
			Type:      "_truncated",
			Text:      fmt.Sprintf("output truncado al alcanzar maxEntries=%d; entradas posteriores descartadas", maxEntries),
			Timestamp: float64(time.Now().UnixMilli()),
		})
	}
	return out, nil
}
