package persistence

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

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/schema"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/catalogassignmentidentityv1"
)

type catalogObjectIdentityCandidate struct {
	ID                string
	APIName           string
	NormalizedAPIName string
}

// validateCatalogObjectAPIName applies the portable Catalog object-key v1
// grammar without normalizing or rewriting the authored API name.
func validateCatalogObjectAPIName(value string) error {
	if _, err := catalogassignmentidentityv1.NormalizeObjectAPIName(value); err != nil {
		return apperrors.NewValidationError(
			constants.FieldSysObject_APIName,
			fmt.Sprintf("Catalog object-key v1: %v", err),
		)
	}
	return nil
}

func prepareCatalogObjectIdentityCandidates(objects []*models.ObjectMetadata) ([]catalogObjectIdentityCandidate, error) {
	candidates := make([]catalogObjectIdentityCandidate, 0, len(objects))
	ids := make(map[string]string, len(objects))
	keys := make(map[string]string, len(objects))
	for index, object := range objects {
		if object == nil {
			return nil, apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf("object metadata entry %d is required", index),
			)
		}
		key, err := catalogassignmentidentityv1.NormalizeObjectAPIName(object.APIName)
		if err != nil {
			return nil, apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf("Catalog object-key v1: %v", err),
			)
		}
		if object.ID == "" {
			object.ID = schema.GenerateObjectID(object.APIName)
		}
		if priorAPIName, duplicate := ids[object.ID]; duplicate && priorAPIName != object.APIName {
			return nil, apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf("object ID %q cannot identify both %q and %q", object.ID, priorAPIName, object.APIName),
			)
		}
		if priorID, duplicate := keys[key.NormalizedAPIName]; duplicate && priorID != object.ID {
			return nil, apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf("object API names collide on Catalog object key %q", key.NormalizedAPIName),
			)
		}
		ids[object.ID] = object.APIName
		keys[key.NormalizedAPIName] = object.ID
		candidates = append(candidates, catalogObjectIdentityCandidate{
			ID:                object.ID,
			APIName:           object.APIName,
			NormalizedAPIName: key.NormalizedAPIName,
		})
	}
	sort.Slice(candidates, func(left, right int) bool {
		if candidates[left].NormalizedAPIName != candidates[right].NormalizedAPIName {
			return candidates[left].NormalizedAPIName < candidates[right].NormalizedAPIName
		}
		return candidates[left].ID < candidates[right].ID
	})
	return candidates, nil
}

func requireCatalogObjectIdentityTransaction(value interface{}) (*sql.Tx, error) {
	transaction, ok := value.(*sql.Tx)
	if !ok || transaction == nil {
		return nil, apperrors.NewInternalError(
			"Catalog object identity writes require a database transaction",
			nil,
		)
	}
	return transaction, nil
}

// assertCatalogObjectIdentityMappings locks every existing row addressed by a
// candidate local ID or normalized portable key and rejects ID transfer,
// rename, and normalized-key collision before the caller's upsert.
func assertCatalogObjectIdentityMappings(
	ctx context.Context,
	exec Executor,
	candidates []catalogObjectIdentityCandidate,
	allowExistingExact bool,
) error {
	return checkCatalogObjectIdentityMappings(ctx, exec, candidates, allowExistingExact, false)
}

// requireCatalogObjectIdentityMappings verifies that an atomic identity claim
// produced every exact ID/API-name mapping. Unlike a pre-write assertion, an
// absent candidate is an error.
func requireCatalogObjectIdentityMappings(
	ctx context.Context,
	exec Executor,
	candidates []catalogObjectIdentityCandidate,
) error {
	return checkCatalogObjectIdentityMappings(ctx, exec, candidates, true, true)
}

