/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package teste2eutils

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	. "github.com/onsi/gomega"
	provisioningv1alpha1 "github.com/openshift-kni/oran-o2ims/api/provisioning/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
	hwmgmtv1alpha1 "github.com/openshift-kni/oran-o2ims/api/hardwaremanagement/v1alpha1"
	"github.com/openshift-kni/oran-o2ims/internal/constants"
)

func VerifyStatusCondition(actualCond, expectedCon metav1.Condition) {
	Expect(actualCond.Type).To(Equal(expectedCon.Type))
	Expect(actualCond.Status).To(Equal(expectedCon.Status))
	Expect(actualCond.Reason).To(Equal(expectedCon.Reason))
	if expectedCon.Message != "" {
		Expect(actualCond.Message).To(ContainSubstring(expectedCon.Message))
	}
}

func VerifyProvisioningStatus(provStatus provisioningv1alpha1.ProvisioningStatus,
	expectedPhase provisioningv1alpha1.ProvisioningPhase, expectedDetail string,
	expectedResources *provisioningv1alpha1.ProvisionedResources) {

	Expect(provStatus.ProvisioningPhase).To(Equal(expectedPhase))
	Expect(provStatus.ProvisioningDetails).To(ContainSubstring(expectedDetail))
	Expect(provStatus.ProvisionedResources).To(Equal(expectedResources))
}

// GetExternalCrdFiles downloads the external CRDs that IMS depends on based on
// the content of the go.mod file. The files are saved in the destDir directory.
func GetExternalCrdFiles(destDir string) error {
	// Cache resolved full commit SHAs to avoid redundant GitHub API calls.
	// Multiple CRDs from the same repo share the same commit SHA.
	fullCommitShaCache := make(map[string]string)

	for _, externalCrd := range ExternalCrdData {
		// Get the commit sha from the go.mod of the IMS repo.
		// Use modulePath if provided, otherwise construct from modName/repoName
		var policyMod string
		if modulePath, exists := externalCrd["modulePath"]; exists && modulePath != "" {
			policyMod = modulePath
		} else {
			policyMod = externalCrd["modName"] + "/" + externalCrd["repoName"]
		}
		_, policyModPseudoVersionNew, err := GetModuleFromGoMod(policyMod)
		if err != nil {
			return fmt.Errorf("error getting module from go.mod: %w", err)
		}
		commitSha := GetGitCommitFromPseudoVersion(policyModPseudoVersionNew)

		// Get the full sha of the commit by calling the github API.
		// Use cached value if we've already resolved this commit.
		url := fmt.Sprintf(GithubCommitsAPI, externalCrd["owner"], externalCrd["repoName"], commitSha)
		fullCommitSha, cached := fullCommitShaCache[url]
		if !cached {
			// Retry with exponential backoff.
			fullCommitSha, err = RetryWithExponentialBackoff(func() (string, error) {
				req, err := http.NewRequest(http.MethodGet, url, nil) //nolint:noctx
				if err != nil {
					return "", fmt.Errorf("error creating request: %w", err)
				}
				if token := os.Getenv("GITHUB_TOKEN"); token != "" {
					req.Header.Set("Authorization", "token "+token)
				}
				resp, err := http.DefaultClient.Do(req)
				if err != nil {
					return "", fmt.Errorf("error getting URL: %w", err)
				}
				defer resp.Body.Close()
				if resp.StatusCode != http.StatusOK {
					return "", fmt.Errorf("unexpected status, got: %d", resp.StatusCode)
				}
				var commit map[string]interface{}
				if err := json.NewDecoder(resp.Body).Decode(&commit); err != nil {
					return "", fmt.Errorf("failed to decode")
				}
				sha, ok := commit["sha"].(string)
				if !ok {
					return "", fmt.Errorf("unexpected type for sha field")
				}
				return sha, nil
			})
			Expect(err).NotTo(HaveOccurred())
			fullCommitShaCache[url] = fullCommitSha
		}
		// Get the CRD file.
		crdFilePath := fmt.Sprintf(
			GithubUserContentLink,
			externalCrd["owner"], externalCrd["repoName"],
			fullCommitSha, externalCrd["crdPath"], externalCrd["crdFileName"])
		err = DownloadFile(crdFilePath, externalCrd["crdFileName"], destDir)
		Expect(err).NotTo(HaveOccurred())
	}

	return nil
}

