// Package actionsrunner owns the official actions/runner compatibility routes.
package actionsrunner

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	serviceactionsrunner "github.com/tidbcloud/AgentTeam/internal/service/actionsrunner"
	"github.com/tidbcloud/AgentTeam/internal/service/automation"
)

const (
	actionsRunnerBodyLimit         = int64(32 * 1024)
	actionsRunnerCallbackBodyLimit = int64(192 * 1024)
)

type routeRegistrar interface {
	Handle(pattern string, handler http.Handler)
}

type actionsRunnerRoute struct {
	pattern string
	handler func(*Handler) http.Handler
}

var actionsRunnerRoutes = []actionsRunnerRoute{
	{"POST /api/v3/actions/runner-registration", func(h *Handler) http.Handler { return http.HandlerFunc(h.RunnerRegistrationBootstrap) }},
	{"GET /api/v3/repos/{owner}/{repo}/actions/runner-groups", func(h *Handler) http.Handler { return http.HandlerFunc(h.RunnerGroups) }},
	{"GET /api/v3/repos/{owner}/{repo}/actions/runners", func(h *Handler) http.Handler { return http.HandlerFunc(h.ListRunners) }},
	{"POST /api/v3/actions/runners/register", func(h *Handler) http.Handler { return http.HandlerFunc(h.RegisterRunner) }},
	{"POST /_oauth/token", func(h *Handler) http.Handler { return http.HandlerFunc(h.OAuthToken) }},
	{"POST /_broker/session", func(h *Handler) http.Handler { return http.HandlerFunc(h.CreateBrokerSession) }},
	{"GET /_broker/message", func(h *Handler) http.Handler { return http.HandlerFunc(h.NextMessage) }},
	{"DELETE /_broker/session", func(h *Handler) http.Handler { return http.HandlerFunc(h.DeleteBrokerSession) }},
	{"POST /_runs/acquirejob", func(h *Handler) http.Handler { return http.HandlerFunc(h.AcquireJob) }},
	{"POST /_runs/renewjob", func(h *Handler) http.Handler { return http.HandlerFunc(h.RenewJob) }},
	{"POST /_runs/completejob", func(h *Handler) http.Handler { return http.HandlerFunc(h.CompleteJob) }},
}

// Service is the official-runner compatibility service surface used by HTTP.
type Service interface {
	RunnerRegistrationBootstrap(ctx context.Context, registrationToken string, brokerURL string) (serviceactionsrunner.RegistrationBootstrap, error)
	ValidateRegistrationToken(ctx context.Context, registrationToken string) error
	RegisterRunner(ctx context.Context, registrationToken string, input serviceactionsrunner.RegisterRunnerInput, brokerURL string, oauthURL string) (serviceactionsrunner.RunnerRegistration, error)
	OAuthTokenExchange(ctx context.Context, clientAssertion string, audience string) (serviceactionsrunner.OAuthToken, error)
	CreateBrokerSession(ctx context.Context, accessToken string, ownerName string) (serviceactionsrunner.BrokerSession, error)
	CloseBrokerSession(ctx context.Context, accessToken string) error
	NextMessage(ctx context.Context, accessToken string, runServiceURL string) (*serviceactionsrunner.TaskAgentMessage, error)
	AcquireJob(ctx context.Context, accessToken string, request serviceactionsrunner.AcquireJobRequest) (map[string]any, error)
	RenewJob(ctx context.Context, accessToken string, request serviceactionsrunner.RenewJobRequest) (serviceactionsrunner.RenewJobResponse, error)
	CompleteJob(ctx context.Context, accessToken string, request serviceactionsrunner.CompleteJobRequest) error
}

// Handler owns official actions/runner compatibility endpoints.
type Handler struct {
	service Service
	baseURL string
}

// NewHandler creates an official actions/runner compatibility HTTP handler.
func NewHandler(service Service, baseURL string) *Handler {
	return &Handler{service: service, baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/")}
}

// RegisterRoutes mounts official actions/runner compatibility routes.
func RegisterRoutes(router routeRegistrar, handler *Handler) {
	if router == nil {
		return
	}
	disabled := handler == nil || handler.service == nil || handler.baseURL == ""
	fallback := disabledHandler{}
	for _, route := range actionsRunnerRoutes {
		var routeHandler http.Handler = fallback
		if !disabled {
			routeHandler = route.handler(handler)
		}
		router.Handle(route.pattern, routeHandler)
	}
}

type disabledHandler struct{}

func (disabledHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
	meshxapi.WriteError(w, meshxapi.NewHTTPError(http.StatusServiceUnavailable, "Actions runner compatibility API is not configured"))
}

// RunnerRegistrationBootstrap handles POST /api/v3/actions/runner-registration.
// It validates the MeshX runner registration token and returns V2 broker
// metadata for config.sh.
func (h *Handler) RunnerRegistrationBootstrap(w http.ResponseWriter, r *http.Request) {
	token, err := remoteAuthToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	if _, err := bindLooseJSON[dto.ActionsRunnerRegistrationBootstrapRequest](w, r); err != nil {
		writeError(w, err)
		return
	}
	bootstrap, err := h.service.RunnerRegistrationBootstrap(r.Context(), token, h.brokerURL())
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerRegistrationBootstrap{
		URL:         bootstrap.BrokerURL,
		TokenSchema: "OAuthAccessToken",
		Token:       "meshx",
		UseV2Flow:   true,
	})
}

