// Package decnetfp provides Caddy modules for HTTP fingerprint capture.
//
// Registered modules:
//   - caddy.listeners.decnet_fp      — post-TLS listener wrapper that taps
//     the h2 client preface (SETTINGS + HEADERS frames via persistent HPACK
//     decoder) and h1 request-line / header bytes, emitting ordered header
//     name lists to /run/decnet/fp.sock (unix datagram).
//   - http.handlers.decnet_fp        — HTTP middleware that emits an
//     access_log record (status code, bytes, protocol) after each response.
//   - caddy.logging.encoders.decnet_jsonl — log encoder stub (registered
//     but not wired into Caddyfile; access_log comes from the handler above).
//
// All modules write JSON lines to a unix datagram socket whose path is
// controlled by DECNET_FP_SOCK (default: /run/decnet/fp.sock).
package decnetfp

import (
	"bytes"
	"crypto/tls"
	"encoding/binary"
	"encoding/json"
	"io"
	"net"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
	"github.com/quic-go/quic-go/http3"
	"go.uber.org/zap"
	"golang.org/x/net/http2/hpack"
)

func init() {
	caddy.RegisterModule(FPListenerWrapper{})
	caddy.RegisterModule(FPHandler{})
	caddy.RegisterModule(DecnetJSONLEncoder{})
	httpcaddyfile.RegisterHandlerDirective("decnet_fp", parseFPHandler)
}

func parseFPHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
	var fp FPHandler
	return &fp, fp.UnmarshalCaddyfile(h.Dispenser)
}

func sockPath() string {
	if p := os.Getenv("DECNET_FP_SOCK"); p != "" {
		return p
	}
	return "/run/decnet/fp.sock"
}

// ── unix datagram sender ──────────────────────────────────────────────────────

var (
	sockMu   sync.Mutex
	sockConn *net.UnixConn
)

func sendFP(record map[string]interface{}) {
	b, err := json.Marshal(record)
	if err != nil {
		return
	}
	sockMu.Lock()
	defer sockMu.Unlock()
	if sockConn == nil {
		conn, err := net.DialUnix("unixgram", nil, &net.UnixAddr{Name: sockPath(), Net: "unixgram"})
		if err != nil {
			return
		}
		sockConn = conn
	}
	sockConn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)) //nolint:errcheck
	sockConn.Write(b)                                                 //nolint:errcheck
}

// ── caddy.listeners.decnet_fp ─────────────────────────────────────────────────

// FPListenerWrapper is a post-TLS Caddy listener wrapper that:
//   - For h2 ALPN connections: taps the h2 client preface (SETTINGS frame)
//     and then continuously parses HEADERS/CONTINUATION frames via a
//     persistent HPACK decoder, emitting ordered header name lists.
//   - For h1 ALPN connections (or plain connections): taps the first request's
//     header bytes, emitting ordered header names from the wire.
//
// Place it AFTER the TLS listener wrapper so it sees post-TLS data:
//
//	listener_wrappers {
//	    tls
//	    decnet_fp
//	}
type FPListenerWrapper struct {
	logger *zap.Logger
}

func (FPListenerWrapper) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "caddy.listeners.decnet_fp",
		New: func() caddy.Module { return new(FPListenerWrapper) },
	}
}

func (w *FPListenerWrapper) Provision(ctx caddy.Context) error {
	w.logger = ctx.Logger()
	return nil
}

func (w *FPListenerWrapper) WrapListener(ln net.Listener) net.Listener {
	return &fpListener{Listener: ln, logger: w.logger}
}

func (w *FPListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	return nil
}

type fpListener struct {
	net.Listener
	logger *zap.Logger
}

func (l *fpListener) Accept() (net.Conn, error) {
	conn, err := l.Listener.Accept()
	if err != nil {
		return conn, err
	}
	remote := conn.RemoteAddr().String()

	tlsConn, ok := conn.(*tls.Conn)
	if !ok {
		// Plain (cleartext) connection — peek to distinguish h2c from h1.
		return &plainTappingConn{Conn: conn, remoteAddr: remote}, nil
	}
	state := tlsConn.ConnectionState()
	switch state.NegotiatedProtocol {
	case "h2":
		return &h2TappingConn{Conn: conn, remoteAddr: remote}, nil
	default:
		// http/1.1 ALPN or no ALPN — post-TLS plaintext is h1; safe to
		// use plainTappingConn (h2c preface won't appear after TLS).
		return &plainTappingConn{Conn: conn, remoteAddr: remote}, nil
	}
}

