package kubernetes

import (
	"encoding/json"
	"fmt"
	"io"
	"strings"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/bytesutil"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promutils"
)

func (p *Pod) key() string {
	return p.Metadata.key()
}

func parsePodList(r io.Reader) (map[string]object, ListMeta, error) {
	var pl PodList
	d := json.NewDecoder(r)
	if err := d.Decode(&pl); err != nil {
		return nil, pl.Metadata, fmt.Errorf("cannot unmarshal PodList: %w", err)
	}
	objectsByKey := make(map[string]object)
	for _, p := range pl.Items {
		objectsByKey[p.key()] = p
	}
	return objectsByKey, pl.Metadata, nil
}

func parsePod(data []byte) (object, error) {
	var p Pod
	if err := json.Unmarshal(data, &p); err != nil {
		return nil, err
	}
	return &p, nil
}

// PodList implements k8s pod list.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podlist-v1-core
type PodList struct {
	Metadata ListMeta
	Items    []*Pod
}

// Pod implements k8s pod.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#pod-v1-core
type Pod struct {
	Metadata ObjectMeta
	Spec     PodSpec
	Status   PodStatus
}

// PodSpec implements k8s pod spec.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podspec-v1-core
type PodSpec struct {
	NodeName       string
	Containers     []Container
	InitContainers []Container
}

// Container implements k8s container.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#container-v1-core
type Container struct {
	Name  string
	Image string
	Ports []ContainerPort
}

// ContainerPort implements k8s container port.
type ContainerPort struct {
	Name          string
	ContainerPort int
	Protocol      string
}

// PodStatus implements k8s pod status.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podstatus-v1-core
type PodStatus struct {
	Phase                 string
	PodIP                 string
	HostIP                string
	Conditions            []PodCondition
	ContainerStatuses     []ContainerStatus
	InitContainerStatuses []ContainerStatus
}

// PodCondition implements k8s pod condition.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#podcondition-v1-core
type PodCondition struct {
	Type   string
	Status string
}

// ContainerStatus implements k8s container status.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#containerstatus-v1-core
type ContainerStatus struct {
	Name        string
	ContainerID string
	State       ContainerState
}

// ContainerState implements k8s container state.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#containerstatus-v1-core
type ContainerState struct {
	Terminated *ContainerStateTerminated
}

// ContainerStateTerminated implements k8s terminated container state.
//
// See https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#containerstatus-v1-core
type ContainerStateTerminated struct {
	ExitCode int
}

func getContainerID(p *Pod, containerName string, isInit bool) string {
	cs := p.getContainerStatus(containerName, isInit)
	if cs == nil {
		return ""
	}
	return cs.ContainerID
}

func isContainerTerminated(p *Pod, containerName string, isInit bool) bool {
	cs := p.getContainerStatus(containerName, isInit)
	if cs == nil {
		return false
	}
	return cs.State.Terminated != nil
}

func (p *Pod) getContainerStatus(containerName string, isInit bool) *ContainerStatus {
	css := p.Status.ContainerStatuses
	if isInit {
		css = p.Status.InitContainerStatuses
	}
	for i := range css {
		cs := &css[i]
		if cs.Name == containerName {
			return cs
		}
	}
	return nil
}

// getTargetLabels returns labels for each port of the given p.
//
// See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#pod
func (p *Pod) getTargetLabels(gw *groupWatcher) []*promutils.Labels {
	if len(p.Status.PodIP) == 0 {
		// Skip pod without IP, since such pods cannnot be scraped.
		return nil
	}
	if isPodPhaseFinished(p.Status.Phase) {
		// Skip already stopped pod, since it cannot be scraped.
		return nil
	}

	var ms []*promutils.Labels
	ms = appendPodLabels(ms, gw, p, p.Spec.Containers, false)
	ms = appendPodLabels(ms, gw, p, p.Spec.InitContainers, true)
	return ms
}

func isPodPhaseFinished(phase string) bool {
	// See https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
	return phase == "Succeeded" || phase == "Failed"

}
func appendPodLabels(ms []*promutils.Labels, gw *groupWatcher, p *Pod, cs []Container, isInit bool) []*promutils.Labels {
	for _, c := range cs {
		if isContainerTerminated(p, c.Name, isInit) {
			// Skip terminated containers
			continue
		}
		for _, cp := range c.Ports {
			ms = appendPodLabelsInternal(ms, gw, p, &c, &cp, isInit)
		}
		if len(c.Ports) == 0 {
			ms = appendPodLabelsInternal(ms, gw, p, &c, nil, isInit)
		}
	}
	return ms
}