// RunnerGroups handles GET /api/v3/repos/{owner}/{repo}/actions/runner-groups.
// It returns the single MVP Default runner group after validating RemoteAuth.
func (h *Handler) RunnerGroups(w http.ResponseWriter, r *http.Request) {
	token, err := remoteAuthToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	if err := h.service.ValidateRegistrationToken(r.Context(), token); err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerGroupList{
		TotalCount:   1,
		RunnerGroups: []dto.ActionsRunnerGroup{{ID: 1, Name: "Default", Default: true}},
	})
}

// ListRunners handles GET /api/v3/repos/{owner}/{repo}/actions/runners.
// It validates RemoteAuth and reports no existing official runners so config.sh
// can proceed with registration.
func (h *Handler) ListRunners(w http.ResponseWriter, r *http.Request) {
	token, err := remoteAuthToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	if err := h.service.ValidateRegistrationToken(r.Context(), token); err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerList{Runners: []dto.ActionsRunner{}})
}

// RegisterRunner handles POST /api/v3/actions/runners/register.
// It stores the official runner OAuth client metadata against the MeshX runner
// referenced by the registration token.
func (h *Handler) RegisterRunner(w http.ResponseWriter, r *http.Request) {
	token, err := remoteAuthToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	body, err := bindLooseJSON[dto.ActionsRunnerRegisterRequest](w, r)
	if err != nil {
		writeError(w, err)
		return
	}
	registration, err := h.service.RegisterRunner(r.Context(), token, body.Input(), h.brokerURL(), h.oauthURL())
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerRegisterResponse{
		ID:   registration.OfficialRunnerID,
		Name: registration.RunnerName,
		Authorization: dto.ActionsRunnerAuthorization{
			AuthorizationURL:       registration.OAuthURL,
			LegacyAuthorizationURL: registration.OAuthURL,
			ServerURL:              registration.BrokerURL,
			ClientID:               registration.ClientID,
		},
	})
}

// OAuthToken handles POST /_oauth/token.
// It validates the runner PS256 client assertion and returns a MeshX-backed
// bearer token for broker and run-service requests.
func (h *Handler) OAuthToken(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, actionsRunnerBodyLimit)
	if err := r.ParseForm(); err != nil {
		writeError(w, meshxapi.NewHTTPError(http.StatusBadRequest, "Invalid OAuth token request"))
		return
	}
	token, err := h.service.OAuthTokenExchange(r.Context(), strings.TrimSpace(r.PostForm.Get("client_assertion")), h.oauthURL())
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerOAuthTokenResponse{AccessToken: token.AccessToken, TokenType: "Bearer", ExpiresIn: token.ExpiresIn})
}

// CreateBrokerSession handles POST /_broker/session.
// It validates the bearer token and returns the MeshX-backed runner session id.
func (h *Handler) CreateBrokerSession(w http.ResponseWriter, r *http.Request) {
	token, err := bearerToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	body, err := bindLooseJSON[dto.ActionsRunnerBrokerSessionRequest](w, r)
	if err != nil {
		writeError(w, err)
		return
	}
	session, err := h.service.CreateBrokerSession(r.Context(), token, body.OwnerName)
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerBrokerSessionResponse{SessionID: session.SessionID, OwnerName: session.OwnerName})
}

// NextMessage handles GET /_broker/message.
// It long-polls MeshX for at most one queue job and returns null when no job is
// available within the bounded poll wait.
func (h *Handler) NextMessage(w http.ResponseWriter, r *http.Request) {
	token, err := bearerToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	message, err := h.service.NextMessage(r.Context(), token, h.runServiceURL())
	if err != nil {
		writeError(w, err)
		return
	}
	if message == nil {
		writeJSON(w, http.StatusOK, nil)
		return
	}
	writeJSON(w, http.StatusOK, dto.ToActionsRunnerTaskAgentMessage(*message))
}

