package helm

import (
	"embed"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"text/template"

	"freeleaps.com/gitops/initializer/api/v1alpha1"
)

//go:embed templates/*
var embeddedTemplates embed.FS

type HelmGenerator struct {
	io.Closer
	Instance   *v1alpha1.ProjectInitialize
	workingDir string
	generated  bool
}

const leftDelim = "[["
const rightDelim = "]]"

func (hg *HelmGenerator) prepareWorkingDir() error {
	path, err := os.MkdirTemp("", fmt.Sprintf("helm-gen-%s-*", hg.Instance.Name))
	if err != nil {
		return fmt.Errorf("failed to create helm generator temp dir: %w", err)
	}
	hg.workingDir = path
	return nil
}

func (hg *HelmGenerator) readTemplate(name string) (*template.Template, error) {
	tpl := template.New(name)
	templateBytes, err := embeddedTemplates.ReadFile(name)
	if err != nil {
		return nil, fmt.Errorf("failed to read template %s: %w", name, err)
	}
	tpl, err = tpl.
		Delims(leftDelim, rightDelim).
		Funcs(funcMap).
		Parse(string(templateBytes))
	if err != nil {
		return nil, fmt.Errorf("failed to parse template %s: %w", name, err)
	}
	return tpl, nil
}

func (hg *HelmGenerator) createMetaFiles() error {
	// create Chart.yaml
	tpl, err := hg.readTemplate("templates/metadata/Chart.yaml.tpl")
	if err != nil {
		return err
	}
	chartYamlFd, err := os.Create(hg.workingDir + "/Chart.yaml")
	if err != nil {
		return fmt.Errorf("failed to create Chart.yaml file: %w", err)
	}
	defer chartYamlFd.Close()
	if err := tpl.Execute(chartYamlFd, hg.Instance.Spec.Helm); err != nil {
		return fmt.Errorf("failed to rendering Chart.yaml with template: %w", err)
	}

	// create values.yaml
	tpl, err = hg.readTemplate("templates/metadata/values.yaml.tpl")
	if err != nil {
		return err
	}
	valuesYamlFd, err := os.Create(hg.workingDir + "/values.yaml")
	if err != nil {
		return fmt.Errorf("failed to create values.yaml file: %w", err)
	}
	defer valuesYamlFd.Close()
	if err := tpl.Execute(valuesYamlFd, hg.Instance.Spec.Helm); err != nil {
		return fmt.Errorf("failed to rendering values.yaml with template: %w", err)
	}

	return nil
}

func (hg *HelmGenerator) createComponentManifests(component v1alpha1.HelmComponentSpec) error {
	workRoot := hg.workingDir + "/templates/" + component.Name
	if err := os.MkdirAll(workRoot, 0755); err != nil {
		return fmt.Errorf("failed to create component directory: %w", err)
	}

	// create deployment.yaml
	tpl, err := hg.readTemplate("templates/deployment/deployment.yaml.tpl")
	if err != nil {
		return err
	}
	deploymentYamlFd, err := os.Create(workRoot + "/deployment.yaml")
	if err != nil {
		return fmt.Errorf("failed to create deployment.yaml file: %w", err)
	}
	defer deploymentYamlFd.Close()
	if err := tpl.Execute(deploymentYamlFd, component); err != nil {
		return fmt.Errorf("failed to rendering deployment.yaml with template: %w", err)
	}

	// create service.yaml if there has services
	if len(component.Services) > 0 {
		tpl, err = hg.readTemplate("templates/service/service.yaml.tpl")
		if err != nil {
			return err
		}
		serviceYamlFd, err := os.Create(workRoot + "/service.yaml")
		if err != nil {
			return fmt.Errorf("failed to create service.yaml file: %w", err)
		}
		defer serviceYamlFd.Close()
		if err := tpl.Execute(serviceYamlFd, component); err != nil {
			return fmt.Errorf("failed to rendering service.yaml with template: %w", err)
		}
	}

	// create ingress.yaml if there has ingress
	if len(component.Ingresses) > 0 {
		tpl, err = hg.readTemplate("templates/ingress/ingress.yaml.tpl")
		if err != nil {
			return err
		}
		ingressYamlFd, err := os.Create(workRoot + "/ingress.yaml")
		if err != nil {
			return fmt.Errorf("failed to create ingress.yaml file: %w", err)
		}
		defer ingressYamlFd.Close()
		if err := tpl.Execute(ingressYamlFd, component); err != nil {
			return fmt.Errorf("failed to rendering ingress.yaml with template: %w", err)
		}
	}

	// create secret.yaml if there has configs
	if len(component.Configs) > 0 {
		for _, config := range component.Configs {
			tpl, err = hg.readTemplate("templates/secret/secret.yaml.tpl")
			if err != nil {
				return err
			}
			secretYamlFd, err := os.Create(workRoot + "/" + config.Name + ".yaml")
			if err != nil {
				return fmt.Errorf("failed to create secret.yaml file: %w", err)
			}
			defer secretYamlFd.Close()
			if err := tpl.Execute(secretYamlFd, struct {
				ComponentName string
				Config        v1alpha1.HelmComponentConfigSpec
			}{
				ComponentName: component.Name,
				Config:        config,
			}); err != nil {
				return fmt.Errorf("failed to rendering secret.yaml with template: %w", err)
			}
		}
	}

	// create certificate if ingress need to sign with tls
	if len(component.Ingresses) > 0 {
		needSign := false
		for _, ingress := range component.Ingresses {
			if !ingress.TLS.Exists {
				needSign = true
				break
			}
		}
		if needSign {
			tpl, err = hg.readTemplate("templates/certificate/certificate.yaml.tpl")
			if err != nil {
				return err
			}
			certificateYamlFd, err := os.Create(workRoot + "/certificate.yaml")
			if err != nil {
				return fmt.Errorf("failed to create certificate.yaml file: %w", err)
			}
			defer certificateYamlFd.Close()
			if err := tpl.Execute(certificateYamlFd, component); err != nil {
				return fmt.Errorf("failed to rendering certificate.yaml with template: %w", err)
			}
		}
	}

	return nil
}

func (hg *HelmGenerator) Generate() error {
	if hg.Instance == nil {
		return errors.New("instance is nil")
	}

	// create temp working dir
	if err := hg.prepareWorkingDir(); err != nil {
		return err
	}

	// create meta files (Chart.yaml, values.yaml, .helmignore)
	if err := hg.createMetaFiles(); err != nil {
		return err
	}

	// create manifests for each component
	for _, componentSpec := range hg.Instance.Spec.Helm.Components {
		if err := hg.createComponentManifests(componentSpec); err != nil {
			return err
		}
	}
	hg.generated = true
	return nil
}

type HelmGeneratedFile struct {
	QualifiedPath string
}

func (hgf *HelmGeneratedFile) ReadAll() ([]byte, error) {
	return os.ReadFile(hgf.QualifiedPath)
}

type HelmGeneratedFileCallback func(f *HelmGeneratedFile) error

func (hg *HelmGenerator) Walk(cb HelmGeneratedFileCallback) error {
	if !hg.generated {
		return errors.New("helm package not generated")
	}

	return filepath.Walk(hg.workingDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			return cb(&HelmGeneratedFile{QualifiedPath: path})
		}
		return nil
	})
}

func (hg *HelmGenerator) Close() error {
	if hg.workingDir != "" {
		return os.RemoveAll(hg.workingDir)
	}
	return nil
}
