package logz

// Do not add more dependencies to this package as it's depended upon by the whole codebase.

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"time"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/fieldz"
	"go.opentelemetry.io/otel/trace"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/sets"
)

const (
	// maxResponseHeaders defines the maximum number of response headers to log.
	maxResponseHeaders = 20
	// responseHeaderFieldPrefix defines a prefix for all header keys when logging arbitrary headers
	responseHeaderFieldPrefix = "header_"
)

func NetAddressFromAddr(addr net.Addr) slog.Attr {
	return NetAddress(addr.String())
}

func NetNetworkFromAddr(addr net.Addr) slog.Attr {
	return NetNetwork(addr.Network())
}

func NetAddress(listenAddress string) slog.Attr {
	return slog.String("net_address", listenAddress)
}

func NetNetwork(listenNetwork string) slog.Attr {
	return slog.String("net_network", listenNetwork)
}

func IsWebSocket(isWebSocket bool) slog.Attr {
	return slog.Bool("is_websocket", isWebSocket)
}

func AgentKey(agentKey api.AgentKey) slog.Attr {
	return slog.String(fieldz.AgentKeyFieldName, agentKey.String())
}

func CommitID(commitID string) slog.Attr {
	return slog.String("commit_id", commitID)
}

// ProjectID is the human-readable GitLab project path (e.g. gitlab-org/gitlab).
func ProjectID(projectID string) slog.Attr {
	return slog.String("project_id", projectID)
}

// ProjectIDN is the human-readable GitLab project numeric ID.
func ProjectIDN(projectID int64) slog.Attr {
	return slog.Int64("project_id_numeric", projectID)
}

func TraceIDFromContext(ctx context.Context) slog.Attr {
	return TraceID(trace.SpanContextFromContext(ctx).TraceID())
}

func TraceID(traceID trace.TraceID) slog.Attr {
	if !traceID.IsValid() {
		return slog.Attr{}
	}
	return slog.String("trace_id", traceID.String())
}

func RedisKey(key []byte) slog.Attr {
	return LazyValue("redis_key", func() slog.Value {
		return slog.StringValue(base64.StdEncoding.EncodeToString(key))
	})
}

// U64Count defines a counter field
// Use for any integer counters.
func U64Count(count uint64) slog.Attr {
	return slog.Uint64("count", count)
}

func TokenLimit(limit uint64) slog.Attr {
	return slog.Uint64("token_limit", limit)
}

func RemovedHashKeys(n int) slog.Attr {
	return slog.Int("removed_hash_keys", n)
}

// ModuleName defines the field for logging either NGitLab-kas or agentk module name.
func ModuleName(name string) slog.Attr {
	return slog.String("mod_name", name)
}

func GatewayURL(gatewayURL string) slog.Attr {
	return slog.String("gateway_url", gatewayURL)
}

func URLPathPrefix(urlPrefix string) slog.Attr {
	return slog.String("url_path_prefix", urlPrefix)
}

func URL(url string) slog.Attr {
	return slog.String("url", url)
}

func URLPath(url string) slog.Attr {
	return slog.String("url_path", url)
}

func GRPCService(service string) slog.Attr {
	return slog.String("grpc_service", service)
}

func GRPCMethod(method string) slog.Attr {
	return slog.String("grpc_method", method)
}

func VulnerabilitiesCount(n int) slog.Attr {
	return slog.Int("vulnerabilities_count", n)
}

func Error(err error) slog.Attr {
	return LazyValue("error", func() slog.Value {
		return slog.StringValue(err.Error())
	})
}

func WorkspaceName(name string) slog.Attr {
	return slog.String("workspace_name", name)
}

func WorkspaceNamespace(namespace string) slog.Attr {
	return slog.String("workspace_namespace", namespace)
}

func StatusCode(code int32) slog.Attr {
	return slog.Int64("status_code", int64(code))
}

func RequestID(requestID string) slog.Attr {
	return slog.String("request_id", requestID)
}

func DurationInMilliseconds(duration time.Duration) slog.Attr {
	return slog.Int64("duration_in_ms", duration.Milliseconds())
}

func PayloadSizeInBytes(size int) slog.Attr {
	return slog.Int("payload_size_in_bytes", size)
}

func WorkspaceDataCount(count int) slog.Attr {
	return slog.Int("workspace_data_count", count)
}

