package installation

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/pinyinsearch"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// InstanceIdentity names one install target: the durable instance key, the
// service handle, and the display name of the service member.
type InstanceIdentity struct {
	InstanceKey string
	Handle      string
	DisplayName string
}

// PreparedInstance is the validated install payload shared by the workspace
// and personal install paths.
type PreparedInstance struct {
	DefinitionID  string
	Version       string
	VersionScopes []string
	ConfigJSON    []byte
}

// InstallCallbacks carry the app-owned side effects the canonical installer
// cannot own: channel membership for workspace installs, workspace state
// deletion for disabled-row reinstalls, definition capability sync, and the
// catalog reconcile hook.
type InstallCallbacks struct {
	EnsureChannelMembership func(ctx context.Context, tx coredb.Tx, workspaceID string, memberID string, now time.Time) error
	DeleteWorkspaceState    func(ctx context.Context, tx coredb.Tx, existing serviceactor.Installation, now time.Time) error
	Reconcile               func(ctx context.Context, tx coredb.Tx, workspaceID string, now time.Time) error
}

// InstallPreparedInstance creates one installation of a prepared App
// definition under the caller's transaction. The caller holds the App
// definition mutation-scope lock. It returns the installation id.
func InstallPreparedInstance(
	ctx context.Context,
	tx coredb.Tx,
	txStore *serviceactor.TxStore,
	workspaceID string,
	createdByMemberID string,
	prepared PreparedInstance,
	identity InstanceIdentity,
	callbacks InstallCallbacks,
	now time.Time,
) (string, error) {
	identity.InstanceKey = strings.TrimSpace(identity.InstanceKey)
	identity.Handle = strings.TrimSpace(identity.Handle)
	identity.DisplayName = strings.TrimSpace(identity.DisplayName)
	if identity.InstanceKey == "" || identity.Handle == "" || identity.DisplayName == "" {
		return "", fmt.Errorf("installation identity is incomplete")
	}
	existing, found, err := txStore.LoadInstallationByInstanceForUpdate(ctx, workspaceID, prepared.DefinitionID, identity.InstanceKey)
	if err != nil {
		return "", err
	}
	if found {
		if existing.Status == serviceactor.StatusActive {
			return "", storeerr.Conflict("app already installed")
		}
		if err := replaceDisabledInstallation(ctx, tx, txStore, workspaceID, existing, callbacks, now); err != nil {
			return "", err
		}
	}

	installationID, err := identifiers.NewEntityID("appinst")
	if err != nil {
		return "", fmt.Errorf("generate app installation id: %w", err)
	}
	pinyinFull, pinyinInitials := pinyinsearch.Variants(identity.DisplayName)
	serviceMemberID, serviceMemberRowID, err := insertServiceMemberWithRetry(ctx, tx, workspaceID, identity.DisplayName, pinyinFull, pinyinInitials, now)
	if err != nil {
		return "", err
	}

	definition, metadata, err := replayDefinitionState(ctx, tx, txStore, prepared.DefinitionID, prepared.Version, now)
	if err != nil {
		return "", err
	}
	installation, err := txStore.UpsertInstallation(ctx, serviceactor.InstallationUpsert{
		ID:                  installationID,
		WorkspaceID:         workspaceID,
		ServiceDefinitionID: definition.ID,
		ServiceKind:         serviceactor.KindApp,
		InstanceKey:         identity.InstanceKey,
		Status:              serviceactor.StatusActive,
		InstalledVersion:    prepared.Version,
		Config:              prepared.ConfigJSON,
		Metadata:            metadata,
		CreatedByMemberID:   createdByMemberID,
		Now:                 now,
	})
	if err != nil {
		return "", err
	}
	if _, err := txStore.UpsertMember(ctx, serviceactor.MemberUpsert{
		ID:                    serviceMemberRowID,
		WorkspaceID:           workspaceID,
		ServiceDefinitionID:   definition.ID,
		ServiceInstallationID: installation.ID,
		MemberID:              serviceMemberID,
		ServiceKind:           serviceactor.KindApp,
		Handle:                identity.Handle,
		DisplayName:           identity.DisplayName,
		AvatarURL:             definition.AvatarURL,
		Status:                serviceactor.StatusActive,
		Now:                   now,
	}); err != nil {
		return "", err
	}
	if err := txStore.SyncGrantScopes(ctx, installation, createdByMemberID, prepared.VersionScopes, now); err != nil {
		return "", err
	}
	reconcile := callbacks.Reconcile
	personalHandle := func(defaultName string) string { return defaultName }
	if strings.HasPrefix(identity.InstanceKey, serviceactor.PersonalInstanceKeyPrefix) {
		ownerMemberID := strings.TrimPrefix(identity.InstanceKey, serviceactor.PersonalInstanceKeyPrefix)
		personalHandle = func(defaultName string) string {
			return PersonalHandle(defaultName, ownerMemberID)
		}
	}
	if err := SyncDefaultExtensions(ctx, tx, txStore, installation, createdByMemberID, personalHandle, func(installationID string, domain string) string {
		return installationID + "/" + domain
	}, func() error {
		if reconcile == nil {
			return nil
		}
		return reconcile(ctx, tx, workspaceID, now)
	}, now); err != nil {
		return "", err
	}
	if callbacks.EnsureChannelMembership != nil {
		if err := callbacks.EnsureChannelMembership(ctx, tx, workspaceID, serviceMemberID, now); err != nil {
			return "", err
		}
	}
	return installationID, nil
}