func appendPodLabelsInternal(ms []*promutils.Labels, gw *groupWatcher, p *Pod, c *Container, cp *ContainerPort, isInit bool) []*promutils.Labels {
	addr := p.Status.PodIP
	if cp != nil {
		addr = discoveryutils.JoinHostPort(addr, cp.ContainerPort)
	}
	m := promutils.GetLabels()
	m.Add("__address__", addr)
	isInitStr := "false"
	if isInit {
		isInitStr = "true"
	}
	m.Add("__meta_kubernetes_pod_container_init", isInitStr)

	containerID := getContainerID(p, c.Name, isInit)
	if containerID != "" {
		m.Add("__meta_kubernetes_pod_container_id", containerID)
	}

	p.appendCommonLabels(m, gw)
	p.appendContainerLabels(m, c, cp)
	return append(ms, m)
}

func (p *Pod) appendContainerLabels(m *promutils.Labels, c *Container, cp *ContainerPort) {
	m.Add("__meta_kubernetes_pod_container_image", c.Image)
	m.Add("__meta_kubernetes_pod_container_name", c.Name)
	if cp != nil {
		m.Add("__meta_kubernetes_pod_container_port_name", cp.Name)
		m.Add("__meta_kubernetes_pod_container_port_number", bytesutil.Itoa(cp.ContainerPort))
		m.Add("__meta_kubernetes_pod_container_port_protocol", cp.Protocol)
	}
}

func (p *Pod) appendEndpointLabels(m *promutils.Labels, eps *Endpoints) {
	m.Add("__meta_kubernetes_endpoints_name", eps.Metadata.Name)
	m.Add("__meta_kubernetes_endpoint_address_target_kind", "Pod")
	m.Add("__meta_kubernetes_endpoint_address_target_name", p.Metadata.Name)
	eps.Metadata.registerLabelsAndAnnotations("__meta_kubernetes_endpoints", m)
}

func (p *Pod) appendEndpointSliceLabels(m *promutils.Labels, eps *EndpointSlice) {
	m.Add("__meta_kubernetes_endpointslice_name", eps.Metadata.Name)
	m.Add("__meta_kubernetes_endpointslice_address_target_kind", "Pod")
	m.Add("__meta_kubernetes_endpointslice_address_target_name", p.Metadata.Name)
	m.Add("__meta_kubernetes_endpointslice_address_type", eps.AddressType)
	eps.Metadata.registerLabelsAndAnnotations("__meta_kubernetes_endpointslice", m)
}

func (p *Pod) appendCommonLabels(m *promutils.Labels, gw *groupWatcher) {
	if gw.attachNodeMetadata {
		m.Add("__meta_kubernetes_node_name", p.Spec.NodeName)
		o := gw.getObjectByRoleLocked("node", p.Metadata.Namespace, p.Spec.NodeName)
		if o != nil {
			n := o.(*Node)
			n.Metadata.registerLabelsAndAnnotations("__meta_kubernetes_node", m)
		}
	}
	m.Add("__meta_kubernetes_pod_name", p.Metadata.Name)
	m.Add("__meta_kubernetes_pod_ip", p.Status.PodIP)
	m.Add("__meta_kubernetes_pod_ready", getPodReadyStatus(p.Status.Conditions))
	m.Add("__meta_kubernetes_pod_phase", p.Status.Phase)
	m.Add("__meta_kubernetes_pod_node_name", p.Spec.NodeName)
	m.Add("__meta_kubernetes_pod_host_ip", p.Status.HostIP)
	m.Add("__meta_kubernetes_pod_uid", p.Metadata.UID)
	m.Add("__meta_kubernetes_namespace", p.Metadata.Namespace)
	if pc := getPodController(p.Metadata.OwnerReferences); pc != nil {
		if pc.Kind != "" {
			m.Add("__meta_kubernetes_pod_controller_kind", pc.Kind)
		}
		if pc.Name != "" {
			m.Add("__meta_kubernetes_pod_controller_name", pc.Name)
		}
	}
	p.Metadata.registerLabelsAndAnnotations("__meta_kubernetes_pod", m)
}

func getPodController(ors []OwnerReference) *OwnerReference {
	for _, or := range ors {
		if or.Controller {
			return &or
		}
	}
	return nil
}

func getPodReadyStatus(conds []PodCondition) string {
	for _, c := range conds {
		if c.Type == "Ready" {
			return toLowerConverter.Transform(c.Status)
		}
	}
	return "unknown"
}

var toLowerConverter = bytesutil.NewFastStringTransformer(strings.ToLower)