package mspmembersync

import (
	"context"
	"errors"
	"fmt"
	"io"
	"sort"
	"strings"

	"gorm.io/gorm"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/utils/container/set"
	"github.com/tidbcloud/aisupport/services/jsm-syncer/models"
)

// JSMClient is the subset of Atlassian organization APIs used by this repair.
type JSMClient interface {
	ListOrganizationUserAccountIDs(ctx context.Context, organizationID string) (map[string]struct{}, error)
	AddUsersToOrganization(ctx context.Context, organizationID string, accountIDs []string) error
}

type Options struct {
	Execute       bool
	CustomerOrgID string
	Output        io.Writer
}

type Summary struct {
	CustomerOrgsScanned       int
	CustomerOrgsSucceeded     int
	CustomerOrgsFailed        int
	CustomerOrgsMissingJSM    int
	MSPMembersScanned         int
	MSPMembersMissingAccount  int
	MSPMembersAlreadyPresent  int
	MSPMemberAdditionsPlanned int
	MSPMembersAdded           int
}

type customerOrganization struct {
	ID       string  `gorm:"column:id"`
	JSMOrgID *string `gorm:"column:jsm_org_id"`
	MSPOrgID *string `gorm:"column:msp_org_id"`
}

type mspMember struct {
	UserID             string  `gorm:"column:user_id"`
	Email              string  `gorm:"column:email"`
	AtlassianAccountID *string `gorm:"column:atlassian_account_id"`
}

// Run adds active MSP-provider members to every related customer's existing
// JSM organization. It is intentionally additive: it never removes a JSM
// organization member and it never creates a missing JSM organization.
func Run(ctx context.Context, db *gorm.DB, jsm JSMClient, options Options) (Summary, error) {
	if options.Output == nil {
		options.Output = io.Discard
	}

	organizations, err := loadCustomerOrganizations(ctx, db, options.CustomerOrgID)
	if err != nil {
		return Summary{}, err
	}

	summary := Summary{CustomerOrgsScanned: len(organizations)}
	fmt.Fprintf(options.Output, "found %d customer organizations with an MSP relation\n", len(organizations))

	var runErrors []error
	for _, org := range organizations {
		if err := ctx.Err(); err != nil {
			return summary, errors.Join(append(runErrors, err)...)
		}

		if err := processCustomerOrganization(ctx, db, jsm, org, options, &summary); err != nil {
			summary.CustomerOrgsFailed++
			fmt.Fprintf(options.Output, "ERROR customer_org=%s: %v\n", org.ID, err)
			runErrors = append(runErrors, fmt.Errorf("customer organization %s: %w", org.ID, err))
			continue
		}
		summary.CustomerOrgsSucceeded++
	}

	return summary, errors.Join(runErrors...)
}

func loadCustomerOrganizations(ctx context.Context, db *gorm.DB, customerOrgID string) ([]customerOrganization, error) {
	var organizations []customerOrganization
	query := db.WithContext(ctx).
		Table("tidbcloud_orgs").
		Select("id, jsm_org_id, msp_org_id").
		Where("msp_org_id IS NOT NULL AND TRIM(msp_org_id) <> ''")
	if customerOrgID = strings.TrimSpace(customerOrgID); customerOrgID != "" {
		query = query.Where("id = ?", customerOrgID)
	}
	if err := query.Order("id ASC").Scan(&organizations).Error; err != nil {
		return nil, fmt.Errorf("scan customer organizations with MSP relations: %w", err)
	}
	return organizations, nil
}

func processCustomerOrganization(
	ctx context.Context,
	db *gorm.DB,
	jsm JSMClient,
	org customerOrganization,
	options Options,
	summary *Summary,
) error {
	mspOrgID := stringValue(org.MSPOrgID)
	jsmOrgID := stringValue(org.JSMOrgID)
	if jsmOrgID == "" {
		summary.CustomerOrgsMissingJSM++
		fmt.Fprintf(
			options.Output,
			"skip customer_org=%s msp_org=%s: jsm_org_id is empty\n",
			org.ID,
			mspOrgID,
		)
		return nil
	}

	members, err := loadActiveMSPMembers(ctx, db, mspOrgID)
	if err != nil {
		return err
	}
	summary.MSPMembersScanned += len(members)

	desiredAccountIDs := set.NewSet[string]()
	missingAccountIDs := 0
	for _, member := range members {
		accountID := stringValue(member.AtlassianAccountID)
		if accountID == "" {
			missingAccountIDs++
			continue
		}
		desiredAccountIDs.Insert(accountID)
	}
	summary.MSPMembersMissingAccount += missingAccountIDs

	actualAccountIDs, err := jsm.ListOrganizationUserAccountIDs(ctx, jsmOrgID)
	if err != nil {
		return fmt.Errorf("list JSM organization %s users: %w", jsmOrgID, err)
	}
	addAccountIDs := desiredAccountIDs.Difference(set.Set[string](actualAccountIDs)).Slice()
	sort.Strings(addAccountIDs)
	alreadyPresent := desiredAccountIDs.Len() - len(addAccountIDs)
	summary.MSPMembersAlreadyPresent += alreadyPresent
	summary.MSPMemberAdditionsPlanned += len(addAccountIDs)

	fmt.Fprintf(
		options.Output,
		"plan customer_org=%s jsm_org=%s msp_org=%s active_msp_members=%d missing_account_id=%d already_present=%d add=%d\n",
		org.ID,
		jsmOrgID,
		mspOrgID,
		len(members),
		missingAccountIDs,
		alreadyPresent,
		len(addAccountIDs),
	)
	if !options.Execute || len(addAccountIDs) == 0 {
		return nil
	}

	if err := jsm.AddUsersToOrganization(ctx, jsmOrgID, addAccountIDs); err != nil {
		return fmt.Errorf("add %d MSP members to JSM organization %s: %w", len(addAccountIDs), jsmOrgID, err)
	}
	summary.MSPMembersAdded += len(addAccountIDs)
	fmt.Fprintf(
		options.Output,
		"added %d MSP members to customer_org=%s jsm_org=%s\n",
		len(addAccountIDs),
		org.ID,
		jsmOrgID,
	)
	return nil
}

func loadActiveMSPMembers(ctx context.Context, db *gorm.DB, mspOrgID string) ([]mspMember, error) {
	var members []mspMember
	err := db.WithContext(ctx).
		Table("tidbcloud_org_memberships").
		Select(`
			tidbcloud_org_memberships.user_id AS user_id,
			tidbcloud_users.email AS email,
			tidbcloud_users.atlassian_account_id AS atlassian_account_id
		`).
		Joins("JOIN tidbcloud_users ON tidbcloud_users.id = tidbcloud_org_memberships.user_id").
		Where(
			"tidbcloud_org_memberships.org_id = ? AND tidbcloud_org_memberships.status = ?",
			mspOrgID,
			models.RelationStatusActive,
		).
		Order("tidbcloud_org_memberships.user_id ASC").
		Scan(&members).Error
	if err != nil {
		return nil, fmt.Errorf("list active MSP organization %s members: %w", mspOrgID, err)
	}
	return members, nil
}

func stringValue(value *string) string {
	if value == nil {
		return ""
	}
	return strings.TrimSpace(*value)
}
