/*
* Copyright (c) 2026 Broadcom. All rights reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* All trademarks, trade names, service marks, and logos referenced
* herein belong to their respective companies.
*
* This software and all information contained therein is confidential
* and proprietary and shall not be duplicated, used, disclosed or
* disseminated in any way except as authorized by the applicable
* license agreement, without the express written permission of Broadcom.
* All authorized reproductions must be marked with this language.
*
* EXCEPT AS SET FORTH IN THE APPLICABLE LICENSE AGREEMENT, TO THE
* EXTENT PERMITTED BY APPLICABLE LAW OR AS AGREED BY BROADCOM IN ITS
* APPLICABLE LICENSE AGREEMENT, BROADCOM PROVIDES THIS DOCUMENTATION
* "AS IS" WITHOUT WARRANTY OF ANY KIND, INCLUDING WITHOUT LIMITATION,
* ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE, OR. NONINFRINGEMENT. IN NO EVENT WILL BROADCOM BE LIABLE TO
* THE END USER OR ANY THIRD PARTY FOR ANY LOSS OR DAMAGE, DIRECT OR
* INDIRECT, FROM THE USE OF THIS DOCUMENTATION, INCLUDING WITHOUT LIMITATION,
* LOST PROFITS, LOST INVESTMENT, BUSINESS INTERRUPTION, GOODWILL, OR
* LOST DATA, EVEN IF BROADCOM IS EXPRESSLY ADVISED IN ADVANCE OF THE
* POSSIBILITY OF SUCH LOSS OR DAMAGE.
*
* AI assistance has been used to generate some or all contents of this file. That includes, but is not limited to, new code, modifying existing code, stylistic edits.
 */
package reconcile

import (
	"bytes"
	"compress/gzip"
	"context"
	"crypto/sha1"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"strings"

	v1 "github.com/caapim/layer7-operator/api/v1"
	securityv1alpha1 "github.com/caapim/layer7-operator/api/v1alpha1"
	"github.com/caapim/layer7-operator/internal/graphman"
	"github.com/caapim/layer7-operator/pkg/util"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
)

