/*
Copyright 2023 The KubeSphere Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package modules

import (
	"context"
	"io/fs"
	"math"
	"os"
	"path/filepath"
	"strings"

	"github.com/cockroachdb/errors"
	kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/klog/v2"
	"k8s.io/utils/ptr"

	"github.com/kubesphere/kubekey/v4/pkg/connector"
	_const "github.com/kubesphere/kubekey/v4/pkg/const"
	"github.com/kubesphere/kubekey/v4/pkg/converter/tmpl"
	"github.com/kubesphere/kubekey/v4/pkg/project"
	"github.com/kubesphere/kubekey/v4/pkg/variable"
)

/*
The Template module processes files using Go templates before copying them to remote hosts.
This module allows users to template files with variables before transferring them.

Configuration:
Users can specify source and destination paths:

template:
  src: /path/to/file    # required: local file path to template
  dest: /remote/path    # required: destination path on remote host
  mode: 0644           # optional: file permissions (default: 0644)

Usage Examples in Playbook Tasks:
1. Basic file templating:
   ```yaml
   - name: Template configuration file
     template:
       src: config.yaml.tmpl
       dest: /etc/app/config.yaml
       mode: 0644
     register: template_result
   ```

2. Template with variables:
   ```yaml
   - name: Template with variables
     template:
       src: app.conf.tmpl
       dest: /etc/app/app.conf
     register: app_config
   ```

Return Values:
- On success: Returns "Success" in stdout
- On failure: Returns error message in stderr
*/

type templateArgs struct {
	src  string
	dest string
	mode *uint32
}

func newTemplateArgs(_ context.Context, raw runtime.RawExtension, vars map[string]any) (*templateArgs, error) {
	var err error
	// check args
	ta := &templateArgs{}
	args := variable.Extension2Variables(raw)

	ta.src, err = variable.StringVar(vars, args, "src")
	if err != nil {
		return nil, errors.New("\"src\" should be string")
	}

	ta.dest, err = variable.StringVar(vars, args, "dest")
	if err != nil {
		return nil, errors.New("\"dest\" should be string")
	}

	mode, err := variable.IntVar(vars, args, "mode")
	if err != nil {
		klog.V(4).InfoS("get mode error", "error", err)
	} else {
		if *mode < 0 || *mode > math.MaxUint32 {
			return nil, errors.New("mode should be uint32")
		}
		ta.mode = ptr.To(uint32(*mode))
	}

	return ta, nil
}

// ModuleTemplate handles the "template" module, processing files with Go templates
func ModuleTemplate(ctx context.Context, options ExecOptions) (string, string, error) {
	ha, err := options.getAllVariables()
	if err != nil {
		return StdoutFailed, StderrGetHostVariable, err
	}

	ta, err := newTemplateArgs(ctx, options.Args, ha)
	if err != nil {
		return StdoutFailed, StderrParseArgument, err
	}

	conn, err := options.getConnector(ctx)
	if err != nil {
		return StdoutFailed, StderrGetConnector, err
	}
	defer conn.Close(ctx)

	if filepath.IsAbs(ta.src) {
		return handleAbsoluteTemplate(ctx, ta, conn, ha)
	}

	return handleRelativeTemplate(ctx, ta, conn, ha, options)
}

func handleAbsoluteTemplate(ctx context.Context, ta *templateArgs, conn connector.Connector, vars map[string]any) (string, string, error) {
	fileInfo, err := os.Stat(ta.src)
	if err != nil {
		return StdoutFailed, "failed to get src file in local path", err
	}

	if fileInfo.IsDir() {
		if err := ta.absDir(ctx, conn, vars); err != nil {
			return StdoutFailed, "failed to template absolute dir", err
		}

		return StdoutSuccess, "", nil
	}

	data, err := os.ReadFile(ta.src)
	if err != nil {
		return StdoutFailed, "failed to read file", err
	}
	if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, vars); err != nil {
		return StdoutFailed, "failed to template file", err
	}

	return StdoutSuccess, "", nil
}

