package repo

import (
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"path/filepath"
	"strings"
	"time"

	"code.gitea.io/gitea/models"
	git_model "code.gitea.io/gitea/models/git"
	repo_model "code.gitea.io/gitea/models/repo"
	unit_model "code.gitea.io/gitea/models/unit"

	"code.gitea.io/gitea/modules/context"
	"code.gitea.io/gitea/modules/git"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/modules/web"
	files_service "code.gitea.io/gitea/services/repository/files"
	hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
	hat_files_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/repository/files"
)

func BatchChangeFile(ctx *context.APIContext) {
	apiBatchOpts := web.GetForm(ctx).(*hat_api.BatchChangeFileOptions)

	if ctx.Repo.Repository.IsEmpty {
		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
		return
	}

	if apiBatchOpts.Header.BranchName == "" {
		apiBatchOpts.Header.BranchName = ctx.Repo.Repository.DefaultBranch
	}

	if apiBatchOpts.Header.Message == "" {
		apiBatchOpts.Header.Message = time.Now().Format("RFC3339")
	}

	if apiBatchOpts.Header.Dates.Author.IsZero() {
		apiBatchOpts.Header.Dates.Author = time.Now()
	}

	if apiBatchOpts.Header.Dates.Committer.IsZero() {
		apiBatchOpts.Header.Dates.Committer = time.Now()
	}

	if batchFileResponse, err := createOrUpdateOrDeleteFiles(ctx, apiBatchOpts); err != nil {
		handleCreateOrUpdateFileError(ctx, err)
	} else {
		ctx.JSON(http.StatusOK, batchFileResponse)
	}
}

func createOrUpdateOrDeleteFiles(ctx *context.APIContext, apiBatchOpts *hat_api.BatchChangeFileOptions) (*hat_api.BatchFileResponse, error) {
	if !canWriteFiles(ctx.Repo) {
		return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
			UserID:   ctx.Doer.ID,
			RepoName: ctx.Repo.Repository.LowerName,
		}
	}
	fileChan := make(chan hat_files_service.BatchSingleFileOption)
	stopChan := make(chan bool)
	errChan := make(chan error)
	exchangeOption := &hat_files_service.ExchangeFileOption{
		FileChan: fileChan,
		StopChan: stopChan,
		ErrChan:  errChan,
	}
	go func() {
		for _, f := range apiBatchOpts.Files {
			if f.Encoding == "base64" {
				content, err := base64.StdEncoding.DecodeString(f.Content)
				exchangeOption.ErrChan <- err
				f.Content = string(content)
			}
			exchangeOption.FileChan <- hat_files_service.BatchSingleFileOption{
				Content:    f.Content,
				TreePath:   f.FilePath,
				ActionType: hat_files_service.ToFileActionType(f.ActionType),
			}
		}
		exchangeOption.StopChan <- true
	}()
	opts := &hat_files_service.BatchUpdateFileOptions{
		Message:   apiBatchOpts.Header.Message,
		OldBranch: apiBatchOpts.Header.BranchName,
		NewBranch: apiBatchOpts.Header.NewBranchName,
		Commiter: &files_service.IdentityOptions{
			Name:  apiBatchOpts.Header.Committer.Name,
			Email: apiBatchOpts.Header.Committer.Email,
		},
		Author: &files_service.IdentityOptions{
			Name:  apiBatchOpts.Header.Author.Name,
			Email: apiBatchOpts.Header.Author.Email,
		},
		Dates: &files_service.CommitDateOptions{
			Author:    apiBatchOpts.Header.Dates.Author,
			Committer: apiBatchOpts.Header.Dates.Committer,
		},
		Signoff: apiBatchOpts.Header.Signoff,
	}

	return hat_files_service.CreateOrUpdateOrDeleteRepofiles(ctx, ctx.Repo.Repository, ctx.Doer, opts, exchangeOption)
}

func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
	if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
		ctx.Error(http.StatusForbidden, "Access", err)
		return
	}
	if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
		models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
		ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
		return
	}
	if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
		ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
		return
	}

	ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
}

func GetReadmeContents(ctx *context.APIContext) {
	if !canReadFiles(ctx.Repo) {
		ctx.Error(http.StatusInternalServerError, "canReadFiles", repo_model.ErrUserDoesNotHaveAccessToRepo{
			UserID:   ctx.ContextUser.ID,
			RepoName: ctx.Repo.Repository.LowerName,
		})
		return
	}

	ref := ctx.FormString("ref")
	readmeSortArr := []string{"readme", "readme.en.md", "readme_en.md", "readme.md", "readme.ch.md", "readme_ch.md", "readme.zh.md", "readme_zh.md", "readme.cn.md", "readme_cn.md"}
	readmePath := ""
	readmePathInArrIndex := 0
	filesListInterface, err := hat_files_service.GetContentsOrList(ctx, ctx.Repo.Repository, "", ref)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
		return
	}
	filesList, ok := filesListInterface.([]*hat_api.ContentsResponse)
	if ok {
		for _, file := range filesList {
			for i, sortItem := range readmeSortArr {
				if strings.ToLower(file.Name) == sortItem {
					if readmePath != "" && readmePathInArrIndex > i {
						continue
					} else {
						readmePathInArrIndex = i
						readmePath = file.Name
					}
				}
			}
		}
	}

	if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, readmePath, ref); err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound("GetContentsOrList", err)
			return
		}
		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)

	} else {
		ctx.JSON(http.StatusOK, fileList)
	}
}

func canWriteFiles(r *context.Repository) bool {
	return r.Permission.CanWrite(unit_model.UnitCode.Type) && !r.Repository.IsMirror && !r.Repository.IsArchived
}

