package records

import (
	"context"
	"errors"
	"net/http"
	"sort"

	"github.com/nexuscrm/permissions"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/models"
	defaultsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recorddefaultsv1"
)

// ContractService is the neutral records surface over the SObject-shaped
// persistence engine. The adapter keeps dialect contracts from depending on the
// engine's historical carrier type while the storage internals migrate at their
// own pace.
type ContractService struct {
	persistence *PersistenceService
}

func NewContractService(persistence *PersistenceService) *ContractService {
	return &ContractService{persistence: persistence}
}

func (s *ContractService) Insert(ctx context.Context, objectName string, data contract.Record, user *models.UserSession) (contract.Record, error) {
	wireData := contractRecordToSObject(data)
	result, err := s.persistence.Insert(ctx, objectName, wireData, user)
	return contractRecordFromSObject(result), err
}

func (s *ContractService) Retrieve(ctx context.Context, objectName string, id string, user *models.UserSession) (contract.Record, error) {
	result, err := s.persistence.Retrieve(ctx, objectName, id, user)
	return contractRecordFromSObject(result), err
}

// ReadSelectedRecord exposes the semantic selected-root implementation to the
// future Runtime adapter without changing the legacy contract.Records plane.
func (s *ContractService) ReadSelectedRecord(ctx context.Context, objectName string, id string, selectedFields []string, user *models.UserSession) (*SelectedRootRecord, error) {
	return s.persistence.ReadSelectedRecord(ctx, objectName, id, selectedFields, user)
}

func (s *ContractService) RetrieveMany(ctx context.Context, objectName string, ids []string, user *models.UserSession) (map[string]contract.Record, error) {
	result, err := s.persistence.RetrieveMany(ctx, objectName, ids, user)
	return contractRecordMapFromSObjects(result), err
}

func (s *ContractService) Update(ctx context.Context, objectName string, id string, updates contract.Record, user *models.UserSession) error {
	wireUpdates := contractRecordToSObject(updates)
	if RequiresSourceAwareRecordTransaction(objectName) {
		return s.persistence.Update(ctx, objectName, id, wireUpdates, user)
	}
	return s.persistence.RunInPessimisticTransaction(ctx, func(txCtx context.Context) error {
		return s.persistence.Update(txCtx, objectName, id, wireUpdates, user)
	})
}

func (s *ContractService) Delete(ctx context.Context, objectName string, id string, user *models.UserSession) error {
	return s.persistence.Delete(ctx, objectName, id, user)
}

func (s *ContractService) UpsertByExternalID(ctx context.Context, objectName string, fieldName string, fieldValue string, data contract.Record, user *models.UserSession) (contract.Record, bool, error) {
	result, created, err := s.persistence.UpsertByExternalID(ctx, objectName, fieldName, fieldValue, contractRecordToSObject(data), user)
	return contractRecordFromSObject(result), created, err
}

func (s *ContractService) RetrieveByField(ctx context.Context, objectName string, fieldName string, fieldValue string, user *models.UserSession) (contract.Record, error) {
	result, err := s.persistence.RetrieveByField(ctx, objectName, fieldName, fieldValue, user)
	return contractRecordFromSObject(result), err
}

func (s *ContractService) CreateRecords(ctx context.Context, records []contract.Record, allOrNone bool, user *models.UserSession) ([]contract.RecordMutationResult, error) {
	return s.persistence.CreateRecords(ctx, contractRecordsToSObjects(records), allOrNone, user)
}

func (s *ContractService) UpdateRecords(ctx context.Context, records []contract.Record, allOrNone bool, user *models.UserSession) ([]contract.RecordMutationResult, error) {
	return s.persistence.UpdateRecords(ctx, contractRecordsToSObjects(records), allOrNone, user)
}

func (s *ContractService) DeleteRecords(ctx context.Context, ids []string, allOrNone bool, user *models.UserSession) ([]contract.RecordMutationResult, error) {
	return s.persistence.DeleteRecords(ctx, ids, allOrNone, user)
}

