package services

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log"
	"sort"
	"strings"
	"time"

	apperrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

const (
	metadataDeployConfigPrefix   = "DEPLOY_"
	metadataDeployCleanupTimeout = 5 * time.Second
)

func (sm *ServiceManager) MetadataDeployContract() contract.MetadataDeploy {
	if sm == nil {
		return nil
	}
	if sm.NeutralMetadataDeploy != nil {
		return sm.NeutralMetadataDeploy
	}
	if sm.System == nil || sm.Persistence == nil || sm.Metadata == nil || sm.QueryContract() == nil || sm.metadataDeployRecordDeleter == nil {
		return nil
	}
	return sm
}

func (sm *ServiceManager) StartMetadataDeploy(ctx context.Context) (string, error) {
	if sm == nil || sm.System == nil {
		return "", apperrors.NewInternalError("metadata deploy service unavailable", nil)
	}
	jobID, err := newMetadataDeployJobID()
	if err != nil {
		return "", apperrors.NewInternalError("failed to allocate metadata deploy id", err)
	}
	if err := sm.storeMetadataDeployResult(ctx, jobID, contract.MetadataDeployResult{
		Done:   false,
		Status: "InProgress",
	}, "Metadata Deploy InProgress"); err != nil {
		log.Printf("metadata deploy: failed to store initial status for %s: %v", jobID, err)
	}
	return jobID, nil
}

func (sm *ServiceManager) CompleteMetadataDeploy(ctx context.Context, jobID string, result contract.MetadataDeployResult) error {
	if sm == nil || sm.System == nil {
		return apperrors.NewInternalError("metadata deploy service unavailable", nil)
	}
	if err := sm.storeMetadataDeployResult(ctx, jobID, result, "Metadata Deploy Result"); err != nil {
		log.Printf("metadata deploy: failed to store final status for %s: %v", jobID, err)
		return nil
	}
	time.AfterFunc(24*time.Hour, func() {
		cleanupCtx, cancel := metadataDeployCleanupContext()
		defer cancel()
		_ = sm.System.DeleteConfig(cleanupCtx, metadataDeployConfigPrefix+jobID)
	})
	return nil
}

func metadataDeployCleanupContext() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), metadataDeployCleanupTimeout)
}

func (sm *ServiceManager) GetMetadataDeployResult(ctx context.Context, jobID string) (*contract.MetadataDeployResult, bool, error) {
	if sm == nil || sm.System == nil {
		return nil, false, apperrors.NewInternalError("metadata deploy service unavailable", nil)
	}
	raw, err := sm.System.GetConfig(ctx, metadataDeployConfigPrefix+jobID)
	if err != nil {
		return nil, false, err
	}
	if raw == nil {
		return nil, false, nil
	}
	result, err := decodeMetadataDeployResult(*raw)
	if err != nil {
		log.Printf("metadata deploy: failed to parse stored status for %s: %v", jobID, err)
		return nil, true, nil
	}
	return result, true, nil
}

