package provider

import (
	"context"
	"crypto"
	"crypto/hmac"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/provider"
)

const (
	githubRegisterCookieName    = "agentteam_github_register_state"
	githubInstallCookieName     = "agentteam_github_install_state"
	githubFlowStateVersion      = "v2"
	githubFlowStateTTL          = time.Hour
	githubWebhookPatchTimeout   = 2 * time.Minute
	githubInstallationsPerPage  = 100
	githubInstallationsMaxPages = 10
	githubInstallationsMaxBytes = 4 << 20
)

type githubFlowState struct {
	Version      string `json:"version"`
	Flow         string `json:"flow"`
	Nonce        string `json:"nonce"`
	UserID       string `json:"user_id"`
	WorkspaceID  string `json:"workspace_id,omitempty"`
	Mode         string `json:"mode"`
	ProposalID   string `json:"proposal_id,omitempty"`
	ReturnTarget string `json:"return_target,omitempty"`
	ExpiresAt    int64  `json:"exp"`
}

type githubManifestConversionResponse struct {
	ID            int64  `json:"id"`
	Slug          string `json:"slug"`
	Name          string `json:"name"`
	ClientID      string `json:"client_id"`
	ClientSecret  string `json:"client_secret"`
	PEM           string `json:"pem"`
	WebhookSecret string `json:"webhook_secret"`
	HTMLURL       string `json:"html_url"`
	Owner         struct {
		Login string `json:"login"`
		ID    int64  `json:"id"`
	} `json:"owner"`
}

type githubAppResponse struct {
	ID       int64  `json:"id"`
	Slug     string `json:"slug"`
	Name     string `json:"name"`
	ClientID string `json:"client_id"`
	HTMLURL  string `json:"html_url"`
	Owner    struct {
		Login string `json:"login"`
		ID    int64  `json:"id"`
	} `json:"owner"`
}

type githubInstallationLookupResponse struct {
	ID          int64             `json:"id"`
	AppID       int64             `json:"app_id"`
	AppSlug     string            `json:"app_slug"`
	HTMLURL     string            `json:"html_url"`
	TargetType  string            `json:"target_type"`
	Permissions map[string]string `json:"permissions"`
	Events      []string          `json:"events"`
	SuspendedAt *time.Time        `json:"suspended_at"`
	Account     struct {
		Login string `json:"login"`
		ID    int64  `json:"id"`
	} `json:"account"`
}

type githubInstallationUpgradeStatus struct {
	MissingPermissions []string
	MissingEvents      []string
}

type githubAppSettingsLocator struct {
	Slug               string
	ExpectedOwnerLogin string
}

type gitHubFlowError struct {
	status  int
	message string
}

func (err gitHubFlowError) Error() string {
	return err.message
}

var (
	githubManifestDefaultPermissions = map[string]string{
		"pull_requests": "read",
		"issues":        "read",
		"metadata":      "read",
	}
	githubManifestDefaultEvents = []string{
		"pull_request",
		"pull_request_review",
		"pull_request_review_comment",
		"issue_comment",
	}
)

func (handler *GitHubHandler) newGitHubFlowState(flow string, userID string, workspaceID string, mode string) (githubFlowState, error) {
	nonceBytes := make([]byte, 24)
	if _, err := rand.Read(nonceBytes); err != nil {
		return githubFlowState{}, fmt.Errorf("generate github flow nonce: %w", err)
	}
	trimmedFlow := strings.TrimSpace(flow)
	trimmedMode := strings.TrimSpace(mode)
	if !validGitHubFlowSetupMode(trimmedFlow, trimmedMode) {
		return githubFlowState{}, fmt.Errorf("invalid github %s flow setup mode %q", trimmedFlow, trimmedMode)
	}

	return githubFlowState{
		Version:     githubFlowStateVersion,
		Flow:        trimmedFlow,
		Nonce:       base64.RawURLEncoding.EncodeToString(nonceBytes),
		UserID:      strings.TrimSpace(userID),
		WorkspaceID: strings.TrimSpace(workspaceID),
		Mode:        trimmedMode,
		ExpiresAt:   time.Now().UTC().Add(githubFlowStateTTL).Unix(),
	}, nil
}

