package run

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"path/filepath"

	"github.com/blang/semver"

	api "github.com/apicurio/apicurio-operators/apicurito/pkg/apis/apicur/v1alpha1"
	config "github.com/apicurio/apicurio-operators/apicurito/pkg/configuration"
	"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/heroku/docker-registry-client/registry"
	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"
	"gopkg.in/yaml.v2"
	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:         "fuse-apicurito",
		DisplayName:  "Red Hat Apicurito Operator",
		OperatorName: "fuse-apicurito",
		CsvDir:       "apicurito-operator",
		Registry:     "registry.redhat.io",
		Context:      "fuse7",
		ImageName:    "fuse-apicurito-rhel7-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

	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 API designer application)\n"
	description += "   - apicurito-generator (Apicurito fuse project generator)\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 += "\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        }\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:      "[encoded data, 6660 characters]",
			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:        "apicuritoes." + api.SchemeGroupVersion.Group,
			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"},
				},
			},
		},
	}

	// opMajor, opMinor, opMicro := config.MajorMinorMicro(version.Version)
	path, err := os.Getwd()
	if err != nil {
		log.Println(err)
	}
	path = path + "/deploy/"

	csvFile := path + "bundle/manifests/apicurito.clusterserviceversion.yaml"
	if err := ensureDir(path); err != nil {
		return err
	}

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

	templateStruct.Annotations["certified"] = "false"
	deployFile := path + "operator.yaml"
	createFile(deployFile, deployment)
	roleFile := path + "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/fuse-apicurito-rhel7-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))
	relatedImages = append(relatedImages, getRelatedImage(constants.Generator16ImageURL))

	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)

	o, err := buildContainer()
	if err != nil {
		return err
	}
	if err = ioutil.WriteFile(filepath.Join(path, "bundle", "container.yaml"), o, 0644); err != nil {
		return err
	}

	od, err := buildDocker(c)
	if err != nil {
		return err
	}
	if err = ioutil.WriteFile(filepath.Join(path, "bundle", "Dockerfile"), od, 0644); err != nil {
		return err
	}

	oa, err := buildAnnotation()
	if err != nil {
		return err
	}
	if err = ioutil.WriteFile(filepath.Join(path, "bundle", "metadata", "annotations.yaml"), oa, 0644); err != nil {
		return err
	}

	_, err = copy(filepath.Join(path, "crds", "apicur.io_apicuritoes_crd.yaml"), filepath.Join(path, "bundle", "manifests", "apicur.io_apicuritoes_crd.yaml"))
	if err != nil {
		return err
	}

	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 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{}) {
	createParentDirIfMissing(filepath)
	f, err := os.Create(filepath)
	defer f.Close()
	if err != nil {
		fmt.Println(err)
		return
	}
	writer := bufio.NewWriter(f)
	_, _ = writer.WriteString("# This file is generated by tools/run/run.go\n")
	err = util.MarshallObject(obj, writer)
	if err != nil {
		fmt.Println(err)
	}
	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
}

// Generate directory tree for OLM bundle. Path must already
// exist
func ensureDir(path string) (err error) {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return err
	}

	err = os.Mkdir(filepath.Join(path, "bundle"), 0755)
	if err != nil && !os.IsExist(err) {
		return err
	}

	err = os.Mkdir(filepath.Join(path, "bundle", "manifests"), 0755)
	if err != nil && !os.IsExist(err) {
		return err
	}

	err = os.Mkdir(filepath.Join(path, "bundle", "metadata"), 0755)
	if err != nil && !os.IsExist(err) {
		return err
	}

	return nil
}

func buildContainer() (out []byte, err error) {
	m := `operator_manifests:
  manifests_dir: manifests
  enable_digest_pinning: true
  enable_repo_replacements: true
  enable_registry_replacements: true
  repo_replacements:
    - registry: registry.redhat.io
      package_mappings:
        fuse-apicurito-operator-container: fuse7
        fuse-apicurito-openshift-container: fuse7
        fuse-apicurito-generator-openshift-container: fuse7`

	out = []byte(m)
	return
}

func buildDocker(c *config.Config) (out []byte, err error) {
	name := "fuse-apicurito"
	channel := fmt.Sprintf("%s-%s.x", name, version.Version)

	m := `FROM scratch

LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1
LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/
LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/
LABEL operators.operatorframework.io.bundle.package.v1=-%s
LABEL operators.operatorframework.io.bundle.channels.v1=%s
LABEL operators.operatorframework.io.bundle.channel.default.v1=%s
LABEL com.redhat.delivery.operator.bundle=true
LABEL com.redhat.openshift.versions="%s"

COPY manifests /manifests/
COPY metadata/annotations.yaml /metadata/annotations.yaml

LABEL name="fuse7/fuse-online-operator-metadata" \
      version="%s" \
      maintainer="Otavio Piske <opiske@redhat.com>" \
      summary="Operator which manages the lifecycle of the Apicurito application." \
      description="Operator which manages the lifecycle of the Apicurito application." \
      com.redhat.component="fuse-apicurito-operator-metadata-container" \
      io.k8s.description="Operator which manages the lifecycle of the Apicurito application." \
      io.k8s.display-name="Red Hat Apicurito Operator" \
      io.openshift.tags="fuse,API"
`
	m = fmt.Sprintf(m, name, channel, channel, c.SupportedOpenShiftVersions, fmt.Sprintf("%s.x", version.Version))
	out = []byte(m)
	return
}

func buildAnnotation() (out []byte, err error) {
	name := "fuse-apicurito"

	channel := fmt.Sprintf("apicurito-%s.x", version.Version)

	m := map[string]map[string]string{
		"annotations": {
			"operators.operatorframework.io.bundle.mediatype.v1":       "registry+v1",
			"operators.operatorframework.io.bundle.manifests.v1":       "manifests/",
			"operators.operatorframework.io.bundle.metadata.v1":        "metadata/",
			"operators.operatorframework.io.bundle.package.v1":         name,
			"operators.operatorframework.io.bundle.channels.v1":        channel,
			"operators.operatorframework.io.bundle.channel.default.v1": channel,
		},
	}
	out, err = yaml.Marshal(m)
	return
}

func copy(src, dst string) (int64, error) {
	sourceFileStat, err := os.Stat(src)
	if err != nil {
		return 0, err
	}

	if !sourceFileStat.Mode().IsRegular() {
		return 0, fmt.Errorf("%s is not a regular file", src)
	}

	source, err := os.Open(src)
	if err != nil {
		return 0, err
	}
	defer source.Close()

	destination, err := os.Create(dst)
	if err != nil {
		return 0, err
	}
	defer destination.Close()
	nBytes, err := io.Copy(destination, source)
	return nBytes, err
}

func createParentDirIfMissing(file string) {
	idx := strings.LastIndex(file, string(filepath.Separator))
	dirName := file[:idx]
	_, err := os.Stat(dirName)
	if os.IsNotExist(err) {
		errDir := os.MkdirAll(dirName, 0755)
		if errDir != nil {
			log.Fatal(err)
		}
	}
}
