package web

import (
	"archive/zip"
	"bytes"
	"errors"
	"html/template"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/google/uuid"
	"github.com/labstack/echo/v4"
	"github.com/thomiceli/opengist/internal/config"
	"github.com/thomiceli/opengist/internal/db"
	"gorm.io/gorm"
)

func gistInit(next echo.HandlerFunc) echo.HandlerFunc {
	return func(ctx echo.Context) error {
		currUser := getUserLogged(ctx)

		userName := ctx.Param("user")
		gistName := ctx.Param("gistname")

		gistName = strings.TrimSuffix(gistName, ".git")

		gist, err := db.GetGist(userName, gistName)
		if err != nil {
			return notFound("Gist not found")
		}

		if gist.Private == db.PrivateVisibility {
			if currUser == nil || currUser.ID != gist.UserID {
				return notFound("Gist not found")
			}
		}

		setData(ctx, "gist", gist)

		if config.C.SshGit {
			var sshDomain string

			if config.C.SshExternalDomain != "" {
				sshDomain = config.C.SshExternalDomain
			} else {
				sshDomain = strings.Split(ctx.Request().Host, ":")[0]
			}

			if config.C.SshPort == "22" {
				setData(ctx, "sshCloneUrl", sshDomain+":"+userName+"/"+gistName+".git")
			} else {
				setData(ctx, "sshCloneUrl", "ssh://"+sshDomain+":"+config.C.SshPort+"/"+userName+"/"+gistName+".git")
			}
		}

		httpProtocol := "http"
		if ctx.Request().TLS != nil || ctx.Request().Header.Get("X-Forwarded-Proto") == "https" {
			httpProtocol = "https"
		}
		setData(ctx, "httpProtocol", strings.ToUpper(httpProtocol))

		var baseHttpUrl string
		// if a custom external url is set, use it
		if config.C.ExternalUrl != "" {
			baseHttpUrl = config.C.ExternalUrl
		} else {
			baseHttpUrl = httpProtocol + "://" + ctx.Request().Host
		}

		if config.C.HttpGit {
			setData(ctx, "httpCloneUrl", baseHttpUrl+"/"+userName+"/"+gistName+".git")
		}

		setData(ctx, "httpCopyUrl", baseHttpUrl+"/"+userName+"/"+gistName)
		setData(ctx, "currentUrl", template.URL(ctx.Request().URL.Path))

		nbCommits, err := gist.NbCommits()
		if err != nil {
			return errorRes(500, "Error fetching number of commits", err)
		}
		setData(ctx, "nbCommits", nbCommits)

		if currUser != nil {
			hasLiked, err := currUser.HasLiked(gist)
			if err != nil {
				return errorRes(500, "Cannot get user like status", err)
			}
			setData(ctx, "hasLiked", hasLiked)
		}

		if gist.Private > 0 {
			setData(ctx, "NoIndex", true)
		}

		return next(ctx)
	}
}

// gistSoftInit try to load a gist (same as gistInit) but does not return a 404 if the gist is not found
// useful for git clients using HTTP to obfuscate the existence of a private gist
func gistSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
	return func(ctx echo.Context) error {
		userName := ctx.Param("user")
		gistName := ctx.Param("gistname")

		gistName = strings.TrimSuffix(gistName, ".git")

		gist, _ := db.GetGist(userName, gistName)
		setData(ctx, "gist", gist)

		return next(ctx)
	}
}

// gistNewPushInit has the same behavior as gistSoftInit but create a new gist empty instead
func gistNewPushSoftInit(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) error {
		setData(c, "gist", new(db.Gist))
		return next(c)
	}
}

