package cgi

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"

	sr "tildegit.org/tjp/sliderule"
	"tildegit.org/tjp/sliderule/gopher"
	"tildegit.org/tjp/sliderule/gopher/gophermap"
	"tildegit.org/tjp/sliderule/logging"
)

// GopherCGIDirectory runs any executable files relative to a root directory on the file system.
//
// It will also find and run any executables part way through the path, so for example
// a request for /foo/bar/baz can also run an executable found at /foo or  /foo/bar. In
// such a case the PATH_INFO environment variable will include the remaining portion of
// the URI path.
func GopherCGIDirectory(fsroot, urlroot, cmd string, settings *gophermap.FileSystemSettings) sr.Handler {
	if settings == nil || !settings.Exec {
		return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil })
	}
	fsroot = strings.TrimRight(fsroot, "/")

	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
		if !strings.HasPrefix(request.Path, urlroot) {
			return nil
		}
		requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")

		fullpath, pathinfo, err := resolveGopherCGI(fsroot, requestpath)
		if err != nil {
			return gopher.Error(err).Response()
		}
		if fullpath == "" {
			return nil
		}

		return runGopherCGI(ctx, request, fullpath, pathinfo, cmd, *settings)
	})
}

// ExecGopherMaps runs any gophermaps
func ExecGopherMaps(fsroot, urlroot, cmd string, settings *gophermap.FileSystemSettings) sr.Handler {
	if settings == nil || !settings.Exec {
		return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response { return nil })
	}
	fsroot = strings.TrimRight(fsroot, "/")

	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
		if !strings.HasPrefix(request.Path, urlroot) {
			return nil
		}
		requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")

		fullpath := filepath.Join(fsroot, requestpath)
		info, err := os.Stat(fullpath)
		if isNotExistError(err) {
			return nil
		}
		if err != nil {
			return gopher.Error(err).Response()
		}

		if info.IsDir() {
			for _, fname := range settings.DirMaps {
				fpath := filepath.Join(fullpath, fname)
				finfo, err := os.Stat(fpath)
				if isNotExistError(err) {
					continue
				}
				if err != nil {
					return gopher.Error(err).Response()
				}

				m := finfo.Mode()
				if m.IsDir() {
					continue
				}
				if !m.IsRegular() || m&5 != 5 {
					continue
				}
				return runGopherCGI(ctx, request, fpath, "/", cmd, *settings)
			}

			return nil
		}

		m := info.Mode()
		if !m.IsRegular() || m&5 != 5 {
			return nil
		}

		return runGopherCGI(ctx, request, fullpath, "/", cmd, *settings)
	})
}

func runGopherCGI(
	ctx context.Context,
	request *sr.Request,
	fullpath string,
	pathinfo string,
	cmd string,
	settings gophermap.FileSystemSettings,
) *sr.Response {
	workdir := filepath.Dir(fullpath)
	if cmd != "" {
		fullpath = cmd
	}

	stderr := &bytes.Buffer{}
	stdout, exitCode, err := RunCGI(ctx, request, fullpath, pathinfo, workdir, stderr)
	if err != nil {
		return gopher.Error(err).Response()
	}
	if exitCode != 0 {
		ctx.Value("warnlog").(logging.Logger).Log(
			"msg", "cgi exited with non-zero exit code",
			"code", exitCode,
			"stderr", stderr.String(),
		)
		return gopher.Error(
			fmt.Errorf("CGI process exited with status %d", exitCode),
		).Response()
	}

	if settings.ParseExtended {
		edoc, err := gophermap.ParseExtended(stdout, request.URL)
		if err != nil {
			return gopher.Error(err).Response()
		}

		doc, _, err := edoc.Compatible(filepath.Dir(fullpath), settings)
		if err != nil {
			return gopher.Error(err).Response()
		}
		return doc.Response()
	}

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

func resolveGopherCGI(fsRoot string, reqPath string) (string, string, error) {
	segments := append([]string{""}, strings.Split(reqPath, "/")...)
	fullpath := fsRoot
	for i, segment := range segments {
		fullpath = filepath.Join(fullpath, segment)

		info, err := os.Stat(fullpath)
		if isNotExistError(err) {
			return "", "", nil
		}
		if err != nil {
			return "", "", err
		}

		if !info.IsDir() {
			if info.Mode()&5 == 5 {
				pathinfo := "/"
				if len(segments) > i+1 {
					pathinfo = path.Join(segments[i:]...)
				}
				return fullpath, pathinfo, nil
			}
			break
		}
	}

	return "", "", nil
}