package jsmorgdedupe

import (
	"context"
	"fmt"
	"io"
	"slices"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"

	"github.com/tidbcloud/aisupport/services/jsm-syncer/internal/integrations/atlassian"
)

const MinimumStoredJSMOrganizationID int64 = 140000

var eligibleSupportPlans = []string{"Enterprise", "Premium", "Developer"}

type Options struct {
	Execute bool
	Delay   time.Duration
	Output  io.Writer
}

type Summary struct {
	Scanned        int
	Succeeded      int
	Failed         int
	Duplicates     int
	Deleted        int
	DatabaseWrites int
	Renamed        int
}

type databaseOrganization struct {
	ID          string `gorm:"column:id"`
	Name        string `gorm:"column:name"`
	Status      string `gorm:"column:status"`
	SupportPlan string `gorm:"column:support_plan"`
	JSMOrgID    string `gorm:"column:jsm_org_id"`
}

func Run(
	ctx context.Context,
	db *gorm.DB,
	jsm atlassian.OrganizationMaintenanceClient,
	options Options,
) (Summary, error) {
	if options.Output == nil {
		options.Output = io.Discard
	}
	if options.Delay < 0 {
		return Summary{}, fmt.Errorf("delay must not be negative")
	}

	organizations, err := loadOrganizations(ctx, db)
	if err != nil {
		return Summary{}, err
	}

	summary := Summary{Scanned: len(organizations)}
	fmt.Fprintf(options.Output, "found %d database organizations to inspect\n", len(organizations))
	for _, org := range organizations {
		duplicates, result, err := processOrganization(ctx, db, jsm, org, options)
		summary.Duplicates += duplicates
		summary.Deleted += result.Deleted
		summary.DatabaseWrites += result.DatabaseWrites
		summary.Renamed += result.Renamed
		if err != nil {
			summary.Failed++
			fmt.Fprintf(options.Output, "ERROR org=%s: %v\n", org.ID, err)
			return summary, fmt.Errorf("organization %s: %w", org.ID, err)
		}
		summary.Succeeded++
	}
	return summary, nil
}

func loadOrganizations(ctx context.Context, db *gorm.DB) ([]databaseOrganization, error) {
	var organizations []databaseOrganization
	err := db.WithContext(ctx).
		Raw(`SELECT id, name, status, support_plan, jsm_org_id
			FROM tidbcloud_orgs
			WHERE support_plan IN ? AND jsm_org_id > ?
			ORDER BY id`, eligibleSupportPlans, MinimumStoredJSMOrganizationID).
		Scan(&organizations).Error
	if err != nil {
		return nil, fmt.Errorf("scan candidate organizations: %w", err)
	}
	return organizations, nil
}

type operationResult struct {
	Deleted        int
	DatabaseWrites int
	Renamed        int
}

func processOrganization(
	ctx context.Context,
	db *gorm.DB,
	jsm atlassian.OrganizationMaintenanceClient,
	org databaseOrganization,
	options Options,
) (int, operationResult, error) {
	fmt.Fprintf(
		options.Output,
		"\ninspect org=%s name=%q status=%s support_plan=%s stored_jsm_org_id=%s\n",
		org.ID,
		org.Name,
		org.Status,
		org.SupportPlan,
		org.JSMOrgID,
	)

	searchResults, err := jsm.FindOrganizationsByTiDBCloudOrgID(ctx, org.ID)
	if err != nil {
		return 0, operationResult{}, fmt.Errorf("search JSM organizations: %w", err)
	}
	organizationIDs, err := sortedUniqueOrganizationIDs(searchResults)
	if err != nil {
		return 0, operationResult{}, err
	}
	if len(organizationIDs) == 0 {
		return 0, operationResult{}, fmt.Errorf("JSM organization search returned no results")
	}

	keptID := organizationIDs[0]
	duplicateIDs := organizationIDs[1:]
	newName := fmt.Sprintf("%s (%s)", strings.TrimSpace(org.Name), org.ID)
	fmt.Fprintf(options.Output, "plan org=%s keep_jsm_org_id=%s delete_jsm_org_ids=%v new_name=%q\n", org.ID, keptID, duplicateIDs, newName)
	if !options.Execute {
		return len(duplicateIDs), operationResult{}, nil
	}

	result := operationResult{}
	for _, duplicateID := range duplicateIDs {
		if err := jsm.DeleteOrganization(ctx, duplicateID); err != nil {
			return len(duplicateIDs), result, fmt.Errorf("delete duplicate JSM organization %s: %w", duplicateID, err)
		}
		result.Deleted++
		fmt.Fprintf(options.Output, "deleted duplicate JSM organization %s for org=%s\n", duplicateID, org.ID)
	}

	if org.JSMOrgID == keptID {
		fmt.Fprintf(options.Output, "database org=%s already has jsm_org_id=%s\n", org.ID, keptID)
	} else {
		dbResult := db.WithContext(ctx).
			Table("tidbcloud_orgs").
			Where("id = ? AND jsm_org_id = ?", org.ID, org.JSMOrgID).
			Update("jsm_org_id", keptID)
		if dbResult.Error != nil {
			return len(duplicateIDs), result, fmt.Errorf("update database jsm_org_id to %s: %w", keptID, dbResult.Error)
		}
		if dbResult.RowsAffected != 1 {
			return len(duplicateIDs), result, fmt.Errorf("update database jsm_org_id to %s affected %d rows; the row may have changed concurrently", keptID, dbResult.RowsAffected)
		}
		result.DatabaseWrites++
		fmt.Fprintf(options.Output, "updated database org=%s jsm_org_id=%s\n", org.ID, keptID)
	}

	if err := wait(ctx, options.Delay); err != nil {
		return len(duplicateIDs), result, err
	}
	if err := jsm.RenameOrganization(ctx, keptID, newName); err != nil {
		return len(duplicateIDs), result, fmt.Errorf("rename kept JSM organization %s to %q: %w", keptID, newName, err)
	}
	result.Renamed++
	fmt.Fprintf(options.Output, "renamed JSM organization %s to %q\n", keptID, newName)
	return len(duplicateIDs), result, nil
}

func sortedUniqueOrganizationIDs(organizations []atlassian.Organization) ([]string, error) {
	idsByNumber := make(map[int64]string, len(organizations))
	for _, org := range organizations {
		id := strings.TrimSpace(org.ID)
		numericID, err := strconv.ParseInt(id, 10, 64)
		if err != nil || numericID <= 0 {
			return nil, fmt.Errorf("JSM organization search returned non-numeric ID %q", org.ID)
		}
		idsByNumber[numericID] = strconv.FormatInt(numericID, 10)
	}

	numericIDs := make([]int64, 0, len(idsByNumber))
	for id := range idsByNumber {
		numericIDs = append(numericIDs, id)
	}
	slices.Sort(numericIDs)
	ids := make([]string, 0, len(numericIDs))
	for _, id := range numericIDs {
		ids = append(ids, idsByNumber[id])
	}
	return ids, nil
}

func wait(ctx context.Context, delay time.Duration) error {
	if delay == 0 {
		return nil
	}
	timer := time.NewTimer(delay)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return context.Cause(ctx)
	case <-timer.C:
		return nil
	}
}