func allGists(ctx echo.Context) error {
	var err error
	var urlPage string

	fromUserStr := ctx.Param("user")
	userLogged := getUserLogged(ctx)
	pageInt := getPage(ctx)

	sort := "created"
	sortText := tr(ctx, "gist.list.sort-by-created")
	order := "desc"
	orderText := tr(ctx, "gist.list.order-by-desc")

	if ctx.QueryParam("sort") == "updated" {
		sort = "updated"
		sortText = tr(ctx, "gist.list.sort-by-updated")
	}

	if ctx.QueryParam("order") == "asc" {
		order = "asc"
		orderText = tr(ctx, "gist.list.order-by-asc")
	}

	setData(ctx, "sort", sortText)
	setData(ctx, "order", orderText)

	var gists []*db.Gist
	var currentUserId uint
	if userLogged != nil {
		currentUserId = userLogged.ID
	} else {
		currentUserId = 0
	}

	if fromUserStr == "" {
		urlctx := ctx.Request().URL.Path
		if strings.HasSuffix(urlctx, "search") {
			setData(ctx, "htmlTitle", "Search results")
			setData(ctx, "mode", "search")
			setData(ctx, "searchQuery", ctx.QueryParam("q"))
			setData(ctx, "searchQueryUrl", template.URL("&q="+ctx.QueryParam("q")))
			urlPage = "search"
			gists, err = db.GetAllGistsFromSearch(currentUserId, ctx.QueryParam("q"), pageInt-1, sort, order)
		} else if strings.HasSuffix(urlctx, "all") {
			setData(ctx, "htmlTitle", "All gists")
			setData(ctx, "mode", "all")
			urlPage = "all"
			gists, err = db.GetAllGistsForCurrentUser(currentUserId, pageInt-1, sort, order)
		}
	} else {
		liked := false
		forked := false

		liked, err = regexp.MatchString(`/[^/]*/liked`, ctx.Request().URL.Path)
		if err != nil {
			return errorRes(500, "Error matching regexp", err)
		}

		forked, err = regexp.MatchString(`/[^/]*/forked`, ctx.Request().URL.Path)
		if err != nil {
			return errorRes(500, "Error matching regexp", err)
		}

		var fromUser *db.User

		fromUser, err = db.GetUserByUsername(fromUserStr)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return notFound("User not found")
			}
			return errorRes(500, "Error fetching user", err)
		}
		setData(ctx, "fromUser", fromUser)

		if countFromUser, err := db.CountAllGistsFromUser(fromUser.ID, currentUserId); err != nil {
			return errorRes(500, "Error counting gists", err)
		} else {
			setData(ctx, "countFromUser", countFromUser)
		}

		if countLiked, err := db.CountAllGistsLikedByUser(fromUser.ID, currentUserId); err != nil {
			return errorRes(500, "Error counting liked gists", err)
		} else {
			setData(ctx, "countLiked", countLiked)
		}

		if countForked, err := db.CountAllGistsForkedByUser(fromUser.ID, currentUserId); err != nil {
			return errorRes(500, "Error counting forked gists", err)
		} else {
			setData(ctx, "countForked", countForked)
		}

		if liked {
			urlPage = fromUserStr + "/liked"
			setData(ctx, "htmlTitle", "All gists liked by "+fromUserStr)
			setData(ctx, "mode", "liked")
			gists, err = db.GetAllGistsLikedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
		} else if forked {
			urlPage = fromUserStr + "/forked"
			setData(ctx, "htmlTitle", "All gists forked by "+fromUserStr)
			setData(ctx, "mode", "forked")
			gists, err = db.GetAllGistsForkedByUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
		} else {
			urlPage = fromUserStr
			setData(ctx, "htmlTitle", "All gists from "+fromUserStr)
			setData(ctx, "mode", "fromUser")
			gists, err = db.GetAllGistsFromUser(fromUser.ID, currentUserId, pageInt-1, sort, order)
		}
	}

	if err != nil {
		return errorRes(500, "Error fetching gists", err)
	}

	if err = paginate(ctx, gists, pageInt, 10, "gists", fromUserStr, 2, "&sort="+sort+"&order="+order); err != nil {
		return errorRes(404, "Page not found", nil)
	}

	setData(ctx, "urlPage", urlPage)
	return html(ctx, "all.html")
}

