package packs

import (
	"context"
	"database/sql"
	"fmt"
	"sort"
	"strings"
)

func newSourceBackedConnectedAppPackSweeperComponent(packName string, declared []sourceBackedConnectedAppDeclarationRow) PackComponent {
	manifestComponent := ManifestComponent{
		Kind:          ComponentKindConnectedApp,
		BusinessKey:   "_pack_sweep",
		Manageability: ManageabilityLocked,
	}
	return &sourceBackedManagedComponent{
		packName:          packName,
		manifestComponent: manifestComponent,
		applyGroupKey:     string(ComponentKindConnectedApp) + "_pack_sweep",
		apply: func(ctx context.Context, deps ApplyDeps) error {
			return applySourceBackedConnectedAppPackSweep(ctx, deps, packName, declared)
		},
		verify: func(ctx context.Context, deps VerifyDeps) error {
			return verifySourceBackedConnectedAppPackSweep(ctx, deps.DB, packName, declared)
		},
	}
}

func applySourceBackedConnectedAppPackSweep(ctx context.Context, deps ApplyDeps, packName string, declared []sourceBackedConnectedAppDeclarationRow) error {
	if deps.DB == nil {
		return fmt.Errorf("database is required")
	}
	declaredSet := connectedAppDeclarationSet(declared)
	stale, err := loadSourceBackedConnectedAppDeclarationsForPack(ctx, deps.DB, packName, declaredSet)
	if err != nil {
		return err
	}
	if len(stale) == 0 {
		return nil
	}
	sort.Slice(stale, func(i, j int) bool {
		if stale[i].ConnectedAppID != stale[j].ConnectedAppID {
			return stale[i].ConnectedAppID < stale[j].ConnectedAppID
		}
		return stale[i].ID < stale[j].ID
	})
	for _, row := range stale {
		if err := upsertSourceBackedConnectedAppStage(ctx, deps.DB, sourceBackedConnectedAppStageRow{
			ID:                row.ID,
			InstalledPackName: row.InstalledPackName,
			ConnectedAppID:    row.ConnectedAppID,
			PayloadHash:       row.PayloadHash,
			IsDeleted:         1,
		}, nil); err != nil {
			return err
		}
	}
	return nil
}

func verifySourceBackedConnectedAppPackSweep(ctx context.Context, db *sql.DB, packName string, declared []sourceBackedConnectedAppDeclarationRow) error {
	if db == nil {
		return fmt.Errorf("database is required")
	}
	declaredSet := connectedAppDeclarationSet(declared)
	stale, err := loadSourceBackedConnectedAppDeclarationsForPack(ctx, db, packName, declaredSet)
	if err != nil {
		return err
	}
	if len(stale) == 0 {
		return nil
	}
	leftover := make([]string, 0, len(stale))
	installing, err := sourceBackedConnectedAppPackIsInstalling(ctx, db, packName)
	if err != nil {
		return err
	}
	for _, row := range stale {
		if installing {
			stage, ok, err := loadSourceBackedConnectedAppStage(ctx, db, packName, row.ConnectedAppID)
			if err != nil {
				return err
			}
			if ok && stage.ID == row.ID && stage.IsDeleted != 0 {
				continue
			}
		}
		leftover = append(leftover, row.ConnectedAppID)
	}
	sort.Strings(leftover)
	if len(leftover) == 0 {
		return nil
	}
	return fmt.Errorf("pack %s has %d undeclared connected_app declaration(s) still live: %s", strings.TrimSpace(packName), len(leftover), strings.Join(leftover, ", "))
}

func loadSourceBackedConnectedAppDeclarationsForPack(ctx context.Context, db *sql.DB, packName string, declaredSet map[string]struct{}) ([]sourceBackedConnectedAppDeclarationRow, error) {
	rows, err := db.QueryContext(ctx, `
SELECT Id, InstalledPackName, ConnectedAppId, PayloadHash, COALESCE(IsDeleted, 0)
  FROM `+sourceBackedConnectedAppDeclarationTable+`
 WHERE InstalledPackName = ? AND COALESCE(IsDeleted, 0) = 0`, strings.TrimSpace(packName))
	if err != nil {
		return nil, fmt.Errorf("query connected_app declarations for sweep (pack %s): %w", strings.TrimSpace(packName), err)
	}
	defer rows.Close()

	var stale []sourceBackedConnectedAppDeclarationRow
	for rows.Next() {
		var row sourceBackedConnectedAppDeclarationRow
		if err := rows.Scan(&row.ID, &row.InstalledPackName, &row.ConnectedAppID, &row.PayloadHash, &row.IsDeleted); err != nil {
			return nil, fmt.Errorf("scan connected_app declaration for sweep: %w", err)
		}
		if _, ok := declaredSet[connectedAppDeclarationKey(row.ConnectedAppID)]; ok {
			continue
		}
		stale = append(stale, row)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate connected_app declarations for sweep: %w", err)
	}
	return stale, nil
}

func connectedAppDeclarationSet(declared []sourceBackedConnectedAppDeclarationRow) map[string]struct{} {
	set := make(map[string]struct{}, len(declared))
	for _, row := range declared {
		if key := connectedAppDeclarationKey(row.ConnectedAppID); key != "" {
			set[key] = struct{}{}
		}
	}
	return set
}

func connectedAppDeclarationKey(appID string) string {
	return strings.ToLower(strings.TrimSpace(appID))
}