func canReadFiles(r *context.Repository) bool {
	return r.Permission.CanRead(unit_model.UnitCode.Type)
}

func GetReadmeContentsByPath(ctx *context.APIContext) {
	if !canReadFiles(ctx.Repo) {
		ctx.Error(http.StatusInternalServerError, "canReadFiles", repo_model.ErrUserDoesNotHaveAccessToRepo{
			UserID:   ctx.ContextUser.ID,
			RepoName: ctx.Repo.Repository.LowerName,
		})
		return
	}
	treePath := ctx.Params("*")
	ref := ctx.FormString("ref")
	readmeSortArr := []string{"readme", "readme.en.md", "readme_en.md", "readme.md", "readme.ch.md", "readme_ch.md", "readme.zh.md", "readme_zh.md", "readme.cn.md", "readme_cn.md"}
	readmePath := ""
	readmePathInArrIndex := 0
	filesListInterface, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
		return
	}
	filesList, ok := filesListInterface.([]*api.ContentsResponse)
	if ok {
		for _, file := range filesList {
			for i, sortItem := range readmeSortArr {
				if strings.ToLower(file.Name) == sortItem {
					if readmePath != "" && readmePathInArrIndex > i {
						continue
					} else {
						readmePathInArrIndex = i
						readmePath = file.Name
					}
				}
			}
		}
	}

	newTreePath := treePath + "/" + readmePath
	if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, newTreePath, ref); err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound("GetContentsOrList", err)
			return
		}
		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)

	} else {
		ctx.JSON(http.StatusOK, fileList)
	}
}

func GetContents(ctx *context.APIContext) {
	if !canReadFiles(ctx.Repo) {
		ctx.Error(http.StatusInternalServerError, "GetContents", repo_model.ErrUserDoesNotHaveAccessToRepo{
			UserID:   ctx.Doer.ID,
			RepoName: ctx.Repo.Repository.Name,
		})
		return
	}

	treePath := ctx.Params("*")
	ref := ctx.FormTrim("ref")

	if fileList, err := hat_files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil {
		if git.IsErrNotExist(err) {
			ctx.NotFound("GetContentsOrList", err)
			return
		}
		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
	} else {
		ctx.JSON(http.StatusOK, fileList)
	}
}

func GetContentsList(ctx *context.APIContext) {
	GetContents(ctx)
}

func FindFiles(ctx *context.APIContext) {
	treePath := ctx.Repo.TreePath

	ref := ctx.FormString("ref")
	if ref == "" {
		ref = ctx.Repo.Repository.DefaultBranch
	}

	keyWords := ctx.FormString("q")
	var fileList []*SearchFileItem
	var err error
	fileList, err = FindFileFromPath(ctx, treePath, ref, keyWords)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "FindFileFromPath", err)
		return
	}
	ctx.JSON(http.StatusOK, fileList)
}

type SearchFileItem struct {
	Name    string  `json:"name"`
	Path    string  `json:"path"`
	SHA     string  `json:"sha"`
	Type    string  `json:"type"`
	Size    int64   `json:"size"`
	URL     *string `json:"url"`
	HTMLURL *string `json:"html_url"`
}

func FindFileFromPath(ctx *context.APIContext, treePath, ref, key string) (fileList []*SearchFileItem, err error) {
	// get the commit object for the ref
	commit, err := ctx.Repo.GitRepo.GetCommit(ref)
	if err != nil {
		ctx.ServerError("GetCommit", err)
		return nil, err
	}
	tree, err2 := commit.SubTree(treePath)
	if err2 != nil {
		ctx.ServerError("SubTree", err)
		return nil, err2
	}
	entries, err3 := tree.ListEntriesRecursiveFast()
	if err3 != nil {
		ctx.ServerError("ListEntries", err3)
		return nil, err3
	}
	fileList = make([]*SearchFileItem, 0)

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		filename := filepath.Base(entry.Name())
		if strings.Contains(strings.ToLower(filename), strings.ToLower(key)) || key == "" {
			name := entry.Name()
			treePath = name

			selfURL, err := url.Parse(ctx.Repo.Repository.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(ref))

			if err != nil {
				return nil, err
			}
			selfURLString := selfURL.String()

			refType := ctx.Repo.GitRepo.GetRefType(ref)
			if refType == "invalid" {
				return nil, fmt.Errorf("no commit found for the ref [ref: %s]", ref)
			}
			// htmlURL, err := url.Parse(fmt.Sprintf("%s/src/%s/%s/%s", ctx.Repo.Repository.HTMLURL(), refType, ref, treePath))
			htmlURL, err := url.Parse(ctx.Repo.Repository.APIURL() + "/src/" + url.QueryEscape(string(refType)) + "/" + url.QueryEscape(ref) + "/" + util.PathEscapeSegments(treePath))
			if err != nil {
				return nil, err
			}
			htmlURLString := htmlURL.String()
			item := &SearchFileItem{
				Name:    filename,
				Path:    treePath,
				SHA:     entry.ID.String(),
				Size:    entry.Size(),
				URL:     &selfURLString,
				HTMLURL: &htmlURLString,
			}
			// Now populate the rest of the ContentsResponse  based on entry type
			if entry.IsRegular() || entry.IsExecutable() {
				item.Type = "file"
			} else if entry.IsDir() {
				item.Type = "dir"
			} else if entry.IsLink() {
				item.Type = "symlink"
			} else if entry.IsSubModule() {
				item.Type = "submodule"
			}
			fileList = append(fileList, item)

		}
	}
	return

}