func gistIndex(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	revision := ctx.Param("revision")

	if revision == "" {
		revision = "HEAD"
	}

	files, err := gist.Files(revision)
	if err != nil {
		return errorRes(500, "Error fetching files", err)
	}

	if len(files) == 0 {
		return notFound("Revision not found")
	}

	setData(ctx, "page", "code")
	setData(ctx, "commit", revision)
	setData(ctx, "files", files)
	setData(ctx, "revision", revision)
	setData(ctx, "htmlTitle", gist.Title)
	return html(ctx, "gist.html")
}

func revisions(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	userName := gist.User.Username
	gistName := gist.Uuid

	pageInt := getPage(ctx)

	commits, err := gist.Log((pageInt - 1) * 10)
	if err != nil {
		return errorRes(500, "Error fetching commits log", err)
	}

	if err := paginate(ctx, commits, pageInt, 10, "commits", userName+"/"+gistName+"/revisions", 2); err != nil {
		return errorRes(404, "Page not found", nil)
	}

	emailsSet := map[string]struct{}{}
	for _, commit := range commits {
		if commit.AuthorEmail == "" {
			continue
		}
		emailsSet[strings.ToLower(commit.AuthorEmail)] = struct{}{}
	}

	emailsUsers, err := db.GetUsersFromEmails(emailsSet)
	if err != nil {
		return errorRes(500, "Error fetching users emails", err)
	}

	setData(ctx, "page", "revisions")
	setData(ctx, "revision", "HEAD")
	setData(ctx, "emails", emailsUsers)
	setData(ctx, "htmlTitle", "Revision of "+gist.Title)

	return html(ctx, "revisions.html")
}

func create(ctx echo.Context) error {
	setData(ctx, "htmlTitle", "Create a new gist")
	return html(ctx, "create.html")
}

func processCreate(ctx echo.Context) error {
	isCreate := false
	if ctx.Request().URL.Path == "/" {
		isCreate = true
	}

	err := ctx.Request().ParseForm()
	if err != nil {
		return errorRes(400, "Bad request", err)
	}

	dto := new(db.GistDTO)
	var gist *db.Gist

	if isCreate {
		setData(ctx, "htmlTitle", "Create a new gist")
	} else {
		gist = getData(ctx, "gist").(*db.Gist)
		setData(ctx, "htmlTitle", "Edit "+gist.Title)
	}

	if err := ctx.Bind(dto); err != nil {
		return errorRes(400, "Cannot bind data", err)
	}

	dto.Files = make([]db.FileDTO, 0)
	fileCounter := 0
	for i := 0; i < len(ctx.Request().PostForm["content"]); i++ {
		name := ctx.Request().PostForm["name"][i]
		content := ctx.Request().PostForm["content"][i]

		if name == "" {
			fileCounter += 1
			name = "gistfile" + strconv.Itoa(fileCounter) + ".txt"
		}

		escapedValue, err := url.QueryUnescape(content)
		if err != nil {
			return errorRes(400, "Invalid character unescaped", err)
		}

		dto.Files = append(dto.Files, db.FileDTO{
			Filename: strings.Trim(name, " "),
			Content:  escapedValue,
		})
	}

	err = ctx.Validate(dto)
	if err != nil {
		addFlash(ctx, validationMessages(&err), "error")
		if isCreate {
			return html(ctx, "create.html")
		} else {
			files, err := gist.Files("HEAD")
			if err != nil {
				return errorRes(500, "Error fetching files", err)
			}
			setData(ctx, "files", files)
			return html(ctx, "edit.html")
		}
	}

	if isCreate {
		gist = dto.ToGist()
	} else {
		gist = dto.ToExistingGist(gist)
	}

	user := getUserLogged(ctx)
	gist.NbFiles = len(dto.Files)

	if isCreate {
		uuidGist, err := uuid.NewRandom()
		if err != nil {
			return errorRes(500, "Error creating an UUID", err)
		}
		gist.Uuid = strings.Replace(uuidGist.String(), "-", "", -1)

		gist.UserID = user.ID
		gist.User = *user
	}

	if gist.Title == "" {
		if ctx.Request().PostForm["name"][0] == "" {
			gist.Title = "gist:" + gist.Uuid
		} else {
			gist.Title = ctx.Request().PostForm["name"][0]
		}
	}

	if len(dto.Files) > 0 {
		split := strings.Split(dto.Files[0].Content, "\n")
		if len(split) > 10 {
			gist.Preview = strings.Join(split[:10], "\n")
		} else {
			gist.Preview = dto.Files[0].Content
		}

		gist.PreviewFilename = dto.Files[0].Filename
	}

	if err = gist.InitRepository(); err != nil {
		return errorRes(500, "Error creating the repository", err)
	}

	if err = gist.AddAndCommitFiles(&dto.Files); err != nil {
		return errorRes(500, "Error adding and committing files", err)
	}

	if isCreate {
		if err = gist.Create(); err != nil {
			return errorRes(500, "Error creating the gist", err)
		}
	} else {
		if err = gist.Update(); err != nil {
			return errorRes(500, "Error updating the gist", err)
		}
	}

	return redirect(ctx, "/"+user.Username+"/"+gist.Uuid)
}

