package controlplane

import (
	"context"
	"database/sql"
	"errors"
	"fmt"

	"github.com/pingcap-inc/nexuscrm-controlplane/packs"
)

type appliedAppPack struct {
	pack        packs.Pack
	payloadHash string
}

func finalizeAppliedAppPacks(ctx context.Context, db *sql.DB, appPacks []appliedAppPack) error {
	return finalizeAppliedAppPacksWith(ctx, db, appPacks, packs.RegisterRuntimeMetadataForPacks, runLayoutMaintenance, nil)
}

func finalizeAppliedAppPacksWith(
	ctx context.Context,
	db *sql.DB,
	appPacks []appliedAppPack,
	registerRuntimeMetadata func(context.Context, *sql.DB, []packs.Pack) error,
	runMaintenance func(*sql.DB) error,
	materializeDerived func(context.Context) error,
) error {
	return finalizeAppliedAppPacksWithActiveHook(ctx, db, appPacks, registerRuntimeMetadata, runMaintenance, materializeDerived, nil, nil)
}

func finalizeAppliedAppPacksWithActiveHook(
	ctx context.Context,
	db *sql.DB,
	appPacks []appliedAppPack,
	registerRuntimeMetadata func(context.Context, *sql.DB, []packs.Pack) error,
	runMaintenance func(*sql.DB) error,
	materializeDerived func(context.Context) error,
	candidateFinalVerify func(context.Context, []packs.Pack) error,
	beforeActiveCommit func(context.Context, *sql.Tx, appliedAppPack) error,
) error {
	if len(appPacks) == 0 {
		return nil
	}

	if err := prepareAppliedAppPacksForActivation(
		ctx,
		db,
		appPacks,
		registerRuntimeMetadata,
		runMaintenance,
		materializeDerived,
		candidateFinalVerify,
	); err != nil {
		if markErr := markAppliedAppPacksFailed(ctx, db, appPacks, err); markErr != nil {
			return errors.Join(err, fmt.Errorf("mark applied packs failed: %w", markErr))
		}
		return err
	}

	for _, appPack := range appPacks {
		var beforeCommit func(context.Context, *sql.Tx) error
		if beforeActiveCommit != nil {
			packToActivate := appPack
			beforeCommit = func(ctx context.Context, tx *sql.Tx) error {
				return beforeActiveCommit(ctx, tx, packToActivate)
			}
		}
		if err := markInstalledPackActiveWithHook(ctx, db, appPack.pack, appPack.payloadHash, beforeCommit); err != nil {
			return err
		}
	}
	return nil
}

// prepareAppliedAppPacksForActivation is the single owner of the maintenance
// and verification chain that must finish while every selected app pack is
// still installing. Callers may use a different exact activation transaction,
// but none may activate before this boundary succeeds.
func prepareAppliedAppPacksForActivation(
	ctx context.Context,
	db *sql.DB,
	appPacks []appliedAppPack,
	registerRuntimeMetadata func(context.Context, *sql.DB, []packs.Pack) error,
	runMaintenance func(*sql.DB) error,
	materializeDerived func(context.Context) error,
	candidateFinalVerify func(context.Context, []packs.Pack) error,
) error {
	return prepareAppliedAppPacksForActivationWithVerifier(
		ctx, db, appPacks, registerRuntimeMetadata, runMaintenance,
		materializeDerived, candidateFinalVerify, nil,
	)
}

func prepareAppliedAppPacksForActivationWithVerifier(
	ctx context.Context,
	db *sql.DB,
	appPacks []appliedAppPack,
	registerRuntimeMetadata func(context.Context, *sql.DB, []packs.Pack) error,
	runMaintenance func(*sql.DB) error,
	materializeDerived func(context.Context) error,
	candidateFinalVerify func(context.Context, []packs.Pack) error,
	verifyAppliedPack func(context.Context, appliedAppPack) error,
) error {
	if len(appPacks) == 0 {
		return nil
	}
	if registerRuntimeMetadata == nil || runMaintenance == nil {
		return fmt.Errorf("app-pack activation preparation operations are incomplete")
	}

	for _, appPack := range appPacks {
		var err error
		if verifyAppliedPack != nil {
			err = verifyAppliedPack(ctx, appPack)
		} else {
			err = appPack.pack.Verify(ctx, packs.VerifyDeps{DB: db, DeepChecks: false})
		}
		if err != nil {
			return fmt.Errorf("pack %s verify: %w", appPack.pack.Name(), err)
		}
	}

	packList := make([]packs.Pack, 0, len(appPacks))
	for _, appPack := range appPacks {
		packList = append(packList, appPack.pack)
	}
	if err := registerRuntimeMetadata(ctx, db, packList); err != nil {
		return fmt.Errorf("rebuild runtime metadata after app packs: %w", err)
	}

	if err := runMaintenance(db); err != nil {
		return fmt.Errorf("layout maintenance after app packs: %w", err)
	}

	if materializeDerived != nil {
		if err := materializeDerived(ctx); err != nil {
			return fmt.Errorf("materialize derived artifacts after app packs: %w", err)
		}
	}

	for _, appPack := range appPacks {
		var err error
		if verifyAppliedPack != nil {
			err = verifyAppliedPack(ctx, appPack)
		} else {
			err = appPack.pack.Verify(ctx, packs.VerifyDeps{DB: db, DeepChecks: false})
		}
		if err != nil {
			return fmt.Errorf("pack %s post-maintenance verify: %w", appPack.pack.Name(), err)
		}
	}
	if candidateFinalVerify != nil {
		if err := candidateFinalVerify(ctx, append([]packs.Pack(nil), packList...)); err != nil {
			return fmt.Errorf("candidate final verify: %w", err)
		}
	}
	return nil
}

func candidateFinalVerifyWithRunner(r *Runner, verify CandidateFinalVerifier) func(context.Context, []packs.Pack) error {
	if verify == nil {
		return nil
	}
	return func(ctx context.Context, candidates []packs.Pack) error {
		return verify(ctx, r, candidates)
	}
}

func markAppliedAppPacksFailed(ctx context.Context, db *sql.DB, appPacks []appliedAppPack, applyErr error) error {
	var errs []error
	for _, appPack := range appPacks {
		if err := markInstalledPackFailed(ctx, db, appPack.pack, appPack.payloadHash, applyErr); err != nil {
			errs = append(errs, fmt.Errorf("%s: %w", appPack.pack.Name(), err))
		}
	}
	return errors.Join(errs...)
}
