package provider

import (
	"context"
	"errors"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
)

const githubDirectConnectionTargetsMax = 1

// GitHubVerifiedConnectionTarget is an internal, server-derived proof of one
// exact GitHub installation lookup. HTTP clients supply installation ids only;
// the API fills the immutable account id and display label from GitHub before
// passing this value to ApplyVerifiedGitHubConnectionsForUser.
type GitHubVerifiedConnectionTarget struct {
	InstallationID string
	DisplayLabel   string
	AccountID      int64
}

// GitHubConnectionsMutationBase is an opaque optimistic-concurrency token for
// one active-App, single-installation connection mutation. The plural type
// name is retained at the internal port boundary only because the HTTP request
// accepts the legacy scalar field or a one-element installation_ids field.
type GitHubConnectionsMutationBase struct {
	workspaceID             string
	memberID                string
	externalRef             string
	configID                string
	configAppID             int64
	configUpdatedAt         time.Time
	installation            githubInstallationMutationSnapshot
	connectionAlreadyActive bool
	connection              githubConnectionMutationSnapshot
}

// PrepareGitHubConnectionsForUser authorizes any active human workspace member
// against an already-active GitHub App/config/claim and captures the exact
// expected target state before provider I/O. It does not reuse the admin
// install authorization because doing so would accidentally grant members the
// unrelated config/delete capabilities guarded by that authorization.
func (store *Store) PrepareGitHubConnectionsForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	installationIDs []string,
) (GitHubAppConfig, []string, GitHubConnectionsMutationBase, error) {
	canonicalIDs, err := canonicalGitHubConnectionInstallationIDs(installationIDs)
	if err != nil {
		return GitHubAppConfig{}, nil, GitHubConnectionsMutationBase{}, err
	}
	trimmedUserID := strings.TrimSpace(userID)
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	expectedMember, err := loadActiveGitHubConnectionMember(
		ctx, store.db, trimmedUserID, trimmedWorkspaceID, false, "",
	)
	if err != nil {
		return GitHubAppConfig{}, nil, GitHubConnectionsMutationBase{}, err
	}

	var config GitHubAppConfig
	var base GitHubConnectionsMutationBase
	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		// Canonical order: definition -> caller membership -> App installation
		// -> config/claim -> target row.
		if err := fenceGitHubProviderMutationInTx(ctx, tx, trimmedWorkspaceID); err != nil {
			return err
		}
		member, err := loadActiveGitHubConnectionMember(
			ctx, tx, trimmedUserID, trimmedWorkspaceID, true, expectedMember.Member.ID,
		)
		if err != nil {
			return err
		}
		installation, err := loadEnabledGitHubInstallationForUpdate(ctx, tx, trimmedWorkspaceID)
		if err != nil {
			return err
		}
		var found bool
		config, found, err = store.loadGitHubAppConfigForWorkspace(ctx, tx, trimmedWorkspaceID, true)
		if err != nil {
			return err
		}
		if !found {
			return badRequest("GitHub App is not registered.")
		}
		if err := requireProviderActiveGitHubAppClaimForUpdate(
			ctx, tx, config.AppID, trimmedWorkspaceID,
		); err != nil {
			return err
		}
		connection, connected, err := loadActiveGitHubConnectionByExternalRefForUpdate(
			ctx, tx, installation.id, canonicalIDs[0],
		)
		if err != nil {
			return err
		}
		base = GitHubConnectionsMutationBase{
			workspaceID:             trimmedWorkspaceID,
			memberID:                strings.TrimSpace(member.Member.ID),
			externalRef:             canonicalIDs[0],
			configID:                strings.TrimSpace(config.ID),
			configAppID:             config.AppID,
			configUpdatedAt:         config.UpdatedAt,
			installation:            installation,
			connectionAlreadyActive: connected,
			connection:              connection,
		}
		return nil
	}); err != nil {
		return GitHubAppConfig{}, nil, GitHubConnectionsMutationBase{}, err
	}
	return config, append([]string(nil), canonicalIDs...), base, nil
}

