package handlers

import (
	"encoding/json"
	"errors"
	"fmt"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
	"net/http"
	"sort"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/hexops/gotextdiff"
	"github.com/hexops/gotextdiff/myers"
	"github.com/hexops/gotextdiff/span"
	"github.com/joomcode/errorx"
	"github.com/komodorio/helm-dashboard/pkg/dashboard/objects"
	"github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
	"github.com/rogpeppe/go-internal/semver"
	log "github.com/sirupsen/logrus"
	"gopkg.in/yaml.v3"
	"helm.sh/helm/v3/pkg/chartutil"
	"helm.sh/helm/v3/pkg/release"
	"helm.sh/helm/v3/pkg/repo"
	helmtime "helm.sh/helm/v3/pkg/time"
	"k8s.io/utils/strings/slices"
)

type HelmHandler struct {
	*Contexted
}

func (h *HelmHandler) getRelease(c *gin.Context) *objects.Release {
	app := h.GetApp(c)
	if app == nil {
		return nil
	}

	rel, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
	if err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return nil
	}
	return rel
}

func (h *HelmHandler) GetReleases(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	rels, err := app.Releases.List()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	res := []*ReleaseElement{}
	for _, r := range rels {
		res = append(res, HReleaseToJSON(r.Orig))
	}

	c.IndentedJSON(http.StatusOK, res)
}

func (h *HelmHandler) Uninstall(c *gin.Context) {
	rel := h.getRelease(c)
	if rel == nil {
		return // error state is set inside
	}

	err := rel.Uninstall()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.Status(http.StatusAccepted)
}