func (handler *GitHubHandler) newGitHubProposalInstallFlowState(
	userID string,
	workspaceID string,
	proposalID string,
) (githubFlowState, error) {
	state, err := handler.newGitHubFlowState(
		"proposal_install", userID, workspaceID, app.GitHubAppSetupModeRequester,
	)
	if err != nil {
		return githubFlowState{}, err
	}
	state.ProposalID = strings.TrimSpace(proposalID)
	if state.ProposalID == "" {
		return githubFlowState{}, fmt.Errorf("github setup proposal id is required")
	}
	return state, nil
}

func validGitHubFlowSetupMode(flow string, mode string) bool {
	switch strings.TrimSpace(flow) {
	case "register":
		return mode == app.GitHubAppSetupModeAdmin || mode == app.GitHubAppSetupModeRequester
	case "install":
		return mode == app.GitHubAppSetupModeAdmin || mode == app.GitHubAppSetupModeRequester
	case "proposal_install":
		return mode == app.GitHubAppSetupModeRequester
	default:
		return false
	}
}

func (handler *GitHubHandler) issueGitHubFlowCookie(ctx *gin.Context, cookieName string, state githubFlowState) error {
	encoded, err := handler.encodeGitHubFlowState(state)
	if err != nil {
		return err
	}
	http.SetCookie(ctx.Writer, &http.Cookie{
		Name:     cookieName,
		Value:    encoded,
		Path:     "/api/github",
		HttpOnly: true,
		Secure:   handler.githubCookieSecure(),
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(time.Until(time.Unix(state.ExpiresAt, 0)).Seconds()),
	})
	return nil
}

func (handler *GitHubHandler) encodeGitHubFlowState(state githubFlowState) (string, error) {
	if len(handler.githubConfig.FlowStateKey) == 0 {
		return "", fmt.Errorf("github flow state key is not configured")
	}
	payloadJSON, err := json.Marshal(state)
	if err != nil {
		return "", fmt.Errorf("encode github flow state: %w", err)
	}
	payload := base64.RawURLEncoding.EncodeToString(payloadJSON)
	return payload + "." + handler.signGitHubFlowPayload(payload), nil
}

func (handler *GitHubHandler) decodeGitHubFlowState(encoded string) (githubFlowState, error) {
	if len(handler.githubConfig.FlowStateKey) == 0 {
		return githubFlowState{}, fmt.Errorf("github flow state key is not configured")
	}
	parts := strings.SplitN(strings.TrimSpace(encoded), ".", 2)
	if len(parts) != 2 {
		return githubFlowState{}, fmt.Errorf("invalid github flow state")
	}
	if !handler.verifyGitHubFlowPayload(parts[0], parts[1]) {
		return githubFlowState{}, fmt.Errorf("invalid github flow state signature")
	}
	payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return githubFlowState{}, fmt.Errorf("decode github flow state payload: %w", err)
	}
	var state githubFlowState
	if err := json.Unmarshal(payloadJSON, &state); err != nil {
		return githubFlowState{}, fmt.Errorf("decode github flow state: %w", err)
	}
	return state, nil
}

func validateGitHubFlowState(state githubFlowState, expectedFlow string) error {
	if strings.TrimSpace(state.Version) != githubFlowStateVersion {
		return fmt.Errorf("unsupported github flow cookie version")
	}
	if strings.TrimSpace(state.Flow) != strings.TrimSpace(expectedFlow) {
		return fmt.Errorf("github flow cookie does not match expected flow")
	}
	if strings.TrimSpace(state.UserID) == "" || strings.TrimSpace(state.WorkspaceID) == "" ||
		!validGitHubFlowSetupMode(state.Flow, strings.TrimSpace(state.Mode)) {
		return fmt.Errorf("invalid github flow cookie setup context")
	}
	if strings.TrimSpace(state.Flow) == "proposal_install" && strings.TrimSpace(state.ProposalID) == "" {
		return fmt.Errorf("invalid github proposal installation context")
	}
	returnTarget := strings.TrimSpace(state.ReturnTarget)
	if strings.TrimSpace(state.Flow) != "register" && returnTarget != "" {
		return fmt.Errorf("invalid github flow return target")
	}
	if returnTarget != "" && !validGitHubRegistrationReturnTarget(returnTarget) {
		return fmt.Errorf("invalid github flow return target")
	}
	if strings.TrimSpace(state.Nonce) == "" {
		return fmt.Errorf("github flow cookie state mismatch")
	}
	if time.Now().UTC().Unix() > state.ExpiresAt {
		return fmt.Errorf("github flow cookie expired")
	}
	return nil
}

