package routes

import (
	"compress/gzip"
	"fmt"
	"log"
	"net/http"
	"path"
	"path/filepath"
	"strconv"
	"strings"

	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/dimfeld/httptreemux/v5"
	"github.com/microcosm-cc/bluemonday"
	"github.com/russross/blackfriday/v2"
	"go.alanpearce.eu/elgit/config"
	"go.alanpearce.eu/elgit/git"
	"go.alanpearce.eu/elgit/templates"
)

type deps struct {
	c        *config.Config
	projects []string
}

func (d *deps) Index(w http.ResponseWriter, r *http.Request, params map[string]string) {
	repos := d.getAllRepos()

	pageData := templates.PageData{
		Meta: d.c.Meta,
	}

	if err := templates.Index(pageData, repos).Render(w); err != nil {
		log.Println(err)
		return
	}
}

func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request, params map[string]string) {
	name := path.Join(params["category"], params["name"])

	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}
	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}

	gr, err := git.Open(path, "")
	if err != nil {
		d.Write404(w)
		return
	}

	commits, err := gr.Commits()
	if err != nil {
		d.Write500(w)
		log.Println(err)
		return
	}

	var readmeContent string
	for _, readme := range d.c.Repo.Readme {
		ext := filepath.Ext(readme)
		content, _ := gr.FileContent(readme)
		if len(content) > 0 {
			switch ext {
			case ".md", ".mkd", ".markdown":
				unsafe := blackfriday.Run(
					[]byte(content),
					blackfriday.WithExtensions(blackfriday.CommonExtensions),
				)
				html := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
				readmeContent = string(html)
			default:
				safe := bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
				readmeContent = fmt.Sprintf(`<pre>%s</pre>`, safe)
			}
			break
		}
	}

	if readmeContent == "" {
		log.Printf("no readme found for %s", name)
	}

	mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
	if err != nil {
		d.Write500(w)
		log.Println(err)
		return
	}

	if len(commits) >= 3 {
		commits = commits[:3]
	}

	pageData := templates.PageData{
		Meta:        d.c.Meta,
		Name:        name,
		DisplayName: getDisplayName(name),
		Ref:         mainBranch,
		Description: getDescription(path),
		Servername:  d.c.Server.Name,
		Gomod:       isGoModule(gr),
	}

	if err := templates.Repo(pageData, commits, readmeContent).Render(w); err != nil {
		log.Println(err)
	}
}

func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request, params map[string]string) {
	name := path.Join(params["category"], params["name"])
	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}
	treePath := strings.TrimSuffix(params["rest"], "/")
	ref := params["ref"]

	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}
	gr, err := git.Open(path, ref)
	if err != nil {
		d.Write404(w)
		return
	}

	files, err := gr.FileTree(treePath)
	if err != nil {
		d.Write500(w)
		log.Println(err)
		return
	}

	data := make(map[string]any)
	data["name"] = name
	data["displayname"] = getDisplayName(name)
	data["ref"] = ref
	data["parent"] = treePath
	data["desc"] = getDescription(path)
	data["dotdot"] = filepath.Dir(treePath)

	d.listFiles(files, data, w)
}

func (d *deps) FileContent(w http.ResponseWriter, r *http.Request, params map[string]string) {
	var raw bool
	if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
		raw = rawParam
	}

	name := path.Join(params["category"], params["name"])
	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}
	treePath := params["rest"]
	ref := params["ref"]

	name = httptreemux.Clean(name)
	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}

	gr, err := git.Open(path, ref)
	if err != nil {
		d.Write404(w)
		return
	}

	contents, err := gr.FileContent(treePath)
	if err != nil {
		d.Write500(w)
		return
	}
	data := make(map[string]any)
	data["name"] = name
	data["displayname"] = getDisplayName(name)
	data["ref"] = ref
	data["desc"] = getDescription(path)
	data["path"] = treePath

	if raw {
		d.showRaw(contents, w)
	} else {
		if d.c.Meta.SyntaxHighlight == "" {
			d.showFile(contents, data, w)
		} else {
			d.showFileWithHighlight(treePath, contents, data, w)
		}
	}
}