func StateStorage(ctx context.Context, params Params, statestore securityv1alpha1.L7StateStore, commit string) error {
	storageSecretName, repositoryPath, _, err := localRepoStorageInfo(params)
	if err != nil {
		return err
	}

	tmpPath := util.StateStoreCacheDir(params.Instance.Name, params.Instance.Namespace)
	commitTracker := commit + ".txt"
	fileName := "latest.json"
	_, dErr := os.Stat(tmpPath)
	if dErr != nil {
		_ = os.MkdirAll(tmpPath, 0755)
	}

	_, fErr := os.Stat(tmpPath + "/" + commitTracker)
	if fErr == nil {
		return nil
	}

	// Retrieve existing secret for Redis
	// this will need to be updated for multi-state store provider support
	if statestore.Spec.Redis.ExistingSecret != "" {
		stateStoreSecret, err := getStateStoreSecret(ctx, statestore.Spec.Redis.ExistingSecret, statestore, params)
		if err != nil {
			return err
		}
		statestore.Spec.Redis.Username = string(stateStoreSecret.Data["username"])
		statestore.Spec.Redis.MasterPassword = string(stateStoreSecret.Data["masterPassword"])
	}

	rc, err := util.RedisClient(&statestore.Spec.Redis)
	if err != nil {
		return fmt.Errorf("failed to connect to state store: %w", err)
	}
	defer rc.Close()

	projects, err := util.DetectGraphmanFolders(repositoryPath)
	if err != nil {
		return err
	}

	bundles := map[string][]byte{}
	compressedBundle := map[string][]byte{}
	for _, p := range projects {
		bundle, err := util.BuildAndValidateBundle(p, false, params.Log)
		if err != nil {
			return err
		}
		bundleGzip, err := util.GzipCompress(bundle)
		if err != nil {
			return err
		}
		var buf bytes.Buffer
		zw := gzip.NewWriter(&buf)
		_, err = zw.Write(bundleGzip)
		if err != nil {
			return err
		}

		if err := zw.Close(); err != nil {
			return err
		}

		keyName := strings.Replace(p, repositoryPath, "", 1)
		keyName = strings.Replace(strings.ReplaceAll(keyName, "/", "-"), "-", "", 1)
		bundles[keyName+".json"] = bundle
		compressedBundle[keyName+".gz"] = bundleGzip

		buf.Reset()
	}

	compressedBundleBytes, err := json.Marshal(compressedBundle)
	if err != nil {
		return err
	}

	redisKey := statestore.Spec.Redis.GroupName + ":" + statestore.Spec.Redis.StoreId + ":repository:" + params.Instance.Namespace + ":" + storageSecretName + ":latest"

	// check for previous version - statestore may be empty
	stateStoreBundleMapString, err := rc.Get(ctx, redisKey).Result()
	if err != nil {
		// if the previous version can't be retrieved, write the current version
		rs := rc.Set(ctx, redisKey, compressedBundleBytes, 0)
		if rs.Err() != nil {
			return fmt.Errorf("failed to reconcile state storage: %w", rs.Err())
		}

		// then write that to file...
		err = os.WriteFile(tmpPath+"/"+fileName, compressedBundleBytes, 0755)
		if err != nil {
			return err
		}
		err = os.WriteFile(tmpPath+"/"+commitTracker, []byte{}, 0755)
		if err != nil {
			return err
		}

		return nil
	}

	stateStoreBundleMap := map[string][]byte{}
	err = json.Unmarshal([]byte(stateStoreBundleMapString), &stateStoreBundleMap)
	if err != nil {
		return err
	}

	for k1, b1bytes := range bundles {
		for k2, b2 := range stateStoreBundleMap {
			if strings.Split(k1, ".")[0] == strings.Split(k2, ".")[0] {
				b2Bytes, err := util.GzipDecompress(b2)
				if err != nil {
					//return fmt.Errorf("failed to decompress bundle 1: %s %w", k2, err)
					b2Bytes = b2
				}

				// reset current mappings from statestore
				// delete mappings should only persist for one version.
				srcBundle := graphman.Bundle{}
				destBundle := graphman.Bundle{}
				err = json.Unmarshal(b2Bytes, &srcBundle)
				if err != nil {
					return fmt.Errorf("failed to unmarshal state store bundle: %s %w", strings.Split(k2, ".")[0], err)
				}
				err = json.Unmarshal(b1bytes, &destBundle)
				if err != nil {
					return fmt.Errorf("failed to unmarshal local bundle: %s %w", strings.Split(k2, ".")[0], err)
				}

				// Reset mappings - removes entities marked for delete and clears all mappings
				// This represents the actual state after the last apply
				err = graphman.ResetMappings(&srcBundle)
				if err != nil {
					return fmt.Errorf("failed to reset mappings: %s %w", strings.Split(k2, ".")[0], err)
				}

				// Calculate delta: current=srcBundle (cleaned), desired=destBundle (filesystem)
				deltaBundle, combinedBundle, err := graphman.CalculateDelta(srcBundle, destBundle)
				if err != nil {
					return fmt.Errorf("failed to subtract current and previous version bundle: %s %w", strings.Split(k2, ".")[0], err)
				}

				deltaBundleBytes, err := json.Marshal(deltaBundle)
				if err != nil {
					return fmt.Errorf("failed to marshal delta bundle: %s %w", strings.Split(k1, ".")[0], err)
				}
				deltaGzip, err := util.GzipCompress(deltaBundleBytes)
				if err != nil {
					return fmt.Errorf("failed to compress delta bundle: %s %w", strings.Split(k1, ".")[0], err)
				}
				compressedBundle[strings.Split(k1, ".")[0]+"-delta.gz"] = deltaGzip

				// Store combined bundle (with delete mappings) for next iteration
				combinedBundleBytes, err := json.Marshal(combinedBundle)
				if err != nil {
					return fmt.Errorf("failed to marshal combined bundle: %s %w", strings.Split(k1, ".")[0], err)
				}
				combinedGzip, err := util.GzipCompress(combinedBundleBytes)
				if err != nil {
					return fmt.Errorf("failed to compress combined bundle: %w", err)
				}
				compressedBundle[strings.Split(k1, ".")[0]+".gz"] = combinedGzip
			}
		}
	}

	// prepare delete bundles for folders that have been removed
	for k2, b2 := range stateStoreBundleMap {
		found := false
		for k1 := range bundles {
			if strings.Split(k2, ".")[0] == strings.Split(k1, ".")[0] || strings.HasSuffix(k2, "-delta.gz") {
				found = true
			}
		}
		if !found {
			b2Bytes, err := util.GzipDecompress(b2)
			if err != nil {
				b2Bytes = b2
			}

			// reset current mappings from statestore
			// delete mappings should only persist for one version.
			srcBundle := graphman.Bundle{}
			err = json.Unmarshal(b2Bytes, &srcBundle)
			if err != nil {
				return fmt.Errorf("failed to unmarshal state store bundle: %s %w", strings.Split(k2, ".")[0], err)
			}

			// Reset mappings to get clean state (what's actually on gateway after last apply)
			err = graphman.ResetMappings(&srcBundle)
			if err != nil {
				return fmt.Errorf("failed to reset mappings: %s %w", strings.Split(k2, ".")[0], err)
			}

			// To delete all entities in srcBundle: current=srcBundle (cleaned), desired=empty
			// This generates delete mappings for everything.
			emptyBundle := graphman.Bundle{}
			deltaBundle, combinedBundle, err := graphman.CalculateDelta(srcBundle, emptyBundle)
			if err != nil {
				return fmt.Errorf("failed to subtract current and previous version bundle: %s %w", strings.Split(k2, ".")[0], err)
			}

			deltaBundleBytes, err := json.Marshal(deltaBundle)
			if err != nil {
				return fmt.Errorf("failed to marshal delta bundle: %s %w", strings.Split(k2, ".")[0], err)
			}

			deltaGzip, err := util.GzipCompress(deltaBundleBytes)
			if err != nil {
				return fmt.Errorf("failed to compress delta bundle: %s %w", strings.Split(k2, ".")[0], err)
			}
			compressedBundle[strings.Split(k2, ".")[0]+"-delta.gz"] = deltaGzip

			// Store combined bundle with delete mappings
			combinedBundleBytes, err := json.Marshal(combinedBundle)
			if err != nil {
				return fmt.Errorf("failed to marshal combined bundle: %s %w", strings.Split(k2, ".")[0], err)
			}
			combinedGzip, err := util.GzipCompress(combinedBundleBytes)
			if err != nil {
				return fmt.Errorf("failed to compress combined bundle: %w", err)
			}
			compressedBundle[strings.Split(k2, ".")[0]+".gz"] = combinedGzip
		}
	}

	compressedBundleBytes, err = json.Marshal(compressedBundle)
	if err != nil {
		return err
	}

	rs := rc.Set(ctx, redisKey, compressedBundleBytes, 0)
	if rs.Err() != nil {
		return fmt.Errorf("failed to reconcile state storage: %w", rs.Err())
	}

	err = os.WriteFile(tmpPath+"/"+fileName, compressedBundleBytes, 0755)
	if err != nil {
		return err
	}

	err = os.WriteFile(tmpPath+"/"+commitTracker, []byte{}, 0755)
	if err != nil {
		return err
	}

	return nil
}