func (handler *GitHubHandler) consumeGitHubFlowCookie(ctx *gin.Context, cookieName string, expectedFlow string, expectedNonce string) (githubFlowState, error) {
	cookie, err := ctx.Request.Cookie(cookieName)
	if err != nil {
		return githubFlowState{}, fmt.Errorf("load github flow cookie: %w", err)
	}
	state, err := handler.decodeGitHubFlowState(cookie.Value)
	if err != nil {
		return githubFlowState{}, err
	}
	if err := validateGitHubFlowState(state, expectedFlow); err != nil {
		return githubFlowState{}, err
	}
	if strings.TrimSpace(state.Nonce) != strings.TrimSpace(expectedNonce) {
		return githubFlowState{}, fmt.Errorf("github flow cookie state mismatch")
	}
	handler.clearGitHubFlowCookie(ctx, cookieName)
	return state, nil
}

func consumeGitHubInstallFlowCookie(
	consume func(expectedFlow string) (githubFlowState, error),
) (githubFlowState, error) {
	state, err := consume("install")
	if err == nil || errors.Is(err, http.ErrNoCookie) {
		return state, err
	}
	return consume("proposal_install")
}

func (handler *GitHubHandler) clearGitHubFlowCookie(ctx *gin.Context, cookieName string) {
	http.SetCookie(ctx.Writer, &http.Cookie{
		Name:     cookieName,
		Value:    "",
		Path:     "/api/github",
		HttpOnly: true,
		Secure:   handler.githubCookieSecure(),
		SameSite: http.SameSiteLaxMode,
		MaxAge:   -1,
		Expires:  time.Unix(0, 0),
	})
}

func (handler *GitHubHandler) signGitHubFlowPayload(payload string) string {
	mac := hmac.New(sha256.New, handler.githubConfig.FlowStateKey)
	mac.Write([]byte(payload))
	return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
}

func (handler *GitHubHandler) verifyGitHubFlowPayload(payload string, signature string) bool {
	expected := handler.signGitHubFlowPayload(payload)
	return hmac.Equal([]byte(expected), []byte(strings.TrimSpace(signature)))
}

func (handler *GitHubHandler) githubCookieSecure() bool {
	parsed, err := url.Parse(strings.TrimSpace(handler.publicBaseURL))
	if err != nil {
		return false
	}
	return strings.EqualFold(parsed.Scheme, "https")
}

func (handler *GitHubHandler) buildGitHubManifestJSON() (string, error) {
	manifest := map[string]any{
		"name": handler.githubManifestName(),
		"url":  handler.publicBaseURL,
		"hook_attributes": map[string]any{
			"url":    handler.githubWebhookURL(),
			"active": true,
		},
		"redirect_url":        handler.githubRegisterCallbackURL(),
		"setup_url":           handler.githubSetupURL(),
		"setup_on_update":     true,
		"public":              true,
		"default_permissions": githubManifestPermissions(),
		"default_events":      githubManifestEvents(),
	}

	encoded, err := json.Marshal(manifest)
	if err != nil {
		return "", fmt.Errorf("encode github app manifest: %w", err)
	}
	return string(encoded), nil
}

func (handler *GitHubHandler) githubManifestName() string {
	parsed, err := url.Parse(strings.TrimSpace(handler.publicBaseURL))
	if err != nil {
		return "AgentTeam (instance)"
	}
	host := strings.TrimSpace(parsed.Hostname())
	if host == "" {
		return "AgentTeam (instance)"
	}
	return fmt.Sprintf("AgentTeam (%s)", host)
}

func (handler *GitHubHandler) githubWebhookURL() string {
	return strings.TrimRight(handler.publicBaseURL, "/") + "/api/apps/github/webhook"
}

func (handler *GitHubHandler) githubRegisterCallbackURL() string {
	return strings.TrimRight(handler.publicBaseURL, "/") + "/api/github/callback"
}

func (handler *GitHubHandler) githubSetupURL() string {
	return strings.TrimRight(handler.publicBaseURL, "/") + "/api/github/setup"
}

