package app

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

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

const githubConnectionAPIRequestTimeout = 10 * time.Second

// Keep these approval-time requirements aligned with the GitHub manifest in
// internal/api/provider. Approval is a separate trust boundary: callback-time
// eligibility may have changed while a request was waiting for an admin.
var githubConnectionRequiredPermissions = map[string]string{
	"pull_requests": "read",
	"issues":        "read",
	"metadata":      "read",
}

var githubConnectionRequiredEvents = []string{
	"pull_request",
	"pull_request_review",
	"pull_request_review_comment",
	"issue_comment",
}

type verifiedGitHubConnectionTarget struct {
	ExternalSubjectRef string
	DisplayLabel       string
	AccountID          int64
}

// verifyGitHubConnectionInstallation uses the current workspace GitHub App
// credentials to prove that the installation still belongs to that App. It is
// deliberately called again at approval time; a callback-time lookup is not a
// durable authorization for a later local connection write.
func (store *Store) verifyGitHubConnectionInstallation(
	ctx context.Context,
	config provider.GitHubAppConfig,
	installationRef string,
) (verifiedGitHubConnectionTarget, error) {
	trimmedRef := strings.TrimSpace(installationRef)
	parsedRef, err := strconv.ParseInt(trimmedRef, 10, 64)
	if err != nil || parsedRef <= 0 || strconv.FormatInt(parsedRef, 10) != trimmedRef {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation is invalid. Submit a new connection request.")
	}
	jwtToken, err := buildGitHubAppJWTForStore(config)
	if err != nil {
		return verifiedGitHubConnectionTarget{}, fmt.Errorf("build github app jwt for connection verification: %w", err)
	}
	baseURL := strings.TrimRight(strings.TrimSpace(store.githubAPIBaseURL), "/")
	if baseURL == "" {
		baseURL = "https://api.github.com"
	}
	requestCtx, cancel := context.WithTimeout(ctx, githubConnectionAPIRequestTimeout)
	defer cancel()
	request, err := http.NewRequestWithContext(
		requestCtx,
		http.MethodGet,
		baseURL+"/app/installations/"+url.PathEscape(trimmedRef),
		nil,
	)
	if err != nil {
		return verifiedGitHubConnectionTarget{}, fmt.Errorf("build github installation verification 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 verifiedGitHubConnectionTarget{}, fmt.Errorf("verify github installation: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode == http.StatusNotFound {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation is no longer available. Submit a new connection request.")
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 4096))
		return verifiedGitHubConnectionTarget{}, fmt.Errorf("verify github installation: upstream status %d", response.StatusCode)
	}
	payload, err := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1))
	if err != nil {
		return verifiedGitHubConnectionTarget{}, fmt.Errorf("read github installation verification response: %w", err)
	}
	if len(payload) > 1<<20 {
		return verifiedGitHubConnectionTarget{}, fmt.Errorf("github installation verification response exceeds 1048576 bytes")
	}
	var body struct {
		ID          int64             `json:"id"`
		AppID       int64             `json:"app_id"`
		AppSlug     string            `json:"app_slug"`
		TargetType  string            `json:"target_type"`
		SuspendedAt *time.Time        `json:"suspended_at"`
		Permissions map[string]string `json:"permissions"`
		Events      []string          `json:"events"`
		Account     struct {
			Login string `json:"login"`
			ID    int64  `json:"id"`
		} `json:"account"`
	}
	// json.Unmarshal requires one complete JSON value and therefore rejects
	// malformed or trailing documents instead of silently accepting the first.
	if err := json.Unmarshal(payload, &body); err != nil {
		return verifiedGitHubConnectionTarget{}, fmt.Errorf("decode github installation verification response: %w", err)
	}
	if body.ID != parsedRef || body.AppID != config.AppID {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation does not belong to the configured App. Submit a new connection request.")
	}
	if expectedSlug := strings.TrimSpace(config.AppSlug); expectedSlug != "" {
		actualSlug := strings.TrimSpace(body.AppSlug)
		if actualSlug == "" || !strings.EqualFold(actualSlug, expectedSlug) {
			return verifiedGitHubConnectionTarget{}, conflict("GitHub installation does not belong to the configured App. Submit a new connection request.")
		}
	}
	if !strings.EqualFold(strings.TrimSpace(body.TargetType), "Organization") {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation is not an organization installation. Submit a new connection request.")
	}
	if body.SuspendedAt != nil {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation is suspended. Submit a new connection request.")
	}
	if !githubConnectionInstallationMeetsRequirements(body.Permissions, body.Events) {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation permissions changed. Submit a new connection request.")
	}
	label := strings.TrimSpace(body.Account.Login)
	if label == "" {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation owner is unavailable. Submit a new connection request.")
	}
	if body.Account.ID <= 0 {
		return verifiedGitHubConnectionTarget{}, conflict("GitHub installation owner is unavailable. Submit a new connection request.")
	}
	return verifiedGitHubConnectionTarget{
		ExternalSubjectRef: trimmedRef,
		DisplayLabel:       label,
		AccountID:          body.Account.ID,
	}, nil
}

func githubConnectionInstallationMeetsRequirements(permissions map[string]string, events []string) bool {
	for permission, requiredLevel := range githubConnectionRequiredPermissions {
		if githubConnectionPermissionLevel(permissions[permission]) < githubConnectionPermissionLevel(requiredLevel) {
			return false
		}
	}
	actualEvents := make(map[string]struct{}, len(events))
	for _, eventType := range events {
		actualEvents[strings.TrimSpace(eventType)] = struct{}{}
	}
	for _, eventType := range githubConnectionRequiredEvents {
		if _, ok := actualEvents[eventType]; !ok {
			return false
		}
	}
	return true
}

func githubConnectionPermissionLevel(value string) int {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "read":
		return 1
	case "write":
		return 2
	case "admin":
		return 3
	default:
		return 0
	}
}
