package guard

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"syscall"

	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/urfave/cli/v3"
	"tangled.org/core/knotserver/sandbox"
	"tangled.org/core/log"
)

func Command() *cli.Command {
	return &cli.Command{
		Name:   "guard",
		Usage:  "role-based access control for git over ssh (not for manual use)",
		Action: Run,
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:     "user",
				Usage:    "allowed git user",
				Required: true,
			},
			&cli.StringFlag{
				Name:  "git-dir",
				Usage: "base directory for git repos",
				Value: "/home/git",
			},
			&cli.StringFlag{
				Name:  "log-path",
				Usage: "path to log file",
				Value: "/home/git/guard.log",
			},
			&cli.StringFlag{
				Name:  "internal-api",
				Usage: "internal API endpoint",
				Value: "http://localhost:5444",
			},
			&cli.StringFlag{
				Name:  "motd-file",
				Usage: "path to message of the day file",
				Value: "/home/git/motd",
			},
			&cli.BoolFlag{
				Name:  "secure-mode",
				Usage: "isolate git subprocesses to their own repository directory",
			},
		},
	}
}

func Run(ctx context.Context, cmd *cli.Command) error {
	l := log.FromContext(ctx)

	incomingUser := cmd.String("user")
	gitDir := cmd.String("git-dir")
	logPath := cmd.String("log-path")
	endpoint := cmd.String("internal-api")
	motdFile := cmd.String("motd-file")
	secureMode := cmd.Bool("secure-mode")

	logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		l.Error("failed to open log file", "error", err)
		return err
	} else {
		fileHandler := slog.NewJSONHandler(logFile, &slog.HandlerOptions{Level: slog.LevelInfo})
		l = slog.New(fileHandler)
	}

	var clientIP string
	if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" {
		parts := strings.Fields(connInfo)
		if len(parts) > 0 {
			clientIP = parts[0]
		}
	}

	if incomingUser == "" {
		l.Error("access denied: no user specified")
		fmt.Fprintln(os.Stderr, "access denied: no user specified")
		os.Exit(-1)
	}

	sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")

	l.Info("connection attempt",
		"user", incomingUser,
		"command", sshCommand,
		"client", clientIP)

	// TODO: greet user with their resolved handle instead of did
	if sshCommand == "" {
		l.Info("access denied: no interactive shells", "user", incomingUser)
		fmt.Fprintf(os.Stderr, "Hi @%s! You've successfully authenticated.\n", incomingUser)
		os.Exit(-1)
	}

	cmdParts := strings.Fields(sshCommand)
	if len(cmdParts) < 2 {
		l.Error("invalid command format", "command", sshCommand)
		fmt.Fprintln(os.Stderr, "invalid command format")
		os.Exit(-1)
	}

	gitCommand := cmdParts[0]
	repoPath := cmdParts[1]

	validCommands := map[string]bool{
		"git-receive-pack":   true,
		"git-upload-pack":    true,
		"git-upload-archive": true,
	}
	if !validCommands[gitCommand] {
		l.Error("access denied: invalid git command", "command", gitCommand)
		fmt.Fprintln(os.Stderr, "access denied: invalid git command")
		return fmt.Errorf("access denied: invalid git command")
	}

	// qualify repo path from internal server which holds the knot config
	qualifiedRepoPath, err := guardAndQualifyRepo(l, endpoint, incomingUser, repoPath, gitCommand)
	if err != nil {
		l.Error("failed to run guard", "err", err)
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoPath)

	l.Info("processing command",
		"user", incomingUser,
		"command", gitCommand,
		"repo", repoPath,
		"fullPath", fullPath,
		"client", clientIP)

	var motdReader io.Reader
	if reader, err := os.Open(motdFile); err != nil {
		if !errors.Is(err, os.ErrNotExist) {
			l.Error("failed to read motd file", "error", err)
		}
		motdReader = strings.NewReader("Welcome to this knot!\n")
	} else {
		motdReader = reader
	}
	if gitCommand == "git-upload-pack" {
		io.WriteString(os.Stderr, "\x02")
	}
	io.Copy(os.Stderr, motdReader)

	gitCmd := exec.Command(gitCommand, fullPath)
	gitCmd.Stdout = os.Stdout
	gitCmd.Stderr = os.Stderr
	gitCmd.Stdin = os.Stdin
	gitCmd.Env = append(os.Environ(),
		fmt.Sprintf("GIT_USER_DID=%s", incomingUser),
	)

	if secureMode {
		sb, warn := sandbox.New(func(repoPath string) (uint32, uint32, error) {
			return sandbox.LookupUIDForRepoPath(gitDir, repoPath)
		})
		if warn != "" {
			l.Warn("secure-mode: sandbox degraded", "reason", warn)
		} else {
			l.Info("secure-mode: wrapping git command", "backend", sb.Name())
		}
		wrapped, wrapErr := sb.Wrap(fullPath, gitCmd)
		if wrapErr != nil {
			l.Error("sandbox wrap failed", "error", wrapErr)
		} else {
			gitCmd = wrapped
		}
	}

	if gitCmd.SysProcAttr == nil {
		gitCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
	}

	if err := gitCmd.Run(); err != nil {
		l.Error("command failed", "error", err)
		fmt.Fprintf(os.Stderr, "command failed: %v\n", err)
		return fmt.Errorf("command failed: %v", err)
	}

	l.Info("command completed",
		"user", incomingUser,
		"command", gitCommand,
		"repo", repoPath,
		"success", true)

	return nil
}

// runs guardAndQualifyRepo logic
func guardAndQualifyRepo(l *slog.Logger, endpoint, incomingUser, repo, gitCommand string) (string, error) {
	u, _ := url.Parse(endpoint + "/guard")
	q := u.Query()
	q.Add("user", incomingUser)
	q.Add("repo", repo)
	q.Add("gitCmd", gitCommand)
	u.RawQuery = q.Encode()

	resp, err := http.Get(u.String())
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	l.Info("Running guard", "url", u.String(), "status", resp.Status)

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	text := string(body)

	switch resp.StatusCode {
	case http.StatusOK:
		return text, nil
	case http.StatusForbidden:
		l.Error("access denied: user not allowed", "did", incomingUser, "reponame", text)
		return text, errors.New("access denied: user not allowed")
	default:
		return "", errors.New(text)
	}
}