// ApplyVerifiedGitHubConnectionsForUser applies the one exact, server-verified
// target. Every local CAS check happens before the upsert/unblock pair.
func (store *Store) ApplyVerifiedGitHubConnectionsForUser(
	ctx context.Context,
	userID string,
	workspaceID string,
	targets []GitHubVerifiedConnectionTarget,
	base GitHubConnectionsMutationBase,
) (AppInstallationView, error) {
	canonicalTargets, err := canonicalVerifiedGitHubConnectionTargets(targets)
	if err != nil {
		return AppInstallationView{}, err
	}
	targetIDs := make([]string, 0, len(canonicalTargets))
	for _, target := range canonicalTargets {
		targetIDs = append(targetIDs, target.InstallationID)
	}
	trimmedUserID := strings.TrimSpace(userID)
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	if !base.validForConnectRequest(trimmedWorkspaceID, targetIDs[0]) {
		return AppInstallationView{}, conflict("GitHub connection selection changed. Restart installation.")
	}
	expectedMember, err := loadActiveGitHubConnectionMember(
		ctx, store.db, trimmedUserID, trimmedWorkspaceID, false, base.memberID,
	)
	if err != nil {
		return AppInstallationView{}, err
	}

	if err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := fenceGitHubProviderMutationInTx(ctx, tx, trimmedWorkspaceID); err != nil {
			return err
		}
		if _, err := loadActiveGitHubConnectionMember(
			ctx, tx, trimmedUserID, trimmedWorkspaceID, true, expectedMember.Member.ID,
		); err != nil {
			return err
		}
		installation, err := loadEnabledGitHubInstallationForUpdate(ctx, tx, trimmedWorkspaceID)
		if err != nil {
			return err
		}
		if !githubInstallationMutationSnapshotsEqual(base.installation, installation) {
			return conflict("GitHub App installation changed. Restart installation.")
		}
		config, found, err := store.loadGitHubAppConfigForWorkspace(ctx, tx, trimmedWorkspaceID, true)
		if err != nil {
			return err
		}
		if !found || strings.TrimSpace(config.ID) != base.configID ||
			config.AppID != base.configAppID || !config.UpdatedAt.Equal(base.configUpdatedAt) {
			return conflict("GitHub configuration changed. Restart installation.")
		}
		if err := requireProviderActiveGitHubAppClaimForUpdate(
			ctx, tx, config.AppID, trimmedWorkspaceID,
		); err != nil {
			return err
		}

		target := canonicalTargets[0]
		// Do not call the upsert helper before validating the target row: that
		// helper intentionally supports reactivation and would otherwise
		// overwrite a concurrent winner.
		currentConnection, connected, err := loadActiveGitHubConnectionByExternalRefForUpdate(
			ctx, tx, installation.id, target.InstallationID,
		)
		if err != nil {
			return err
		}
		if base.connectionAlreadyActive {
			if !connected {
				return conflict("GitHub connection state changed. Restart installation.")
			}
			if !githubConnectionMutationSnapshotsEqual(base.connection, currentConnection) {
				return conflict("GitHub connection state changed. Restart installation.")
			}
			// Desired state was already satisfied at Prepare and is unchanged.
			return nil
		}
		if connected {
			// A concurrent identical request (or a lost response followed by a
			// retry) reached the desired state. Do not upsert/overwrite;
			// converge by returning the authoritative current installation view.
			return nil
		}

		now := time.Now().UTC()
		if _, err := store.upsertAppConnectionInTx(ctx, tx, UpsertAppConnectionParams{
			InstallationID:     installation.id,
			ExternalSubjectRef: target.InstallationID,
			DisplayLabel:       target.DisplayLabel,
			Credentials: map[string]any{
				"webhook_secret": config.WebhookSecret,
			},
			CreatedByUserID: trimmedUserID,
		}); err != nil {
			return err
		}
		if err := store.setGitHubSubscriptionsBlockedInTx(
			ctx, tx, trimmedWorkspaceID, target.InstallationID, false, now,
		); err != nil {
			return err
		}
		return nil
	}); err != nil {
		return AppInstallationView{}, err
	}
	readCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
	defer cancel()
	return store.appInstallationViewByID(readCtx, trimmedWorkspaceID, base.installation.id)
}

