package run

import (
	"bufio"
	"bytes"
	"fmt"
	"net/http"

	api "github.com/apicurio/apicurio-operators/apicurito/pkg/apis/apicur/v1alpha1"
	config "github.com/apicurio/apicurio-operators/apicurito/pkg/configuration"
	"github.com/heroku/docker-registry-client/registry"

	"github.com/apicurio/apicurio-operators/apicurito/tools/components"
	"github.com/apicurio/apicurio-operators/apicurito/tools/constants"
	"github.com/apicurio/apicurio-operators/apicurito/tools/util"
	"github.com/apicurio/apicurio-operators/apicurito/version"
	"github.com/blang/semver"
	oimagev1 "github.com/openshift/api/image/v1"
	csvv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/api/apis/operators/v1alpha1"
	olmversion "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/version"
	"github.com/tidwall/sjson"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbac "k8s.io/api/rbac/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"k8s.io/apimachinery/pkg/util/json"

	"os"

	"strconv"
	"strings"
	"time"
)

var (
	rh         = "Red Hat"
	maturity   = "alpha"
	maintainer = "Apicurito Project"
	csv        = csvSetting{

		Name:         "apicurito",
		DisplayName:  "Apicurito Operator",
		OperatorName: "apicurito-operator",
		CsvDir:       "apicurito-operator",
		Registry:     "registry.redhat.io",
		Context:      "fuse7-tech-preview",
		ImageName:    "fuse-apicurito-operator",
		Tag:          constants.Apicurito16ImageTag,
	}
)