func (sm *ServiceManager) ApplyMetadataDeployRecord(ctx context.Context, req contract.MetadataDeployRecordRequest, user *models.UserSession) (*contract.MetadataDeployRecordResult, error) {
	if sm == nil || sm.QueryContract() == nil {
		return nil, apperrors.NewInternalError("metadata deploy service unavailable", nil)
	}
	objectName := strings.TrimSpace(req.ObjectAPIName)
	if objectName == "" {
		return nil, apperrors.NewInternalError("metadata deploy object is required", nil)
	}
	matchFilter, err := metadataDeployMatchFilter(req.MatchFields)
	if err != nil {
		return nil, err
	}
	if req.Operation == contract.MetadataDeployRecordOperationDelete {
		return sm.deleteMetadataDeployRecord(ctx, objectName, matchFilter, req, user)
	}
	if req.Operation != contract.MetadataDeployRecordOperationUpsert {
		return nil, apperrors.NewInternalError(
			"unsupported metadata deploy record operation",
			fmt.Errorf("operation %q is not supported", req.Operation),
		)
	}
	if sm.Persistence == nil || sm.Metadata == nil {
		return nil, apperrors.NewInternalError("metadata deploy service unavailable", nil)
	}

	schema := sm.Metadata.GetSchema(ctx, objectName)
	createLifecycleHandles := metadataDeployLifecycleCapabilityConfigured(schema, contract.RecordLifecycleAfterCreateInTx, req.RequiredCreateLifecycleCapability)
	updateLifecycleHandles := metadataDeployLifecycleCapabilityConfigured(schema, contract.RecordLifecycleAfterUpdateInTx, req.RequiredUpdateLifecycleCapability)

	var result contract.MetadataDeployRecordResult
	deployCtx := contract.WithRecordLifecycleDiagnosticReporter(ctx, func(diagnostic contract.RecordLifecycleDiagnostic) {
		componentType := diagnostic.ComponentType
		if componentType == "" {
			componentType = req.DiagnosticComponentType
		}
		result.Diagnostics = append(result.Diagnostics, contract.MetadataDeployDiagnostic{
			Problem:       diagnostic.Problem,
			ComponentType: componentType,
		})
	})

	err = sm.Persistence.RunInPessimisticTransaction(deployCtx, func(txCtx context.Context) error {
		queryResult, err := sm.QueryContract().Query(txCtx, queryspec.QuerySpec{
			ObjectAPIName: objectName,
			Filter:        queryspec.FormulaPredicate(matchFilter),
			Limit:         1,
		}, user)
		if err != nil && !strings.Contains(err.Error(), "not found") {
			return apperrors.NewInternalError("failed to query "+objectName, err)
		}

		lifecycleHandles := createLifecycleHandles
		if err == nil && queryResult != nil && len(queryResult.Records) > 0 {
			recordID, _ := queryResult.Records[0][constants.FieldID].(string)
			if recordID == "" {
				return apperrors.NewInternalError("metadata deploy matched record without Id", nil)
			}
			if err := sm.Persistence.Update(txCtx, objectName, recordID, metadataDeployRecordToSObject(req.UpdateRecord), user); err != nil {
				return apperrors.NewInternalError("failed to update "+objectName, err)
			}
			lifecycleHandles = updateLifecycleHandles
		} else {
			if _, err := sm.Persistence.Insert(txCtx, objectName, metadataDeployRecordToSObject(req.CreateRecord), user); err != nil {
				return apperrors.NewInternalError("failed to insert "+objectName, err)
			}
			result.Created = true
		}

		if !lifecycleHandles {
			capability := req.RequiredCreateLifecycleCapability
			if !result.Created {
				capability = req.RequiredUpdateLifecycleCapability
			}
			return apperrors.NewInternalError(
				objectName+" lifecycle materializer is not configured",
				fmt.Errorf("missing %s hook on %s for metadata deploy", capability, objectName),
			)
		}
		return nil
	})
	if err != nil {
		return &result, err
	}
	return &result, nil
}

func (sm *ServiceManager) deleteMetadataDeployRecord(
	ctx context.Context,
	objectName string,
	matchFilter string,
	req contract.MetadataDeployRecordRequest,
	user *models.UserSession,
) (*contract.MetadataDeployRecordResult, error) {
	deleter := sm.metadataDeployRecordDeleter
	coordinator := sm.RecordMutationCoordinatorContract()
	if deleter == nil || coordinator == nil {
		return nil, apperrors.NewInternalError("metadata deploy service unavailable", nil)
	}

	result := &contract.MetadataDeployRecordResult{}
	err := coordinator.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		queryResult, queryErr := sm.QueryContract().Query(txCtx, queryspec.QuerySpec{
			ObjectAPIName: objectName,
			Filter:        queryspec.FormulaPredicate(matchFilter),
			Limit:         2,
		}, user)
		if queryErr != nil && !strings.Contains(queryErr.Error(), "not found") {
			return apperrors.NewInternalError("failed to query "+objectName, queryErr)
		}

		matchCount := 0
		if queryErr == nil && queryResult != nil {
			matchCount = len(queryResult.Records)
		}
		if matchCount != 1 {
			return apperrors.NewInternalError(
				"metadata deploy delete requires exactly one matching "+objectName+" record",
				fmt.Errorf("matched %d records", matchCount),
			)
		}

		recordID, _ := queryResult.Records[0][constants.FieldID].(string)
		recordID = strings.TrimSpace(recordID)
		if recordID == "" {
			return apperrors.NewInternalError("metadata deploy matched record without Id", nil)
		}
		if err := deleter.DeleteMetadataDeployRecord(txCtx, objectName, recordID, req.MatchFields, user); err != nil {
			return apperrors.NewInternalError("failed to delete "+objectName, err)
		}
		result.Deleted = true
		return nil
	})
	if err != nil {
		return result, err
	}
	return result, nil
}