func GetStateStoreChecksum(ctx context.Context, params Params, statestore securityv1alpha1.L7StateStore) (commit string, err error) {
	if params.Instance.Spec.Type != v1.RepositoryTypeStateStore || params.Instance.Spec.StateStoreKey == "" {
		return "", fmt.Errorf("repository %s in namespace %s does not use a statestore or no statestore key defined, please check your repository configuration", params.Instance.Name, params.Instance.Namespace)
	}

	if statestore.Spec.Redis.ExistingSecret != "" {
		stateStoreSecret, err := getStateStoreSecret(ctx, statestore.Spec.Redis.ExistingSecret, statestore, params)
		if err != nil {
			return "", err
		}
		statestore.Spec.Redis.Username = string(stateStoreSecret.Data["username"])
		statestore.Spec.Redis.MasterPassword = string(stateStoreSecret.Data["masterPassword"])
	}

	rc, err := util.RedisClient(&statestore.Spec.Redis)
	if err != nil {
		return "", fmt.Errorf("failed to connect to state store: %w", err)
	}
	defer rc.Close()

	bundle, err := rc.Get(ctx, params.Instance.Spec.StateStoreKey).Result()
	if err != nil {
		return "", fmt.Errorf("failed to retrieve bundle from state store: %w", err)
	}

	h := sha1.New()
	h.Write([]byte(bundle))

	return fmt.Sprintf("%x", h.Sum(nil)), nil

}