func checkCatalogObjectIdentityMappings(
	ctx context.Context,
	exec Executor,
	candidates []catalogObjectIdentityCandidate,
	allowExistingExact bool,
	requireEveryCandidate bool,
) error {
	if len(candidates) == 0 {
		return nil
	}

	placeholders := strings.TrimSuffix(strings.Repeat("?,", len(candidates)), ",")
	queryText := "SELECT `Id`, `ApiName` FROM `_System_Object` WHERE `Id` IN (" +
		placeholders + ") OR LOWER(`ApiName`) IN (" + placeholders + ") FOR UPDATE"
	args := make([]interface{}, 0, len(candidates)*2)
	byID := make(map[string]catalogObjectIdentityCandidate, len(candidates))
	byKey := make(map[string]catalogObjectIdentityCandidate, len(candidates))
	exactIDs := make(map[string]struct{}, len(candidates))
	for _, candidate := range candidates {
		args = append(args, candidate.ID)
		byID[candidate.ID] = candidate
	}
	for _, candidate := range candidates {
		args = append(args, candidate.NormalizedAPIName)
		byKey[candidate.NormalizedAPIName] = candidate
	}

	rows, err := exec.QueryContext(ctx, queryText, args...)
	if err != nil {
		return apperrors.NewInternalError("failed to lock Catalog object identity mappings", err)
	}
	defer rows.Close()

	for rows.Next() {
		var existingID, existingAPIName string
		if err := rows.Scan(&existingID, &existingAPIName); err != nil {
			return apperrors.NewInternalError("failed to scan Catalog object identity mapping", err)
		}
		existingKey, err := catalogassignmentidentityv1.NormalizeObjectAPIName(existingAPIName)
		if err != nil {
			return apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf(
					"existing object %q has noncanonical API name %q; remediate it before writing Catalog object identities",
					existingID,
					existingAPIName,
				),
			)
		}

		if candidate, found := byID[existingID]; found && candidate.APIName != existingAPIName {
			return apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf(
					"object ID %q is immutably bound to API name %q and cannot be rebound to %q",
					existingID,
					existingAPIName,
					candidate.APIName,
				),
			)
		}
		if candidate, found := byKey[existingKey.NormalizedAPIName]; found && candidate.ID != existingID {
			return apperrors.NewValidationError(
				constants.FieldSysObject_APIName,
				fmt.Sprintf(
					"Catalog object key %q is already bound to object ID %q and cannot be rebound to %q",
					existingKey.NormalizedAPIName,
					existingID,
					candidate.ID,
				),
			)
		}
		if !allowExistingExact {
			if candidate, found := byID[existingID]; found && candidate.APIName == existingAPIName {
				return apperrors.NewConflictError("Object Metadata", "api_name", existingAPIName)
			}
		}
		if candidate, found := byID[existingID]; found && candidate.APIName == existingAPIName {
			exactIDs[candidate.ID] = struct{}{}
		}
	}
	if err := rows.Err(); err != nil {
		return apperrors.NewInternalError("failed while locking Catalog object identity mappings", err)
	}
	if requireEveryCandidate {
		for _, candidate := range candidates {
			if _, found := exactIDs[candidate.ID]; !found {
				return apperrors.NewInternalError(
					fmt.Sprintf(
						"Catalog object identity claim did not persist exact mapping %q -> %q",
						candidate.NormalizedAPIName,
						candidate.ID,
					),
					nil,
				)
			}
		}
	}
	return nil
}

// prepareCatalogObjectIdentityClaimValues builds the complete authored rows
// used by the caller's serialized first-writer claim. The caller relies on the
// case-insensitive ApiName unique constraint and primary ID constraint to choose
// one mapping, then immediately verifies that exact mapping in the same
// transaction before changing any existing row's non-identity metadata.
func (r *SchemaRepository) prepareCatalogObjectIdentityClaimValues(
	candidates []catalogObjectIdentityCandidate,
	objects []*models.ObjectMetadata,
) ([]map[string]interface{}, error) {
	if len(candidates) == 0 {
		return nil, nil
	}
	objectsByID := make(map[string]*models.ObjectMetadata, len(objects))
	for _, object := range objects {
		if object != nil {
			objectsByID[object.ID] = object
		}
	}
	values := make([]map[string]interface{}, 0, len(candidates))
	for _, candidate := range candidates {
		object := objectsByID[candidate.ID]
		if object == nil || object.APIName != candidate.APIName {
			return nil, apperrors.NewInternalError(
				fmt.Sprintf("missing source object for Catalog identity claim %q", candidate.ID),
				nil,
			)
		}
		objectValues, err := r.prepareObjectDBMap(object)
		if err != nil {
			return nil, apperrors.NewInternalError(
				fmt.Sprintf("failed to prepare Catalog identity claim for %s", candidate.APIName),
				err,
			)
		}
		values = append(values, objectValues)
	}
	return values, nil
}
