package automation

import (
	"context"
	"fmt"
	"strings"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// reconcileApprovalShares makes the set of approval-owned _System_RecordShare
// rows for an instance's target record match the set of currently pending
// approvers. It is idempotent and derives the desired state from live
// ProcessInstanceWorkitem rows, so callers invoke it after any state transition
// (submit, approve, reject, reassign, recall) without tracking which shares to
// add or remove. A missed or replayed transition self-heals on the next call.
//
// Salesforce does not auto-share records with approvers; this is an explicit,
// always-on NexusCRM substrate behavior recorded as a delta in
// docs/design/salesforce-v63-api-contract.md. Shares are read-only and last
// only while the approver's workitem is pending.
//
// All reads and writes run under SystemContext so they bypass the
// _System_RecordShare lock gate (the target record is locked during approval)
// and stay inside the caller's transaction for atomicity with the state change.
func (s *ApprovalService) reconcileApprovalShares(ctx context.Context, instanceID string, user *models.UserSession) error {
	shareCtx := processTableWriteContext(ctx, "ApprovalService: reconcile approver record shares")

	instanceFilter := fmt.Sprintf("Id == '%s'", escapeApprovalFilterValue(instanceID))
	instResult, err := s.query.QueryWithFilter(shareCtx, constants.TableProcessInstance, instanceFilter, user, "", "", 1)
	if err != nil {
		return fmt.Errorf("reconcileApprovalShares: load ProcessInstance %s: %w", instanceID, err)
	}
	if instResult == nil || len(instResult.Records) == 0 {
		return nil
	}
	instance := instResult.Records[0]
	targetObjectID, _ := instance["TargetObjectId"].(string)
	processDefID, _ := instance["ProcessDefinitionId"].(string)
	if strings.TrimSpace(targetObjectID) == "" || strings.TrimSpace(processDefID) == "" {
		return fmt.Errorf("reconcileApprovalShares: instance %s missing TargetObjectId/ProcessDefinitionId", instanceID)
	}

	processDef, err := s.loadProcessDefinitionForAction(shareCtx, processDefID, user)
	if err != nil {
		return fmt.Errorf("reconcileApprovalShares: %w", err)
	}
	objectAPIName, _ := processDef["TableEnumOrId"].(string)
	if strings.TrimSpace(objectAPIName) == "" {
		return fmt.Errorf("reconcileApprovalShares: process definition %s missing TableEnumOrId", processDefID)
	}

	// Desired = distinct actors across open workitems. When the instance is
	// terminal its workitems are already deleted, so this is empty and every
	// approval share gets revoked below.
	desired, err := s.pendingApproverIDs(shareCtx, instanceID, user)
	if err != nil {
		return err
	}
	current, err := s.existingApprovalShares(shareCtx, objectAPIName, targetObjectID, user)
	if err != nil {
		return err
	}

	for actorID := range desired {
		if _, ok := current[actorID]; ok {
			continue
		}
		shareRecord := models.SObject{
			constants.FieldSysRecordShare_ObjectAPIName:   objectAPIName,
			constants.FieldSysRecordShare_RecordID:        targetObjectID,
			constants.FieldSysRecordShare_ShareWithUserID: actorID,
			constants.FieldSysRecordShare_AccessLevel:     constants.PermRead,
			constants.FieldSysRecordShare_Reason:          constants.RecordShareReasonApproval,
		}
		if _, insErr := s.persistence.Insert(shareCtx, constants.TableRecordShare, shareRecord, user); insErr != nil {
			return fmt.Errorf("reconcileApprovalShares: grant share to %s on %s/%s: %w", actorID, objectAPIName, targetObjectID, insErr)
		}
	}

	for actorID, shareIDs := range current {
		if _, ok := desired[actorID]; ok {
			continue
		}
		// Revoke every active approval share for this actor, not just one row, so
		// a duplicate left by an earlier reconcile cannot strand read access.
		for _, shareID := range shareIDs {
			if delErr := s.persistence.Delete(shareCtx, constants.TableRecordShare, shareID, user); delErr != nil {
				return fmt.Errorf("reconcileApprovalShares: revoke share %s from %s: %w", shareID, actorID, delErr)
			}
		}
	}

	return nil
}

// pendingApproverIDs returns the distinct ActorIds of the open workitems for an
// approval instance. Approver routing only resolves to user ids.
func (s *ApprovalService) pendingApproverIDs(ctx context.Context, instanceID string, user *models.UserSession) (map[string]struct{}, error) {
	filter := fmt.Sprintf("ProcessInstanceId == '%s'", escapeApprovalFilterValue(instanceID))
	records, err := s.queryAllPaged(ctx, constants.TableProcessInstanceWorkitem, filter, user)
	if err != nil {
		return nil, fmt.Errorf("reconcileApprovalShares: query workitems for %s: %w", instanceID, err)
	}
	desired := make(map[string]struct{})
	for _, item := range records {
		actorID, _ := item["ActorId"].(string)
		if strings.TrimSpace(actorID) != "" {
			desired[actorID] = struct{}{}
		}
	}
	return desired, nil
}

// existingApprovalShares returns the active approval-owned share rows for a
// record, keyed by the user they grant access to and listing every share id for
// that user (normally one, but a duplicate left by an earlier reconcile is kept
// so revocation can remove all of them). The query layer already excludes
// soft-deleted rows (Find applies ExcludeDeleted for IsDeleted-bearing tables);
// the explicit skip below is defensive belt-and-suspenders, not the sole guard,
// so a revoked share is never mistaken for an active one.
func (s *ApprovalService) existingApprovalShares(ctx context.Context, objectAPIName, recordID string, user *models.UserSession) (map[string][]string, error) {
	filter := fmt.Sprintf(
		"%s == '%s' && %s == '%s' && %s == '%s'",
		constants.FieldSysRecordShare_ObjectAPIName, escapeApprovalFilterValue(objectAPIName),
		constants.FieldSysRecordShare_RecordID, escapeApprovalFilterValue(recordID),
		constants.FieldSysRecordShare_Reason, constants.RecordShareReasonApproval,
	)
	records, err := s.queryAllPaged(ctx, constants.TableRecordShare, filter, user)
	if err != nil {
		return nil, fmt.Errorf("reconcileApprovalShares: query existing shares for %s/%s: %w", objectAPIName, recordID, err)
	}
	current := make(map[string][]string)
	for _, row := range records {
		if approvalShareIsDeleted(row[constants.FieldSysRecordShare_IsDeleted]) {
			continue
		}
		actorID, _ := row[constants.FieldSysRecordShare_ShareWithUserID].(string)
		shareID, _ := row[constants.FieldID].(string)
		if strings.TrimSpace(actorID) == "" || strings.TrimSpace(shareID) == "" {
			continue
		}
		current[actorID] = append(current[actorID], shareID)
	}
	return current, nil
}

// queryAllPaged reads every row matching filter, paging past the single-read
// row cap so an approval lifecycle decision is never made on a silently
// truncated set — a dropped workitem strands its approval share (former approver
// keeps read access), and a dropped share row misses a grant or revoke.
// QueryWithFilter exposes no offset, so each pass re-reads from the start with a
// larger window until the result is no longer truncated; it fails closed at a
// ceiling rather than returning a partial authorization-bearing set.
func (s *ApprovalService) queryAllPaged(ctx context.Context, objectName, filter string, user *models.UserSession) ([]models.SObject, error) {
	const (
		pageSize = 200
		ceiling  = 100000
	)
	limit := pageSize
	for {
		result, err := s.query.QueryWithFilter(ctx, objectName, filter, user, "", "", limit)
		if err != nil {
			return nil, err
		}
		if result == nil {
			return nil, nil
		}
		if result.Done || len(result.Records) < limit {
			return result.Records, nil
		}
		if limit >= ceiling {
			return nil, fmt.Errorf("queryAllPaged: %s result exceeds %d rows; refusing to silently truncate", objectName, ceiling)
		}
		limit *= 4
	}
}

// approvalShareIsDeleted reads the IsDeleted flag defensively across the bool /
// numeric / string shapes a query layer may return it as.
func approvalShareIsDeleted(v interface{}) bool {
	switch t := v.(type) {
	case bool:
		return t
	case int:
		return t != 0
	case int64:
		return t != 0
	case float64:
		return t != 0
	case string:
		return t == "1" || strings.EqualFold(t, "true")
	default:
		return false
	}
}