func Run() error {
	c := &config.Config{}
	if err := c.Config(nil); err != nil {
		return err
	}

	imageShaMap := map[string]string{}

	operatorName := csv.Name + "operator"

	templateStruct := &csvv1.ClusterServiceVersion{}
	templateStruct.SetGroupVersionKind(csvv1.SchemeGroupVersion.WithKind("ClusterServiceVersion"))
	csvStruct := &csvv1.ClusterServiceVersion{}
	strategySpec := &csvStrategySpec{}
	json.Unmarshal(csvStruct.Spec.InstallStrategy.StrategySpecRaw, strategySpec)

	templateStrategySpec := &csvStrategySpec{}

	deployment := components.GetDeployment(csv.OperatorName, csv.Registry, csv.Context, csv.ImageName, csv.Tag, "Always", c)
	templateStrategySpec.Deployments = append(templateStrategySpec.Deployments, []csvDeployments{{Name: csv.OperatorName, Spec: deployment.Spec}}...)
	role := components.GetRole(csv.OperatorName)
	templateStrategySpec.Permissions = append(templateStrategySpec.Permissions, []csvPermissions{{ServiceAccountName: deployment.Spec.Template.Spec.ServiceAccountName, Rules: role.Rules}}...)
	clusterRole := components.GetClusterRole(csv.OperatorName)
	templateStrategySpec.ClusterPermissions = append(templateStrategySpec.ClusterPermissions, []StrategyDeploymentPermissions{{ServiceAccountName: deployment.Spec.Template.Spec.ServiceAccountName, Rules: clusterRole.Rules}}...)

	updatedStrat, err := json.Marshal(templateStrategySpec)
	if err != nil {
		panic(err)
	}
	templateStruct.Spec.InstallStrategy.StrategySpecRaw = updatedStrat
	templateStruct.Spec.InstallStrategy.StrategyName = "deployment"
	csvVersionedName := operatorName + ".v" + version.Version
	templateStruct.Name = csvVersionedName
	templateStruct.Namespace = "placeholder"
	annotdescrip := "Manages the installation and upgrades of apicurito, a small/minimal version of Apicurio"
	var description = "Apicurito is a small/minimal version of Apicurio, a standalone API design studio that can be used to create new or edit existing API designs (using the OpenAPI specification).\n"
	description += "\n"
	description += "This operator supports the installation and upgrade of apicurito. Apicurito components are:\n"
	description += "   - apicurito-ui (apicurito application)\n"
	description += "   - apicurito route (to access apicurito from outside openshift)\n"
	description += "\n"
	description += "### How to install\n"
	description += "When the operator is installed (you have created a subscription and the operator is running in the selected namespace) create a new CR of Kind Apicurito (click the Create New button). The CR spec contains all defaults.\n"
	description += "\n"
	description += "At the moment, following fields are supported as part of the CR:\n"
	description += "   - size: how many pods your the apicurito operand will have.\n"
	description += "   - image: the apicurito image, this can be found [here](https://hub.docker.com/r/apicurio/apicurito-ui/tags). Changing this image in an existing installation will trigger an upgrade of the operand."
	description += "\n"
	description += "### How to upgrade\n"
	description += "Upgrades are trigered by updating the image field in the CR. This can be done manually via the Openshift console, or with kubeclt:\n"
	description += "```\n"
	description += "$ cat apicurito_cr.yaml\n"
	description += "    apiVersion: apicur.io/v1alpha1\n"
	description += "      kind: Apicurito\n"
	description += "      metadata:\n"
	description += "        name: apicurito-service\n"
	description += "      spec:\n"
	description += "        size: 3\n"
	description += "        image: apicurio/apicurito-ui:newversion\n"
	description += ""
	description += " $ kubectl apply -f apicurito_cr.yaml\n"
	description += "```\n"

	repository := "https://github.com/Apicurio/apicurio-operators/tree/master/apicurito"
	examples := []string{"{\n        \"apiVersion\": \"apicur.io/v1alpha1\",\n        \"kind\": \"Apicurito\",\n        \"metadata\": {\n          \"name\": \"apicurito-service\"\n        },\n        \"spec\": {\n          \"size\": 3,\n          \"image\": \"registry.redhat.io/fuse7/fuse-apicurito@sha256:1177f9ee841f95f40ea0b0de76f28c3a7b01ebef5ab335674f24bb5c17f88431\"\n        }\n      }"}

	templateStruct.SetAnnotations(
		map[string]string{
			"capabilities":   "Seamless Upgrades",
			"categories":     "Integration & Delivery",
			"certified":      "false",
			"createdAt":      time.Now().Format("2006-01-02 15:04:05"),
			"containerImage": deployment.Spec.Template.Spec.Containers[0].Image,
			"support":        "Apicurito Project",
			"description":    annotdescrip,
			"repository":     repository,
			"alm-examples":   "[" + strings.Join(examples, ",") + "]",
		},
	)
	/*templateStruct.SetLabels(
		map[string]string{
			"operator-" + csv.Name: "true",
		},
	)*/

	var opVersion olmversion.OperatorVersion
	templateStruct.Spec.DisplayName = csv.DisplayName
	templateStruct.Spec.Description = description
	templateStruct.Spec.Keywords = []string{"api", "apicurio", "apicurito"}
	opVersion.Version = semver.MustParse(version.Version)
	templateStruct.Spec.Version = opVersion
	templateStruct.Spec.Maturity = maturity
	templateStruct.Spec.Maintainers = []csvv1.Maintainer{{Name: maintainer, Email: "apicurio@lists.jboss.org"}}
	templateStruct.Spec.Provider = csvv1.AppLink{Name: rh}
	templateStruct.Spec.Links = []csvv1.AppLink{
		{Name: "Apicurito source code", URL: "https://github.com/Apicurio/apicurito"},
		{Name: "Apicurito operator source code", URL: "https://github.com/Apicurio/apicurio-operators/tree/master/apicurito"},
	}

	templateStruct.Spec.Icon = []csvv1.Icon{
		{
			Data:      " ",
			MediaType: "image/png",
		},
	}
	tLabels := map[string]string{
		//"alm-owner-" + csv.Name: operatorName,
		"name": operatorName,
	}
	templateStruct.Spec.Labels = tLabels
	templateStruct.Spec.Selector = &metav1.LabelSelector{MatchLabels: tLabels}
	templateStruct.Spec.InstallModes = []csvv1.InstallMode{
		{Type: csvv1.InstallModeTypeOwnNamespace, Supported: true},
		{Type: csvv1.InstallModeTypeSingleNamespace, Supported: true},
		{Type: csvv1.InstallModeTypeMultiNamespace, Supported: false},
		{Type: csvv1.InstallModeTypeAllNamespaces, Supported: false},
	}
	templateStruct.Spec.Replaces = operatorName + ".v" + version.PriorVersion
	templateStruct.Spec.CustomResourceDefinitions.Owned = []csvv1.CRDDescription{
		{
			Version:     api.SchemeGroupVersion.Version,
			Kind:        "Apicurito",
			DisplayName: "Apicurito CRD",
			Description: "CRD for Apicurito",
			Name:        "apicuritos." + api.SchemeGroupVersion.Group,
			/*Resources: []csvv1.APIResourceReference{

				{
					Kind:    "StatefulSet",
					Version: appsv1.SchemeGroupVersion.String(),
				},
				{
					Kind:    "Secret",
					Version: corev1.SchemeGroupVersion.String(),
				},
				{
					Kind:    "Service",
					Version: corev1.SchemeGroupVersion.String(),
				},

				{
					Kind:    "ImageStream",
					Version: oimagev1.SchemeGroupVersion.String(),
				},
			},*/
			SpecDescriptors: []csvv1.SpecDescriptor{

				{
					Description:  "The number of Apicurito pods to deploy",
					DisplayName:  "Size",
					Path:         "size",
					XDescriptors: []string{"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Deployment", "urn:alm:descriptor:com.tectonic.ui:podCount"},
				},
				{
					Description:  "The image used for the Apicurito deployment",
					DisplayName:  "Image",
					Path:         "image",
					XDescriptors: []string{"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Deployment", "urn:alm:descriptor:com.tectonic.ui:text"},
				},
			},
		},
	}

	opMajor, opMinor, opMicro := config.MajorMinorMicro(version.Version)
	csvFile := "deploy/manifests" + "/" + opMajor + "." + opMinor + "." + opMicro + "/" + csvVersionedName + ".clusterserviceversion.yaml"

	imageName, _, _ := config.GetImage(deployment.Spec.Template.Spec.Containers[0].Image)
	relatedImages := []image{}

	templateStruct.Annotations["certified"] = "false"
	deployFile := "deploy/operator.yaml"
	createFile(deployFile, deployment)
	roleFile := "deploy/role.yaml"
	createFile(roleFile, role)

	relatedImages = append(relatedImages, image{Name: imageName, Image: deployment.Spec.Template.Spec.Containers[0].Image})

	imageRef := constants.ImageRef{
		TypeMeta: metav1.TypeMeta{
			APIVersion: oimagev1.SchemeGroupVersion.String(),
			Kind:       "ImageStream",
		},
		Spec: constants.ImageRefSpec{
			Tags: []constants.ImageRefTag{
				{
					// Needs to match the component name for upstream and downstream.
					Name: "fuse7-tech-preview/fuse-apicurito-operator",
					From: &corev1.ObjectReference{
						// Needs to match the image that is in your CSV that you want to replace.
						Name: deployment.Spec.Template.Spec.Containers[0].Image,
						Kind: "DockerImage",
					},
				},
			},
		},
	}

	imageRef.Spec.Tags = append(imageRef.Spec.Tags, constants.ImageRefTag{
		Name: constants.Apicurito16Component,
		From: &corev1.ObjectReference{
			Name: constants.Apicurito16ImageURL,
			Kind: "DockerImage",
		},
	})

	relatedImages = append(relatedImages, getRelatedImage(constants.Apicurito16ImageURL))

	if GetBoolEnv("DIGESTS") {

		for _, tagRef := range imageRef.Spec.Tags {

			if _, ok := imageShaMap[tagRef.From.Name]; !ok {
				imageShaMap[tagRef.From.Name] = ""
				imageName, imageTag, imageContext := config.GetImage(tagRef.From.Name)
				repo := imageContext + "/" + imageName

				digests, err := RetriveFromRedHatIO(repo, imageTag)
				if err != nil {
					fmt.Fprintln(os.Stderr, err)
				}
				if len(digests) > 1 {
					imageShaMap[tagRef.From.Name] = strings.ReplaceAll(tagRef.From.Name, ":"+imageTag, "@"+digests[len(digests)-1])
				}
			}
		}
	}

	//not sure if we required mage-references file in the future So comment out for now.

	//imageFile := "deploy/olm-catalog/" + csv.CsvDir + "/" + opMajor + "." + opMinor + "." + opMicro + "/" + "image-references"
	//createFile(imageFile, imageRef)

	var templateInterface interface{}
	if len(relatedImages) > 0 {
		templateJSON, err := json.Marshal(templateStruct)
		if err != nil {
			fmt.Println(err)
		}
		result, err := sjson.SetBytes(templateJSON, "spec.relatedImages", relatedImages)
		if err != nil {
			fmt.Println(err)

		}
		if err = json.Unmarshal(result, &templateInterface); err != nil {
			fmt.Println(err)
		}
	} else {
		templateInterface = templateStruct
	}

	// find and replace images with SHAs where necessary
	templateByte, err := json.Marshal(templateInterface)
	if err != nil {
		fmt.Println(err)
	}
	for from, to := range imageShaMap {
		if to != "" {
			templateByte = bytes.ReplaceAll(templateByte, []byte(from), []byte(to))
		}
	}
	if err = json.Unmarshal(templateByte, &templateInterface); err != nil {
		fmt.Println(err)
	}
	createFile(csvFile, &templateInterface)
	packageFile := "deploy/manifests/" + csv.Name + ".package.yaml"
	p, err := os.Create(packageFile)
	defer p.Close()
	if err != nil {
		return err
	}
	pwr := bufio.NewWriter(p)
	pwr.WriteString("#! package-manifest: " + csvFile + "\n")
	packagedata := packageStruct{
		PackageName: csv.Name,
		Channels: []channel{
			{
				Name:       maturity,
				CurrentCSV: operatorName + ".v" + version.PriorVersion,
			},
			{
				Name:       maturity + "-offline",
				CurrentCSV: csvVersionedName,
			},
		},
		DefaultChannel: maturity,
	}
	util.MarshallObject(packagedata, pwr)
	pwr.Flush()

	return nil
}

