package browser

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

// HarHeader es el formato HAR 1.2 estandar (name/value).
type HarHeader struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

// HarEntry mapea una peticion HTTP capturada via CDP a un entry HAR.
type HarEntry struct {
	StartedDateTime string `json:"startedDateTime"` // ISO 8601
	Time            int64  `json:"time"`            // ms
	Request         struct {
		Method      string      `json:"method"`
		URL         string      `json:"url"`
		HTTPVersion string      `json:"httpVersion"`
		Headers     []HarHeader `json:"headers"`
		QueryString []HarHeader `json:"queryString"`
		Cookies     []HarHeader `json:"cookies"`
		HeadersSize int         `json:"headersSize"`
		BodySize    int         `json:"bodySize"`
		PostData    *struct {
			MimeType string `json:"mimeType"`
			Text     string `json:"text"`
		} `json:"postData,omitempty"`
	} `json:"request"`
	Response struct {
		Status      int         `json:"status"`
		StatusText  string      `json:"statusText"`
		HTTPVersion string      `json:"httpVersion"`
		Headers     []HarHeader `json:"headers"`
		Cookies     []HarHeader `json:"cookies"`
		Content     struct {
			Size     int    `json:"size"`
			MimeType string `json:"mimeType"`
			Text     string `json:"text,omitempty"`
		} `json:"content"`
		RedirectURL string `json:"redirectURL"`
		HeadersSize int    `json:"headersSize"`
		BodySize    int    `json:"bodySize"`
	} `json:"response"`
	Cache   struct{} `json:"cache"`
	Timings struct {
		Send    int `json:"send"`
		Wait    int `json:"wait"`
		Receive int `json:"receive"`
	} `json:"timings"`
	ServerIPAddress string `json:"serverIPAddress,omitempty"`
	XRequestID      string `json:"_requestId"`
	XError          string `json:"_error,omitempty"`
}

// HarLog es el wrapper top-level de un archivo HAR.
type HarLog struct {
	Log struct {
		Version string `json:"version"`
		Creator struct {
			Name    string `json:"name"`
			Version string `json:"version"`
		} `json:"creator"`
		Pages   []any      `json:"pages"`
		Entries []HarEntry `json:"entries"`
	} `json:"log"`
}