// ── Plain connection tap (h1 and h2c) ────────────────────────────────────────

const h1MaxHeaderBuf = 8192

// plainTappingConn handles post-accept connections that are not TLS h2.
// It peeks the first bytes to distinguish h2c prior-knowledge from h1,
// then routes to the appropriate parser.  Used for both cleartext (port 80)
// and TLS-h1 (ALPN "" or "http/1.1") connections.
type plainTappingConn struct {
	net.Conn
	once       sync.Once
	buf        bytes.Buffer
	reader     io.Reader
	remoteAddr string
}

// tapWriter is a non-blocking io.Writer that drops if the channel is full.
type tapWriter struct {
	ch chan<- []byte
}

func (t tapWriter) Write(p []byte) (int, error) {
	cp := make([]byte, len(p))
	copy(cp, p)
	select {
	case t.ch <- cp:
	default:
	}
	return len(p), nil
}

func (c *plainTappingConn) Read(b []byte) (int, error) {
	c.once.Do(func() {
		// Peek exactly len(h2ClientPreface) bytes to detect h2c prior knowledge.
		preface := make([]byte, len(h2ClientPreface))
		n, _ := io.ReadFull(c.Conn, preface)
		c.buf.Write(preface[:n])

		if n == len(h2ClientPreface) && string(preface) == h2ClientPreface {
			// h2c prior-knowledge connection — run the same SETTINGS+HPACK tap.
			hdr9 := make([]byte, 9)
			if m, err := io.ReadFull(c.Conn, hdr9); m == 9 && err == nil {
				c.buf.Write(hdr9)
				frameLen := int(hdr9[0])<<16 | int(hdr9[1])<<8 | int(hdr9[2])
				frameType := hdr9[3]
				if frameType == 0x4 && frameLen > 0 && frameLen <= 16384 {
					payload := make([]byte, frameLen)
					if _, err := io.ReadFull(c.Conn, payload); err == nil {
						c.buf.Write(payload)
						go parseAndSendH2Settings(c.remoteAddr, payload)
					}
				}
			}
			tap := make(chan []byte, 256)
			c.reader = io.TeeReader(io.MultiReader(&c.buf, c.Conn), tapWriter{ch: tap})
			go parseH2HeadersLoop(c.remoteAddr, tap)
			return
		}

		// h1 — buffer up to h1MaxHeaderBuf or until \r\n\r\n.
		scratch := make([]byte, h1MaxHeaderBuf)
		for c.buf.Len() < h1MaxHeaderBuf {
			nn, err := c.Conn.Read(scratch[:h1MaxHeaderBuf-c.buf.Len()])
			c.buf.Write(scratch[:nn])
			if bytes.Contains(c.buf.Bytes(), []byte("\r\n\r\n")) {
				break
			}
			if err != nil {
				break
			}
		}
		go parseAndSendH1Headers(c.remoteAddr, c.buf.Bytes())
		c.reader = io.MultiReader(&c.buf, c.Conn)
	})
	if c.reader == nil {
		return c.Conn.Read(b)
	}
	return c.reader.Read(b)
}

func parseAndSendH1Headers(remoteAddr string, raw []byte) {
	idx := bytes.Index(raw, []byte("\r\n\r\n"))
	if idx < 0 {
		idx = len(raw)
	}
	lines := bytes.Split(raw[:idx], []byte("\r\n"))
	if len(lines) == 0 {
		return
	}
	// First line: "GET /path HTTP/1.1"
	requestLine := string(lines[0])
	var method, path, proto string
	parts := bytes.Fields(lines[0])
	if len(parts) >= 3 {
		method = string(parts[0])
		path = string(parts[1])
		proto = string(parts[2])
	}

	var ordered [][]string // [[name, value], ...]
	var cookie, acceptLang string
	for _, line := range lines[1:] {
		sep := bytes.IndexByte(line, ':')
		if sep < 0 {
			continue
		}
		name := string(bytes.ToLower(bytes.TrimSpace(line[:sep])))
		value := string(bytes.TrimSpace(line[sep+1:]))
		ordered = append(ordered, []string{name, value})
		switch name {
		case "cookie":
			cookie = value
		case "accept-language":
			acceptLang = value
		}
	}

	sendFP(map[string]interface{}{
		"kind":            "http_request_headers",
		"remote_addr":     remoteAddr,
		"proto_tag":       "h1",
		"request_line":    requestLine,
		"method":          method,
		"path":            path,
		"proto":           proto,
		"headers_ordered": ordered,
		"cookie":          cookie,
		"accept_language": acceptLang,
		"ts":              time.Now().UTC().Format(time.RFC3339),
	})
}

