package provider

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

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

func (handler *GitHubHandler) listGitHubInstallations(ctx context.Context, config provider.GitHubAppConfig) ([]githubInstallationLookupResponse, error) {
	jwtToken, err := buildGitHubAppJWT(config)
	if err != nil {
		return nil, fmt.Errorf("build github app authentication: %w", err)
	}

	installations := make([]githubInstallationLookupResponse, 0)
	seenInstallationIDs := make(map[int64]struct{})
	// One extra page distinguishes an exact max-page result from a truncated
	// result without returning partial discovery data.
	for page := 1; page <= githubInstallationsMaxPages+1; page++ {
		request, err := http.NewRequestWithContext(
			ctx,
			http.MethodGet,
			fmt.Sprintf(
				"%s/app/installations?per_page=%d&page=%d",
				strings.TrimRight(handler.githubConfig.APIBaseURL, "/"),
				githubInstallationsPerPage,
				page,
			),
			nil,
		)
		if err != nil {
			return nil, fmt.Errorf("build github installations request: %w", err)
		}
		request.Header.Set("Accept", "application/vnd.github+json")
		request.Header.Set("Authorization", "Bearer "+jwtToken)

		response, err := handler.httpClient.Do(request)
		if err != nil {
			return nil, fmt.Errorf("load github installations: %w", err)
		}
		if response.StatusCode < 200 || response.StatusCode >= 300 {
			response.Body.Close()
			return nil, fmt.Errorf("github installations lookup failed: status=%d", response.StatusCode)
		}

		body, readErr := io.ReadAll(
			io.LimitReader(response.Body, githubInstallationsMaxBytes+1),
		)
		closeErr := response.Body.Close()
		if readErr != nil {
			return nil, fmt.Errorf("read github installations: %w", readErr)
		}
		if closeErr != nil {
			return nil, fmt.Errorf("close github installations response: %w", closeErr)
		}
		if len(body) > githubInstallationsMaxBytes {
			return nil, fmt.Errorf("github installations response exceeds %d bytes", githubInstallationsMaxBytes)
		}
		var pageInstallations []githubInstallationLookupResponse
		if err := json.Unmarshal(body, &pageInstallations); err != nil {
			return nil, fmt.Errorf("decode github installations: %w", err)
		}
		if len(pageInstallations) > githubInstallationsPerPage {
			return nil, fmt.Errorf("github installations page exceeds %d entries", githubInstallationsPerPage)
		}

		if page > githubInstallationsMaxPages {
			if len(pageInstallations) > 0 {
				return nil, fmt.Errorf("github app has more than %d installations", githubInstallationsMaxPages*githubInstallationsPerPage)
			}
			return installations, nil
		}
		if len(installations)+len(pageInstallations) > githubInstallationsMaxPages*githubInstallationsPerPage {
			return nil, fmt.Errorf("github app has more than %d installations", githubInstallationsMaxPages*githubInstallationsPerPage)
		}
		for _, installation := range pageInstallations {
			if installation.ID <= 0 || strings.TrimSpace(installation.TargetType) == "" {
				return nil, fmt.Errorf("github installation response is incomplete")
			}
			if _, duplicate := seenInstallationIDs[installation.ID]; duplicate {
				return nil, fmt.Errorf("github installation response contains duplicate id %d", installation.ID)
			}
			seenInstallationIDs[installation.ID] = struct{}{}
			// A GitHub App can also be installed on a personal account. That
			// unsupported candidate must not poison otherwise valid organization
			// discovery; exact selection remains fail-closed below.
			if !strings.EqualFold(strings.TrimSpace(installation.TargetType), "Organization") {
				continue
			}
			if strings.TrimSpace(installation.Account.Login) == "" || installation.Account.ID <= 0 {
				return nil, fmt.Errorf("github installation response is incomplete")
			}
			if err := validateGitHubInstallationForApp(config, installation); err != nil {
				return nil, err
			}
			if installation.SuspendedAt != nil {
				continue
			}
			// Discovery is an action list, not an inventory. Installations that
			// still need the GitHub owner to accept required permissions/events
			// cannot produce a complete AgentTeam connection, so do not offer them as
			// selectable candidates.
			if !githubInstallationMeetsManifestRequirements(installation) {
				continue
			}
			installations = append(installations, installation)
		}
		if len(pageInstallations) < githubInstallationsPerPage {
			return installations, nil
		}
	}

	return nil, fmt.Errorf("github installation discovery exceeded its page limit")
}