func canonicalGitHubConnectionInstallationIDs(values []string) ([]string, error) {
	if len(values) != githubDirectConnectionTargetsMax {
		return nil, badRequest("Exactly one GitHub installation id is required.")
	}
	type parsedTarget struct {
		id    int64
		value string
	}
	parsed := make([]parsedTarget, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		id, err := strconv.ParseInt(trimmed, 10, 64)
		if value != trimmed || err != nil || id <= 0 || strconv.FormatInt(id, 10) != trimmed {
			return nil, badRequest("GitHub installation ids must be canonical positive decimal strings.")
		}
		if _, duplicate := seen[trimmed]; duplicate {
			return nil, badRequest("GitHub installation ids must be unique.")
		}
		seen[trimmed] = struct{}{}
		parsed = append(parsed, parsedTarget{id: id, value: trimmed})
	}
	slices.SortFunc(parsed, func(left, right parsedTarget) int {
		switch {
		case left.id < right.id:
			return -1
		case left.id > right.id:
			return 1
		default:
			return 0
		}
	})
	result := make([]string, 0, len(parsed))
	for _, target := range parsed {
		result = append(result, target.value)
	}
	return result, nil
}

func canonicalVerifiedGitHubConnectionTargets(
	targets []GitHubVerifiedConnectionTarget,
) ([]GitHubVerifiedConnectionTarget, error) {
	ids := make([]string, 0, len(targets))
	byID := make(map[string]GitHubVerifiedConnectionTarget, len(targets))
	for _, target := range targets {
		id := strings.TrimSpace(target.InstallationID)
		label := strings.TrimSpace(target.DisplayLabel)
		if label == "" || target.AccountID <= 0 {
			return nil, conflict("GitHub installation identity is incomplete. Restart installation.")
		}
		ids = append(ids, id)
		target.InstallationID = id
		target.DisplayLabel = label
		byID[id] = target
	}
	canonicalIDs, err := canonicalGitHubConnectionInstallationIDs(ids)
	if err != nil {
		return nil, err
	}
	result := make([]GitHubVerifiedConnectionTarget, 0, len(canonicalIDs))
	for _, id := range canonicalIDs {
		result = append(result, byID[id])
	}
	return result, nil
}

func (base GitHubConnectionsMutationBase) validForConnectRequest(
	workspaceID string,
	targetID string,
) bool {
	return strings.TrimSpace(base.workspaceID) == strings.TrimSpace(workspaceID) &&
		strings.TrimSpace(base.memberID) != "" &&
		strings.TrimSpace(base.installation.id) != "" &&
		strings.TrimSpace(base.configID) != "" &&
		base.configAppID > 0 &&
		!base.configUpdatedAt.IsZero() &&
		base.externalRef == targetID
}

func loadActiveGitHubConnectionMember(
	ctx context.Context,
	queryer coredb.QueryRower,
	userID string,
	workspaceID string,
	forUpdate bool,
	expectedMemberID string,
) (access.HumanWorkspaceMember, error) {
	var (
		member access.HumanWorkspaceMember
		err    error
	)
	if forUpdate {
		member, err = access.LoadHumanWorkspaceMemberForUpdate(ctx, queryer, userID, workspaceID)
	} else {
		member, err = access.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
	}
	if err != nil {
		if errors.Is(err, access.ErrForbidden) || errors.Is(err, access.ErrNotFound) {
			return access.HumanWorkspaceMember{}, ErrForbidden
		}
		return access.HumanWorkspaceMember{}, err
	}
	if strings.TrimSpace(member.Member.ID) == "" ||
		strings.TrimSpace(member.Member.WorkspaceID) != strings.TrimSpace(workspaceID) ||
		member.Member.UserID == nil || strings.TrimSpace(*member.Member.UserID) != strings.TrimSpace(userID) {
		return access.HumanWorkspaceMember{}, ErrForbidden
	}
	if strings.TrimSpace(expectedMemberID) != "" &&
		strings.TrimSpace(member.Member.ID) != strings.TrimSpace(expectedMemberID) {
		return access.HumanWorkspaceMember{}, conflict("GitHub connection membership changed. Restart installation.")
	}
	return member, nil
}

func requireProviderActiveGitHubAppClaimForUpdate(
	ctx context.Context,
	tx coredb.Tx,
	appID int64,
	workspaceID string,
) error {
	claim, found, err := loadProviderGitHubAppClaimByAppIDForUpdate(ctx, tx, appID)
	if err != nil {
		return err
	}
	if !found || claim.appID != appID ||
		claim.workspaceID != strings.TrimSpace(workspaceID) ||
		claim.state != providerGitHubAppClaimStateActive || claim.token != "" {
		return conflict("GitHub App active ownership changed. Restart installation.")
	}
	return nil
}