// parseStateStoreKeyPayload returns bundle JSON bytes from the Redis value: UTF-8 JSON object or gzip-compressed JSON object.
// The document must be a top-level JSON object ({...}), not an array.
func parseStateStoreKeyPayload(blob []byte) ([]byte, error) {
	blob = bytes.TrimSpace(blob)
	if len(blob) == 0 {
		return nil, fmt.Errorf("empty state store payload")
	}
	if len(blob) >= 2 && blob[0] == 0x1f && blob[1] == 0x8b {
		raw, err := util.GzipDecompress(blob)
		if err != nil {
			return nil, fmt.Errorf("gzip decompress state store payload: %w", err)
		}
		blob = bytes.TrimSpace(raw)
		if len(blob) == 0 {
			return nil, fmt.Errorf("empty state store payload after decompress")
		}
	}
	if blob[0] != '{' {
		return nil, fmt.Errorf("state store payload must be a top-level JSON object")
	}
	return blob, nil
}

// stateStorePayloadToLatestJSON builds the JSON bytes for latest.json (map[string][]byte with one *.gz key).
func stateStorePayloadToLatestJSON(repoName string, blob []byte) ([]byte, error) {
	bundleJSON, err := parseStateStoreKeyPayload(blob)
	if err != nil {
		return nil, err
	}
	gz, err := util.GzipCompress(bundleJSON)
	if err != nil {
		return nil, fmt.Errorf("failed to compress bundle: %w", err)
	}
	return json.Marshal(map[string][]byte{repoName + ".gz": gz})
}

// BuildStateStoreRepositoryCache writes util.StateStoreCacheDir/latest.json for RepositoryTypeStateStore: GET spec.stateStoreKey
// (read-only), single entry {name}.gz with gzip of bundle JSON. Does not write repository data back to Redis.
func BuildStateStoreRepositoryCache(ctx context.Context, params Params, statestore securityv1alpha1.L7StateStore, commit string) error {
	tmpPath := util.StateStoreCacheDir(params.Instance.Name, params.Instance.Namespace)
	commitTracker := commit + ".txt"
	fileName := "latest.json"
	if _, err := os.Stat(tmpPath); os.IsNotExist(err) {
		if err := os.MkdirAll(tmpPath, 0755); err != nil {
			return err
		}
	}

	latestFullPath := tmpPath + "/" + fileName
	if _, err := os.Stat(tmpPath + "/" + commitTracker); err == nil {
		if _, err := os.Stat(latestFullPath); err == nil {
			return nil
		}
	}

	if statestore.Spec.Redis.ExistingSecret != "" {
		stateStoreSecret, err := getStateStoreSecret(ctx, statestore.Spec.Redis.ExistingSecret, statestore, params)
		if err != nil {
			return err
		}
		statestore.Spec.Redis.Username = string(stateStoreSecret.Data["username"])
		statestore.Spec.Redis.MasterPassword = string(stateStoreSecret.Data["masterPassword"])
	}

	rc, err := util.RedisClient(&statestore.Spec.Redis)
	if err != nil {
		return fmt.Errorf("failed to connect to state store: %w", err)
	}
	defer rc.Close()

	currentStr, err := rc.Get(ctx, params.Instance.Spec.StateStoreKey).Result()
	if err != nil {
		return fmt.Errorf("failed to read stateStoreKey from redis: %w", err)
	}

	compressedBundleBytes, err := stateStorePayloadToLatestJSON(params.Instance.Name, []byte(currentStr))
	if err != nil {
		return err
	}

	if err := os.WriteFile(latestFullPath, compressedBundleBytes, 0755); err != nil {
		return err
	}
	if err := os.WriteFile(tmpPath+"/"+commitTracker, []byte{}, 0755); err != nil {
		return err
	}

	return nil
}