func RetriveFromRedHatIO(image string, imageTag string) ([]string, error) {

	url := "https://" + constants.RedHatImageRegistry

	username := "" // anonymous
	password := "" // anonymous

	if userToken := strings.Split(os.Getenv("REDHATIO_TOKEN"), ":"); len(userToken) > 1 {
		username = userToken[0]
		password = userToken[1]
	}
	hub, err := registry.New(url, username, password)
	digests := []string{}
	if err != nil {
		fmt.Println(err)
	} else {
		tags, err := hub.Tags(image)
		if err != nil {
			fmt.Println(err)
		}
		// do not follow redirects - this is critical so we can get the registry digest from Location in redirect response
		hub.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		}
		if _, exists := find(tags, imageTag); exists {
			req, err := http.NewRequest("GET", url+"/v2/"+image+"/manifests/"+imageTag, nil)
			if err != nil {
				fmt.Println(err)
			}
			req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")
			resp, err := hub.Client.Do(req)
			if err != nil {
				fmt.Println(err)
			}
			if resp != nil {
				defer resp.Body.Close()
			}
			if resp.StatusCode == 302 || resp.StatusCode == 301 {
				digestURL, err := resp.Location()
				if err != nil {
					fmt.Println(err)

				}

				if digestURL != nil {
					if url := strings.Split(digestURL.EscapedPath(), "/"); len(url) > 1 {
						digests = url

						return url, err
					}
				}
			}
		}

	}
	return digests, err
}

