/*
Copyright 2024 The Kubernetes Authors.

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 ibmcloud

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

	klog "k8s.io/klog/v2"
)

// Constants for IBM Cloud API
const (
	userAgent          = "kubernetes/cluster-autoscaler"
	ibmAPIBaseURLV2    = "https://containers.cloud.ibm.com/global/v2"
	expectedContentType = "application/json"
)

// Struct to manage IBM Cloud interactions
type ibmManagerRest struct {
	authToken  string
	baseURL    *url.URL
	nodePools  map[string]*ibmManagerNodePool
}

// Node pool struct for IBM Cloud
type ibmManagerNodePool struct {
	clusterName       string
	apiServerEndpoint string
	region            string
	plan              string
	os                string
	cloudinit         string
	hostnamePattern   string
	sshKeyIDs         []int
	osPartitionSize   int
}

// Helper function to make an API request to IBM Cloud
func (mgr *ibmManagerRest) request(ctx context.Context, method, pathUrl string, jsonData []byte) ([]byte, error) {
	reqUrl, err := url.Parse(fmt.Sprintf("%s%s", mgr.baseURL, pathUrl))
	if err != nil {
		return nil, fmt.Errorf("invalid request path %s: %v", pathUrl, err)
	}

	req, err := http.NewRequestWithContext(ctx, method, reqUrl.String(), bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", mgr.authToken))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", userAgent)
	dump, _ := httputil.DumpRequestOut(req, true)
	klog.V(2).Infof("%s", string(dump))

	client := &http.Client{}

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to perform request: %w", err)
	}

	defer func() {
		if err := resp.Body.Close(); err != nil {
			klog.Errorf("failed to close response body: %v", err)
		}
	}()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		return body, nil
	}

	// Error handling for IBM Cloud API
	errorResponse := &ErrorResponse{Response: resp}
	if len(body) > 0 {
		if err := json.Unmarshal(body, errorResponse); err != nil {
			return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
		}
	}

	return nil, errorResponse
}

// Function to list servers in IBM Cloud
func (mgr *ibmManagerRest) listIbmServers(ctx context.Context, clusterID string) ([]Server, error) {
	req := fmt.Sprintf("/v2/clusters/%s/servers", clusterID)

	result, err := mgr.request(ctx, "GET", req, nil)
	if err != nil {
		return nil, err
	}

	var servers []Server
	if err := json.Unmarshal(result, &servers); err != nil {
		return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
	}

	return servers, nil
}

// Function to create a new node in IBM Cloud
func (mgr *ibmManagerRest) createNode(ctx context.Context, clusterID, cloudinit, nodegroup string) error {
	udvars := CloudInitTemplateData{
		BootstrapTokenID:     os.Getenv("BOOTSTRAP_TOKEN_ID"),
		BootstrapTokenSecret: os.Getenv("BOOTSTRAP_TOKEN_SECRET"),
		APIServerEndpoint:    mgr.getNodePoolDefinition(nodegroup).apiServerEndpoint,
		NodeGroup:            nodegroup,
	}

	ud, err := renderTemplate(cloudinit, udvars)
	if err != nil {
		return fmt.Errorf("failed to create userdata from template: %w", err)
	}

	hnvars := HostnameTemplateData{
		ClusterName: mgr.getNodePoolDefinition(nodegroup).clusterName,
		NodeGroup:   nodegroup,
		RandString8: randString8(),
	}
	hn, err := renderTemplate(mgr.getNodePoolDefinition(nodegroup).hostnamePattern, hnvars)
	if err != nil {
		return fmt.Errorf("failed to create hostname from template: %w", err)
	}

	req := fmt.Sprintf("/v2/vpc/createNode")

	reqBody := map[string]interface{}{
		"clusterID":       clusterID,
		"hostname":        hn,
		"region":          mgr.getNodePoolDefinition(nodegroup).region,
		"plan":            mgr.getNodePoolDefinition(nodegroup).plan,
		"image":           mgr.getNodePoolDefinition(nodegroup).os,
		"projectID":       mgr.getNodePoolDefinition(nodegroup).projectID,
		"userData":        base64.StdEncoding.EncodeToString([]byte(ud)),
		"sshKeys":         mgr.getNodePoolDefinition(nodegroup).sshKeyIDs,
		"tags":            map[string]string{"k8s-cluster": mgr.getNodePoolDefinition(nodegroup).clusterName, "k8s-nodepool": nodegroup},
		"osPartitionSize": mgr.getNodePoolDefinition(nodegroup).osPartitionSize,
		"provider":        "vpc",
	}

	jsonValue, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("failed to marshal create request: %w", err)
	}

	if _, err := mgr.request(ctx, "POST", req, jsonValue); err != nil {
		return err
	}

	klog.Infof("Created new node in IBM Cloud Kubernetes Service.")

	return nil
}

// Function to delete a node in IBM Cloud
func (mgr *ibmManagerRest) deleteNode(ctx context.Context, clusterID, nodegroup string, id int) error {
	req := fmt.Sprintf("/v2/vpc/deleteNode")

	reqBody := map[string]interface{}{
		"clusterID": clusterID,
		"nodeID":    id,
		"provider":  "vpc",
	}

	jsonValue, err := json.Marshal(reqBody)
	if err != nil {
		return fmt.Errorf("failed to marshal delete request: %w", err)
	}

	if _, err := mgr.request(ctx, "POST", req, jsonValue); err != nil {
		return err
	}

	klog.Infof("Deleted node %d in IBM Cloud Kubernetes Service.", id)
	return nil
}

// Helper function to get node pool definition
func (mgr *ibmManagerRest) getNodePoolDefinition(nodegroup string) *ibmManagerNodePool {
	nodePoolDefinition, ok := mgr.nodePools[nodegroup]
	if !ok {
		nodePoolDefinition, ok = mgr.nodePools["default"]
		if !ok {
			klog.Fatalf("No default cloud-config was found")
		}
		klog.V(1).Infof("No cloud-config was found for %s, using default", nodegroup)
	}

	return nodePoolDefinition
}

// Helper function to render templates
func renderTemplate(str string, vars interface{}) (string, error) {
	tmpl, err := template.New("tmpl").Parse(str)
	if err != nil {
		return "", fmt.Errorf("failed to parse template %q, %w", str, err)
	}

	var tmplBytes bytes.Buffer

	if err := tmpl.Execute(&tmplBytes, vars); err != nil {
		return "", fmt.Errorf("failed to execute template: %w", err)
	}

	return tmplBytes.String(), nil
}