func (handler *GitHubHandler) githubSettingsRedirectURL(status string) string {
	values := url.Values{}
	trimmedStatus := strings.TrimSpace(status)
	if trimmedStatus != "" {
		values.Set("github_app", trimmedStatus)
	}
	return handler.githubSettingsRedirectURLWithQuery(values)
}

func (handler *GitHubHandler) githubSettingsRedirectURLWithQuery(values url.Values) string {
	basePath := "/apps/github"
	if len(values) == 0 {
		return basePath
	}
	return basePath + "?" + values.Encode()
}

func githubBadRequest(message string) error {
	return gitHubFlowError{
		status:  http.StatusBadRequest,
		message: strings.TrimSpace(message),
	}
}

func githubBadGateway(message string) error {
	return gitHubFlowError{
		status:  http.StatusBadGateway,
		message: strings.TrimSpace(message),
	}
}

func githubForbidden(message string) error {
	return gitHubFlowError{
		status:  http.StatusForbidden,
		message: strings.TrimSpace(message),
	}
}

func statusForGitHubFlowError(err error) (int, bool) {
	var flowErr gitHubFlowError
	if errors.As(err, &flowErr) {
		return flowErr.status, true
	}
	return 0, false
}

func (handler *GitHubHandler) exchangeGitHubManifestCode(ctx context.Context, code string) (githubManifestConversionResponse, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(handler.githubConfig.APIBaseURL, "/")+"/app-manifests/"+url.PathEscape(strings.TrimSpace(code))+"/conversions", nil)
	if err != nil {
		return githubManifestConversionResponse{}, fmt.Errorf("build github manifest conversion request: %w", err)
	}
	request.Header.Set("Accept", "application/vnd.github+json")

	response, err := handler.httpClient.Do(request)
	if err != nil {
		return githubManifestConversionResponse{}, fmt.Errorf("exchange github manifest code: %w", err)
	}
	defer response.Body.Close()

	if response.StatusCode < 200 || response.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
		return githubManifestConversionResponse{}, fmt.Errorf("github manifest conversion failed: status=%d body=%s", response.StatusCode, strings.TrimSpace(string(body)))
	}

	var converted githubManifestConversionResponse
	if err := json.NewDecoder(response.Body).Decode(&converted); err != nil {
		return githubManifestConversionResponse{}, fmt.Errorf("decode github manifest conversion: %w", err)
	}
	if err := validateGitHubManifestConversion(converted); err != nil {
		return githubManifestConversionResponse{}, err
	}
	return converted, nil
}

func validateGitHubManifestConversion(converted githubManifestConversionResponse) error {
	if converted.ID <= 0 ||
		strings.TrimSpace(converted.Slug) == "" ||
		strings.TrimSpace(converted.Name) == "" ||
		strings.TrimSpace(converted.ClientID) == "" ||
		strings.TrimSpace(converted.ClientSecret) == "" ||
		strings.TrimSpace(converted.PEM) == "" ||
		strings.TrimSpace(converted.WebhookSecret) == "" ||
		strings.TrimSpace(converted.HTMLURL) == "" ||
		converted.Owner.ID <= 0 ||
		strings.TrimSpace(converted.Owner.Login) == "" {
		return fmt.Errorf("github manifest conversion returned incomplete app credentials")
	}
	return nil
}

func (handler *GitHubHandler) parseGitHubAppSettingsURL(raw string) (githubAppSettingsLocator, error) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return githubAppSettingsLocator{}, githubBadRequest("GitHub App settings URL is required")
	}

	parsed, err := url.Parse(trimmed)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return githubAppSettingsLocator{}, githubBadRequest("Invalid GitHub App settings URL")
	}

	baseURL, err := url.Parse(strings.TrimSpace(handler.githubConfig.BaseURL))
	if err == nil && strings.TrimSpace(baseURL.Host) != "" && !strings.EqualFold(parsed.Host, baseURL.Host) {
		return githubAppSettingsLocator{}, githubBadRequest("GitHub App settings URL must use the configured GitHub host")
	}

	parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
	switch {
	case len(parts) == 3 &&
		strings.EqualFold(parts[0], "settings") &&
		strings.EqualFold(parts[1], "apps") &&
		strings.TrimSpace(parts[2]) != "":
		return githubAppSettingsLocator{Slug: strings.TrimSpace(parts[2])}, nil
	case len(parts) == 5 &&
		strings.EqualFold(parts[0], "organizations") &&
		strings.TrimSpace(parts[1]) != "" &&
		strings.EqualFold(parts[2], "settings") &&
		strings.EqualFold(parts[3], "apps") &&
		strings.TrimSpace(parts[4]) != "":
		return githubAppSettingsLocator{
			Slug:               strings.TrimSpace(parts[4]),
			ExpectedOwnerLogin: strings.TrimSpace(parts[1]),
		}, nil
	default:
		return githubAppSettingsLocator{}, githubBadRequest("GitHub App settings URL must point to a GitHub App settings page")
	}
}