func find(slice []string, val string) (int, bool) {
	for i, item := range slice {
		if item == val {
			return i, true
		}
	}
	return -1, false
}

type csvSetting struct {
	Name         string `json:"name"`
	DisplayName  string `json:"displayName"`
	OperatorName string `json:"operatorName"`
	CsvDir       string `json:"csvDir"`
	Registry     string `json:"repository"`
	Context      string `json:"context"`
	ImageName    string `json:"imageName"`
	Tag          string `json:"tag"`
}
type csvPermissions struct {
	ServiceAccountName string              `json:"serviceAccountName"`
	Rules              []rbacv1.PolicyRule `json:"rules"`
}
type csvDeployments struct {
	Name string                `json:"name"`
	Spec appsv1.DeploymentSpec `json:"spec,omitempty"`
}
type csvStrategySpec struct {
	Permissions        []csvPermissions                `json:"permissions"`
	Deployments        []csvDeployments                `json:"deployments"`
	ClusterPermissions []StrategyDeploymentPermissions `json:"clusterPermissions,omitempty"`
}

type StrategyDeploymentPermissions struct {
	ServiceAccountName string            `json:"serviceAccountName"`
	Rules              []rbac.PolicyRule `json:"rules"`
}
type channel struct {
	Name       string `json:"name"`
	CurrentCSV string `json:"currentCSV"`
}
type packageStruct struct {
	PackageName    string    `json:"packageName"`
	Channels       []channel `json:"channels"`
	DefaultChannel string    `json:"defaultChannel"`
}
type image struct {
	Name  string `json:"name"`
	Image string `json:"image"`
}

func getRelatedImage(imageURL string) image {
	imageName, _, _ := config.GetImage(imageURL)
	return image{
		Name:  imageName,
		Image: imageURL,
	}
}

func fileExists(filename string) bool {
	info, err := os.Stat(filename)
	if os.IsNotExist(err) {
		return false
	}
	return !info.IsDir()
}

func createFile(filepath string, obj interface{}) {
	f, err := os.Create(filepath)
	defer f.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	writer := bufio.NewWriter(f)
	util.MarshallObject(obj, writer)
	writer.Flush()
}

func GetBoolEnv(key string) bool {
	val := GetEnv(key, "false")
	ret, err := strconv.ParseBool(val)
	if err != nil {
		return false
	}
	return ret
}

func GetEnv(key, fallback string) string {
	value, exists := os.LookupEnv(key)
	if !exists {
		value = fallback
	}
	return value
}
