package ebpf

import (
	"debug/elf"
	"encoding/binary"
	"fmt"
	"strconv"
	"strings"
)

// TLSOffsets contains struct offsets for extracting TLS key material from an
// SSL* object at runtime. Two chain variants exist:
//
// 4-hop (OpenSSL 3.2+):
//
//	SSL* + SSLToWRL → WRL* + WRLToEncCtx → EVP_CIPHER_CTX* + EncCtxToAlgctx → algctx* + AlgctxToH → H
//
// 3-hop (OpenSSL 3.0-3.1):
//
//	SSL* + SSLToWRL → EVP_CIPHER_CTX* + EncCtxToAlgctx → algctx* + AlgctxToH → H
//
// The 3-hop chain skips the record layer indirection. SSLToWRL stores the
// enc_write_ctx offset directly, and WRLToEncCtx=0 signals 3-hop mode to BPF.
//
// These are version-specific and must match the OpenSSL build in the container.
type TLSOffsets struct {
	SSLToWRL       uint32 // 4-hop: SSL* → WRL*. 3-hop: SSL* → EVP_CIPHER_CTX* (when WRLToEncCtx=0)
	WRLToEncCtx    uint32 // wrl* → EVP_CIPHER_CTX*. 0 = 3-hop mode (no record layer)
	EncCtxToAlgctx uint32 // enc_ctx* + off → algctx/PROV_GCM_CTX* (pointer deref)
	AlgctxToH      uint32 // algctx* + off → H (16 bytes, direct read)
	SSLToVersion   uint32 // SSL_CONNECTION* + off → int version (0xFFFFFFFF=unknown, use heuristic)
	// SSLToWBIO is the offset from the SSL/SSL_CONNECTION pointer to its
	// `wbio` BIO* field. The BPF data plane reads SSL → wbio → BIO.num to
	// recover the socket fd, then looks it up in conn_ip_map to confirm
	// client-mode (set by connect()). 3.0/3.1 use struct ssl_st where
	// wbio sits at 24; 3.2+ wrapped SSL in ssl_connection_st and moved it
	// to 88. Before this field existed, the BPF program hardcoded 88 —
	// which silently broke every 3.0/3.1 workload by reading garbage and
	// tripping the server-mode guard in bpf_uprobe_ssl_write.
	SSLToWBIO uint32
}