func (handler *GitHubHandler) loadGitHubAppBySlug(ctx context.Context, slug string) (githubAppResponse, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(handler.githubConfig.APIBaseURL, "/")+"/apps/"+url.PathEscape(strings.TrimSpace(slug)), nil)
	if err != nil {
		return githubAppResponse{}, githubBadGateway("Failed to load GitHub App metadata")
	}
	request.Header.Set("Accept", "application/vnd.github+json")

	response, err := handler.httpClient.Do(request)
	if err != nil {
		return githubAppResponse{}, githubBadGateway("Failed to load GitHub App metadata")
	}
	defer response.Body.Close()

	switch response.StatusCode {
	case http.StatusOK:
	default:
		if response.StatusCode == http.StatusNotFound {
			return githubAppResponse{}, githubBadRequest("GitHub App not found for the provided settings URL")
		}
		return githubAppResponse{}, githubBadGateway("Failed to load GitHub App metadata")
	}

	var app githubAppResponse
	if err := json.NewDecoder(response.Body).Decode(&app); err != nil {
		return githubAppResponse{}, githubBadGateway("Failed to decode GitHub App metadata")
	}
	return app, nil
}

func (handler *GitHubHandler) loadAuthenticatedGitHubApp(ctx context.Context, config provider.GitHubAppConfig) (githubAppResponse, error) {
	jwtToken, err := buildGitHubAppJWT(config)
	if err != nil {
		return githubAppResponse{}, githubBadRequest("Invalid GitHub App private key")
	}

	request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(handler.githubConfig.APIBaseURL, "/")+"/app", nil)
	if err != nil {
		return githubAppResponse{}, githubBadGateway("Failed to verify the GitHub App private key")
	}
	request.Header.Set("Accept", "application/vnd.github+json")
	request.Header.Set("Authorization", "Bearer "+jwtToken)

	response, err := handler.httpClient.Do(request)
	if err != nil {
		return githubAppResponse{}, githubBadGateway("Failed to verify the GitHub App private key")
	}
	defer response.Body.Close()

	switch response.StatusCode {
	case http.StatusOK:
	default:
		if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden || response.StatusCode == http.StatusNotFound {
			return githubAppResponse{}, githubBadRequest("The provided GitHub App private key does not match the GitHub App settings URL")
		}
		return githubAppResponse{}, githubBadGateway("Failed to verify the GitHub App private key")
	}

	var app githubAppResponse
	if err := json.NewDecoder(response.Body).Decode(&app); err != nil {
		return githubAppResponse{}, githubBadGateway("Failed to decode GitHub App metadata")
	}
	return app, nil
}

func generateGitHubWebhookSecret() (string, error) {
	buffer := make([]byte, 32)
	if _, err := rand.Read(buffer); err != nil {
		return "", fmt.Errorf("generate github webhook secret: %w", err)
	}
	return base64.RawURLEncoding.EncodeToString(buffer), nil
}

