package handlers

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"net/http"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/go-chi/chi/v5"
	"golang.org/x/crypto/bcrypt"
	"xorm.io/xorm"

	"github.com/forgeo/forgebucket/internal/config"
	"github.com/forgeo/forgebucket/internal/models"
)

type GitHTTPHandler struct {
	db  *xorm.Engine
	cfg *config.Config
}

func NewGitHTTPHandler(db *xorm.Engine, cfg *config.Config) *GitHTTPHandler {
	return &GitHTTPHandler{db: db, cfg: cfg}
}

// ServeGit is the entry point for all git smart-HTTP requests.
// URL shape: /{owner}/{repo}.git/{service}
func (h *GitHTTPHandler) ServeGit(w http.ResponseWriter, r *http.Request) {
	owner := chi.URLParam(r, "owner")
	repoGit := chi.URLParam(r, "repoGit") // e.g. "myrepo.git"
	repoName := strings.TrimSuffix(repoGit, ".git")

	// Resolve repo from DB
	var ownerUser models.User
	if found, _ := h.db.Where("username = ?", owner).Get(&ownerUser); !found {
		http.NotFound(w, r)
		return
	}
	var repo models.Repository
	if found, _ := h.db.Where("owner_id = ? AND name = ?", ownerUser.ID, repoName).Get(&repo); !found {
		http.NotFound(w, r)
		return
	}

	// Determine service from URL / query string
	service := r.URL.Query().Get("service")
	if service == "" {
		// POST bodies encode the service in the path
		path := r.URL.Path
		if strings.HasSuffix(path, "/git-upload-pack") {
			service = "git-upload-pack"
		} else if strings.HasSuffix(path, "/git-receive-pack") {
			service = "git-receive-pack"
		}
	}

	// Require authentication for push; allow anonymous read for public repos
	var authedUser string
	user, authed := h.basicAuth(r)
	if authed {
		authedUser = user
	} else if service == "git-receive-pack" || repo.IsPrivate {
		w.Header().Set("WWW-Authenticate", `Basic realm="ForgeBucket"`)
		http.Error(w, "authentication required", http.StatusUnauthorized)
		return
	}

	// Build PATH_INFO: /{reponame}.git/{suffix}
	// Strip the /{owner}/{repoGit} prefix from the raw URL path to get the suffix.
	prefix := "/" + owner + "/" + repoGit
	suffix := strings.TrimPrefix(r.URL.Path, prefix)
	if suffix == "" {
		suffix = "/"
	}
	pathInfo := "/" + repoGit + suffix

	projectRoot := filepath.Dir(repo.DiskPath)

	env := []string{
		"GIT_PROJECT_ROOT=" + projectRoot,
		"GIT_HTTP_EXPORT_ALL=1",
		"PATH_INFO=" + pathInfo,
		"QUERY_STRING=" + r.URL.RawQuery,
		"REQUEST_METHOD=" + r.Method,
		"SERVER_PROTOCOL=HTTP/1.1",
		"SERVER_SOFTWARE=ForgeBucket/1.0",
		"REMOTE_ADDR=" + r.RemoteAddr,
		"HOME=/tmp",
		"GIT_TERMINAL_PROMPT=0",
	}
	if ct := r.Header.Get("Content-Type"); ct != "" {
		env = append(env, "CONTENT_TYPE="+ct)
	}
	if cl := r.ContentLength; cl > 0 {
		env = append(env, "CONTENT_LENGTH="+strconv.FormatInt(cl, 10))
	}
	if authedUser != "" {
		env = append(env, "REMOTE_USER="+authedUser)
	}

	gitExec, err := exec.LookPath("git")
	if err != nil {
		http.Error(w, "git not found on server", http.StatusInternalServerError)
		return
	}

	if err := runGitBackend(r.Context(), w, r.Body, gitExec, env); err != nil {
		http.Error(w, fmt.Sprintf("git http-backend: %v", err), http.StatusInternalServerError)
	}
}

// basicAuth validates HTTP Basic Auth credentials against the DB.
func (h *GitHTTPHandler) basicAuth(r *http.Request) (username string, ok bool) {
	u, p, hasAuth := r.BasicAuth()
	if !hasAuth {
		return "", false
	}
	var user models.User
	found, _ := h.db.Where("username = ?", u).Get(&user)
	if !found {
		return "", false
	}
	if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(p)) != nil {
		return "", false
	}
	return u, true
}

// runGitBackend executes `git http-backend` as a CGI subprocess and forwards
// its response (headers + body) to the HTTP response writer.
func runGitBackend(ctx context.Context, w http.ResponseWriter, body io.Reader, gitExec string, env []string) error {
	cmd := exec.CommandContext(ctx, gitExec, "http-backend")
	cmd.Env = env

	pr, pw := io.Pipe()
	cmd.Stdout = pw
	cmd.Stdin = body

	var stderrBuf strings.Builder
	cmd.Stderr = &stderrBuf

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("start: %w", err)
	}

	// Close write-end of pipe once git finishes
	done := make(chan error, 1)
	go func() {
		err := cmd.Wait()
		pw.Close()
		done <- err
	}()

	// Parse CGI response: headers then body
	br := bufio.NewReader(pr)
	statusCode := http.StatusOK

	for {
		line, err := br.ReadString('\n')
		line = strings.TrimRight(line, "\r\n")
		if line == "" {
			break
		}
		if strings.HasPrefix(line, "Status:") {
			rest := strings.TrimSpace(strings.TrimPrefix(line, "Status:"))
			if parts := strings.SplitN(rest, " ", 2); len(parts) >= 1 {
				if code, e := strconv.Atoi(parts[0]); e == nil {
					statusCode = code
				}
			}
		} else if idx := strings.Index(line, ":"); idx > 0 {
			key := strings.TrimSpace(line[:idx])
			val := strings.TrimSpace(line[idx+1:])
			w.Header().Set(key, val)
		}
		if err != nil {
			break
		}
	}

	w.WriteHeader(statusCode)
	io.Copy(w, br) //nolint:errcheck

	waitErr := <-done
	if waitErr != nil && stderrBuf.Len() > 0 {
		return fmt.Errorf("%w: %s", waitErr, stderrBuf.String())
	}
	return waitErr
}