// ── H2 client preface + HPACK continuous tap ──────────────────────────────────

const h2ClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

type h2TappingConn struct {
	net.Conn
	once       sync.Once
	buf        bytes.Buffer
	reader     io.Reader
	remoteAddr string
}

func (c *h2TappingConn) Read(b []byte) (int, error) {
	c.once.Do(func() {
		// Buffer client preface (24 B) + first frame header (9 B).
		hdr := make([]byte, len(h2ClientPreface)+9)
		if _, err := io.ReadFull(c.Conn, hdr); err != nil {
			c.buf.Write(hdr)
			c.reader = io.MultiReader(&c.buf, c.Conn)
			return
		}
		c.buf.Write(hdr)

		frameLen := int(hdr[len(h2ClientPreface)])<<16 |
			int(hdr[len(h2ClientPreface)+1])<<8 |
			int(hdr[len(h2ClientPreface)+2])
		frameType := hdr[len(h2ClientPreface)+3]

		if frameType == 0x4 && frameLen > 0 && frameLen <= 16384 {
			payload := make([]byte, frameLen)
			if _, err := io.ReadFull(c.Conn, payload); err == nil {
				c.buf.Write(payload)
				go parseAndSendH2Settings(c.remoteAddr, payload)
			}
		}

		// Start HPACK continuous parse on a non-blocking channel tap.
		tap := make(chan []byte, 256)
		c.reader = io.TeeReader(io.MultiReader(&c.buf, c.Conn), tapWriter{ch: tap})
		go parseH2HeadersLoop(c.remoteAddr, tap)
	})
	if c.reader == nil {
		return c.Conn.Read(b)
	}
	return c.reader.Read(b)
}

func parseAndSendH2Settings(remoteAddr string, payload []byte) {
	settings := make(map[string]uint32)
	frameOrder := make([]uint16, 0, len(payload)/6)
	for i := 0; i+6 <= len(payload); i += 6 {
		id := binary.BigEndian.Uint16(payload[i : i+2])
		val := binary.BigEndian.Uint32(payload[i+2 : i+6])
		settings[settingName(id)] = val
		frameOrder = append(frameOrder, id)
	}
	sendFP(map[string]interface{}{
		"kind":        "h2_settings",
		"remote_addr": remoteAddr,
		"settings":    settings,
		"frame_order": frameOrder,
		"ts":          time.Now().UTC().Format(time.RFC3339),
	})
}

func settingName(id uint16) string {
	switch id {
	case 0x1:
		return "HEADER_TABLE_SIZE"
	case 0x2:
		return "ENABLE_PUSH"
	case 0x3:
		return "MAX_CONCURRENT_STREAMS"
	case 0x4:
		return "INITIAL_WINDOW_SIZE"
	case 0x5:
		return "MAX_FRAME_SIZE"
	case 0x6:
		return "MAX_HEADER_LIST_SIZE"
	case 0x8:
		return "ENABLE_CONNECT_PROTOCOL"
	default:
		if id >= 0xf000 {
			return "GREASE"
		}
		return "UNKNOWN"
	}
}

// hpackConn holds per-connection HPACK decoder state.
type hpackConn struct {
	decoder      *hpack.Decoder
	currentFields []hpack.HeaderField
}

func newHpackConn(maxTableSize uint32) *hpackConn {
	hc := &hpackConn{}
	hc.decoder = hpack.NewDecoder(maxTableSize, func(f hpack.HeaderField) {
		hc.currentFields = append(hc.currentFields, f)
	})
	return hc
}

// decode decodes a header block fragment, appending to existing and returning combined.
func (hc *hpackConn) decode(fragment []byte, existing []hpack.HeaderField) []hpack.HeaderField {
	hc.currentFields = existing
	hc.decoder.Write(fragment) //nolint:errcheck
	return hc.currentFields
}