func (handler *GitHubHandler) updateGitHubAppWebhookConfig(ctx context.Context, config provider.GitHubAppConfig) error {
	jwtToken, err := buildGitHubAppJWT(config)
	if err != nil {
		return githubBadRequest("Invalid GitHub App private key")
	}

	payloadJSON, err := json.Marshal(map[string]string{
		"url":          handler.githubWebhookURL(),
		"content_type": "json",
		"secret":       config.WebhookSecret,
	})
	if err != nil {
		return githubBadGateway("Failed to prepare the GitHub App webhook update")
	}

	request, err := http.NewRequestWithContext(ctx, http.MethodPatch, strings.TrimRight(handler.githubConfig.APIBaseURL, "/")+"/app/hook/config", strings.NewReader(string(payloadJSON)))
	if err != nil {
		return githubBadGateway("Failed to prepare the GitHub App webhook update")
	}
	request.Header.Set("Accept", "application/vnd.github+json")
	request.Header.Set("Authorization", "Bearer "+jwtToken)
	request.Header.Set("Content-Type", "application/json")

	response, err := handler.httpClient.Do(request)
	if err != nil {
		return githubBadGateway("Failed to update the GitHub App webhook")
	}
	defer response.Body.Close()

	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return githubBadGateway("Failed to update the GitHub App webhook")
	}
	return nil
}

func (handler *GitHubHandler) recoverGitHubAppConfig(
	ctx context.Context,
	userID string,
	workspaceID string,
	settingsURL string,
	privateKeyPEM string,
	setupMode string,
) (githubVerifiedSetupResult, error) {
	switch strings.TrimSpace(setupMode) {
	case app.GitHubAppSetupModeAdmin:
	case app.GitHubAppSetupModeRequester:
		return githubVerifiedSetupResult{}, fmt.Errorf("requester GitHub recovery must use a setup proposal without webhook mutation")
	default:
		return githubVerifiedSetupResult{}, fmt.Errorf("unsupported github setup mode %q", setupMode)
	}
	if handler.adminStore == nil {
		return githubVerifiedSetupResult{}, fmt.Errorf("github setup store is not configured")
	}
	// Resume the exact owner-bound lease before current-role gates or provider
	// credential verification. Begin already authorized this operation; forcing
	// a downgraded/promoted caller through new-operation admission could strand
	// a provider PATCH and its only matching encrypted webhook secret.
	resumedConfig, resumedLease, resumed, err := handler.adminStore.ResumeGitHubSetupVerificationForUser(
		ctx, userID, workspaceID, setupMode,
	)
	if err != nil {
		return githubVerifiedSetupResult{}, err
	}
	if resumed {
		return handler.finishGitHubAppRecovery(ctx, userID, workspaceID, setupMode, resumedConfig, resumedLease)
	}

	currentMode, err := handler.githubAppSetupModeForUser(ctx, userID, workspaceID)
	if err != nil {
		return githubVerifiedSetupResult{}, err
	}
	if strings.TrimSpace(currentMode) != strings.TrimSpace(setupMode) {
		if strings.TrimSpace(setupMode) == app.GitHubAppSetupModeRequester {
			return githubVerifiedSetupResult{}, githubForbidden("Workspace administrators must configure GitHub directly")
		}
		return githubVerifiedSetupResult{}, githubForbidden("Workspace administrators must configure GitHub")
	}
	if strings.TrimSpace(setupMode) == app.GitHubAppSetupModeAdmin {
		// Config may legitimately be missing, so authorize against the active
		// installation. Begin repeats this check transactionally after provider
		// verification and before any webhook mutation.
		if err := handler.store.AuthorizeGitHubAppInstallForUser(ctx, userID, workspaceID); err != nil {
			return githubVerifiedSetupResult{}, err
		}
	}
	provisionalConfig, err := handler.verifyGitHubAppRecovery(ctx, settingsURL, privateKeyPEM)
	if err != nil {
		return githubVerifiedSetupResult{}, err
	}

	verificationInput := app.GitHubSetupVerificationInput{
		Mode:                      setupMode,
		RequireActiveInstallation: strings.TrimSpace(setupMode) == app.GitHubAppSetupModeAdmin,
		Config: provider.UpsertGitHubAppConfigParams{
			AppID:         provisionalConfig.AppID,
			AppSlug:       provisionalConfig.AppSlug,
			AppName:       provisionalConfig.AppName,
			HTMLURL:       provisionalConfig.HTMLURL,
			ClientID:      provisionalConfig.ClientID,
			ClientSecret:  provisionalConfig.ClientSecret,
			PrivateKeyPEM: provisionalConfig.PrivateKeyPEM,
			WebhookSecret: provisionalConfig.WebhookSecret,
			OwnerLogin:    provisionalConfig.OwnerLogin,
			OwnerID:       provisionalConfig.OwnerID,
		},
	}
	if strings.TrimSpace(setupMode) == app.GitHubAppSetupModeAdmin {
		activeConfig, found, err := handler.store.LoadGitHubAppConfigForWorkspace(ctx, workspaceID)
		if err != nil {
			return githubVerifiedSetupResult{}, err
		}
		if found {
			verificationInput.ExpectedConfigID = activeConfig.ID
			verificationInput.ExpectedConfigUpdatedAt = activeConfig.UpdatedAt
			if activeConfig.AppID == provisionalConfig.AppID && strings.TrimSpace(activeConfig.WebhookSecret) != "" {
				// Updating the same active App must not rotate its webhook secret
				// before the corresponding local config CAS can commit.
				verificationInput.Config.WebhookSecret = activeConfig.WebhookSecret
			}
		}
	}

	lease, err := handler.adminStore.BeginGitHubAppSetupVerificationForUser(ctx, userID, workspaceID, verificationInput)
	if err != nil {
		return githubVerifiedSetupResult{}, err
	}
	return handler.finishGitHubAppRecovery(ctx, userID, workspaceID, setupMode, provisionalConfig, lease)
}

