/*
 * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package services

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/consts"
	"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/common/utils"
	"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/crds"
	"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/models"
	"github.com/dynemo-ai/dynemo/deploy/compoundai/api-server/api/schemas"
	"github.com/rs/zerolog/log"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
)

type deploymentManagementService struct{}

var DeploymentManagementService = deploymentManagementService{}

type DMSConfiguration struct {
	Version string      `json:"version"`
	Data    interface{} `json:"data"`
}

type DMSCreateRequest struct {
	Name          string                  `json:"name"`
	Namespace     string                  `json:"namespace"`
	ResourceType  crds.CustomResourceType `json:"type"`
	Configuration interface{}             `json:"configuration"`
	Labels        map[string]string       `json:"labels"`
}

type DMSResponseStatus struct {
	Status  string `json:"status"`
	Message string `json:"message"`
}

type DMSCreateResponse struct {
	Id            string            `json:"id"`
	Status        DMSResponseStatus `json:"status"`
	Configuration interface{}       `json:"configuration"`
}

func (s *deploymentManagementService) Create(ctx context.Context, deploymentTarget *models.DeploymentTarget, deployOption *models.DeployOption, ownership *schemas.OwnershipSchema) (*models.DeploymentTarget, error) {
	dmsHost, dmsPort, err := getDMSPortAndHost()
	if err != nil {
		log.Error().Msg(err.Error())
		return nil, err
	}

	url := fmt.Sprintf("http://%s:%s/v1/deployments", dmsHost, dmsPort)
	deployment, err := DeploymentService.Get(ctx, deploymentTarget.DeploymentId)

	if err != nil {
		log.Info().Msg("Could not find associated deployment")
		return nil, err
	}

	defer func() {
		if err != nil {
			s.Delete(ctx, deploymentTarget)
		}
	}()

	compoundNimDeployment, compoundNimRequest := s.transformToDMSRequestsV1alpha1(deployment, deploymentTarget, ownership)

	body, err := sendRequest(compoundNimDeployment, url, http.MethodPost)
	if err != nil {
		return nil, err
	}
	var result DMSCreateResponse
	err = json.Unmarshal(body, &result)
	if err != nil {
		fmt.Println("Error unmarshaling:", err)
		return nil, err
	}
	deploymentTarget.KubeDeploymentId = result.Id

	body, err = sendRequest(compoundNimRequest, url, http.MethodPost)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(body, &result)
	if err != nil {
		fmt.Println("Error unmarshaling:", err)
		return nil, err
	}

	deploymentTarget.KubeRequestId = result.Id
	return deploymentTarget, nil
}

func (s *deploymentManagementService) Delete(ctx context.Context, deploymentTarget *models.DeploymentTarget) error {
	dmsHost, dmsPort, err := getDMSPortAndHost()
	if err != nil {
		log.Error().Msg(err.Error())
		return err
	}

	if deploymentTarget.KubeDeploymentId != "" {
		urlDeployment := fmt.Sprintf("http://%s:%s/v1/deployments/%s", dmsHost, dmsPort, deploymentTarget.KubeDeploymentId)
		_, err := sendRequest(nil, urlDeployment, http.MethodDelete)
		if err != nil {
			return err
		}
	}

	if deploymentTarget.KubeRequestId != "" {
		urlRequest := fmt.Sprintf("http://%s:%s/v1/deployments/%s", os.Getenv("DMS_HOST"), os.Getenv("DMS_PORT"), deploymentTarget.KubeRequestId)
		_, err := sendRequest(nil, urlRequest, http.MethodDelete)
		if err != nil {
			return err
		}
	}

	return nil
}

func (s *deploymentManagementService) transformToDMSRequestsV1alpha1(deployment *models.Deployment, deploymentTarget *models.DeploymentTarget, ownership *schemas.OwnershipSchema) (compoundNimDeployment DMSCreateRequest, compoundNimRequest DMSCreateRequest) {
	translatedTag := s.translateCompoundNimVersionTagToRFC1123(deploymentTarget.CompoundNimVersionTag)

	livenessProbe, readinessProbe := createProbeSpecs(deploymentTarget.Config.DeploymentOverrides)

	compoundNimDeployment = DMSCreateRequest{
		Name:         deployment.Name,
		Namespace:    deployment.KubeNamespace,
		ResourceType: crds.CompoundNimDeployment,
		Configuration: crds.CompoundNimDeploymentConfigurationV1Alpha1{
			Data: crds.CompoundNimDeploymentData{
				CompoundNimVersion: translatedTag,
				Resources:          *deploymentTarget.Config.Resources,
				ExternalServices:   deploymentTarget.Config.ExternalServices,
				LivenessProbe:      livenessProbe,
				ReadinessProbe:     readinessProbe,
			},
			Version: crds.ApiVersion,
		},
		Labels: map[string]string{
			consts.NgcOrganizationHeaderName: ownership.OrganizationId,
			consts.NgcUserHeaderName:         ownership.UserId,
		},
	}

	compoundNimRequest = DMSCreateRequest{
		Name:         translatedTag,
		Namespace:    deployment.KubeNamespace,
		ResourceType: crds.CompoundNimRequest,
		Configuration: crds.CompoundNimRequestConfigurationV1Alpha1{
			Data: crds.CompoundNimRequestData{
				CompoundNimVersionTag: deploymentTarget.CompoundNimVersionTag,
			},
			Version: crds.ApiVersion,
		},
		Labels: map[string]string{
			consts.NgcOrganizationHeaderName: ownership.OrganizationId,
			consts.NgcUserHeaderName:         ownership.UserId,
		},
	}
	return
}

func createProbeSpecs(deploymentOverrides *schemas.DeploymentOverrides) (livenessProbe *corev1.Probe, readinessProbe *corev1.Probe) {
	if deploymentOverrides != nil && deploymentOverrides.ColdStartTimeout != nil {
		livenessProbe = &corev1.Probe{
			InitialDelaySeconds: *deploymentOverrides.ColdStartTimeout,
			TimeoutSeconds:      20,
			FailureThreshold:    6,
			ProbeHandler: corev1.ProbeHandler{
				HTTPGet: &corev1.HTTPGetAction{
					Path: "/livez",
					Port: intstr.FromString(consts.CompoundNimContainerPortName),
				},
			},
		}

		readinessProbe = &corev1.Probe{
			InitialDelaySeconds: *deploymentOverrides.ColdStartTimeout,
			TimeoutSeconds:      5,
			FailureThreshold:    12,
			ProbeHandler: corev1.ProbeHandler{
				HTTPGet: &corev1.HTTPGetAction{
					Path: "/readyz",
					Port: intstr.FromString(consts.CompoundNimContainerPortName),
				},
			},
		}
	}

	return
}

func getDMSPortAndHost() (string, string, error) {
	dmsHost, err := utils.MustGetEnv("DMS_HOST")
	if err != nil {
		return "", "", err
	}

	dmsPort, err := utils.MustGetEnv("DMS_PORT")
	if err != nil {
		return "", "", err
	}

	return dmsHost, dmsPort, nil
}

/**
 * Translates a Compound NIM Version tag to a valid RFC 1123 DNS label.
 *
 * This function makes the following modifications to the input string:
 * 1. Replaces all ":" characters with "--" because colons are not permitted in DNS labels.
 * 2. If the resulting string exceeds the 63-character limit imposed by RFC 1123, it truncates
 *    the string to 63 characters.
 *
 * @param {string} tag - The original CompoundAI Nim tag that needs to be converted.
 * @returns {string} - A string that complies with the RFC 1123 DNS label format.
 *
 * Example:
 *   Input: "nim:latest"
 *   Output: "nim--latest"
 */
func (s *deploymentManagementService) translateCompoundNimVersionTagToRFC1123(tag string) string {
	translated := strings.ReplaceAll(tag, ":", "--")

	// If the length exceeds 63 characters, truncate it
	if len(translated) > 63 {
		translated = translated[:63]
	}

	return translated
}

func sendRequest(payload interface{}, url string, method string) ([]byte, error) {
	jsonData, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %v", err)
	}

	req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %v", err)
	}

	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("received non-OK response: %v, %s", resp.Status, body)
	}

	return body, nil
}