// RebuildCacheFromStateStore rebuilds the local filesystem bundle cache for a git/http repository
// that has a StateStoreReference, by reading the last-known-good bundle from Redis and writing it
// to util.StateStoreCacheDir/latest.json. This is the fallback path invoked when git (or the HTTP
// endpoint) is unavailable after an operator pod restart wiped the ephemeral filesystem.
// It returns an error when Redis is unreachable or no prior successful sync exists for this repository.
func RebuildCacheFromStateStore(ctx context.Context, params Params, statestore securityv1alpha1.L7StateStore) error {
	storageSecretName, _, _, err := localRepoStorageInfo(params)
	if err != nil {
		return fmt.Errorf("could not determine storage secret name: %w", err)
	}

	if statestore.Spec.Redis.ExistingSecret != "" {
		stateStoreSecret, err := getStateStoreSecret(ctx, statestore.Spec.Redis.ExistingSecret, statestore, params)
		if err != nil {
			return err
		}
		statestore.Spec.Redis.Username = string(stateStoreSecret.Data["username"])
		statestore.Spec.Redis.MasterPassword = string(stateStoreSecret.Data["masterPassword"])
	}

	rc, err := util.RedisClient(&statestore.Spec.Redis)
	if err != nil {
		return fmt.Errorf("failed to connect to state store: %w", err)
	}
	defer rc.Close()

	key := statestore.Spec.Redis.GroupName + ":" + statestore.Spec.Redis.StoreId + ":repository:" + params.Instance.Namespace + ":" + storageSecretName + ":latest"
	bundleMapStr, err := rc.Get(ctx, key).Result()
	if err != nil {
		return fmt.Errorf("no cached state in state store for key %s: %w", key, err)
	}

	tmpPath := util.StateStoreCacheDir(params.Instance.Name, params.Instance.Namespace)
	if err := os.MkdirAll(tmpPath, 0755); err != nil {
		return err
	}

	return os.WriteFile(tmpPath+"/latest.json", []byte(bundleMapStr), 0755)
}

func localRepoStorageInfo(params Params) (storageSecretName string, repositoryPath string, ext string, err error) {
	ext = params.Instance.Spec.Branch
	if ext == "" {
		ext = params.Instance.Spec.Tag
	}
	switch strings.ToLower(string(params.Instance.Spec.Type)) {
	case "http":
		fileURL, err := url.Parse(params.Instance.Spec.Endpoint)
		if err != nil {
			return "", "", "", err
		}
		path := fileURL.Path
		segments := strings.Split(path, "/")
		fileName := segments[len(segments)-1]
		ext = strings.Split(fileName, ".")[len(strings.Split(fileName, "."))-1]
		folderName := strings.ReplaceAll(fileName, "."+ext, "")
		if ext == "gz" && strings.Split(fileName, ".")[len(strings.Split(fileName, "."))-2] == "tar" {
			folderName = strings.ReplaceAll(fileName, ".tar.gz", "")
		}
		storageSecretName = params.Instance.Name + "-repository-" + folderName
		ext = folderName
		return storageSecretName, "/tmp/" + params.Instance.Name + "-" + params.Instance.Namespace + "-" + ext, ext, nil
	case "git":
		// Flatten any '/' in the ref so the storage-secret name is valid and the
		// returned checkout path matches the directory CloneRepository created
		// (both apply SafeRef to the same ref).
		ext = util.SafeRef(ext)
		storageSecretName = params.Instance.Name + "-repository-" + ext
		return storageSecretName, "/tmp/" + params.Instance.Name + "-" + params.Instance.Namespace + "-" + ext, ext, nil
	default:
		params.Log.Info("repository type not set or unsupported", "name", params.Instance.Name, "namespace", params.Instance.Name)
		return "", "", "", errors.New("repository type not set or unsupported")
	}
}

func getStateStore(ctx context.Context, params Params) (securityv1alpha1.L7StateStore, error) {
	statestore := securityv1alpha1.L7StateStore{}

	err := params.Client.Get(ctx, types.NamespacedName{Name: params.Instance.Spec.StateStoreReference, Namespace: params.Instance.Namespace}, &statestore)
	if err != nil {
		return statestore, err
	}
	return statestore, nil
}

func getStateStoreSecret(ctx context.Context, name string, statestore securityv1alpha1.L7StateStore, params Params) (*corev1.Secret, error) {
	statestoreSecret := &corev1.Secret{}

	err := params.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: statestore.Namespace}, statestoreSecret)
	if err != nil {
		return statestoreSecret, err
	}
	return statestoreSecret, nil
}
