package status

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/giolekva/pcloud/core/installer/kube"

	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
)

type ingressMonitor struct {
	d dynamic.Interface
}

type ingress struct {
	APIVersion string   `json:"apiVersion"`
	Kind       string   `json:"kind"`
	Metadata   Metadata `json:"metadata"`
	Spec       struct {
		TLS []struct {
			SecretName string `json:"secretName"`
		} `json:"tls"`
	} `json:"spec"`
}

type certificate struct {
	APIVersion string   `json:"apiVersion"`
	Kind       string   `json:"kind"`
	Metadata   Metadata `json:"metadata"`
	Spec       struct {
		SecretName string `json:"secretName"`
	} `json:"spec"`
}

func (c certificate) OwnerReferences() []OwnerReference {
	return c.Metadata.OwnerReferences
}

type certificateRequest struct {
	Metadata Metadata `json:"metadata"`
	Status   struct {
		Conditions []struct {
			Type   string `json:"type"`
			Status string `json:"status"`
		} `json:"conditions"`
	} `json:"status"`
}

func (c certificateRequest) OwnerReferences() []OwnerReference {
	return c.Metadata.OwnerReferences
}

func (c certificateRequest) IsReady() bool {
	for _, cond := range c.Status.Conditions {
		if cond.Type == "Ready" && cond.Status == "True" {
			return true
		}
	}
	return false
}

func (m *ingressMonitor) Get(ref ResourceRef) (Status, error) {
	ctx := context.Background()
	res, err := m.d.Resource(
		schema.GroupVersionResource{
			Group:    "networking.k8s.io",
			Version:  "v1",
			Resource: "ingresses",
		},
	).Namespace(ref.Namespace).Get(ctx, ref.Name, metav1.GetOptions{})
	if err != nil {
		if errors.IsNotFound(err) {
			return StatusNotFound, nil
		}
		return StatusNoStatus, err
	}
	b, err := res.MarshalJSON()
	if err != nil {
		return StatusNoStatus, err
	}
	var r ingress
	if err := json.Unmarshal(b, &r); err != nil {
		return StatusNoStatus, err
	}
	id, ok := r.Metadata.Annotations["dodo.cloud/id"]
	if !ok {
		fmt.Printf("## missing dodo.cloud/id: %+v\n", ref)
		// TODO(gio): pass down annotations from helm release to resources
		// return StatusNoStatus, nil
	} else if id != ref.Id {
		return StatusNotFound, nil
	}
	certs, err := decodeResources[certificate](m.d.Resource(
		schema.GroupVersionResource{
			Group:    "cert-manager.io",
			Version:  "v1",
			Resource: "certificates",
		},
	).Namespace(ref.Namespace).List(ctx, metav1.ListOptions{}))
	certs = filterByOwner(certs, OwnerReference{r.APIVersion, r.Kind, r.Metadata.Name})
	if err != nil {
		return StatusNotFound, nil
	}
	certReqs, err := decodeResources[certificateRequest](m.d.Resource(
		schema.GroupVersionResource{
			Group:    "cert-manager.io",
			Version:  "v1",
			Resource: "certificaterequests",
		},
	).Namespace(ref.Namespace).List(ctx, metav1.ListOptions{}))
	if err != nil {
		return StatusNotFound, nil
	}
	if len(r.Spec.TLS) != len(certs) {
		return StatusProcessing, nil
	}
	for _, tls := range r.Spec.TLS {
		var cert *certificate
		for _, c := range certs {
			if tls.SecretName == c.Spec.SecretName {
				cert = &c
				break
			}
		}
		if cert == nil {
			return StatusProcessing, nil
		}
		reqs := filterByOwner(certReqs, OwnerReference{cert.APIVersion, cert.Kind, cert.Metadata.Name})
		for _, req := range reqs {
			if !req.IsReady() {
				return StatusProcessing, nil
			}
		}
	}
	return StatusSuccess, nil
}

func NewIngressMonitor(kubeconfig string) (ResourceMonitor, error) {
	c, err := kube.NewKubeClient(kube.KubeConfigOpts{KubeConfigPath: kubeconfig})
	if err != nil {
		return nil, err
	}
	d := dynamic.New(c.RESTClient())
	return &ingressMonitor{d}, nil
}

func filterByOwner[T ResourceWithOwnerReferences](certs []T, owner OwnerReference) []T {
	ret := []T{}
	for _, i := range certs {
		for _, o := range i.OwnerReferences() {
			if owner.APIVersion == o.APIVersion && owner.Kind == o.Kind && owner.Name == o.Name {
				ret = append(ret, i)
				break
			}
		}
	}
	return ret
}

func decodeResources[T any](list *unstructured.UnstructuredList, err error) ([]T, error) {
	if err != nil {
		return nil, err
	}
	ret := []T{}
	for _, i := range list.Items {
		b, err := i.MarshalJSON()
		if err != nil {
			return nil, err
		}
		var r T
		if err := json.Unmarshal(b, &r); err != nil {
			return nil, err
		}
		ret = append(ret, r)
	}
	return ret, nil
}