// CdpHarRecord activa Network.* events en la conexion, ejecuta la funcion
// `action` y captura todas las peticiones HTTP que ocurran durante. Despues
// de que `action` retorne, espera `settleMs` ms para recoger eventos
// rezagados (loadingFinished tipicamente llega despues que responseReceived).
//
// Retorna el HAR como JSON string listo para escribir a disco o servir.
//
// Body de respuestas NO se captura en v0 — requiere llamar Network.getResponseBody
// por requestId al recibir loadingFinished, lo cual añade latencia y complica.
// v1 puede activarlo via opts.
//
// La conexion debe estar abierta. La funcion no la cierra.
func CdpHarRecord(c *CDPConn, action func() error, settleMs int) (string, error) {
	if c == nil {
		return "", fmt.Errorf("cdp har record: conexion nula")
	}
	if action == nil {
		action = func() error { return nil }
	}
	if settleMs <= 0 {
		settleMs = 1500
	}

	type pending struct {
		entry      HarEntry
		startTs    float64 // CDP timestamp (monotonic seconds)
		endTs      float64
		hasRequest bool
	}
	var (
		mu      sync.Mutex
		entries = map[string]*pending{}
	)

	// 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
	}
	headersOf := func(h map[string]any) []HarHeader {
		out := []HarHeader{}
		for k, v := range h {
			if s, ok := v.(string); ok {
				out = append(out, HarHeader{Name: k, Value: s})
			}
		}
		return out
	}

	cancel1 := c.OnEvent("Network.requestWillBeSent", func(method string, p map[string]any) {
		reqID := str(p, "requestId")
		if reqID == "" {
			return
		}
		req, _ := p["request"].(map[string]any)
		hdrs, _ := req["headers"].(map[string]any)
		ts := num(p, "timestamp")
		wallTime := num(p, "wallTime") // unix epoch seconds (float)

		mu.Lock()
		defer mu.Unlock()
		e, ok := entries[reqID]
		if !ok {
			e = &pending{}
			entries[reqID] = e
		}
		e.entry.XRequestID = reqID
		e.entry.Request.Method = str(req, "method")
		e.entry.Request.URL = str(req, "url")
		e.entry.Request.HTTPVersion = "HTTP/1.1"
		e.entry.Request.Headers = headersOf(hdrs)
		e.entry.Request.QueryString = []HarHeader{}
		e.entry.Request.Cookies = []HarHeader{}
		e.entry.Request.HeadersSize = -1
		e.entry.Request.BodySize = -1
		if pd, ok := req["postData"].(string); ok && pd != "" {
			e.entry.Request.PostData = &struct {
				MimeType string `json:"mimeType"`
				Text     string `json:"text"`
			}{
				MimeType: str(hdrs, "Content-Type"),
				Text:     pd,
			}
		}
		if wallTime > 0 {
			e.entry.StartedDateTime = time.Unix(0, int64(wallTime*1e9)).UTC().Format(time.RFC3339Nano)
		} else {
			e.entry.StartedDateTime = time.Now().UTC().Format(time.RFC3339Nano)
		}
		e.startTs = ts
		e.hasRequest = true
	})
	defer cancel1()

	cancel2 := c.OnEvent("Network.responseReceived", func(method string, p map[string]any) {
		reqID := str(p, "requestId")
		if reqID == "" {
			return
		}
		resp, _ := p["response"].(map[string]any)
		hdrs, _ := resp["headers"].(map[string]any)
		mu.Lock()
		defer mu.Unlock()
		e, ok := entries[reqID]
		if !ok {
			e = &pending{}
			entries[reqID] = e
		}
		e.entry.Response.Status = int(num(resp, "status"))
		e.entry.Response.StatusText = str(resp, "statusText")
		e.entry.Response.HTTPVersion = str(resp, "protocol")
		if e.entry.Response.HTTPVersion == "" {
			e.entry.Response.HTTPVersion = "HTTP/1.1"
		}
		e.entry.Response.Headers = headersOf(hdrs)
		e.entry.Response.Cookies = []HarHeader{}
		e.entry.Response.Content.MimeType = str(resp, "mimeType")
		e.entry.Response.Content.Size = int(num(resp, "encodedDataLength"))
		e.entry.Response.HeadersSize = -1
		e.entry.Response.BodySize = -1
		e.entry.ServerIPAddress = str(resp, "remoteIPAddress")
	})
	defer cancel2()

	cancel3 := c.OnEvent("Network.loadingFinished", func(method string, p map[string]any) {
		reqID := str(p, "requestId")
		ts := num(p, "timestamp")
		mu.Lock()
		defer mu.Unlock()
		if e, ok := entries[reqID]; ok {
			e.endTs = ts
		}
	})
	defer cancel3()

	cancel4 := c.OnEvent("Network.loadingFailed", func(method string, p map[string]any) {
		reqID := str(p, "requestId")
		ts := num(p, "timestamp")
		errText := str(p, "errorText")
		mu.Lock()
		defer mu.Unlock()
		if e, ok := entries[reqID]; ok {
			e.endTs = ts
			e.entry.XError = errText
		}
	})
	defer cancel4()

	// Habilitar Network domain.
	if _, err := c.sendCDP("Network.enable", nil); err != nil {
		return "", fmt.Errorf("cdp har record: Network.enable: %w", err)
	}
	defer c.sendCDP("Network.disable", nil)

	// Ejecutar accion del usuario.
	actionErr := action()

	// Esperar eventos rezagados (loadingFinished suele llegar despues).
	time.Sleep(time.Duration(settleMs) * time.Millisecond)

	// Construir HAR.
	mu.Lock()
	defer mu.Unlock()
	var har HarLog
	har.Log.Version = "1.2"
	har.Log.Creator.Name = "navegator/cdp_har_record"
	har.Log.Creator.Version = "0.1.0"
	har.Log.Pages = []any{}
	har.Log.Entries = make([]HarEntry, 0, len(entries))
	for _, e := range entries {
		if !e.hasRequest {
			continue
		}
		if e.endTs > 0 && e.startTs > 0 {
			e.entry.Time = int64((e.endTs - e.startTs) * 1000)
		}
		// HAR exige timings con send/wait/receive >= 0; usar 0/total/0 si no
		// tenemos breakdown.
		e.entry.Timings.Send = 0
		e.entry.Timings.Wait = int(e.entry.Time)
		e.entry.Timings.Receive = 0
		har.Log.Entries = append(har.Log.Entries, e.entry)
	}

	out, err := json.MarshalIndent(har, "", "  ")
	if err != nil {
		return "", fmt.Errorf("cdp har record: marshal: %w", err)
	}
	if actionErr != nil {
		return string(out), fmt.Errorf("cdp har record: action: %w", actionErr)
	}
	return string(out), nil
}