func (handler *GitHubHandler) finishGitHubAppRecovery(
	ctx context.Context,
	userID string,
	workspaceID string,
	setupMode string,
	provisionalConfig provider.GitHubAppConfig,
	lease app.GitHubSetupVerificationLease,
) (githubVerifiedSetupResult, error) {
	if strings.TrimSpace(setupMode) != app.GitHubAppSetupModeAdmin {
		return githubVerifiedSetupResult{}, fmt.Errorf("requester GitHub recovery must use a setup proposal without webhook mutation")
	}
	leaseFinished := false
	providerMutationStarted := false
	defer func() {
		if leaseFinished || providerMutationStarted {
			return
		}
		abortCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
		defer cancel()
		_ = handler.adminStore.AbortGitHubSetupVerificationForUser(abortCtx, userID, workspaceID, lease.ID)
	}()
	provisionalConfig.WebhookSecret = lease.WebhookSecret
	if err := handler.adminStore.MarkGitHubSetupVerificationProviderMutationStartedForUser(
		ctx, userID, workspaceID, lease.ID,
	); err != nil {
		return githubVerifiedSetupResult{}, err
	}
	providerMutationStarted = true
	patchCtx, cancelPatch := context.WithTimeout(ctx, githubWebhookPatchTimeout)
	err := handler.updateGitHubAppWebhookConfig(patchCtx, provisionalConfig)
	cancelPatch()
	if err != nil {
		return githubVerifiedSetupResult{}, err
	}

	result := githubVerifiedSetupResult{mode: strings.TrimSpace(setupMode)}
	result.config, err = handler.adminStore.FinalizeGitHubSetupVerificationForWorkspaceAdmin(ctx, userID, workspaceID, lease.ID)
	if err != nil {
		return githubVerifiedSetupResult{}, err
	}
	leaseFinished = true
	// Credential recovery only verifies and stores the GitHub App itself. It
	// must not enumerate or replay provider installations into AgentTeam: every
	// additional connection is activated by the explicit installation flow.
	return result, nil
}