// RetryWithExponentialBackoff retries a function with exponential backoff
func RetryWithExponentialBackoff(fn func() (string, error)) (string, error) {
	maxRetries := 3
	baseDelay := 100 * time.Millisecond

	for i := 0; i < maxRetries; i++ {
		result, err := fn()
		if err == nil {
			return result, nil
		}

		if i == maxRetries-1 {
			return "", err
		}

		delay := baseDelay * time.Duration(1<<uint(i))
		time.Sleep(delay)
	}

	return "", fmt.Errorf("max retries exceeded")
}

func DownloadFile(rawUrl, filename, dirpath string) error {
	// Parse the URL
	parsedURL, err := url.Parse(rawUrl)
	if err != nil {
		return fmt.Errorf("download %s: invalid URL %q: %w", filename, rawUrl, err)
	}

	// Check that the scheme is "http" or "https" and the domain is trusted
	if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
		return fmt.Errorf("download %s: url scheme must be http or https, got %q", filename, parsedURL.Scheme)
	}

	_, err = os.Stat(filename)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("download %s: error checking existing file: %w", filename, err)
	}

	resp, err := http.Get(rawUrl) //nolint
	if err != nil {
		return fmt.Errorf("download %s: HTTP GET failed for %s: %w", filename, rawUrl, err)
	}
	defer resp.Body.Close()

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

	// Validate HTTP status code
	if resp.StatusCode != http.StatusOK {
		// Include the first 200 bytes of the response body for debugging (often reveals
		// GitHub rate-limit messages or 404 HTML pages that would otherwise be silently
		// saved as a "YAML" file and cause a cryptic parse error later).
		bodyPreview := string(body)
		if len(bodyPreview) > 200 {
			bodyPreview = bodyPreview[:200] + "..."
		}
		return fmt.Errorf("download %s: unexpected HTTP status %d from %s — body preview: %s",
			filename, resp.StatusCode, rawUrl, bodyPreview)
	}

	// Validate the downloaded content looks like a Kubernetes CRD YAML, not an HTML error page
	content := string(body)
	if strings.HasPrefix(strings.TrimSpace(content), "<") {
		bodyPreview := content
		if len(bodyPreview) > 200 {
			bodyPreview = bodyPreview[:200] + "..."
		}
		return fmt.Errorf("download %s: received HTML instead of YAML from %s — body preview: %s",
			filename, rawUrl, bodyPreview)
	}
	if !strings.Contains(content, "kind: CustomResourceDefinition") {
		bodyPreview := content
		if len(bodyPreview) > 200 {
			bodyPreview = bodyPreview[:200] + "..."
		}
		return fmt.Errorf("download %s: content does not contain 'kind: CustomResourceDefinition' from %s — body preview: %s",
			filename, rawUrl, bodyPreview)
	}

	destPath := filepath.Join(dirpath, filename)
	out, err := os.Create(destPath)
	if err != nil {
		return fmt.Errorf("download %s: error creating file %s: %w", filename, destPath, err)
	}
	defer out.Close()

	_, err = out.Write(body)
	if err != nil {
		return fmt.Errorf("download %s: error writing to file %s: %w", filename, destPath, err)
	}

	fmt.Fprintf(os.Stderr, "INFO: Successfully downloaded CRD %s (%d bytes)\n", filename, len(body))
	return nil
}

func CreateNodeResources(ctx context.Context, c client.Client, npName string) {
	node := CreateNode(MasterNodeName, "idrac-virtualmedia+https://10.16.2.1/redfish/v1/Systems/System.Embedded.1", "bmc-secret", "controller", constants.DefaultNamespace, npName, nil)
	// Create both the standard secret and the mock server expected secrets
	secretNames := []string{BmcSecretName, "test-node-1-bmc-secret", "master-node-2-bmc-secret"}
	secrets := CreateSecrets(secretNames, constants.DefaultNamespace)
	CreateResources(ctx, c, []*hwmgmtv1alpha1.AllocatedNode{node}, secrets)
}