// DeleteBrokerSession handles DELETE /_broker/session.
// It validates the bearer token and accepts the runner's session close request.
func (h *Handler) DeleteBrokerSession(w http.ResponseWriter, r *http.Request) {
	token, err := bearerToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	if err := h.service.CloseBrokerSession(r.Context(), token); err != nil {
		writeError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// AcquireJob handles POST /_runs/acquirejob.
// It accepts the MeshX queue lease and returns a minimal official
// AgentJobRequestMessage for shell run steps.
func (h *Handler) AcquireJob(w http.ResponseWriter, r *http.Request) {
	token, err := bearerToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	body, err := bindLooseJSON[dto.ActionsRunnerAcquireJobRequest](w, r)
	if err != nil {
		writeError(w, err)
		return
	}
	message, err := h.service.AcquireJob(r.Context(), token, body.Input(h.runServiceURL()))
	if err != nil {
		writeRunServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, message)
}

// RenewJob handles POST /_runs/renewjob.
// It extends the MeshX queue lease for the official runner job.
func (h *Handler) RenewJob(w http.ResponseWriter, r *http.Request) {
	token, err := bearerToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	body, err := bindLooseJSON[dto.ActionsRunnerRenewJobRequest](w, r)
	if err != nil {
		writeError(w, err)
		return
	}
	lease, err := h.service.RenewJob(r.Context(), token, body.Input())
	if err != nil {
		writeRunServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, dto.ActionsRunnerRenewJobResponse{LockedUntil: lease.LockedUntil.UTC().Format(time.RFC3339Nano)})
}

// CompleteJob handles POST /_runs/completejob.
// It maps the official runner conclusion to MeshX job completion or failure.
func (h *Handler) CompleteJob(w http.ResponseWriter, r *http.Request) {
	token, err := bearerToken(r)
	if err != nil {
		writeError(w, err)
		return
	}
	body, err := bindLooseJSONLimit[dto.ActionsRunnerCompleteJobRequest](w, r, actionsRunnerCallbackBodyLimit)
	if err != nil {
		writeError(w, err)
		return
	}
	if err := h.service.CompleteJob(r.Context(), token, body.Input()); err != nil {
		writeRunServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func bindLooseJSON[T any](w http.ResponseWriter, r *http.Request) (T, error) {
	return bindLooseJSONLimit[T](w, r, actionsRunnerBodyLimit)
}

func bindLooseJSONLimit[T any](w http.ResponseWriter, r *http.Request, maxBodyBytes int64) (T, error) {
	var body T
	if r == nil || r.Body == nil {
		return body, io.EOF
	}
	r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
	defer r.Body.Close()
	decoder := json.NewDecoder(r.Body)
	if err := decoder.Decode(&body); err != nil {
		var maxBytesErr *http.MaxBytesError
		if errors.As(err, &maxBytesErr) {
			return body, meshxapi.NewHTTPError(http.StatusRequestEntityTooLarge, "Actions runner request body exceeds maximum size")
		}
		return body, meshxapi.NewHTTPError(http.StatusBadRequest, "Invalid JSON body")
	}
	var extra any
	if err := decoder.Decode(&extra); err != nil && !errors.Is(err, io.EOF) {
		return body, meshxapi.NewHTTPError(http.StatusBadRequest, "Invalid JSON body")
	} else if err == nil {
		return body, meshxapi.NewHTTPError(http.StatusBadRequest, "Invalid JSON body")
	}
	return body, nil
}

func remoteAuthToken(r *http.Request) (string, error) {
	return authorizationToken(r, "RemoteAuth", "Actions runner registration token is required")
}

func bearerToken(r *http.Request) (string, error) {
	return authorizationToken(r, "Bearer", "Actions runner bearer token is required")
}

func authorizationToken(r *http.Request, scheme string, message string) (string, error) {
	fields := strings.Fields(strings.TrimSpace(r.Header.Get("Authorization")))
	if len(fields) == 2 && strings.EqualFold(fields[0], scheme) {
		return fields[1], nil
	}
	return "", meshxapi.NewHTTPError(http.StatusUnauthorized, message)
}

func writeJSON(w http.ResponseWriter, status int, body any) {
	meshxapi.WriteResponse(w, meshxapi.Response{Status: status, Body: body})
}

func writeError(w http.ResponseWriter, err error) {
	status := automation.HTTPStatus(err)
	if status != http.StatusInternalServerError {
		meshxapi.WriteError(w, meshxapi.NewHTTPError(status, err.Error()))
		return
	}
	meshxapi.WriteError(w, err)
}

func writeRunServiceError(w http.ResponseWriter, err error) {
	status := automation.HTTPStatus(err)
	if status == http.StatusBadRequest {
		status = http.StatusUnprocessableEntity
	}
	if status == http.StatusInternalServerError {
		meshxapi.WriteError(w, err)
		return
	}
	writeJSON(w, status, map[string]any{
		"source":       "actions-run-service",
		"statusCode":   status,
		"errorMessage": err.Error(),
	})
}

func (h *Handler) brokerURL() string     { return h.baseURL + "/_broker/" }
func (h *Handler) oauthURL() string      { return h.baseURL + "/_oauth/token" }
func (h *Handler) runServiceURL() string { return h.baseURL + "/_runs/" }