func (d *deps) Archive(w http.ResponseWriter, r *http.Request, params map[string]string) {
	name := path.Join(params["category"], params["name"])
	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}

	file := params["file"]

	// TODO: extend this to add more files compression (e.g.: xz)
	if !strings.HasSuffix(file, ".tar.gz") {
		d.Write404(w)
		return
	}

	ref := strings.TrimSuffix(file, ".tar.gz")

	// This allows the browser to use a proper name for the file when
	// downloading
	filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
	setContentDisposition(w, filename)
	setGZipMIME(w)

	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}

	gr, err := git.Open(path, ref)
	if err != nil {
		d.Write404(w)
		return
	}

	gw := gzip.NewWriter(w)
	defer gw.Close()

	prefix := fmt.Sprintf("%s-%s", name, ref)
	err = gr.WriteTar(gw, prefix)
	if err != nil {
		// once we start writing to the body we can't report error anymore
		// so we are only left with printing the error.
		log.Println(err)
		return
	}

	err = gw.Flush()
	if err != nil {
		// once we start writing to the body we can't report error anymore
		// so we are only left with printing the error.
		log.Println(err)
		return
	}
}

func (d *deps) Log(w http.ResponseWriter, r *http.Request, params map[string]string) {
	name := path.Join(params["category"], params["name"])
	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}
	ref := params["ref"]

	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}

	gr, err := git.Open(path, ref)
	if err != nil {
		d.Write404(w)
		return
	}

	commits, err := gr.Commits()
	if err != nil {
		d.Write500(w)
		log.Println(err)
		return
	}

	pageData := templates.PageData{
		Meta:        d.c.Meta,
		Name:        name,
		DisplayName: getDisplayName(name),
		Ref:         ref,
		Description: getDescription(path),
		Log:         true,
	}

	if err := templates.Log(pageData, commits).Render(w); err != nil {
		log.Println(err)
		return
	}
}

func (d *deps) Diff(w http.ResponseWriter, r *http.Request, params map[string]string) {
	name := path.Join(params["category"], params["name"])
	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}
	ref := params["ref"]

	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}
	gr, err := git.Open(path, ref)
	if err != nil {
		d.Write404(w)
		return
	}

	diff, err := gr.Diff()
	if err != nil {
		d.Write500(w)
		log.Println(err)
		return
	}

	pageData := templates.PageData{
		Meta:        d.c.Meta,
		Name:        name,
		Stat:        diff.Stat,
		Diff:        diff.Diff,
		DisplayName: getDisplayName(name),
		Ref:         ref,
		Description: getDescription(path),
	}

	if err := templates.Commit(pageData, diff).Render(w); err != nil {
		log.Println(err)
		return
	}
}

func (d *deps) Refs(w http.ResponseWriter, r *http.Request, params map[string]string) {
	name := path.Join(params["category"], params["name"])
	if d.isNotAllowed(name) {
		d.Write404(w)
		return
	}

	path, err := d.GetCleanPath(name)
	if err != nil {
		log.Printf("getcleanpath error: %v", err)
		d.Write404(w)
		return
	}

	gr, err := git.Open(path, "")
	if err != nil {
		d.Write404(w)
		return
	}

	tags, err := gr.Tags()
	if err != nil {
		// Non-fatal, we *should* have at least one branch to show.
		log.Println(err)
	}

	branches, err := gr.Branches()
	if err != nil {
		log.Println(err)
		d.Write500(w)
		return
	}

	pageData := templates.PageData{
		Meta:        d.c.Meta,
		Name:        name,
		DisplayName: getDisplayName(name),
		Description: getDescription(path),
	}

	if err := templates.Refs(pageData, branches, tags).Render(w); err != nil {
		log.Println(err)
		return
	}
}

func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request, params map[string]string) {
	f := params["file"]
	f = httptreemux.Clean(f)
	f, err := securejoin.SecureJoin(d.c.Dirs.Static, f)
	if err != nil {
		d.Write404(w)
		return
	}

	http.ServeFile(w, r, f)
}