func AnyJSONValue(key string, value any) slog.Attr {
	if pr, ok := value.(proto.Message); ok {
		return ProtoJSONValue(key, pr)
	}

	return JSONValue(key, value)
}

func ProtoJSONValue(key string, value proto.Message) slog.Attr {
	return LazyValue(key, func() slog.Value {
		data, err := protojson.Marshal(value)
		if err != nil {
			return slog.StringValue(fmt.Sprintf("protojson.Marshal: %v", err))
		}
		return slog.StringValue(string(data))
	})
}

func JSONValue(key string, value any) slog.Attr {
	return LazyValue(key, func() slog.Value {
		data, err := json.Marshal(value)
		if err != nil {
			return slog.StringValue(fmt.Sprintf("json.Marshal: %v", err))
		}
		return slog.StringValue(string(data))
	})
}

func TargetNamespace(namespace string) slog.Attr {
	return slog.String("target_namespace", namespace)
}

func PodName(podName string) slog.Attr {
	return slog.String("pod_name", podName)
}

func PodNamespace(podNamespace string) slog.Attr {
	return slog.String("pod_namespace", podNamespace)
}

func PodStatus(podStatus string) slog.Attr {
	return slog.String("pod_status", podStatus)
}

func NamespacedName(n string) slog.Attr {
	return slog.String("namespaced_name", n)
}

func ProjectsToReconcile(p sets.Set[string]) slog.Attr {
	return StringSet("projects_to_reconcile", p)
}

func GitRepositoryURL(url string) slog.Attr {
	return slog.String("gitrepository_url", url)
}

func ObjectKey(objKey string) slog.Attr {
	return slog.String("object_key", objKey)
}

func GVR(gvr schema.GroupVersionResource) slog.Attr {
	return slog.Group("k8s_gvr",
		slog.String("group", gvr.Group),
		slog.String("version", gvr.Version),
		slog.String("resource", gvr.Resource),
	)
}

func GV(gvr schema.GroupVersion) slog.Attr {
	return slog.Group("k8s_gv",
		slog.String("group", gvr.Group),
		slog.String("version", gvr.Version),
	)
}

func InventoryName(name string) slog.Attr {
	return slog.String("inventory_name", name)
}

func InventoryNamespace(namespace string) slog.Attr {
	return slog.String("inventory_namespace", namespace)
}

func K8sObjectName(name string) slog.Attr {
	return slog.String("object_name", name)
}

func K8sObjectNamespace(name string) slog.Attr {
	return slog.String("object_namespace", name)
}

func TunnelsByAgent(numTunnels int) slog.Attr {
	return slog.Int("tunnels_by_agent", numTunnels)
}

func FullReconciliationInterval(interval time.Duration) slog.Attr {
	return slog.Duration("full_reconciliation_interval", interval)
}

func PartialReconciliationInterval(interval time.Duration) slog.Attr {
	return slog.Duration("partial_reconciliation_interval", interval)
}

func LabelSelector(selector string) slog.Attr {
	return slog.String("label_selector", selector)
}

func WatchID(id string) slog.Attr {
	return slog.String("watch_id", id)
}

func Reason(reason string) slog.Attr {
	return slog.String("reason", reason)
}

func Msg(msg string) slog.Attr {
	return slog.String("msg", msg)
}

func ListenerName(name string) slog.Attr {
	return slog.String("listener_name", name)
}

func Attempt(attempt int) slog.Attr {
	return slog.Int("attempt", attempt)
}

func FlowScript(path string) slog.Attr {
	return slog.String("flow_script", path)
}

func EventID(id string) slog.Attr {
	return slog.String("event_id", id)
}

func EventType(typ string) slog.Attr {
	return slog.String("event_type", typ)
}

func EventHandler(name string) slog.Attr {
	return slog.String("event_handler", name)
}

func NumberOfEventHandlers(n int) slog.Attr {
	return slog.Int("number_of_event_handlers", n)
}

func IsScriptPrint() slog.Attr {
	return slog.Bool("is_script_print", true)
}

func IsResponseHeaders() slog.Attr {
	return slog.Bool("is_response_headers", true)
}

func ResponseHeaders(h http.Header) []any {
	as := make([]any, 0, min(len(h), maxResponseHeaders))
	for k, v := range h {
		if len(as) >= maxResponseHeaders {
			break
		}

		as = append(as, slog.Any(fmt.Sprintf("%s%s", responseHeaderFieldPrefix, k), v))
	}
	return as
}
