// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

//go:build !go1.25

package repository

import (
	"context"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	repo_model "forgejo.org/models/repo"
	"forgejo.org/modules/git"
	"forgejo.org/modules/log"
	"forgejo.org/modules/util"
)

func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository, tmpDir string) error {
	commitTimeStr := time.Now().Format(time.RFC3339)
	authorSig := repo.Owner.NewGitSig()

	// Because this may call hooks we should pass in the environment
	env := append(os.Environ(),
		"GIT_AUTHOR_NAME="+authorSig.Name,
		"GIT_AUTHOR_EMAIL="+authorSig.Email,
		"GIT_AUTHOR_DATE="+commitTimeStr,
		"GIT_COMMITTER_NAME="+authorSig.Name,
		"GIT_COMMITTER_EMAIL="+authorSig.Email,
		"GIT_COMMITTER_DATE="+commitTimeStr,
	)

	// Clone to temporary path and do the init commit.
	templateRepoPath := templateRepo.RepoPath()
	if err := git.Clone(ctx, templateRepoPath, tmpDir, git.CloneRepoOptions{
		Depth:  1,
		Branch: templateRepo.DefaultBranch,
	}); err != nil {
		return fmt.Errorf("git clone: %w", err)
	}

	if err := util.RemoveAll(path.Join(tmpDir, ".git")); err != nil {
		return fmt.Errorf("remove git dir: %w", err)
	}

	// Variable expansion
	gt, err := checkGiteaTemplate(tmpDir)
	if err != nil {
		return fmt.Errorf("checkGiteaTemplate: %w", err)
	}

	if gt != nil {
		if err := util.Remove(gt.Path); err != nil {
			return fmt.Errorf("remove .giteatemplate: %w", err)
		}

		// Avoid walking tree if there are no globs
		if len(gt.Globs()) > 0 {
			// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
			root, err := os.OpenRoot(tmpDir)
			if err != nil {
				return fmt.Errorf("open root: %w", err)
			}
			defer root.Close()

			tmpDirSlash := strings.TrimSuffix(filepath.ToSlash(tmpDir), "/") + "/"
			if err := filepath.WalkDir(tmpDirSlash, func(path string, d os.DirEntry, walkErr error) error {
				if walkErr != nil {
					return walkErr
				}

				if d.IsDir() {
					return nil
				}

				base := strings.TrimPrefix(filepath.ToSlash(path), tmpDirSlash)
				for _, g := range gt.Globs() {
					if g.Match(base) {
						// `path` will be an absolute filepath from `WalkDir`, but `os.Root` requires all accesses are
						// relative file paths from the root -- use `relPath` from here out.
						relPath, err := filepath.Rel(tmpDir, path)
						if err != nil {
							return err
						}

						// FIXME: root.ReadFile(relPath) in go 1.25
						file, err := root.Open(relPath)
						if err != nil {
							return err
						}
						content, err := io.ReadAll(file)
						if err != nil {
							return err
						}

						targetPath := generateExpansion(relPath, templateRepo, generateRepo, true)
						if targetPath != relPath {
							// template expansion in filename; remove the original file from the repo
							err = root.Remove(relPath)
							if err != nil {
								return err
							}
						}

						// Create parent subdirectories if needed or continue silently if it exists
						// FIXME: root.MkdirAll(filepath.Dir(targetPath), 0o755) in go 1.25
						var dirs []string
						dirPath := filepath.Dir(targetPath)
						for dirPath != "." && dirPath != "" {
							dirs = append([]string{filepath.Base(dirPath)}, dirs...)
							dirPath = filepath.Dir(dirPath)
						}
						subdir := ""
						for _, dir := range dirs {
							subdir = filepath.Join(subdir, dir)
							if err := root.Mkdir(subdir, 0o755); err != nil && !os.IsExist(err) {
								return err
							}
						}

						// FIXME: root.WriteFile in go 1.25
						file, err = root.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
						if err != nil {
							return err
						}
						_, err = file.Write([]byte(generateExpansion(string(content), templateRepo, generateRepo, false)))
						if err != nil {
							return err
						}

						break
					}
				}
				return nil
			}); err != nil {
				return err
			}
		}
	}

	if err := git.InitRepository(ctx, tmpDir, false, templateRepo.ObjectFormatName); err != nil {
		return err
	}

	repoPath := repo.RepoPath()
	if stdout, _, err := git.NewCommand(ctx, "remote", "add", "origin").AddDynamicArguments(repoPath).
		SetDescription(fmt.Sprintf("generateRepoCommit (git remote add): %s to %s", templateRepoPath, tmpDir)).
		RunStdString(&git.RunOpts{Dir: tmpDir, Env: env}); err != nil {
		log.Error("Unable to add %v as remote origin to temporary repo to %s: stdout %s\nError: %v", repo, tmpDir, stdout, err)
		return fmt.Errorf("git remote add: %w", err)
	}

	// set default branch based on whether it's specified in the newly generated repo or not
	defaultBranch := repo.DefaultBranch
	if strings.TrimSpace(defaultBranch) == "" {
		defaultBranch = templateRepo.DefaultBranch
	}

	return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch)
}

func checkGiteaTemplate(tmpDir string) (*GiteaTemplate, error) {
	configDirs := []string{".forgejo", ".gitea"}
	var templateFilePath string

	// All file access should be done through `root` to avoid file traversal attacks, especially with symlinks
	root, err := os.OpenRoot(tmpDir)
	if err != nil {
		return nil, fmt.Errorf("open root: %w", err)
	}
	defer root.Close()

	for _, dir := range configDirs {
		candidatePath := filepath.Join(dir, "template")
		if _, err := root.Stat(candidatePath); err == nil {
			templateFilePath = candidatePath
			break
		} else if !os.IsNotExist(err) {
			return nil, err
		}
	}

	if templateFilePath == "" {
		return nil, nil
	}

	// FIXME: root.ReadFile(relPath) in go 1.25
	file, err := root.Open(templateFilePath)
	if err != nil {
		return nil, err
	}
	content, err := io.ReadAll(file)
	if err != nil {
		return nil, err
	}

	return &GiteaTemplate{
		Path:    templateFilePath,
		Content: content,
	}, nil
}