func handleRelativeTemplate(ctx context.Context, ta *templateArgs, conn connector.Connector, vars map[string]any, options ExecOptions) (string, string, error) {
	pj, err := project.New(ctx, options.Playbook, false)
	if err != nil {
		return StdoutFailed, "failed to get playbook", nil
	}

	relPath := filepath.Join(options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath], _const.ProjectRolesTemplateDir, ta.src)
	fileInfo, err := pj.Stat(relPath)
	if err != nil {
		return StdoutFailed, "failed to stat relative path", err
	}

	if fileInfo.IsDir() {
		if err := handleRelativeDir(ctx, pj, relPath, ta, conn, vars); err != nil {
			return StdoutFailed, "failed to template relative dir", err
		}

		return StdoutSuccess, "", nil
	}

	data, err := pj.ReadFile(relPath)
	if err != nil {
		return StdoutFailed, "failed to read relative file", err
	}
	if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, vars); err != nil {
		return StdoutFailed, "failed to template relative file", err
	}

	return StdoutSuccess, "", nil
}

func handleRelativeDir(ctx context.Context, pj project.Project, relPath string, ta *templateArgs, conn connector.Connector, vars map[string]any) error {
	return pj.WalkDir(relPath, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() { // only deal file
			return nil
		}
		info, err := d.Info()
		if err != nil {
			return errors.Wrapf(err, "failed to get file %q info", path)
		}

		mode := info.Mode()
		if ta.mode != nil {
			mode = os.FileMode(*ta.mode)
		}

		data, err := pj.ReadFile(path)
		if err != nil {
			return errors.Wrapf(err, "failed to read file %q", path)
		}
		result, err := tmpl.Parse(vars, string(data))
		if err != nil {
			return errors.Wrapf(err, "failed to parse file %q", path)
		}

		dest := ta.dest
		if strings.HasSuffix(ta.dest, "/") {
			rel, err := pj.Rel(relPath, path)
			if err != nil {
				return errors.Wrap(err, "failed to get relative filepath")
			}
			dest = filepath.Join(ta.dest, rel)
		}

		return conn.PutFile(ctx, result, dest, mode)
	})
}

// relFile when template.src is relative file, get file from project, parse it, and copy to remote.
func (ta templateArgs) readFile(ctx context.Context, data string, mode fs.FileMode, conn connector.Connector, vars map[string]any) error {
	result, err := tmpl.Parse(vars, data)
	if err != nil {
		return err
	}

	dest := ta.dest
	if strings.HasSuffix(ta.dest, "/") {
		dest = filepath.Join(ta.dest, filepath.Base(ta.src))
	}

	if ta.mode != nil {
		mode = os.FileMode(*ta.mode)
	}

	return conn.PutFile(ctx, result, dest, mode)
}

// absDir when template.src is absolute dir, get all files by os, parse it, and copy to remote.
func (ta templateArgs) absDir(ctx context.Context, conn connector.Connector, vars map[string]any) error {
	if err := filepath.WalkDir(ta.src, func(path string, d fs.DirEntry, err error) error {
		if d.IsDir() { // only copy file
			return nil
		}
		if err != nil {
			return errors.WithStack(err)
		}

		// get file old mode
		info, err := d.Info()
		if err != nil {
			return errors.Wrapf(err, "failed to get file %q info", path)
		}
		mode := info.Mode()
		if ta.mode != nil {
			mode = os.FileMode(*ta.mode)
		}
		// read file
		data, err := os.ReadFile(path)
		if err != nil {
			return errors.Wrapf(err, "failed to read file %q", path)
		}
		result, err := tmpl.Parse(vars, string(data))
		if err != nil {
			return errors.Wrapf(err, "failed to parse file %q", path)
		}
		// copy file to remote
		dest := ta.dest
		if strings.HasSuffix(ta.dest, "/") {
			rel, err := filepath.Rel(ta.src, path)
			if err != nil {
				return errors.Wrap(err, "failed to get relative filepath")
			}
			dest = filepath.Join(ta.dest, rel)
		}

		if err := conn.PutFile(ctx, result, dest, mode); err != nil {
			return errors.Wrap(err, "failed to put file")
		}

		return nil
	}); err != nil {
		return errors.Wrapf(err, "failed to walk dir %q", ta.src)
	}

	return nil
}

func init() {
	utilruntime.Must(RegisterModule("template", ModuleTemplate))
}