func CreateResources(ctx context.Context, c client.Client, nodes []*hwmgmtv1alpha1.AllocatedNode, secrets []*corev1.Secret) {
	for _, node := range nodes {
		err := c.Create(ctx, node)
		if err != nil && !errors.IsAlreadyExists(err) {
			Expect(err).ToNot(HaveOccurred())
		}
	}
	for _, secret := range secrets {
		err := c.Create(ctx, secret)
		if err != nil && !errors.IsAlreadyExists(err) {
			Expect(err).ToNot(HaveOccurred())
		}
	}
}

func CreateNode(name, bmcAddress, bmcSecret, groupName, namespace, narName string, interfaces []*hwmgmtv1alpha1.Interface) *hwmgmtv1alpha1.AllocatedNode {
	if interfaces == nil {
		interfaces = []*hwmgmtv1alpha1.Interface{
			{
				Name:       "eno1",
				Label:      constants.BootInterfaceLabel,
				MACAddress: "00:00:00:01:20:30",
			},
			{
				Name:       "eth0",
				Label:      "base-interface",
				MACAddress: "00:00:00:01:20:31",
			},
			{
				Name:       "eth1",
				Label:      "data-interface",
				MACAddress: "00:00:00:01:20:32",
			},
		}
	}
	return &hwmgmtv1alpha1.AllocatedNode{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: hwmgmtv1alpha1.AllocatedNodeSpec{
			NodeAllocationRequest: narName,
			GroupName:             groupName,
			HwMgrNodeId:           name,
			HwMgrNodeNs:           namespace,
		},
		Status: hwmgmtv1alpha1.AllocatedNodeStatus{
			BMC: &hwmgmtv1alpha1.BMC{
				Address:         bmcAddress,
				CredentialsName: bmcSecret,
			},
			Interfaces: interfaces,
		},
	}
}

func CreateSecrets(names []string, namespace string) []*corev1.Secret {
	var secrets []*corev1.Secret
	for _, name := range names {
		secrets = append(secrets, &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name,
				Namespace: namespace,
			},
		})
	}
	return secrets
}

// BMHData holds data for creating BareMetalHost test resources.
type BMHData struct {
	Name           string
	Namespace      string
	MacAddress     string
	BmcAddress     string
	ServerType     string
	Colour         string
	ResourcePoolId string
}

// Helper function to create BareMetalHost
func CreateBareMetalHost(d BMHData) *metal3v1alpha1.BareMetalHost {
	return &metal3v1alpha1.BareMetalHost{
		ObjectMeta: metav1.ObjectMeta{
			Name:      d.Name,
			Namespace: d.Namespace,
			Labels: map[string]string{
				"resourceselector.clcm.openshift.io/server-type":   d.ServerType,
				"resourceselector.clcm.openshift.io/server-colour": d.Colour,
				constants.LabelResourcePoolName:                    d.ResourcePoolId,
			},
		},
		Spec: metal3v1alpha1.BareMetalHostSpec{
			Online: true,
			BMC: metal3v1alpha1.BMCDetails{
				Address:         d.BmcAddress,
				CredentialsName: fmt.Sprintf("%s-bmc-secret", d.Name),
			},
			BootMACAddress: d.MacAddress,
		},
	}
}

// Helper function to create HardwareData CR
func CreateHardwareData(bmhName string, bmhData struct {
	Name             string
	MacAddress       string
	BmcAddress       string
	Hostname         string
	RamMB            int32
	HwProfile        string
	Colour           string
	StorageSizeBytes metal3v1alpha1.Capacity
	IsPreferred      bool
}) *metal3v1alpha1.HardwareData {
	return &metal3v1alpha1.HardwareData{
		ObjectMeta: metav1.ObjectMeta{
			Name:      bmhData.Name,
			Namespace: constants.DefaultNamespace,
		},
		Spec: metal3v1alpha1.HardwareDataSpec{
			HardwareDetails: &metal3v1alpha1.HardwareDetails{
				Hostname: bmhData.Hostname,
				CPU: metal3v1alpha1.CPU{
					Arch: "x86_64",
				},
				RAMMebibytes: int(bmhData.RamMB),
				NIC: []metal3v1alpha1.NIC{
					{
						Name: "eno1",
						MAC:  bmhData.MacAddress,
					},
					{
						Name: "eth0",
						MAC:  fmt.Sprintf("%s:01", bmhData.MacAddress[:14]),
					},
					{
						Name: "eth1",
						MAC:  fmt.Sprintf("%s:02", bmhData.MacAddress[:14]),
					},
				},
				Storage: []metal3v1alpha1.Storage{
					{
						Name:         "sda",
						SizeBytes:    bmhData.StorageSizeBytes,
						Rotational:   false,
						Type:         "SSD",
						Model:        "Samsung SSD 980 PRO 1TB",
						SerialNumber: fmt.Sprintf("SN-%s", bmhData.Name),
					},
				},
			},
		},
	}
}