func parseH2HeadersLoop(remoteAddr string, tap <-chan []byte) {
	var buf []byte
	prefaceLen := len(h2ClientPreface)
	prefaceSkipped := false
	hc := newHpackConn(4096)

	type streamState struct {
		fields []hpack.HeaderField
	}
	streams := make(map[uint32]*streamState)

	for chunk := range tap {
		buf = append(buf, chunk...)

		if !prefaceSkipped {
			if len(buf) < prefaceLen {
				continue
			}
			buf = buf[prefaceLen:]
			prefaceSkipped = true
		}

		for len(buf) >= 9 {
			frameLen := int(buf[0])<<16 | int(buf[1])<<8 | int(buf[2])
			frameType := buf[3]
			flags := buf[4]
			streamID := binary.BigEndian.Uint32(buf[5:9]) & 0x7fffffff

			total := 9 + frameLen
			if len(buf) < total {
				break
			}
			payload := buf[9:total]
			buf = buf[total:]

			switch frameType {
			case 0x4: // SETTINGS from client
				if flags&0x1 != 0 { // ACK
					break
				}
				for i := 0; i+6 <= len(payload); i += 6 {
					id := binary.BigEndian.Uint16(payload[i : i+2])
					val := binary.BigEndian.Uint32(payload[i+2 : i+6])
					if id == 0x1 { // SETTINGS_HEADER_TABLE_SIZE
						hc.decoder.SetMaxDynamicTableSize(val)
					}
				}

			case 0x1, 0x9: // HEADERS, CONTINUATION
				fragment := payload
				if frameType == 0x1 {
					off := 0
					var padLen byte
					if flags&0x08 != 0 { // PADDED
						if len(fragment) < 1 {
							continue
						}
						padLen = fragment[0]
						off = 1
					}
					if flags&0x20 != 0 { // PRIORITY
						off += 5
					}
					end := len(fragment) - int(padLen)
					if off > end || end < 0 {
						continue
					}
					fragment = fragment[off:end]
				}

				ss, ok := streams[streamID]
				if !ok {
					ss = &streamState{}
					streams[streamID] = ss
				}
				ss.fields = hc.decode(fragment, ss.fields)

				if flags&0x04 != 0 { // END_HEADERS
					emitH2RequestHeaders(remoteAddr, streamID, ss.fields)
					delete(streams, streamID)
				}
			}
		}
	}
}

func emitH2RequestHeaders(remoteAddr string, streamID uint32, fields []hpack.HeaderField) {
	ordered := make([][]string, 0, len(fields))
	var cookie, acceptLang, method, path string
	for _, f := range fields {
		ordered = append(ordered, []string{f.Name, f.Value})
		switch f.Name {
		case "cookie":
			cookie = f.Value
		case "accept-language":
			acceptLang = f.Value
		case ":method":
			method = f.Value
		case ":path":
			path = f.Value
		}
	}
	sendFP(map[string]interface{}{
		"kind":            "http_request_headers",
		"remote_addr":     remoteAddr,
		"stream_id":       streamID,
		"proto_tag":       "h2",
		"method":          method,
		"path":            path,
		"headers_ordered": ordered,
		"cookie":          cookie,
		"accept_language": acceptLang,
		"ts":              time.Now().UTC().Format(time.RFC3339),
	})
}

// ── http.handlers.decnet_fp ───────────────────────────────────────────────────

// FPHandler is HTTP middleware that emits an access_log record (status code,
// bytes, proto) after each response via the fp socket.  For h3 requests it
// also emits a best-effort http_request_headers record (header order degraded
// — QPACK decode order is preserved by quic-go but the Go map randomises it
// further; canonical h3 header order requires request-stream QPACK tapping
// which is a follow-up task).
type FPHandler struct {
	logger *zap.Logger
}

func (FPHandler) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.handlers.decnet_fp",
		New: func() caddy.Module { return new(FPHandler) },
	}
}

func (h *FPHandler) Provision(ctx caddy.Context) error {
	h.logger = ctx.Logger()
	return nil
}

func (h *FPHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	return nil
}

// responseCapture captures the status code and bytes written.
type responseCapture struct {
	http.ResponseWriter
	status int
	bytes  int
}