func toggleVisibility(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)

	gist.Private = (gist.Private + 1) % 3
	if err := gist.Update(); err != nil {
		return errorRes(500, "Error updating this gist", err)
	}

	addFlash(ctx, "Gist visibility has been changed", "success")
	return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
}

func deleteGist(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)

	if err := gist.Delete(); err != nil {
		return errorRes(500, "Error deleting this gist", err)
	}

	addFlash(ctx, "Gist has been deleted", "success")
	return redirect(ctx, "/")
}

func like(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	currentUser := getUserLogged(ctx)

	hasLiked, err := currentUser.HasLiked(gist)
	if err != nil {
		return errorRes(500, "Error checking if user has liked a gist", err)
	}

	if hasLiked {
		err = gist.RemoveUserLike(getUserLogged(ctx))
	} else {
		err = gist.AppendUserLike(getUserLogged(ctx))
	}

	if err != nil {
		return errorRes(500, "Error liking/dislking this gist", err)
	}

	redirectTo := "/" + gist.User.Username + "/" + gist.Uuid
	if r := ctx.QueryParam("redirecturl"); r != "" {
		redirectTo = r
	}
	return redirect(ctx, redirectTo)
}

func fork(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	currentUser := getUserLogged(ctx)

	alreadyForked, err := gist.GetForkParent(currentUser)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return errorRes(500, "Error checking if gist is already forked", err)
	}

	if gist.User.ID == currentUser.ID {
		addFlash(ctx, "Unable to fork own gists", "error")
		return redirect(ctx, "/"+gist.User.Username+"/"+gist.Uuid)
	}

	if alreadyForked.ID != 0 {
		return redirect(ctx, "/"+alreadyForked.User.Username+"/"+alreadyForked.Uuid)
	}

	uuidGist, err := uuid.NewRandom()
	if err != nil {
		return errorRes(500, "Error creating an UUID", err)
	}

	newGist := &db.Gist{
		Uuid:            strings.Replace(uuidGist.String(), "-", "", -1),
		Title:           gist.Title,
		Preview:         gist.Preview,
		PreviewFilename: gist.PreviewFilename,
		Description:     gist.Description,
		Private:         gist.Private,
		UserID:          currentUser.ID,
		ForkedID:        gist.ID,
		NbFiles:         gist.NbFiles,
	}

	if err = newGist.CreateForked(); err != nil {
		return errorRes(500, "Error forking the gist in database", err)
	}

	if err = gist.ForkClone(currentUser.Username, newGist.Uuid); err != nil {
		return errorRes(500, "Error cloning the repository while forking", err)
	}
	if err = gist.IncrementForkCount(); err != nil {
		return errorRes(500, "Error incrementing the fork count", err)
	}

	addFlash(ctx, "Gist has been forked", "success")

	return redirect(ctx, "/"+currentUser.Username+"/"+newGist.Uuid)
}