// Helper function to create BMC secret
func CreateBMCSecret(name string) *corev1.Secret {
	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("%s-bmc-secret", name),
			Namespace: constants.DefaultNamespace,
		},
		Type: corev1.SecretTypeOpaque,
		Data: map[string][]byte{
			"username": []byte("admin"),
			"password": []byte("password123"),
		},
	}
}

// Create HostFirmwareSettings CR
func CreateHostFirmwareSettings(bmhName, bmhNamespace string) *metal3v1alpha1.HostFirmwareSettings {
	return &metal3v1alpha1.HostFirmwareSettings{
		ObjectMeta: metav1.ObjectMeta{
			Name:      bmhName,
			Namespace: bmhNamespace,
		},
		Spec: metal3v1alpha1.HostFirmwareSettingsSpec{
			Settings: map[string]intstr.IntOrString{},
		},
	}
}

// Update HostFirmwareSettings Status
func UpdateHostFirmwareSettingsStatus(
	schemaName, schemaNamespace string,
	statusSettings map[string]string,
	valid, changeDetected metav1.ConditionStatus, observedGeneration int64,
) metal3v1alpha1.HostFirmwareSettingsStatus {
	return metal3v1alpha1.HostFirmwareSettingsStatus{
		FirmwareSchema: &metal3v1alpha1.SchemaReference{
			Name:      schemaName,
			Namespace: schemaNamespace,
		},
		Settings: statusSettings,
		Conditions: []metav1.Condition{
			{
				Type:               string(metal3v1alpha1.FirmwareSettingsValid),
				Status:             valid,
				Reason:             "Success",
				LastTransitionTime: metav1.Now(),
				ObservedGeneration: observedGeneration,
			},
			{
				Type:               string(metal3v1alpha1.FirmwareSettingsChangeDetected),
				Status:             changeDetected,
				LastTransitionTime: metav1.Now(),
				Reason:             "Success",
				ObservedGeneration: observedGeneration,
			},
		},
	}
}

// Create HostFirmwareComponents CR
func CreateHostFirmwareComponents(bmhName, bmhNamespace string) *metal3v1alpha1.HostFirmwareComponents {
	return &metal3v1alpha1.HostFirmwareComponents{
		ObjectMeta: metav1.ObjectMeta{
			Name:      bmhName,
			Namespace: bmhNamespace,
		},
		Spec: metal3v1alpha1.HostFirmwareComponentsSpec{
			Updates: []metal3v1alpha1.FirmwareUpdate{},
		},
	}
}

// Update HostFirmwareComponents Status

func UpdateHostFirmwareComponentsStatus(
	bmhName, bmhNamespace string,
	components []metal3v1alpha1.FirmwareComponentStatus,
	valid, changeDetected metav1.ConditionStatus, observedGeneration int64,
) metal3v1alpha1.HostFirmwareComponentsStatus {
	return metal3v1alpha1.HostFirmwareComponentsStatus{
		Components: components,
		Conditions: []metav1.Condition{
			{
				Type:               string(metal3v1alpha1.HostFirmwareComponentsValid),
				Status:             valid,
				Reason:             "OK",
				LastTransitionTime: metav1.Now(),
				ObservedGeneration: observedGeneration,
			},
			{
				Type:               string(metal3v1alpha1.HostFirmwareComponentsChangeDetected),
				Status:             changeDetected,
				Reason:             "OK",
				LastTransitionTime: metav1.Now(),
				ObservedGeneration: observedGeneration,
			},
		},
	}
}

// Create FirmwareSchema CR
func CreateFirmwareSchema(name, namespace, vendor, model string, schema map[string]metal3v1alpha1.SettingSchema) *metal3v1alpha1.FirmwareSchema {
	return &metal3v1alpha1.FirmwareSchema{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: metal3v1alpha1.FirmwareSchemaSpec{
			HardwareVendor: vendor,
			HardwareModel:  model,
			Schema:         schema,
		},
	}
}