func (s *ContractService) UpsertRecords(ctx context.Context, objectName string, externalIDField string, records []contract.Record, allOrNone bool, user *models.UserSession) ([]contract.RecordMutationResult, error) {
	if err := rejectGenericListViewMutation(objectName, "collection upsert"); err != nil {
		return nil, err
	}
	return s.persistence.UpsertRecords(ctx, objectName, externalIDField, contractRecordsToSObjects(records), allOrNone, user)
}

func (s *ContractService) RetrieveRecords(ctx context.Context, objectName string, ids []string, fields []string, user *models.UserSession) ([]contract.Record, error) {
	result, err := s.persistence.RetrieveRecords(ctx, objectName, ids, fields, user)
	return contractRecordsFromSObjects(result), err
}

// CreateTreeNodes is the neutral tree-create surface, exposed here so a caller
// that holds this adapter as a contract.Records value can type-assert it to
// contract.TreeNodeWriter and reach the neutral engine path. The adapter
// enforces the generic-list-view mutation guard UNCONDITIONALLY over the full
// typed-node object closure before delegating: the engine's guard in
// PersistenceService.CreateTreeNodes is bypass-conditional (skipped under
// BypassesCRUDandFLS), so relying on it alone would let a privileged caller
// create _System_ListView rows through this generic tree surface. Running
// rejectGenericListViewTreeNodeMutation here puts an unconditional adapter guard
// above the (conditional) engine guard. The typed IR crosses the boundary
// unchanged, so no SObject/map conversion is needed.
func (s *ContractService) CreateTreeNodes(ctx context.Context, nodes []contract.TreeNode, user *models.UserSession) ([]contract.TreeCreateResult, error) {
	if err := rejectGenericListViewTreeNodeMutation(nodes); err != nil {
		return nil, err
	}
	return s.persistence.CreateTreeNodes(ctx, nodes, user)
}

func (s *ContractService) ObjectTypeForID(ctx context.Context, id string) (string, error) {
	return s.persistence.GetRecordType(ctx, id)
}

func (s *ContractService) ObjectTypesForIDs(ctx context.Context, ids []string) (map[string]string, error) {
	return s.persistence.ResolveObjectTypes(ctx, ids)
}

// ObjectTypeCandidatesForIDs is an optional neutral capability for transports
// that must inspect every current owner of an opaque ID before mutation.
func (s *ContractService) ObjectTypeCandidatesForIDs(ctx context.Context, ids []string) (map[string][]string, error) {
	return s.persistence.ResolveObjectTypeCandidates(ctx, ids)
}

func (s *ContractService) RetrieveWithAccess(ctx context.Context, objectName string, id string, user *models.UserSession) (contract.Record, *contract.RecordAccess, error) {
	result, access, err := s.persistence.RetrieveWithAccess(ctx, objectName, id, user)
	return contractRecordFromSObject(result), contractRecordAccessFromModel(access), err
}

func (s *ContractService) BatchRecordAccess(ctx context.Context, objectName string, ids []string, user *models.UserSession) (map[string]contract.RecordAccess, error) {
	if len(ids) == 0 {
		return map[string]contract.RecordAccess{}, nil
	}
	schema := s.persistence.metadata.GetSchema(ctx, objectName)
	if schema == nil {
		return map[string]contract.RecordAccess{}, nil
	}
	recordMap, err := s.persistence.RetrieveManyRaw(ctx, objectName, ids, user)
	if err != nil {
		return nil, err
	}
	records := make([]models.SObject, 0, len(recordMap))
	for _, rec := range recordMap {
		records = append(records, rec)
	}
	accessResults := s.persistence.permissions.BatchCheckRecordAccess(ctx, schema, records, user)
	result := make(map[string]contract.RecordAccess, len(accessResults))
	for id, access := range accessResults {
		result[id] = contractRecordAccessFromModelValue(access)
	}
	return result, nil
}