func (handler *GitHubHandler) loadGitHubInstallation(ctx context.Context, config provider.GitHubAppConfig, installationID string) (githubInstallationLookupResponse, error) {
	trimmedInstallationID := strings.TrimSpace(installationID)
	parsedInstallationID, err := strconv.ParseInt(trimmedInstallationID, 10, 64)
	if err != nil || parsedInstallationID <= 0 || strconv.FormatInt(parsedInstallationID, 10) != trimmedInstallationID {
		return githubInstallationLookupResponse{}, fmt.Errorf("github installation id is invalid")
	}
	jwtToken, err := buildGitHubAppJWT(config)
	if err != nil {
		return githubInstallationLookupResponse{}, err
	}

	request, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(handler.githubConfig.APIBaseURL, "/")+"/app/installations/"+url.PathEscape(trimmedInstallationID), nil)
	if err != nil {
		return githubInstallationLookupResponse{}, fmt.Errorf("build github installation request: %w", err)
	}
	request.Header.Set("Accept", "application/vnd.github+json")
	request.Header.Set("Authorization", "Bearer "+jwtToken)

	response, err := handler.httpClient.Do(request)
	if err != nil {
		return githubInstallationLookupResponse{}, fmt.Errorf("load github installation: %w", err)
	}
	defer response.Body.Close()

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

	body, err := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1))
	if err != nil {
		return githubInstallationLookupResponse{}, fmt.Errorf("read github installation: %w", err)
	}
	if len(body) > 1<<20 {
		return githubInstallationLookupResponse{}, fmt.Errorf("github installation response exceeds 1048576 bytes")
	}
	var installation githubInstallationLookupResponse
	if err := json.Unmarshal(body, &installation); err != nil {
		return githubInstallationLookupResponse{}, fmt.Errorf("decode github installation: %w", err)
	}
	if installation.ID != parsedInstallationID {
		return githubInstallationLookupResponse{}, fmt.Errorf("github installation id mismatch")
	}
	if err := validateGitHubInstallationForApp(config, installation); err != nil {
		return githubInstallationLookupResponse{}, err
	}
	if strings.TrimSpace(installation.Account.Login) == "" || installation.Account.ID <= 0 {
		return githubInstallationLookupResponse{}, fmt.Errorf("github installation owner is unavailable")
	}
	if !strings.EqualFold(strings.TrimSpace(installation.TargetType), "Organization") {
		return githubInstallationLookupResponse{}, fmt.Errorf("github installation is not an organization installation")
	}
	if installation.SuspendedAt != nil {
		return githubInstallationLookupResponse{}, fmt.Errorf("github installation is suspended")
	}
	return installation, nil
}

func validateGitHubInstallationForApp(config provider.GitHubAppConfig, installation githubInstallationLookupResponse) error {
	if installation.AppID != config.AppID {
		return fmt.Errorf("github installation app mismatch")
	}
	if expectedSlug := strings.TrimSpace(config.AppSlug); expectedSlug != "" {
		actualSlug := strings.TrimSpace(installation.AppSlug)
		if actualSlug == "" || !strings.EqualFold(actualSlug, expectedSlug) {
			return fmt.Errorf("github installation slug mismatch")
		}
	}
	return nil
}

func githubManifestPermissions() map[string]string {
	cloned := make(map[string]string, len(githubManifestDefaultPermissions))
	for permission, level := range githubManifestDefaultPermissions {
		cloned[permission] = level
	}
	return cloned
}

func githubManifestEvents() []string {
	return append([]string(nil), githubManifestDefaultEvents...)
}

func githubInstallationUpgradeRequirements(installation githubInstallationLookupResponse) githubInstallationUpgradeStatus {
	actualPermissions := make(map[string]string, len(installation.Permissions))
	for permission, level := range installation.Permissions {
		actualPermissions[strings.TrimSpace(permission)] = strings.TrimSpace(level)
	}

	missingPermissions := make([]string, 0)
	for permission, expectedLevel := range githubManifestDefaultPermissions {
		actualLevel := actualPermissions[permission]
		if githubPermissionLevel(actualLevel) < githubPermissionLevel(expectedLevel) {
			missingPermissions = append(missingPermissions, permission)
		}
	}
	slices.Sort(missingPermissions)

	actualEvents := make(map[string]struct{}, len(installation.Events))
	for _, eventType := range installation.Events {
		trimmed := strings.TrimSpace(eventType)
		if trimmed == "" {
			continue
		}
		actualEvents[trimmed] = struct{}{}
	}

	missingEvents := make([]string, 0)
	for _, eventType := range githubManifestDefaultEvents {
		if _, ok := actualEvents[eventType]; ok {
			continue
		}
		missingEvents = append(missingEvents, eventType)
	}

	return githubInstallationUpgradeStatus{
		MissingPermissions: missingPermissions,
		MissingEvents:      missingEvents,
	}
}

func githubInstallationMeetsManifestRequirements(installation githubInstallationLookupResponse) bool {
	upgrade := githubInstallationUpgradeRequirements(installation)
	return len(upgrade.MissingPermissions) == 0 && len(upgrade.MissingEvents) == 0
}

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