// verifyGitHubAppRecovery validates a settings URL/private-key pair against
// both GitHub's public App metadata and the authenticated App endpoint. It has
// no provider mutation or AgentTeam activation side effect, which lets the fresh
// administrator setup flow discover installations before explicit final
// confirmation.
func (handler *GitHubHandler) verifyGitHubAppRecovery(
	ctx context.Context,
	settingsURL string,
	privateKeyPEM string,
) (provider.GitHubAppConfig, error) {
	locator, err := handler.parseGitHubAppSettingsURL(settingsURL)
	if err != nil {
		return provider.GitHubAppConfig{}, err
	}

	trimmedPrivateKey := strings.TrimSpace(privateKeyPEM)
	if trimmedPrivateKey == "" {
		return provider.GitHubAppConfig{}, githubBadRequest("GitHub App private key is required")
	}
	if _, err := buildGitHubAppJWT(provider.GitHubAppConfig{PrivateKeyPEM: trimmedPrivateKey}); err != nil {
		return provider.GitHubAppConfig{}, githubBadRequest("Invalid GitHub App private key")
	}

	publicApp, err := handler.loadGitHubAppBySlug(ctx, locator.Slug)
	if err != nil {
		return provider.GitHubAppConfig{}, err
	}
	if locator.ExpectedOwnerLogin != "" && !strings.EqualFold(locator.ExpectedOwnerLogin, publicApp.Owner.Login) {
		return provider.GitHubAppConfig{}, githubBadRequest("GitHub App settings URL does not match the resolved GitHub App owner")
	}

	provisionalConfig := provider.GitHubAppConfig{
		AppID:         publicApp.ID,
		AppSlug:       publicApp.Slug,
		AppName:       publicApp.Name,
		HTMLURL:       publicApp.HTMLURL,
		ClientID:      publicApp.ClientID,
		PrivateKeyPEM: trimmedPrivateKey,
		OwnerLogin:    publicApp.Owner.Login,
		OwnerID:       publicApp.Owner.ID,
	}

	authenticatedApp, err := handler.loadAuthenticatedGitHubApp(ctx, provisionalConfig)
	if err != nil {
		return provider.GitHubAppConfig{}, err
	}
	if authenticatedApp.ID != provisionalConfig.AppID || !strings.EqualFold(authenticatedApp.Slug, provisionalConfig.AppSlug) {
		return provider.GitHubAppConfig{}, githubBadRequest("The provided GitHub App private key does not match the GitHub App settings URL")
	}

	if strings.TrimSpace(authenticatedApp.ClientID) != "" {
		provisionalConfig.ClientID = authenticatedApp.ClientID
	}
	if strings.TrimSpace(authenticatedApp.Name) != "" {
		provisionalConfig.AppName = authenticatedApp.Name
	}
	if strings.TrimSpace(authenticatedApp.HTMLURL) != "" {
		provisionalConfig.HTMLURL = authenticatedApp.HTMLURL
	}
	if strings.TrimSpace(authenticatedApp.Owner.Login) != "" {
		provisionalConfig.OwnerLogin = authenticatedApp.Owner.Login
		provisionalConfig.OwnerID = authenticatedApp.Owner.ID
	}

	webhookSecret, err := generateGitHubWebhookSecret()
	if err != nil {
		return provider.GitHubAppConfig{}, githubBadGateway("Failed to generate a new GitHub App webhook secret")
	}
	provisionalConfig.WebhookSecret = webhookSecret
	return provisionalConfig, nil
}

func buildGitHubAppJWT(config provider.GitHubAppConfig) (string, error) {
	privateKey, err := parseGitHubRSAPrivateKey(config.PrivateKeyPEM)
	if err != nil {
		return "", err
	}

	now := time.Now().UTC()
	headerJSON, err := json.Marshal(map[string]string{
		"alg": "RS256",
		"typ": "JWT",
	})
	if err != nil {
		return "", fmt.Errorf("encode github jwt header: %w", err)
	}
	payloadJSON, err := json.Marshal(map[string]any{
		"iat": now.Add(-time.Minute).Unix(),
		"exp": now.Add(9 * time.Minute).Unix(),
		"iss": githubJWTIssuer(config),
	})
	if err != nil {
		return "", fmt.Errorf("encode github jwt payload: %w", err)
	}

	header := base64.RawURLEncoding.EncodeToString(headerJSON)
	payload := base64.RawURLEncoding.EncodeToString(payloadJSON)
	signingInput := header + "." + payload

	sum := sha256.Sum256([]byte(signingInput))
	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, sum[:])
	if err != nil {
		return "", fmt.Errorf("sign github jwt: %w", err)
	}

	return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature), nil
}

func parseGitHubRSAPrivateKey(encoded string) (*rsa.PrivateKey, error) {
	block, _ := pem.Decode([]byte(strings.TrimSpace(encoded)))
	if block == nil {
		return nil, fmt.Errorf("decode github private key PEM")
	}

	if privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
		return privateKey, nil
	}
	parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("parse github private key: %w", err)
	}
	privateKey, ok := parsedKey.(*rsa.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("github private key is not RSA")
	}
	return privateKey, nil
}

func githubJWTIssuer(config provider.GitHubAppConfig) string {
	if clientID := strings.TrimSpace(config.ClientID); clientID != "" {
		return clientID
	}
	return strconv.FormatInt(config.AppID, 10)
}