// BatchRecordReadAccess returns only the caller-effective read decisions for
// the requested records. RetrieveManyRaw is already the canonical object-Read
// plus row-visibility path and omits missing/hidden rows, so a caller that only
// consumes Read must not expand the decision into Edit/Delete/approval-lock
// evaluation through BatchCheckRecordAccess.
func (s *ContractService) BatchRecordReadAccess(ctx context.Context, objectName string, ids []string, user *models.UserSession) (map[string]contract.RecordAccess, error) {
	if len(ids) == 0 {
		return map[string]contract.RecordAccess{}, nil
	}
	recordMap, err := s.persistence.RetrieveManyRaw(ctx, objectName, ids, user)
	if err != nil {
		return nil, err
	}
	result := make(map[string]contract.RecordAccess, len(recordMap))
	for id := range recordMap {
		result[id] = contract.RecordAccess{Read: true}
	}
	return result, nil
}

// EvaluateRecordAccess is the coarse Runtime-owned access operation. It keeps
// object resolution and per-object engine evaluation behind one RPC without
// adding those transport semantics to the legacy contract.RecordAccessReader
// interface or contract.Deps.
func (s *ContractService) EvaluateRecordAccess(ctx context.Context, objectName *string, ids []string, user *models.UserSession) (map[string]contract.RecordAccess, error) {
	return s.evaluateResolvedRecordAccess(ctx, objectName, ids, user, s.BatchRecordAccess)
}

// EvaluateRecordReadAccess is the coarse read-only counterpart used by
// Runtime compositions that need existence-masked parent visibility but do not
// consume mutation or lock facts. Object resolution remains identical to
// EvaluateRecordAccess; only the deliberately narrower batch evaluator differs.
func (s *ContractService) EvaluateRecordReadAccess(ctx context.Context, objectName *string, ids []string, user *models.UserSession) (map[string]contract.RecordAccess, error) {
	return s.evaluateResolvedRecordAccess(ctx, objectName, ids, user, s.BatchRecordReadAccess)
}

type recordAccessBatchEvaluator func(context.Context, string, []string, *models.UserSession) (map[string]contract.RecordAccess, error)

func (s *ContractService) evaluateResolvedRecordAccess(ctx context.Context, objectName *string, ids []string, user *models.UserSession, evaluate recordAccessBatchEvaluator) (map[string]contract.RecordAccess, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if len(ids) == 0 {
		return map[string]contract.RecordAccess{}, nil
	}
	if s == nil || s.persistence == nil || evaluate == nil {
		return nil, errors.New("record-access evaluator is not configured")
	}
	if objectName != nil {
		access, err := evaluate(ctx, *objectName, ids, user)
		if err != nil {
			return nil, err
		}
		return readableRecordAccess(access), nil
	}

	resolved, err := s.resolveRecordAccessObjectTypes(ctx, ids)
	if err != nil {
		return nil, err
	}
	grouped := make(map[string][]string)
	seen := make(map[string]map[string]struct{})
	for _, id := range ids {
		object := resolved[id]
		if object == "" {
			continue
		}
		if seen[object] == nil {
			seen[object] = make(map[string]struct{})
		}
		if _, duplicate := seen[object][id]; duplicate {
			continue
		}
		seen[object][id] = struct{}{}
		grouped[object] = append(grouped[object], id)
	}
	objects := make([]string, 0, len(grouped))
	for object := range grouped {
		objects = append(objects, object)
	}
	sort.Strings(objects)
	result := make(map[string]contract.RecordAccess)
	for _, object := range objects {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		access, err := evaluate(ctx, object, grouped[object], user)
		if err != nil {
			if recordAccessForbidden(err) {
				continue
			}
			return nil, err
		}
		for id, decision := range readableRecordAccess(access) {
			result[id] = decision
		}
	}
	return result, nil
}