func (rc *responseCapture) WriteHeader(status int) {
	rc.status = status
	rc.ResponseWriter.WriteHeader(status)
}

func (rc *responseCapture) Write(b []byte) (int, error) {
	n, err := rc.ResponseWriter.Write(b)
	rc.bytes += n
	if rc.status == 0 {
		rc.status = 200
	}
	return n, err
}

func (h *FPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	rc := &responseCapture{ResponseWriter: w}
	err := next.ServeHTTP(rc, r)

	protoTag := "h1"
	if r.ProtoMajor == 2 {
		protoTag = "h2"
	} else if r.ProtoMajor == 3 {
		protoTag = "h3"
	}

	status := rc.status
	if status == 0 {
		status = 200
	}

	go sendFP(map[string]interface{}{
		"kind":        "access_log",
		"remote_addr": r.RemoteAddr,
		"method":      r.Method,
		"path":        r.URL.Path,
		"proto":       r.Proto,
		"proto_tag":   protoTag,
		"status":      status,
		"bytes":       rc.bytes,
		"ts":          time.Now().UTC().Format(time.RFC3339),
	})

	// For h3: emit h3_settings (once per connection, deduped by remote_addr),
	// then emit best-effort http_request_headers (map order — QPACK frame order
	// is not available without a stream tap; degraded but usable for JA4H).
	if r.ProtoMajor == 3 {
		if settingser, ok := w.(http3.Settingser); ok {
			select {
			case <-settingser.ReceivedSettings():
				s := settingser.Settings()
				settingsMap := make(map[string]interface{})
				if s.EnableDatagrams {
					settingsMap["H3_DATAGRAM"] = uint64(1)
				}
				if s.EnableExtendedConnect {
					settingsMap["ENABLE_CONNECT_PROTOCOL"] = uint64(1)
				}
				for id, val := range s.Other {
					settingsMap[h3SettingName(id)] = val
				}
				go sendFP(map[string]interface{}{
					"kind":        "h3_settings",
					"remote_addr": r.RemoteAddr,
					"settings":    settingsMap,
					"ts":          time.Now().UTC().Format(time.RFC3339),
				})
			default:
				// Settings not yet received — skip; the access_log record is sufficient.
			}
		}

		ordered := make([][]string, 0, len(r.Header))
		var cookie, acceptLang string
		for name, vals := range r.Header {
			v := ""
			if len(vals) > 0 {
				v = vals[0]
			}
			ordered = append(ordered, []string{strings.ToLower(name), v})
			switch http.CanonicalHeaderKey(name) {
			case "Cookie":
				cookie = v
			case "Accept-Language":
				acceptLang = v
			}
		}
		go sendFP(map[string]interface{}{
			"kind":            "http_request_headers",
			"remote_addr":     r.RemoteAddr,
			"proto_tag":       "h3",
			"method":          r.Method,
			"path":            r.URL.Path,
			"headers_ordered": ordered,
			"cookie":          cookie,
			"accept_language": acceptLang,
			"h3_order_note":   "degraded_map_iteration",
			"ts":              time.Now().UTC().Format(time.RFC3339),
		})
	}

	return err
}

var (
	_ caddy.Provisioner           = (*FPListenerWrapper)(nil)
	_ caddy.ListenerWrapper       = (*FPListenerWrapper)(nil)
	_ caddyfile.Unmarshaler       = (*FPListenerWrapper)(nil)
	_ caddy.Provisioner           = (*FPHandler)(nil)
	_ caddyhttp.MiddlewareHandler = (*FPHandler)(nil)
	_ caddyfile.Unmarshaler       = (*FPHandler)(nil)
)

// ── caddy.logging.encoders.decnet_jsonl ──────────────────────────────────────

// DecnetJSONLEncoder is a registered Caddy module stub.  A full zapcore.Encoder
// implementation (required for `log { format decnet_jsonl }`) is deferred;
// access_log records are emitted by FPHandler.ServeHTTP instead.
type DecnetJSONLEncoder struct {
	logger *zap.Logger
}

func (DecnetJSONLEncoder) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "caddy.logging.encoders.decnet_jsonl",
		New: func() caddy.Module { return new(DecnetJSONLEncoder) },
	}
}

func (e *DecnetJSONLEncoder) Provision(ctx caddy.Context) error {
	e.logger = ctx.Logger()
	return nil
}