// opensslOffsetTable maps OpenSSL major.minor version strings to their TLS
// struct offsets. Determined by compiling OpenSSL at each version and
// inspecting struct layouts with pahole/gdb/offsetof.
//
// To add a new version:
//  1. Build OpenSSL with debug symbols
//  2. Use pahole or gdb to find:
//     - offsetof(SSL, enc_write_ctx)
//     - offsetof(EVP_CIPHER_CTX, cipher_data)
//     - offsetof(EVP_AES_GCM_CTX, gcm) + offsetof(GCM128_CONTEXT, Htable)
//     - offsetof(SSL, s3) + offsetof(SSL3_STATE, tmp) + offsetof(tmp, new_cipher) + offsetof(SSL_CIPHER, id)
//  3. Add a row to this table
//
// TODO: These offsets are placeholders. Must be verified per version/arch.
var opensslOffsetTable = map[string]TLSOffsets{
	// Offsets verified via pahole on both aarch64 and x86_64 (identical).
	// Generated by tools/openssl-offsets/discover.sh.
	//
	// 4-hop chain (3.2+): SSL_CONNECTION.rlayer.wrl → enc_ctx → algctx → gcm.H
	// 3-hop chain (3.0-3.1): SSL.enc_write_ctx → algctx → gcm.H
	//   SSLToWRL stores enc_write_ctx offset; WRLToEncCtx=0 signals 3-hop to BPF.

	// SSLToWBIO is the SSL → wbio field offset (used by ssl_read_fd in the
	// BPF data plane). 3.0/3.1 use struct ssl_st where wbio = 24 (verified
	// empirically on OpenSSL 3.0.13 via SSL_set_bio + pointer scan: rbio
	// at 16, wbio at 24). 3.2+ wrapped SSL inside ssl_connection_st and
	// moved wbio to 88. Before this field was added, the BPF program
	// hardcoded 88 — which silently broke every 3.0/3.1 caller.

	// OpenSSL 3.5.x — SSLToWRL grew due to new fields in SSL_CONNECTION.
	// SSLToVersion=72: ssl_connection_st.version (after 64-byte embedded ssl_st).
	"3.5": {SSLToWRL: 3208, WRLToEncCtx: 4128, EncCtxToAlgctx: 176, AlgctxToH: 328, SSLToVersion: 72, SSLToWBIO: 88},

	// OpenSSL 3.2.x–3.4.x — identical offsets across these versions.
	// TODO: verify SSLToVersion for 3.2-3.4 via pahole (likely 72, same as 3.5).
	"3.4": {SSLToWRL: 3056, WRLToEncCtx: 4128, EncCtxToAlgctx: 168, AlgctxToH: 328, SSLToVersion: 0xFFFFFFFF, SSLToWBIO: 88},
	"3.3": {SSLToWRL: 3056, WRLToEncCtx: 4128, EncCtxToAlgctx: 168, AlgctxToH: 328, SSLToVersion: 0xFFFFFFFF, SSLToWBIO: 88},
	"3.2": {SSLToWRL: 3056, WRLToEncCtx: 4128, EncCtxToAlgctx: 168, AlgctxToH: 328, SSLToVersion: 0xFFFFFFFF, SSLToWBIO: 88},

	// OpenSSL 3.0.x–3.1.x — 3-hop chain (no record layer indirection).
	// SSLToWRL stores enc_write_ctx=2168; WRLToEncCtx=0 signals 3-hop to BPF.
	// SSLToVersion=0: ssl_st.version is at offset 0 (verified via pahole on Debian bookworm).
	// SSLToWBIO=24: empirically confirmed on Ubuntu 24.04's libssl3 (3.0.13).
	"3.1": {SSLToWRL: 2168, WRLToEncCtx: 0, EncCtxToAlgctx: 168, AlgctxToH: 328, SSLToVersion: 0, SSLToWBIO: 24},
	"3.0": {SSLToWRL: 2168, WRLToEncCtx: 0, EncCtxToAlgctx: 168, AlgctxToH: 328, SSLToVersion: 0, SSLToWBIO: 24},
}

// DetectOpenSSLVersion reads an OpenSSL/libssl shared library from a
// container's filesystem and determines its version. Returns the version
// string (e.g., "3.2.1") and the corresponding struct offsets.
//
// The library is accessed via /proc/<pid>/root/<libPath> to reach into
// the container's overlay filesystem.
func DetectOpenSSLVersion(pid int, libPath string) (version string, offsets TLSOffsets, err error) {
	// If libPath is already an absolute host path (e.g., /proc/PID/exe),
	// use it directly. Otherwise, prefix with /proc/PID/root for container paths.
	hostPath := libPath
	if !strings.HasPrefix(libPath, "/proc/") {
		hostPath = fmt.Sprintf("/proc/%d/root%s", pid, libPath)
	}

	f, err := elf.Open(hostPath)
	if err != nil {
		return "", TLSOffsets{}, fmt.Errorf("opening ELF %s: %w", hostPath, err)
	}
	defer func() { _ = f.Close() }()

	version, err = readOpenSSLVersion(f)
	if err != nil {
		return "", TLSOffsets{}, fmt.Errorf("reading OpenSSL version from %s: %w", hostPath, err)
	}

	// Extract major.minor for offset lookup
	majorMinor := extractMajorMinor(version)
	offsets, ok := opensslOffsetTable[majorMinor]
	if !ok {
		return version, TLSOffsets{}, fmt.Errorf("unsupported OpenSSL version %s (major.minor=%s)", version, majorMinor)
	}

	if offsets.SSLToWRL == 0 {
		return version, offsets, fmt.Errorf("OpenSSL %s offsets not yet calibrated (SSLToWRL=0)", version)
	}

	return version, offsets, nil
}

