package syw

import (
	"bytes"
	"context"
	"mime"
	"os"
	"path"
	"path/filepath"
	"strings"
	"text/template"

	"tildegit.org/tjp/sliderule"
	"tildegit.org/tjp/sliderule/gopher"
)

// GopherRouter builds a router that will handle gopher requests in a directory of git repositories.
//
// The routes it defines are:
//    /                                    .gph listing of the repositories in the directory
//    /:repository                         .gph overview of the repository
//    /:repository/branches                .gph list of branches/head
//    /:repository/tags                    .gph listing of tags
//    /:repository/refs/:ref               .gph overview of a ref
//    /:repository/refs/:ref/tree          .gph listing of a ref's root directory
//    /:repository/refs/:ref/tree/*path     for directories:.gph list of contents
//                                          for files: raw files (guessed item type text/binary/image/etc)
//    /:repository/diffstat/:fromref/:toref text diffstat between two refs
//    /:repository/diff/:fromref/:toref     text diff between two refs
//
// The overrides argument can provide templates to define the behavior of nearly all of the above routes.
// All of them have default implementations, so the argument can be nil, but otherwise the template names
// used are:
//    repo_root.gph      gophermap at /
//    repo_home.gph      gophermap at /:repository
//    branch_list.gph    gophermap at /:repository/branches
//    tag_list.gph       gophermap at /:repository/tags
//    ref.gph            gophermap at /:repository/refs/:ref
//    tree.gph           gophermap at direcotry paths under /:repository/refs/:ref/tree/*path
//                       (file paths return the raw files without any template involved)
//    diffstat.gph.txt   plain text diffstat at /:repository/diffstat/:fromref/:toref
//    diff.gph.txt       plain text diff at /:repository/diff/:fromref/:toref
//
// Most of the templates above are rendered with an object with 6 fields:
//    Ctx:      the context.Context from the request
//    Repo:     a *syw.Repository corresponding to <repodir>/:repository
//    Params:   the map[string]string of the route parameters
//    Host:     the hostname of the running server
//    Port:     the port number of the running server
//    Selector: the selector in the current request
//
// The only exception is repo_root.gph, which is instead rendered with a slice of the repo names.
//
// All templates have 3 additional functions made available to them:
//    combine: func(string, ...string) string - successively combines paths using url.URL.ResolveReference
//    join: func(string, ...string) string - successively joins path segments
//    rawtext: func(selector, host, port, text string) string renders text lines as gopher info-message lines.
func GopherRouter(repodir string, overrides *template.Template) *sliderule.Router {
	tmpl, err := addTemplates(gopherTemplate, overrides)
	if err != nil {
		panic(err)
	}

	repoRouter := &sliderule.Router{}
	repoRouter.Use(assignRepo(repodir))
	repoRouter.Route("/branches", runGopherTemplate(tmpl, "branch_list.gph", gopher.MenuType))
	repoRouter.Route("/tags", runGopherTemplate(tmpl, "tag_list.gph", gopher.MenuType))
	repoRouter.Route("/refs/:ref", runGopherTemplate(tmpl, "ref.gph", gopher.MenuType))
	repoRouter.Route("/refs/:ref/tree", gopherTreePath(tmpl, false))
	repoRouter.Route("/refs/:ref/tree/*path", gopherTreePath(tmpl, true))
	repoRouter.Route("/diffstat/:fromref/:toref", runGopherTemplate(tmpl, "diffstat.gph.txt", gopher.TextFileType))
	repoRouter.Route("/diff/:fromref/:toref", runGopherTemplate(tmpl, "diff.gph.txt", gopher.TextFileType))

	router := &sliderule.Router{}
	router.Route("/", gopherRoot(repodir, tmpl))
	router.Route("/:"+reponamekey, assignRepo(repodir)(runGopherTemplate(tmpl, "repo_home.gph", gopher.MenuType)))
	router.Mount("/:"+reponamekey, repoRouter)

	return router
}

func gopherRoot(repodir string, tmpl *template.Template) sliderule.Handler {
	return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
		entries, err := os.ReadDir(repodir)
		if err != nil {
			return gopher.Error(err).Response()
		}

		names := []string{}
		for _, item := range entries {
			if Open(filepath.Join(repodir, item.Name())) != nil {
				names = append(names, item.Name())
			}
		}

		buf := &bytes.Buffer{}
		obj := map[string]any{
			"Repos":    names,
			"Host":     request.Hostname(),
			"Port":     request.Port(),
			"Selector": request.Path,
		}
		if err := tmpl.ExecuteTemplate(buf, "repo_root.gph", obj); err != nil {
			return gopher.Error(err).Response()
		}

		return gopher.File(gopher.MenuType, buf)
	})
}

func gopherTreePath(tmpl *template.Template, haspath bool) sliderule.Handler {
	return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
		repo := ctx.Value(repokey).(*Repository)
		params := sliderule.RouteParams(ctx)

		t := "tree"
		if haspath {
			var err error
			t, err = repo.Type(ctx, params["ref"] + ":" + params["path"])
			if err != nil {
				return gopher.Error(err).Response()
			}
		}

		if t != "blob" {
			if !haspath {
				params["path"] = ""
			}
			return runGopherTemplate(tmpl, "tree.gph", gopher.MenuType).Handle(ctx, request)
		}

		body, err := repo.Blob(ctx, params["ref"], params["path"])
		if err != nil {
			return gopher.Error(err).Response()
		}

		filetype := gopher.MenuType
		ext := path.Ext(params["path"])
		if ext != ".gph" && params["path"] != "gophermap" {
			mtype := mime.TypeByExtension(ext)
			if strings.HasPrefix(mtype, "text/") {
				filetype = gopher.TextFileType
			} else {
				filetype = gopher.BinaryFileType
			}
		}

		return gopher.File(filetype, bytes.NewBuffer(body))
	})
}

func runGopherTemplate(tmpl *template.Template, name string, filetype sliderule.Status) sliderule.Handler {
	return sliderule.HandlerFunc(func(ctx context.Context, request *sliderule.Request) *sliderule.Response {
		obj := map[string]any{
			"Ctx":      ctx,
			"Repo":     ctx.Value(repokey),
			"Params":   sliderule.RouteParams(ctx),
			"Host":     request.Hostname(),
			"Port":     request.Port(),
			"Selector": request.Path,
		}
		buf := &bytes.Buffer{}

		if err := tmpl.ExecuteTemplate(buf, name, obj); err != nil {
			return gopher.Error(err).Response()
		}

		return gopher.File(filetype, buf)
	})
}