/*
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/base64"
	"encoding/json"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"net/http/httputil"
	"net/url"
	"os"
	"path"
	"strconv"
	"strings"
	"text/template"
	"time"

	"gopkg.in/gcfg.v1"
	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
	"k8s.io/autoscaler/cluster-autoscaler/config"
	"k8s.io/autoscaler/cluster-autoscaler/utils/gpu"
	"k8s.io/autoscaler/cluster-autoscaler/version"
	klog "k8s.io/klog/v2"
	schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework"
)

const (
	userAgent                    = "kubernetes/cluster-autoscaler/" + version.ClusterAutoscalerVersion
	expectedAPIContentTypePrefix = "application/json"
	ibmPrefix                    = "ibmcloud://"
)

var regionEndpoints = map[string]string{
	"us-east": "https://us-east.iaas.cloud.ibm.com/v1",
	"us-south": "https://us-south.iaas.cloud.ibm.com/v1",
	"ca-tor": "https://ca-tor.iaas.cloud.ibm.com/v1",
	"br-sao": "https://br-sao.iaas.cloud.ibm.com/v1",
	"eu-de": "https://eu-de.iaas.cloud.ibm.com/v1",
	"eu-es": "https://eu-es.iaas.cloud.ibm.com/v1",
	"eu-gb": "https://eu-gb.iaas.cloud.ibm.com/v1",
	"au-syd": "https://au-syd.iaas.cloud.ibm.com/v1",
	"jp-osa": "https://jp-osa.iaas.cloud.ibm.com/v1",
	"jp-tok": "https://jp-tok.iaas.cloud.ibm.com/v1",
}

type instanceType struct {
	InstanceName string
	CPU          int64
	MemoryMb     int64
	GPU          int64
}

type ibmManagerNodePool struct {
	clusterName       string
	projectID         int
	apiServerEndpoint string
	region            string
	plan              string
	os                string
	cloudinit         string
	hostnamePattern   string
	sshKeyIDs         []int
	osPartitionSize   int
	waitTimeStep      time.Duration
}

type ibmManagerRest struct {
	authToken  string
	baseURL    *url.URL
	nodePools  map[string]*ibmManagerNodePool
	plans      map[string]*Plan
	planUpdate time.Time
}

// ConfigNodepool options only include the project-id for now
type ConfigNodepool struct {
	ClusterName       string   `gcfg:"cluster-name"`
	ProjectID         int      `gcfg:"project-id"`
	APIServerEndpoint string   `gcfg:"api-server-endpoint"`
	Region            string   `gcfg:"region"`
	Plan              string   `gcfg:"plan"`
	OS                string   `gcfg:"os"`
	SSHKeys           []string `gcfg:"ssh-key-ids"`
	CloudInit         string   `gcfg:"cloudinit"`
	HostnamePattern   string   `gcfg:"hostname-pattern"`
	OsPartitionSize   int      `gcfg:"os-partition-size"`
}

// IsEmpty determine if this is an empty config
func (c ConfigNodepool) IsEmpty() bool {
	return c.ClusterName == "" && c.CloudInit == "" && c.Region == "" && c.Plan == "" && c.ProjectID == 0
}

// ConfigFile is used to read and store information from the cloud configuration file
type ConfigFile struct {
	DefaultNodegroupdef ConfigNodepool             `gcfg:"global"`
	Nodegroupdef        map[string]*ConfigNodepool `gcfg:"nodegroupdef"`
}

// CloudInitTemplateData represents the variables that can be used in cloudinit templates
type CloudInitTemplateData struct {
	BootstrapTokenID     string
	BootstrapTokenSecret string
	APIServerEndpoint    string
	NodeGroup            string
}

// HostnameTemplateData represents the template variables used to construct host names for new nodes
type HostnameTemplateData struct {
	ClusterName string
	NodeGroup   string
	RandString8 string
}

// ErrorResponse is the http response used on errors
type ErrorResponse struct {
	Response    *http.Response
	Errors      []string `json:"errors"`
	SingleError string   `json:"error"`
}

var multipliers = map[string]int64{
	"KB": 1024,
	"MB": 1024 * 1024,
	"GB": 1024 * 1024 * 1024,
	"TB": 1024 * 1024 * 1024 * 1024,
}

// Error implements the error interface
func (r *ErrorResponse) Error() string {
	return fmt.Sprintf("%v %v: %d %v %v",
		r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(r.Errors, ", "), r.SingleError)
}

// createIbmManagerRest sets up the client and returns an ibmManagerRest.
func createIbmManagerRest(configReader io.Reader, discoverOpts cloudprovider.NodeGroupDiscoveryOptions, opts config.AutoscalingOptions) (*ibmManagerRest, error) {
	cfg := ConfigFile{
		DefaultNodegroupdef: ConfigNodepool{},
		Nodegroupdef:        map[string]*ConfigNodepool{},
	}

	if configReader != nil {
		if err := gcfg.ReadInto(&cfg, configReader); err != nil {
			klog.Errorf("Couldn't read config: %v", err)
			return nil, err
		}
	}

	var manager ibmManagerRest
	manager.nodePools = make(map[string]*ibmManagerNodePool)

	if _, ok := cfg.Nodegroupdef["default"]; !ok {
		cfg.Nodegroupdef["default"] = &cfg.DefaultNodegroupdef
	}

	if cfg.Nodegroupdef["default"].IsEmpty() {
		klog.Fatalf("No \"default\" or [Global] nodepool definition was found")
	}

	ibmAuthToken := os.Getenv("IBM_AUTH_TOKEN")
	if len(ibmAuthToken) == 0 {
		klog.Fatalf("IBM_AUTH_TOKEN is required and missing")
	}

	manager.authToken = ibmAuthToken

	region := cfg.Nodegroupdef["default"].Region
	baseURL, ok := regionEndpoints[region]
	if !ok {
		return nil, fmt.Errorf("unsupported region %s", region)
	}

	base, err := url.Parse(baseURL)
	if err != nil {
		return nil, fmt.Errorf("invalid baseURL %s: %v", baseURL, err)
	}

	manager.baseURL = base

	projectID := cfg.Nodegroupdef["default"].ProjectID
	apiServerEndpoint := cfg.Nodegroupdef["default"].APIServerEndpoint

	for key, nodepool := range cfg.Nodegroupdef {
		if opts.ClusterName == "" && nodepool.ClusterName == "" {
			klog.Fatalf("The cluster-name parameter must be set")
		} else if opts.ClusterName != "" && nodepool.ClusterName == "" {
			nodepool.ClusterName = opts.ClusterName
		}

		var sshKeyIDs []int
		for i, keyIDString := range nodepool.SSHKeys {
			keyID, err := strconv.ParseInt(keyIDString, 10, 32)
			if err != nil {
				return nil, fmt.Errorf("invalid ssh-key ID at position %d: %s; it must be an integer", i, keyIDString)
			}
			sshKeyIDs = append(sshKeyIDs, int(keyID))
		}
		manager.nodePools[key] = &ibmManagerNodePool{
			projectID:         projectID,
			apiServerEndpoint: apiServerEndpoint,
			clusterName:       nodepool.ClusterName,
			region:            nodepool.Region,
			plan:              nodepool.Plan,
			os:                nodepool.OS,
			cloudinit:         nodepool.CloudInit,
			sshKeyIDs:         sshKeyIDs,
			hostnamePattern:   nodepool.HostnamePattern,
			osPartitionSize:   nodepool.OsPartitionSize,
		}
	}

	return &manager, nil
}

func (mgr *ibmManagerRest) request(ctx context.Context, method, pathUrl string, jsonData []byte) ([]byte, error) {
	u, err := url.Parse(pathUrl)
	if err != nil {
		return nil, fmt.Errorf("invalid request path %s: %v", pathUrl, err)
	}
	reqUrl := mgr.baseURL.ResolveReference(u)

	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)
	}

	ct := resp.Header.Get("Content-Type")
	if !strings.HasPrefix(ct, expectedAPIContentTypePrefix) {
		errorResponse := &ErrorResponse{Response: resp}
		errorResponse.SingleError = fmt.Sprintf("Unexpected Content-Type: %s with status: %s", ct, resp.Status)
		return nil, errorResponse
	}

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

	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
}

// Additional methods and functions would follow here, mirroring the structure from the original code.
// Make sure to update each function to use IBM Cloud specifics and mocked responses.