// readOpenSSLVersion attempts to find the OpenSSL version string in the
// library's .rodata section. Looks for patterns like "OpenSSL 3.2.1 ".
func readOpenSSLVersion(f *elf.File) (string, error) {
	// Try .rodata first, then .data
	for _, name := range []string{".rodata", ".data"} {
		sec := f.Section(name)
		if sec == nil {
			continue
		}
		data, err := sec.Data()
		if err != nil {
			continue
		}
		if v := findVersionInData(data); v != "" {
			return v, nil
		}
	}

	// Fallback: check for OpenSSL_version_num symbol
	syms, err := f.DynamicSymbols()
	if err == nil {
		for _, sym := range syms {
			if sym.Name == "OpenSSL_version_num" && sym.Value != 0 {
				return readVersionFromSymbol(f, &sym)
			}
		}
	}

	return "", fmt.Errorf("could not determine OpenSSL version")
}

// findVersionInData scans a byte slice for "OpenSSL X.Y.Z" patterns.
func findVersionInData(data []byte) string {
	prefix := []byte("OpenSSL ")
	var best string
	for i := 0; i+len(prefix)+5 < len(data); i++ {
		if data[i] != 'O' {
			continue
		}
		if !startsWith(data[i:], prefix) {
			continue
		}
		// Extract version string after "OpenSSL "
		start := i + len(prefix)
		end := start
		for end < len(data) && end-start < 20 && data[end] != ' ' && data[end] != 0 && data[end] != '\n' {
			end++
		}
		version := string(data[start:end])
		// Validate: must look like X.Y or X.Y.Z
		if len(version) >= 3 && version[0] >= '0' && version[0] <= '9' {
			// Prefer more specific versions (X.Y.Z over X.Y) and higher versions.
			// Binaries may embed multiple version strings (e.g., Node.js bundles
			// OpenSSL 3.5.5 but also references the system's OpenSSL 3.0).
			if best == "" || versionBetter(version, best) {
				best = version
			}
		}
	}
	return best
}

// versionBetter returns true if a is a better match than b.
// Prefers versions with more components (3.5.5 > 3.0) and higher values.
func versionBetter(a, b string) bool {
	aParts := strings.SplitN(a, ".", 4)
	bParts := strings.SplitN(b, ".", 4)
	// More components = more specific = better
	if len(aParts) != len(bParts) {
		return len(aParts) > len(bParts)
	}
	// Same specificity: compare numerically (handles 3.10 > 3.9)
	for i := range aParts {
		ai, _ := strconv.Atoi(aParts[i])
		bi, _ := strconv.Atoi(bParts[i])
		if ai != bi {
			return ai > bi
		}
	}
	return false
}

func startsWith(data, prefix []byte) bool {
	if len(data) < len(prefix) {
		return false
	}
	for i := range prefix {
		if data[i] != prefix[i] {
			return false
		}
	}
	return true
}

// readVersionFromSymbol reads the version number from the OpenSSL_version_num
// symbol value. The version number is encoded as 0xMNN00PP0 where M=major,
// NN=minor, PP=patch (OpenSSL 3.x encoding).
func readVersionFromSymbol(f *elf.File, sym *elf.Symbol) (string, error) {
	// For OpenSSL 3.x, the version_num function returns a value, not a constant.
	// We can try to read from the .data section at the symbol's address.
	for _, sec := range f.Sections {
		if sec.Addr > sym.Value || sym.Value >= sec.Addr+sec.Size {
			continue
		}
		offset := sym.Value - sec.Addr
		data, err := sec.Data()
		if err != nil || offset+4 > uint64(len(data)) {
			continue
		}
		var num uint32
		if f.ByteOrder == binary.LittleEndian {
			num = binary.LittleEndian.Uint32(data[offset:])
		} else {
			num = binary.BigEndian.Uint32(data[offset:])
		}
		if num > 0 {
			major := (num >> 28) & 0xF
			minor := (num >> 20) & 0xFF
			patch := (num >> 4) & 0xFF
			return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil
		}
	}
	return "", fmt.Errorf("could not read version from symbol")
}

// extractMajorMinor returns "X.Y" from a version string like "3.2.1".
func extractMajorMinor(version string) string {
	parts := strings.SplitN(version, ".", 3)
	if len(parts) >= 2 {
		return parts[0] + "." + parts[1]
	}
	return version
}