func (h *HelmHandler) Rollback(c *gin.Context) {
	rel := h.getRelease(c)
	if rel == nil {
		return // error state is set inside
	}

	revn, err := strconv.Atoi(c.PostForm("revision"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	err = rel.Rollback(revn)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.Status(http.StatusAccepted)
}

func (h *HelmHandler) History(c *gin.Context) {
	rel := h.getRelease(c)
	if rel == nil {
		return // error state is set inside
	}

	revs, err := rel.History()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	res := []*HistoryElement{}
	for _, r := range revs {
		res = append(res, HReleaseToHistElem(r.Orig))
	}

	sort.Slice(res, func(i, j int) bool {
		return res[i].Revision < res[j].Revision
	})

	c.IndentedJSON(http.StatusOK, res)
}

func (h *HelmHandler) Resources(c *gin.Context) {
	// can't enable the client cache because resource list changes with time

	rel := h.getRelease(c)
	if rel == nil {
		return // error state is set inside
	}

	res, err := objects.ParseManifests(rel.Orig.Manifest)
	if err != nil {
		res = append(res, &v1.Carp{
			TypeMeta: metav1.TypeMeta{Kind: "ManifestParseError"},
			ObjectMeta: metav1.ObjectMeta{
				Name: err.Error(),
			},
			Spec: v1.CarpSpec{},
			Status: v1.CarpStatus{
				Phase:   "BrokenManifest",
				Message: err.Error(),
			},
		})
		//_ = c.AbortWithError(http.StatusInternalServerError, err)
		//return
	}

	if c.Query("health") != "" { // we need  to query k8s for health status
		app := h.GetApp(c)
		if app == nil {
			_ = c.AbortWithError(http.StatusInternalServerError, err)
			return
		}
		for _, obj := range res {
			ns := obj.Namespace
			if ns == "" {
				ns = c.Param("ns")
			}
			info, err := app.K8s.GetResourceInfo(obj.Kind, ns, obj.Name)
			if err != nil {
				log.Warnf("Failed to get resource info for %s %s/%s: %+v", obj.Name, ns, obj.Name, err)
				info = &v1.Carp{}
			}
			obj.Status = *EnhanceStatus(info, err)
		}
	}

	c.IndentedJSON(http.StatusOK, res)
}

func (h *HelmHandler) RepoVersions(c *gin.Context) {
	qp, err := utils.GetQueryProps(c)
	if err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	repos, err := app.Repositories.Containing(qp.Name)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	res := []*RepoChartElement{}
	for _, r := range repos {
		res = append(res, &RepoChartElement{
			Name:        r.Name,
			Version:     r.Version,
			AppVersion:  r.AppVersion,
			Description: r.Description,
			Repository:  r.Annotations[objects.AnnRepo],
			URLs:        r.URLs,
		})
	}

	c.IndentedJSON(http.StatusOK, res)
}

func (h *HelmHandler) RepoLatestVer(c *gin.Context) {
	qp, err := utils.GetQueryProps(c)
	if err != nil {
		_ = c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	rep, err := app.Repositories.Containing(qp.Name)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	res := []*RepoChartElement{}
	for _, r := range rep {
		res = append(res, &RepoChartElement{
			Name:        r.Name,
			Version:     r.Version,
			AppVersion:  r.AppVersion,
			Description: r.Description,
			Repository:  r.Annotations[objects.AnnRepo],
			URLs:        r.URLs,
		})
	}

	sort.Slice(res, func(i, j int) bool {
		return semver.Compare(res[i].Version, res[j].Version) > 0
	})

	if len(res) > 0 {
		c.IndentedJSON(http.StatusOK, res[:1])
	} else {
		// caching it to avoid too many requests
		found, err := h.Data.Cache.String("chart-artifacthub-query/"+qp.Name, nil, func() (string, error) {
			return h.repoFromArtifactHub(qp.Name)
		})
		if err != nil {
			_ = c.AbortWithError(http.StatusInternalServerError, err)
			return
		}

		if found == "" {
			c.Status(http.StatusNoContent)
		} else {
			c.Header("Content-Type", "application/json")
			c.String(http.StatusOK, found)
		}
	}
}

func (h *HelmHandler) RepoCharts(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	rep, err := app.Repositories.Get(c.Param("name"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	charts, err := rep.Charts()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	installed, err := app.Releases.List()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	enrichRepoChartsWithInstalled(charts, installed)

	sort.Slice(charts, func(i, j int) bool {
		return charts[i].Name < charts[j].Name
	})

	c.IndentedJSON(http.StatusOK, charts)
}

func enrichRepoChartsWithInstalled(charts []*repo.ChartVersion, installed []*objects.Release) {
	for _, rchart := range charts {
		for _, rel := range installed {
			if rchart.Metadata.Name == rel.Orig.Chart.Name() {
				log.Debugf("Matched") // TODO: restore implementation
				// TODO: there can be more than one
				//rchart.InstalledNamespace = rel.Orig.Namespace
				//rchart.InstalledName = rel.Orig.Name
			}
		}
	}
}

func (h *HelmHandler) RepoUpdate(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	rep, err := app.Repositories.Get(c.Param("name"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	err = rep.Update()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.Status(http.StatusNoContent)
}

func (h *HelmHandler) Install(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	values := map[string]interface{}{}
	err := yaml.Unmarshal([]byte(c.PostForm("values")), &values)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	justTemplate := c.PostForm("preview") == "true"
	ns := c.Param("ns")
	if ns == "[empty]" {
		ns = ""
	}

	rel, err := app.Releases.Install(ns, c.PostForm("name"), repoChart, c.PostForm("version"), justTemplate, values)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	if justTemplate {
		c.IndentedJSON(http.StatusOK, rel)
	} else {
		c.IndentedJSON(http.StatusAccepted, rel)
	}
}

func (h *HelmHandler) checkLocalRepo(repoChart string) (string, error) {
	if strings.HasPrefix(repoChart, "file://") {
		repoChart = repoChart[len("file://"):]
		if !slices.Contains(h.Data.LocalCharts, repoChart) {
			return "", fmt.Errorf("chart path is not present in local charts: %s", repoChart)
		}
	}
	return repoChart, nil
}

func (h *HelmHandler) Upgrade(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	existing, err := app.Releases.ByName(c.Param("ns"), c.Param("name"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	values := map[string]interface{}{}
	err = yaml.Unmarshal([]byte(c.PostForm("values")), &values)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	repoChart, err := h.checkLocalRepo(c.PostForm("chart"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	justTemplate := c.PostForm("preview") == "true"
	rel, err := existing.Upgrade(repoChart, c.PostForm("version"), justTemplate, values)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	if justTemplate {
		c.IndentedJSON(http.StatusOK, rel)
	} else {
		c.IndentedJSON(http.StatusAccepted, rel)
	}
}

func (h *HelmHandler) RunTests(c *gin.Context) {
	rel := h.getRelease(c)
	if rel == nil {
		return // error state is set inside
	}

	out, err := rel.RunTests()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.String(http.StatusOK, out)
}

func (h *HelmHandler) GetInfoSection(c *gin.Context) {
	if c.Query("revision") != "" { // don't cache if latest is requested
		h.EnableClientCache(c)
	}

	rel := h.getRelease(c)
	if rel == nil {
		return // error state is set inside
	}

	revn, err := strconv.Atoi(c.Query("revision"))
	if c.Query("revision") != "" && err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	rev, err := rel.GetRev(revn)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	var revDiff *objects.Release
	revS := c.Query("revisionDiff")
	if revS != "" {
		revN, err := strconv.Atoi(revS)
		if err != nil {
			_ = c.AbortWithError(http.StatusInternalServerError, err)
			return
		}

		revDiff, err = rel.GetRev(revN)
		if err != nil {
			_ = c.AbortWithError(http.StatusInternalServerError, err)
			return
		}
	}

	flag := c.Query("userDefined") == "true"

	res, err := h.handleGetSection(rev, c.Param("section"), revDiff, flag)
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.String(http.StatusOK, res)
}

func (h *HelmHandler) RepoValues(c *gin.Context) {
	h.EnableClientCache(c)

	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	repoChart, err := h.checkLocalRepo(c.Query("chart"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	out, err := app.Repositories.GetChartValues(repoChart, c.Query("version"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	c.String(http.StatusOK, out)
}

func (h *HelmHandler) RepoList(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	repos, err := app.Repositories.List()
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	out := []RepositoryElement{}
	for _, r := range repos {
		out = append(out, RepositoryElement{
			Name: r.Name(),
			URL:  r.URL(),
		})
	}

	c.IndentedJSON(http.StatusOK, out)
}

func (h *HelmHandler) RepoAdd(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	// TODO: more repo options to accept
	err := app.Repositories.Add(c.PostForm("name"), c.PostForm("url"), c.PostForm("username"), c.PostForm("password"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.Status(http.StatusNoContent)
}

func (h *HelmHandler) RepoDelete(c *gin.Context) {
	app := h.GetApp(c)
	if app == nil {
		return // sets error inside
	}

	err := app.Repositories.Delete(c.Param("name"))
	if err != nil {
		_ = c.AbortWithError(http.StatusInternalServerError, err)
		return
	}
	c.Status(http.StatusNoContent)
}

func (h *HelmHandler) handleGetSection(rel *objects.Release, section string, rDiff *objects.Release, flag bool) (string, error) {
	sections := map[string]objects.SectionFn{
		"manifests": func(qp *release.Release, b bool) (string, error) { return qp.Manifest, nil },
		"notes":     func(qp *release.Release, b bool) (string, error) { return qp.Info.Notes, nil },
		"values": func(qp *release.Release, b bool) (string, error) {
			allVals := qp.Config

			if !b {
				merged, err := chartutil.CoalesceValues(qp.Chart, qp.Config)
				if err != nil {
					return "", errorx.Decorate(err, "failed to merge chart vals with user defined")
				}
				allVals = merged
			}

			if len(allVals) > 0 {
				data, err := yaml.Marshal(allVals)
				if err != nil {
					return "", errorx.Decorate(err, "failed to serialize values into YAML")
				}
				return string(data), nil
			}
			return "", nil
		},
	}

	functor, found := sections[section]
	if !found {
		return "", errors.New("unsupported section: " + section)
	}

	if rDiff != nil {
		ext := ".yaml"
		if section == "notes" {
			ext = ".txt"
		}

		res, err := RevisionDiff(functor, ext, rDiff.Orig, rel.Orig, flag)
		if err != nil {
			return "", err
		}
		return res, nil
	}

	res, err := functor(rel.Orig, flag)
	if err != nil {
		return "", errorx.Decorate(err, "failed to get section info")
	}
	return res, nil
}

func (h *HelmHandler) repoFromArtifactHub(name string) (string, error) {
	results, err := objects.QueryArtifactHub(name)
	if err != nil {
		log.Warnf("Failed to query ArtifactHub: %s", err)
		return "", nil // swallowing the error to not annoy users
	}

	if len(results) == 0 {
		return "", nil
	}

	sort.SliceStable(results, func(i, j int) bool {
		ri, rj := results[i], results[j]

		// we prefer official repos
		if ri.Repository.Official && !rj.Repository.Official {
			return true
		}

		// more popular
		if ri.Stars != rj.Stars {
			return ri.Stars > rj.Stars
		}

		// or from verified publishers
		if ri.Repository.VerifiedPublisher && !rj.Repository.VerifiedPublisher {
			return true
		}

		// or with more recent app version
		c := semver.Compare("v"+ri.AppVersion, "v"+rj.AppVersion)
		if c != 0 {
			return c > 0
		}

		// shorter repo name is usually closer to officials
		return len(ri.Repository.Name) < len(rj.Repository.Name)
	})

	r := results[0]
	buf, err := json.Marshal([]*RepoChartElement{{
		Name:            r.Name,
		Version:         r.Version,
		AppVersion:      r.AppVersion,
		Description:     r.Description,
		Repository:      r.Repository.Name,
		URLs:            []string{r.Repository.Url},
		IsSuggestedRepo: true,
	}})
	if err != nil {
		return "", err
	}

	return string(buf), nil
}

type RepoChartElement struct { // TODO: do we need it at all? there is existing repo.ChartVersion in Helm
	Name        string `json:"name"`
	Version     string `json:"version"`
	AppVersion  string `json:"app_version"`
	Description string `json:"description"`

	InstalledNamespace string   `json:"installed_namespace"`
	InstalledName      string   `json:"installed_name"`
	Repository         string   `json:"repository"`
	URLs               []string `json:"urls"`
	IsSuggestedRepo    bool     `json:"isSuggestedRepo"`
}

func HReleaseToJSON(o *release.Release) *ReleaseElement {
	return &ReleaseElement{
		Name:         o.Name,
		Namespace:    o.Namespace,
		Revision:     strconv.Itoa(o.Version),
		Updated:      o.Info.LastDeployed,
		Status:       o.Info.Status,
		Chart:        fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
		ChartName:    o.Chart.Name(),
		ChartVersion: o.Chart.Metadata.Version,
		AppVersion:   o.Chart.AppVersion(),
		Icon:         o.Chart.Metadata.Icon,
		Description:  o.Chart.Metadata.Description,
	}
}

type ReleaseElement struct {
	Name         string         `json:"name"`
	Namespace    string         `json:"namespace"`
	Revision     string         `json:"revision"`
	Updated      helmtime.Time  `json:"updated"`
	Status       release.Status `json:"status"`
	Chart        string         `json:"chart"`
	ChartName    string         `json:"chartName"`
	ChartVersion string         `json:"chartVersion"`
	AppVersion   string         `json:"app_version"`
	Icon         string         `json:"icon"`
	Description  string         `json:"description"`
}

type RepositoryElement struct {
	Name string `json:"name"`
	URL  string `json:"url"`
}

type HistoryElement struct {
	Revision    int            `json:"revision"`
	Updated     helmtime.Time  `json:"updated"`
	Status      release.Status `json:"status"`
	Chart       string         `json:"chart"`
	AppVersion  string         `json:"app_version"`
	Description string         `json:"description"`

	ChartName string `json:"chart_name"` // custom addition on top of Helm
	ChartVer  string `json:"chart_ver"`  // custom addition on top of Helm
	HasTests  bool   `json:"has_tests"`
}

func HReleaseToHistElem(o *release.Release) *HistoryElement {
	return &HistoryElement{
		Revision:    o.Version,
		Updated:     o.Info.LastDeployed,
		Status:      o.Info.Status,
		Chart:       fmt.Sprintf("%s-%s", o.Chart.Name(), o.Chart.Metadata.Version),
		AppVersion:  o.Chart.AppVersion(),
		Description: o.Info.Description,
		ChartName:   o.Chart.Name(),
		ChartVer:    o.Chart.Metadata.Version,
		HasTests:    releaseHasTests(o),
	}
}

func RevisionDiff(functor objects.SectionFn, ext string, revision1 *release.Release, revision2 *release.Release, flag bool) (string, error) {
	if revision1 == nil || revision2 == nil {
		log.Debugf("One of revisions is nil: %v %v", revision1, revision2)
		return "", nil
	}

	manifest1, err := functor(revision1, flag)
	if err != nil {
		return "", err
	}

	manifest2, err := functor(revision2, flag)
	if err != nil {
		return "", err
	}

	diff := GetDiff(manifest1, manifest2, strconv.Itoa(revision1.Version)+ext, strconv.Itoa(revision2.Version)+ext)
	return diff, nil
}

func GetDiff(text1 string, text2 string, name1 string, name2 string) string {
	edits := myers.ComputeEdits(span.URIFromPath(""), text1, text2)
	unified := gotextdiff.ToUnified(name1, name2, text1, edits)
	diff := fmt.Sprint(unified)
	log.Debugf("The diff is: %s", diff)
	return diff
}

func releaseHasTests(o *release.Release) bool {
	for _, h := range o.Hooks {
		for _, e := range h.Events {
			if e == release.HookTest {
				return true
			}
		}
	}
	return false
}