func insertServiceMemberRow(
	ctx context.Context,
	tx coredb.Tx,
	memberID string,
	workspaceID string,
	displayName string,
	pinyinFull string,
	pinyinInitials string,
	now time.Time,
) error {
	if _, err := tx.Exec(ctx, `
INSERT INTO members (id, workspace_id, type, display_name, display_name_pinyin, display_name_pinyin_initials, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'service', $3, $4, $5, NULL, NULL, 'active', $6, $6)
`, memberID, workspaceID, displayName, pinyinFull, pinyinInitials, now); err != nil {
		return fmt.Errorf("insert app service member: %w", err)
	}
	return nil
}

// replaceDisabledInstallation clears one disabled durable installation so a
// fresh install can bind the same instance identity: the caller's
// workspace-state cleanup runs first, the replaced service member's parent
// members row is deactivated, and the durable row removal cascades the
// service_* children.
func replaceDisabledInstallation(
	ctx context.Context,
	tx coredb.Tx,
	txStore *serviceactor.TxStore,
	workspaceID string,
	existing serviceactor.Installation,
	callbacks InstallCallbacks,
	now time.Time,
) error {
	oldMember, err := txStore.LoadMemberByInstallation(ctx, existing.ID)
	if err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
UPDATE members
SET status = 'inactive',
    updated_at = $3
WHERE workspace_id = $1
  AND id = $2
  AND type = 'service'
`, workspaceID, oldMember.MemberID, now); err != nil {
		return fmt.Errorf("deactivate replaced app service member: %w", err)
	}
	if callbacks.DeleteWorkspaceState != nil {
		if err := callbacks.DeleteWorkspaceState(ctx, tx, existing, now); err != nil {
			return err
		}
	}
	if _, err := tx.Exec(ctx, `
DELETE FROM service_installations
WHERE id = $1
  AND status <> 'active'
`, existing.ID); err != nil {
		return fmt.Errorf("clear disabled installation: %w", err)
	}
	return nil
}

// replayDefinitionState materializes the definition, its complete capability
// set, and the pinned installation metadata for one install.
func replayDefinitionState(
	ctx context.Context,
	tx coredb.Tx,
	txStore *serviceactor.TxStore,
	definitionID string,
	version string,
	now time.Time,
) (serviceactor.Definition, []byte, error) {
	// The caller holds the App-definition mutation lock; the replay reads
	// are locking current-reads so a definition/version/manifest committed
	// while this transaction waited on the lock is what gets materialized.
	seed, err := LoadDefinitionSeedForUpdate(ctx, tx, definitionID)
	if err != nil {
		return serviceactor.Definition{}, nil, err
	}
	definition, err := UpsertDefinitionForSeed(ctx, txStore, seed, now)
	if err != nil {
		return serviceactor.Definition{}, nil, err
	}
	if err := SyncDefinitionCapabilities(ctx, txStore, definition, seed.Manifest, now); err != nil {
		return serviceactor.Definition{}, nil, err
	}
	metadata, err := LoadServiceInstallationMetadataForUpdate(ctx, tx, definitionID, version)
	if err != nil {
		return serviceactor.Definition{}, nil, err
	}
	return definition, metadata, nil
}

// insertServiceMemberWithRetry generates the visible member identity
// (raw member id) and the service_members projection row id (svcmem_*), retrying
// the members INSERT on a primary-key duplicate up to four times, and
// guaranteeing the two identities stay distinct.
func insertServiceMemberWithRetry(
	ctx context.Context,
	tx coredb.Tx,
	workspaceID string,
	displayName string,
	pinyinFull string,
	pinyinInitials string,
	now time.Time,
) (string, string, error) {
	const attempts = 4
	var lastErr error
	for attempt := 0; attempt < attempts; attempt++ {
		memberID, err := identifiers.NewMemberID()
		if err != nil {
			return "", "", fmt.Errorf("generate app service member id: %w", err)
		}
		recordID, err := identifiers.NewEntityID("svcmem")
		if err != nil {
			return "", "", fmt.Errorf("generate service member record id: %w", err)
		}
		if recordID == memberID {
			lastErr = fmt.Errorf("service member ids collided")
			continue
		}
		err = insertServiceMemberRow(ctx, tx, memberID, workspaceID, displayName, pinyinFull, pinyinInitials, now)
		if err == nil {
			return memberID, recordID, nil
		}
		lastErr = err
		if constraint, unique := coredb.UniqueViolationConstraint(err); !unique || constraint != "PRIMARY" {
			// insertServiceMemberRow already applies the single
			// "insert app service member" context.
			return "", "", err
		}
	}
	return "", "", fmt.Errorf("insert app service member after %d attempts: %w", attempts, lastErr)
}