// resolveRecordAccessObjectTypes uses the request-bound metadata identity for
// Catalog prefix-addressed IDs before falling back to physical table discovery for
// opaque IDs. Key-prefix ownership identifies an object shape; the later
// BatchRecordAccess read remains the authority for record existence and access.
// This keeps UUID/legacy compatibility without probing every live object table
// for the common prefixed-ID path.
func (s *ContractService) resolveRecordAccessObjectTypes(ctx context.Context, ids []string) (map[string]string, error) {
	resolved := make(map[string]string)
	unresolved := make([]string, 0, len(ids))
	seen := make(map[string]struct{}, len(ids))
	for _, id := range ids {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		if _, duplicate := seen[id]; duplicate {
			continue
		}
		seen[id] = struct{}{}
		if s.persistence.metadata != nil {
			if object, ok := s.persistence.metadata.ResolveObjectForRecordID(ctx, id); ok {
				resolved[id] = object
				continue
			}
		}
		unresolved = append(unresolved, id)
	}
	if len(unresolved) == 0 {
		return resolved, nil
	}

	discovered, err := s.ObjectTypesForIDs(ctx, unresolved)
	if err != nil {
		return nil, err
	}
	for id, object := range discovered {
		resolved[id] = object
	}
	return resolved, nil
}

func readableRecordAccess(access map[string]contract.RecordAccess) map[string]contract.RecordAccess {
	result := make(map[string]contract.RecordAccess, len(access))
	for id, decision := range access {
		if decision.Read {
			result[id] = decision
		}
	}
	return result
}

func recordAccessForbidden(err error) bool {
	var status interface{ HTTPStatus() int }
	return errors.As(err, &status) && status.HTTPStatus() == http.StatusForbidden
}

func (s *ContractService) ValidateSiblingMaxDefaultConfig(ctx context.Context, objectName string, targetField string) error {
	if s == nil || s.persistence == nil || s.persistence.metadata == nil {
		return nil
	}
	schema := s.persistence.metadata.GetSchema(ctx, objectName)
	field := models.FindField(schema, targetField)
	return ValidateSiblingMaxDefaultConfig(schema, field)
}

func (s *ContractService) ValidateSiblingMaxDefaultParentForCreate(ctx context.Context, objectName string, parentField string, parentValue any, user *models.UserSession) error {
	return s.persistence.ValidateSiblingMaxDefaultParentForCreate(ctx, objectName, parentField, parentValue, user)
}

func (s *ContractService) PreviewSiblingMaxDefault(ctx context.Context, objectName string, targetField string, parentField string, parentValue any) (int64, error) {
	return s.persistence.PreviewSiblingMaxDefault(ctx, objectName, targetField, parentField, parentValue)
}

// PreviewCreateDefaults exposes the neutral Runtime-owned dynamic-default
// operation without widening the legacy contract.Records interface.
func (s *ContractService) PreviewCreateDefaults(ctx context.Context, request defaultsemantic.Request, user *models.UserSession) ([]defaultsemantic.Field, error) {
	return s.persistence.PreviewCreateDefaults(ctx, request, user)
}

func (s *ContractService) RunInPessimisticTransaction(ctx context.Context, fn func(context.Context) error) error {
	return s.persistence.RunInPessimisticTransaction(ctx, fn)
}

func (s *ContractService) RequiresSourceAwareTransaction(objectName string) bool {
	return RequiresSourceAwareRecordTransaction(objectName)
}

func (s *ContractService) ListRecycleBin(ctx context.Context, user *models.UserSession, scope contract.RecycleBinScope) (*contract.RecycleBinListResult, error) {
	result, err := s.persistence.GetRecycleBinItems(ctx, user, RecycleBinScope(scope))
	if err != nil {
		return nil, err
	}
	return contractRecycleBinListFromRecords(result), nil
}

func (s *ContractService) RestoreRecycleBinRecord(ctx context.Context, recordID string, user *models.UserSession) error {
	return s.persistence.RestoreRecord(ctx, recordID, user)
}