func rawFile(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
	if err != nil {
		return errorRes(500, "Error getting file content", err)
	}

	if file == nil {
		return notFound("File not found")
	}

	return plainText(ctx, 200, file.Content)
}

func downloadFile(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	file, err := gist.File(ctx.Param("revision"), ctx.Param("file"), false)
	if err != nil {
		return errorRes(500, "Error getting file content", err)
	}

	if file == nil {
		return notFound("File not found")
	}

	ctx.Response().Header().Set("Content-Type", "text/plain")
	ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+file.Filename)
	ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(file.Content)))
	_, err = ctx.Response().Write([]byte(file.Content))

	if err != nil {
		return errorRes(500, "Error downloading the file", err)
	}

	return nil
}

func edit(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)

	files, err := gist.Files("HEAD")
	if err != nil {
		return errorRes(500, "Error fetching files from repository", err)
	}

	setData(ctx, "files", files)
	setData(ctx, "htmlTitle", "Edit "+gist.Title)

	return html(ctx, "edit.html")
}

func downloadZip(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	revision := ctx.Param("revision")

	files, err := gist.Files(revision)
	if err != nil {
		return errorRes(500, "Error fetching files from repository", err)
	}
	if len(files) == 0 {
		return notFound("No files found in this revision")
	}

	zipFile := new(bytes.Buffer)

	zipWriter := zip.NewWriter(zipFile)

	for _, file := range files {
		fh := &zip.FileHeader{
			Name:   file.Filename,
			Method: zip.Deflate,
		}
		f, err := zipWriter.CreateHeader(fh)
		if err != nil {
			return errorRes(500, "Error adding a file the to the zip archive", err)
		}
		_, err = f.Write([]byte(file.Content))
		if err != nil {
			return errorRes(500, "Error adding file content the to the zip archive", err)
		}
	}
	err = zipWriter.Close()
	if err != nil {
		return errorRes(500, "Error closing the zip archive", err)
	}

	ctx.Response().Header().Set("Content-Type", "application/zip")
	ctx.Response().Header().Set("Content-Disposition", "attachment; filename="+gist.Uuid+".zip")
	ctx.Response().Header().Set("Content-Length", strconv.Itoa(len(zipFile.Bytes())))
	_, err = ctx.Response().Write(zipFile.Bytes())
	if err != nil {
		return errorRes(500, "Error writing the zip archive", err)
	}
	return nil
}

func likes(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)

	pageInt := getPage(ctx)

	likers, err := gist.GetUsersLikes(pageInt - 1)
	if err != nil {
		return errorRes(500, "Error getting users who liked this gist", err)
	}

	if err = paginate(ctx, likers, pageInt, 30, "likers", gist.User.Username+"/"+gist.Uuid+"/likes", 1); err != nil {
		return errorRes(404, "Page not found", nil)
	}

	setData(ctx, "htmlTitle", "Like for "+gist.Title)
	setData(ctx, "revision", "HEAD")
	return html(ctx, "likes.html")
}

func forks(ctx echo.Context) error {
	gist := getData(ctx, "gist").(*db.Gist)
	pageInt := getPage(ctx)

	currentUser := getUserLogged(ctx)
	var fromUserID uint = 0
	if currentUser != nil {
		fromUserID = currentUser.ID
	}

	forks, err := gist.GetForks(fromUserID, pageInt-1)
	if err != nil {
		return errorRes(500, "Error getting users who liked this gist", err)
	}

	if err = paginate(ctx, forks, pageInt, 30, "forks", gist.User.Username+"/"+gist.Uuid+"/forks", 2); err != nil {
		return errorRes(404, "Page not found", nil)
	}

	setData(ctx, "htmlTitle", "Forks for "+gist.Title)
	setData(ctx, "revision", "HEAD")
	return html(ctx, "forks.html")
}