func (sm *ServiceManager) storeMetadataDeployResult(ctx context.Context, jobID string, result contract.MetadataDeployResult, description string) error {
	if strings.TrimSpace(jobID) == "" {
		return apperrors.NewInternalError("metadata deploy id is required", nil)
	}
	encoded, err := json.Marshal(result)
	if err != nil {
		return apperrors.NewInternalError("failed to encode metadata deploy result", err)
	}
	return sm.System.SetConfig(ctx, metadataDeployConfigPrefix+jobID, string(encoded), false, description)
}

func newMetadataDeployJobID() (string, error) {
	var raw [8]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return "", err
	}
	return hex.EncodeToString(raw[:])[:15], nil
}

func metadataDeployMatchFilter(fields map[string]string) (string, error) {
	if len(fields) == 0 {
		return "", apperrors.NewInternalError("metadata deploy match fields are required", nil)
	}
	keys := make([]string, 0, len(fields))
	values := make(map[string]string, len(fields))
	for rawKey, value := range fields {
		key := strings.TrimSpace(rawKey)
		if key == "" {
			return "", apperrors.NewInternalError("metadata deploy match field name is required", nil)
		}
		keys = append(keys, key)
		values[key] = value
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, key := range keys {
		parts = append(parts, fmt.Sprintf("%s == '%s'", key, strings.ReplaceAll(values[key], "'", "\\'")))
	}
	return strings.Join(parts, " && "), nil
}

func metadataDeployLifecycleCapabilityConfigured(schema *models.ObjectMetadata, phase contract.RecordLifecyclePhase, capability string) bool {
	capability = strings.TrimSpace(capability)
	if schema == nil || capability == "" {
		return false
	}
	for _, candidate := range schema.RecordLifecycleCapabilitiesForPhase(string(phase)) {
		if strings.EqualFold(strings.TrimSpace(candidate), capability) {
			return true
		}
	}
	return false
}

func metadataDeployRecordToSObject(record contract.Record) models.SObject {
	out := make(models.SObject, len(record))
	for key, value := range record {
		out[key] = value
	}
	return out
}

type metadataDeployStoredResult struct {
	Done              bool                                      `json:"done"`
	Status            string                                    `json:"status"`
	Success           bool                                      `json:"success,omitempty"`
	ErrorMessage      string                                    `json:"errorMessage,omitempty"`
	ComponentFailures []contract.MetadataDeployComponentFailure `json:"componentFailures,omitempty"`
	Details           *metadataDeployStoredDetails              `json:"details,omitempty"`
}

type metadataDeployStoredDetails struct {
	ComponentFailures []contract.MetadataDeployComponentFailure `json:"componentFailures,omitempty"`
}

func decodeMetadataDeployResult(raw string) (*contract.MetadataDeployResult, error) {
	var stored metadataDeployStoredResult
	if err := json.Unmarshal([]byte(raw), &stored); err != nil {
		return nil, err
	}
	failures := stored.ComponentFailures
	if len(failures) == 0 && stored.Details != nil {
		failures = stored.Details.ComponentFailures
	}
	return &contract.MetadataDeployResult{
		Done:              stored.Done,
		Status:            stored.Status,
		Success:           stored.Success,
		ErrorMessage:      stored.ErrorMessage,
		ComponentFailures: failures,
	}, nil
}