func (s *ContractService) PurgeRecycleBinRecord(ctx context.Context, recordID string, user *models.UserSession) error {
	return s.persistence.PurgeRecord(ctx, recordID, user)
}

// rejectGenericListViewTreeNodeMutation walks the neutral node closure via the
// engine's groupMembershipClosureObjectNamesFromTreeNodes (same package) and
// rejects a generic list-view mutation for every object in it, UNCONDITIONALLY.
// This is the adapter-level guard the CreateTreeNodes surface runs before
// delegating, so the guard fires regardless of the caller's security-context
// bypass.
func rejectGenericListViewTreeNodeMutation(nodes []contract.TreeNode) error {
	for _, objectName := range groupMembershipClosureObjectNamesFromTreeNodes(nodes) {
		if err := rejectGenericListViewMutation(objectName, "tree create"); err != nil {
			return err
		}
	}
	return nil
}

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

func contractRecordsToSObjects(records []contract.Record) []models.SObject {
	if records == nil {
		return nil
	}
	out := make([]models.SObject, 0, len(records))
	for _, record := range records {
		out = append(out, contractRecordToSObject(record))
	}
	return out
}

func contractRecordFromSObject(record models.SObject) contract.Record {
	if record == nil {
		return nil
	}
	out := make(contract.Record, len(record))
	for key, value := range record {
		if models.IsRecordMetadataKey(key) {
			continue
		}
		out[key] = value
	}
	if objectType := models.RecordObjectType(record); objectType != "" {
		out[models.ObjectTypeMetadataKey] = objectType
	}
	return out
}

func contractRecordAccessFromModel(access *permissions.RecordAccessResult) *contract.RecordAccess {
	if access == nil {
		return nil
	}
	out := contractRecordAccessFromModelValue(*access)
	return &out
}

func contractRecordAccessFromModelValue(access permissions.RecordAccessResult) contract.RecordAccess {
	return contract.RecordAccess{
		Read:   access.HasReadAccess,
		Update: access.HasEditAccess,
		Delete: access.HasDeleteAccess,
		Locked: access.IsLocked,
	}
}

func contractRecordsFromSObjects(records []models.SObject) []contract.Record {
	if records == nil {
		return nil
	}
	out := make([]contract.Record, 0, len(records))
	for _, record := range records {
		out = append(out, contractRecordFromSObject(record))
	}
	return out
}

func contractRecordMapFromSObjects(records map[string]models.SObject) map[string]contract.Record {
	if records == nil {
		return nil
	}
	out := make(map[string]contract.Record, len(records))
	for key, record := range records {
		out[key] = contractRecordFromSObject(record)
	}
	return out
}

func contractRecycleBinListFromRecords(result *RecycleBinListResult) *contract.RecycleBinListResult {
	if result == nil {
		return nil
	}
	out := &contract.RecycleBinListResult{
		Items:         make([]contract.RecycleBinItem, 0, len(result.Items)),
		FailedObjects: make([]contract.RecycleBinFailure, 0, len(result.FailedObjects)),
	}
	for _, item := range result.Items {
		out.Items = append(out.Items, contract.RecycleBinItem{
			ID:               item.ID,
			RecordID:         item.RecordID,
			RecordName:       item.RecordName,
			ObjectAPIName:    item.ObjectAPIName,
			DeletedBy:        item.DeletedBy,
			DeletedDate:      item.DeletedDate,
			CreatedDate:      item.CreatedDate,
			LastModifiedDate: item.LastModifiedDate,
		})
	}
	for _, failure := range result.FailedObjects {
		out.FailedObjects = append(out.FailedObjects, contract.RecycleBinFailure{
			ObjectAPIName: failure.ObjectAPIName,
			ErrorCode:     failure.ErrorCode,
			Message:       failure.Message,
		})
	}
	return out
}
