package app

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

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

func (store *Store) listGitHubPullRequestResourcesForCommit(
	ctx context.Context,
	installation webhookAppInstallation,
	repositoryFullName string,
	commitSHA string,
) ([]githubPullRequestResource, error) {
	config, found, err := store.providerStore().LoadGitHubAppConfigForWorkspace(ctx, installation.WorkspaceID)
	if err != nil {
		return nil, err
	}
	if !found {
		return nil, nil
	}

	installationRef := strings.TrimSpace(installation.ExternalSubjectRef)
	if installationRef == "" {
		return nil, nil
	}

	token, err := store.createGitHubInstallationAccessToken(ctx, config, installationRef)
	if err != nil {
		return nil, err
	}

	requestURL := fmt.Sprintf(
		"%s/repos/%s/commits/%s/pulls",
		strings.TrimRight(store.githubAPIBaseURL, "/"),
		url.PathEscape(strings.ToLower(strings.TrimSpace(repositoryFullName))),
		url.PathEscape(strings.TrimSpace(commitSHA)),
	)
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
	if err != nil {
		return nil, fmt.Errorf("build github commit pull request lookup request: %w", err)
	}
	request.Header.Set("Accept", "application/vnd.github+json")
	request.Header.Set("Authorization", "Bearer "+token)

	response, err := store.appHTTPClient.Do(request)
	if err != nil {
		return nil, fmt.Errorf("lookup github pull requests for commit: %w", err)
	}
	defer response.Body.Close()

	if response.StatusCode == http.StatusNotFound {
		return nil, nil
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
		return nil, fmt.Errorf("github commit pull request lookup failed: status=%d body=%s", response.StatusCode, strings.TrimSpace(string(body)))
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, fmt.Errorf("read github commit pull request response: %w", err)
	}
	var items []model.GitHubCommitPullRequest
	if err := model.UnmarshalJSONObject("github commit pull requests", body, &items); err != nil {
		return nil, fmt.Errorf("decode github commit pull requests: %w", err)
	}

	resources := make([]githubPullRequestResource, 0, len(items))
	for _, item := range items {
		resource, ok := githubPullRequestResourceFromNumber(repositoryFullName, item.Number.String(), item.HTMLURL)
		if !ok {
			continue
		}
		resources = append(resources, resource)
	}
	return resources, nil
}

func (store *Store) createGitHubInstallationAccessToken(ctx context.Context, config provider.GitHubAppConfig, installationRef string) (string, error) {
	jwtToken, err := buildGitHubAppJWTForStore(config)
	if err != nil {
		return "", fmt.Errorf("build github app jwt: %w", err)
	}

	requestURL := fmt.Sprintf(
		"%s/app/installations/%s/access_tokens",
		strings.TrimRight(store.githubAPIBaseURL, "/"),
		url.PathEscape(strings.TrimSpace(installationRef)),
	)
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, nil)
	if err != nil {
		return "", fmt.Errorf("build github installation access token request: %w", err)
	}
	request.Header.Set("Accept", "application/vnd.github+json")
	request.Header.Set("Authorization", "Bearer "+jwtToken)

	response, err := store.appHTTPClient.Do(request)
	if err != nil {
		return "", fmt.Errorf("request github installation access token: %w", err)
	}
	defer response.Body.Close()

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

	var tokenResponse model.GitHubInstallationTokenResponse
	body, err := io.ReadAll(response.Body)
	if err != nil {
		return "", fmt.Errorf("read github installation access token response: %w", err)
	}
	if err := model.UnmarshalJSONObject("github installation access token", body, &tokenResponse); err != nil {
		return "", fmt.Errorf("decode github installation access token: %w", err)
	}
	if strings.TrimSpace(tokenResponse.Token) == "" {
		return "", fmt.Errorf("github installation access token response missing token")
	}
	return strings.TrimSpace(tokenResponse.Token), nil
}

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

	now := time.Now().UTC()
	headerJSON, err := model.MarshalJSONObject("github app jwt header", model.GitHubAppJWTHeader{
		Algorithm: "RS256",
		Type:      "JWT",
	})
	if err != nil {
		return "", fmt.Errorf("encode github jwt header: %w", err)
	}
	payloadJSON, err := model.MarshalJSONObject("github app jwt payload", model.GitHubAppJWTClaims{
		IssuedAt:  now.Add(-time.Minute).Unix(),
		ExpiresAt: now.Add(9 * time.Minute).Unix(),
		Issuer:    githubJWTIssuerForStore(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 parseGitHubRSAPrivateKeyForStore(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 githubJWTIssuerForStore(config provider.GitHubAppConfig) string {
	if clientID := strings.TrimSpace(config.ClientID); clientID != "" {
		return clientID
	}
	return strconv.FormatInt(config.AppID, 10)
}
