import ApiError from '@/shared/utils/errors/ApiError';
import {
  groupByQuery,
  parsePopulateString,
} from '@/shared/utils/mongooseUtils';
import httpStatus from 'http-status';
import mongoose, { FilterQuery, Types, UpdateQuery } from 'mongoose';
import axios from 'axios';
import aws4 from 'aws4';
import {
  buildPriceRangeFilter,
  buildSearchPipeline,
  processFilter,
  setDefaultOptions,
} from './individualProperties.constant';
import {
  getPropertyLocationCountResponse,
  IPropertyDoc,
  NewCreatedProperty,
  propertyLocationAggregateResponse,
  UpdatePropertyBody,
} from './individualProperties.interface';
import IndividualProperties from './individualProperties.model';
import { validateCustomFields } from '@/shared/utils/customFieldValidation';
import responseCodes from '@/shared/utils/responseCode/responseCode';
import { CustomFormNames } from '../customFields/customFields.constant';
import { transformGroupByResult } from './individualProperties.utils';
import { safeDeleteById } from '@/shared/utils/guard/ref-guard';
import { defaultStatus } from '@/shared/utils/responseCode/httpStatusAlias';
import { findProspectLeadsForProperty } from './prospects/prospects.service';

import SubCategory from '../master/property/subCategory/subCategory.model';
import Category from '../master/property/category/category.model';
import { Team } from '../teams/teams.model';
import { getObjectId } from '@/shared/utils/commonHelper';
import { createNotification } from '../notification/notification.service';
import { Project } from '../project';
import {
  NotificationStatus,
  UserType,
} from '../notification/notification.constant';
import config from '@/shared/config/config';

const { IndividualPropertiesResponseCodes } = responseCodes;

export interface QueryResult<T = Document> {
  results: T[];
  page: number;
  limit: number;
  totalPages: number;
  totalResults: number;
}

/**
 * Builds visibility filter for properties based on user permissions
 * @param userId - User's ObjectId
 * @param userType - User's type (e.g., 'admin')
 * @param userTeams - Array of user's team ObjectIds
 * @returns MongoDB filter object for visibility
 */
const buildVisibilityFilter = (
  userId: Types.ObjectId,
  userType: string,
  userTeams: Types.ObjectId[],
): Record<string, unknown> => {
  const visibilityConditions: Record<string, unknown>[] = [];

  // Always visible to creator
  visibilityConditions.push({ createdBy: userId });

  // Always visible to admin
  if (userType === 'admin') {
    // Admin can see all, so no additional filter needed
    return {};
  }

  // If not admin, add conditions for visibility
  visibilityConditions.push({
    $or: [
      // Default visibility: visibleToUsers and visibleToTeams are empty/null
      {
        $and: [
          {
            $or: [
              { visibleToUsers: { $exists: false } },
              { visibleToUsers: { $size: 0 } },
            ],
          },
          {
            $or: [
              { visibleToTeams: { $exists: false } },
              { visibleToTeams: { $size: 0 } },
            ],
          },
        ],
      },
      { isPublicVisibility: true },
      { visibleToUsers: userId },
      { visibleToTeams: { $in: userTeams } },
    ],
  });

  return { $or: visibilityConditions };
};

/**
 * Create a primary property
 * @param {NewCreatedProperty} propertyBody
 * @returns {Promise<IPropertyDoc>}
 */
export const createIndividualProperties = async (
  propertyBody: NewCreatedProperty & { addedBy: string },
): Promise<IPropertyDoc> => {
  const customFields = await validateCustomFields({
    customFields: propertyBody.customFields,
    companyId: propertyBody.companyId,
    formName: CustomFormNames.INDIVIDUAL_PROPERTY,
    errorCode:
      IndividualPropertiesResponseCodes.INDIVIDUALPROPERTIES_INVALID_CUSTOM_FIELDS,
  });
  const createdProperty = (await IndividualProperties.create({
    ...propertyBody,
    ...(customFields && { customFields }),
  })) as IPropertyDoc;

  const teamLead = await Team.findOne({
    $or: [
      { members: getObjectId(propertyBody.createdBy) },
      { lead: getObjectId(propertyBody.createdBy) },
    ],
  }).lean();

  const project = await Project.findOne({
    _id: propertyBody.project,
  });

  const appRedirect = {
    screenType: 'Property_Details',
    id: createdProperty._id as Types.ObjectId,
  };

  if (
    teamLead &&
    teamLead?._id &&
    teamLead?.lead &&
    createdProperty.status === 'active'
  )
    // send notification to Team manager and lead
    await createNotification(
      {
        title: 'New Property Added',
        description: `${propertyBody.title} in ${project.projectName} • ${propertyBody?.carpetArea} • ${propertyBody.price} added by ${propertyBody.addedBy}.`,
        userType: UserType.SELF,
        user: teamLead.lead as Types.ObjectId,
        status: NotificationStatus.SEND,
      },
      appRedirect,
    );
  return createdProperty as IPropertyDoc;
};

/**
 * Query for primary properties
 * @param {Object} filter - Mongo filter
 * @param {Object} options - Query options
 * @param {string} [options.sortBy] - Sort option in the format: sortField:(desc|asc)
 * @param {number} [options.limit] - Maximum number of results per page (default = 10)
 * @param {number} [options.page] - Current page (default = 1)
 * @returns {Promise<QueryResult>}
 */

export const queryPrimaryProperties = async (
  filter: Record<string, unknown>,
  options: Record<string, unknown>,
  userId?: Types.ObjectId,
  userType?: string,
  userTeams?: Types.ObjectId[],
): Promise<QueryResult<IPropertyDoc>> => {
  try {
    setDefaultOptions(options);
    const updatedFilter = processFilter(filter);

    // Apply visibility filter if user info provided
    if (userId && userType && userTeams) {
      const visibilityFilter = buildVisibilityFilter(
        userId,
        userType,
        userTeams,
      );
      (updatedFilter as any).$and = (updatedFilter as any).$and || [];
      (updatedFilter as any).$and.push(visibilityFilter);
    }

    // Run the base query (normal vs search)
    let pageResult;

    if (filter.search) {
      const searchValue = filter.search as string;
      const pipeline = buildSearchPipeline(
        updatedFilter,
        searchValue,
        options.fields as string,
      );

      pageResult = await IndividualProperties.paginate(
        {},
        {
          ...options,
          aggregation: pipeline,
        },
      );
    } else {
      pageResult = await IndividualProperties.paginate(updatedFilter, options);
    }

    const items = pageResult.results ?? pageResult.docs ?? [];

    await Promise.all(
      items.map((row) =>
        findProspectLeadsForProperty({
          propertyId: String(row._id ?? row.id),
          companyId: filter.companyId as string, // if this is a string, cast to ObjectId
        }).then(({ total }) => {
          row.prospectCount = total; // append directly into the same response array
        }),
      ),
    );

    // keep the original shape untouched
    if ('results' in pageResult) pageResult.results = items;
    else pageResult.docs = items;

    return pageResult;
  } catch (_error) {
    throw new ApiError(
      defaultStatus.INTERNAL_SERVER_ERROR,
      'Failed to query  properties',
      true,
      '',
      IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_ERROR,
    );
  }
};

/**
 * Get primary property by id
 * @param {mongoose.Types.ObjectId} id
 * @returns {Promise<IPropertyDoc | null>}
 */

export const getIndividualPropertiesById = async (
  id: mongoose.Types.ObjectId,
  userId?: Types.ObjectId,
  userType?: string,
  userTeams?: Types.ObjectId[],
): Promise<IPropertyDoc | null> => {
  const populateStr =
    'propertyTags:name;propertyType:name;locality;city:loc name;companyId:name logo;project:projectName mediaUrls;amenities;configuration:name;subcategory:name;updatedBy:firstName email lastName createdAt updatedAt;createdBy:firstName email lastName createdAt updatedAt mediaUrls';
  const populateFields = parsePopulateString(populateStr);

  let filter: any = { _id: id };

  // Apply visibility filter if user info provided
  if (userId && userType && userTeams) {
    const visibilityFilter = buildVisibilityFilter(userId, userType, userTeams);
    filter.$and = filter.$and || [];
    filter.$and.push(visibilityFilter);
  }

  const property = await IndividualProperties.findOne(filter)
    .populate(populateFields)
    .lean();

  if (!property) return null;

  // const groupedResponse: Record<string, unknown> = {};
  // for (const [groupName, keys] of Object.entries(fieldGroups))
  //   groupedResponse[groupName] = groupFields(
  //     property,
  //     keys as (keyof IPropertyDoc)[],
  //   );

  return property as unknown as IPropertyDoc;
};

/**
 * Update individual property by id
 * @param {mongoose.Types.ObjectId} propertyId
 * @param {UpdatePropertyBody} updateBody
 * @returns {Promise<IPropertyDoc | null>}
 */
export const updateIndividualPropertiesById = async (
  propertyId: mongoose.Types.ObjectId,
  updateBody: UpdatePropertyBody,
): Promise<IPropertyDoc | null> => {
  const { customFields, companyId, facilities, ...rest } = updateBody;

  let validatedCustomFields: Record<string, unknown> = {};
  if (customFields && Object.keys(customFields).length > 0)
    validatedCustomFields = await validateCustomFields({
      customFields,
      companyId: companyId,
      formName: CustomFormNames.INDIVIDUAL_PROPERTY,
      errorCode:
        IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_INVALID_CUSTOM_FIELDS,
    });

  const updateQuery: UpdateQuery<IPropertyDoc> = { ...rest };

  if (Object.keys(validatedCustomFields).length > 0)
    updateQuery.customFields = validatedCustomFields;

  // Dynamically handle all arrays under facilities with $addToSet
  if (facilities && typeof facilities === 'object') {
    updateQuery.$addToSet = {};

    for (const [facilityKey, facilityArray] of Object.entries(facilities))
      if (Array.isArray(facilityArray) && facilityArray.length > 0)
        // e.g., 'facilities.Schools', 'facilities.Hospitals'
        updateQuery.$addToSet[`facilities.${facilityKey}`] = {
          $each: facilityArray,
        };

    // Remove direct overwrite for facilities.* that are merged, do not use overwrite for whole doc
    if (Object.keys(updateQuery.$addToSet).length === 0)
      delete updateQuery.$addToSet;
  }

  const updateOptions = {
    new: true,
    runValidators: true,
    ...(updateQuery.$addToSet ? {} : { overwrite: true }),
  };

  const updatedProperty = await IndividualProperties.findByIdAndUpdate(
    propertyId,
    updateQuery,
    updateOptions,
  );

  if (!updatedProperty)
    throw new ApiError(httpStatus.NOT_FOUND, 'Individual property not found');

  return updatedProperty;
};

/**
 * Delete primary property by id
 * @param {string} propertyId
 * @returns {Promise<boolean>}
 */
export const deleteIndividualPropertiesById = async (
  propertyId: string,
): Promise<boolean> => {
  try {
    await safeDeleteById(
      IndividualProperties,
      propertyId,
      IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_IN_USE,
    );
    return true;
  } catch (error) {
    if (error instanceof ApiError) throw error;
    throw new ApiError(
      defaultStatus.OK,
      'Failed to delete property',
      true,
      '',
      IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_ERROR,
    );
  }
};

export const queryPrimaryPropertiesApp = async (
  rawFilter: Record<string, unknown>,
  options: Record<string, unknown>,
  userId?: Types.ObjectId,
  userType?: string,
  userTeams?: Types.ObjectId[],
): Promise<QueryResult<IPropertyDoc>> => {
  try {
    const { search, ...rest } = rawFilter;

    const filter: FilterQuery<IPropertyDoc> = {};

    // 🔍 Search filter
    if (typeof search === 'string' && search.trim() !== '')
      filter.$or = [{ title: { $regex: search.trim(), $options: 'i' } }];

    // 🆔 Known ObjectId fields
    const objectIdFields = [
      'propertyType',
      'subcategory',
      'city',
      'locality',
      'configuration',
      'project',
      'createdBy',
      'companyId',
    ];

    // ⚙️ Handle remaining filters
    Object.entries(rest).forEach(([key, value]) => {
      if (value !== undefined && value !== null && value !== '')
        if (
          objectIdFields.includes(key) &&
          typeof value === 'string' &&
          Types.ObjectId.isValid(value)
        )
          filter[key] = new Types.ObjectId(value);
        else if (key === 'price' && typeof value === 'string')
          // 🎯 Use price range builder
          Object.assign(filter, buildPriceRangeFilter(value));
        else filter[key] = value;
    });

    // Apply visibility filter if user info provided
    if (userId && userType && userTeams) {
      const visibilityFilter = buildVisibilityFilter(
        userId,
        userType,
        userTeams,
      );
      (filter as any).$and = (filter as any).$and || [];
      (filter as any).$and.push(visibilityFilter);
    }

    // 📦 Paginate
    const pageResult = await IndividualProperties.paginate(filter, options);
    const items = pageResult.results ?? pageResult.docs ?? [];

    // 🔗 Enrich with prospect counts (similar to queryPrimaryProperties)
    await Promise.all(
      items.map((row) =>
        findProspectLeadsForProperty({
          propertyId: String(row._id ?? row.id),
          companyId: rawFilter.companyId as string,
        }).then(({ total }) => {
          row.prospectCount = total;
        }),
      ),
    );

    // keep original shape intact
    if ('results' in pageResult) pageResult.results = items;
    else pageResult.docs = items;

    return pageResult;
  } catch (err) {
    console.log('🚀 ~ queryPrimaryPropertiesApp ~ err:', err);
    throw new ApiError(
      defaultStatus.INTERNAL_SERVER_ERROR,
      'Failed to query app properties',
      true,
      '',
      IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_ERROR,
    );
  }
};

export const getPropertyLocationCount =
  async (): Promise<getPropertyLocationCountResponse> => {
    let query = groupByQuery({
      fieldsArray: [{ key: 'locality', value: 'localityData.name' }],
      sortBy: 'count:desc',
      lookup: {
        from: 'areas',
        localField: 'locality',
        foreignField: '_id',
        as: 'localityData',
      },
      unwindField: 'localityData',
    });

    const propertyLocationCount: propertyLocationAggregateResponse[] =
      await IndividualProperties.aggregate(query);

    const propertyLocationCountFormatted = transformGroupByResult(
      propertyLocationCount,
    );
    return propertyLocationCountFormatted;
  };

export const getProjectNamesFromProperties = async (
  companyId: string,
  status?: string,
) => {
  const match: mongoose.FilterQuery<IPropertyDoc> = {
    companyId: new mongoose.Types.ObjectId(companyId),
    project: { $ne: null },
  };

  if (status) match.status = status as IPropertyDoc['status'];

  const pipeline: mongoose.PipelineStage[] = [
    { $match: match },
    {
      $lookup: {
        from: 'projects',
        localField: 'project',
        foreignField: '_id',
        as: 'projectDoc',
      },
    },
    { $unwind: '$projectDoc' },
    {
      $lookup: {
        from: 'areas',
        localField: 'projectDoc.locality',
        foreignField: '_id',
        as: 'localityData',
      },
    },
    {
      $lookup: {
        from: 'cities',
        localField: 'projectDoc.city',
        foreignField: '_id',
        as: 'cityData',
      },
    },
    {
      $group: {
        _id: '$projectDoc._id',
        projectName: { $first: '$projectDoc.projectName' },
        localityId: { $first: '$projectDoc.locality' },
        localityName: { $first: { $arrayElemAt: ['$localityData.name', 0] } },
        cityId: { $first: '$projectDoc.city' },
        cityName: { $first: { $arrayElemAt: ['$cityData.name', 0] } },
      },
    },
    {
      $project: {
        _id: 0,
        id: '$_id',
        projectName: 1,
        locality: {
          id: '$localityId',
          name: '$localityName',
        },
        city: {
          id: '$cityId',
          name: '$cityName',
        },
      },
    },
    { $sort: { projectName: 1 } },
  ];

  return IndividualProperties.aggregate(pipeline);
};

export const defaultFormValues = async () => {
  const categoryPattern = new RegExp('residential', 'i');
  const subCategoryPattern = new RegExp('apartment / flat', 'i');

  const subCategoryByName = await SubCategory.findOne({
    name: subCategoryPattern,
  }).select('_id');
  const categoryByName = await Category.findOne({
    name: categoryPattern,
  }).select('_id');

  return {
    subCategory: subCategoryByName?._id,
    category: categoryByName?._id,
  };
};

export const updatePropertySource = async (
  propertyId: mongoose.Types.ObjectId,
  source: Array<{ platform: string; id: string }>,
): Promise<IPropertyDoc | null> => {
  const updatedProperty = await IndividualProperties.findByIdAndUpdate(
    propertyId,
    { source },
    { new: true, runValidators: true },
  );

  if (!updatedProperty)
    throw new ApiError(httpStatus.NOT_FOUND, 'Individual property not found');

  return updatedProperty;
};

export const generatePropertyNameAndDescription = async (payload) => {
  try {
    const apiUrl = config?.propertyAiApiUrl;
    const url = new URL(apiUrl);
    const body = JSON.stringify(payload);

    const options = {
      host: url.host,
      path: url.pathname + url.search,
      method: 'POST',
      service: 'lambda',
      region: config.aiApiRegion,
      headers: {
        'Content-Type': 'application/json',
      },
      body,
    };

    const signed = aws4.sign(options, {
      accessKeyId: config.aiApiAccessKey,
      secretAccessKey: config.aiApiSecretKey,
    });

    const response = await axios.post(apiUrl, payload, {
      headers: signed.headers,
    });
    return response.data;
  } catch (error) {
    console.error('Error calling property AI API:', error);
    throw new ApiError(
      httpStatus.INTERNAL_SERVER_ERROR,
      'Failed to generate property name and description',
      true,
      '',
      IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_ERROR,
    );
  }
};

/**
 * Get visibility settings for a property
 * @param propertyId - Property ObjectId
 * @returns Visibility settings
 */
export const getPropertyVisibility = async (
  propertyId: Types.ObjectId,
): Promise<{
  visibleToUsers: Types.ObjectId[];
  visibleToTeams: Types.ObjectId[];
} | null> => {
  const property = await IndividualProperties.findById(propertyId)
    .select('visibleToUsers visibleToTeams')
    .lean();
  if (!property) return null;
  return {
    visibleToUsers: (property.visibleToUsers as Types.ObjectId[]) || [],
    visibleToTeams: (property.visibleToTeams as Types.ObjectId[]) || [],
  };
};

/**
 * Update visibility settings for a property
 * @param propertyId - Property ObjectId
 * @param visibleToUsers - Array of user ObjectIds
 * @param visibleToTeams - Array of team ObjectIds
 * @returns Updated property
 */
export const updatePropertyVisibility = async (
  propertyId: Types.ObjectId,
  visibleToUsers: string[],
  visibleToTeams: string[],
): Promise<IPropertyDoc | null> => {
  const updatedProperty = await IndividualProperties.findByIdAndUpdate(
    propertyId,
    {
      visibleToUsers: visibleToUsers.map((id) => getObjectId(id)),
      visibleToTeams: visibleToTeams.map((id) => getObjectId(id)),
    },
    { new: true, runValidators: true },
  );
  return updatedProperty;
};

export const bulkDeleteIndividualPropertiesById = async (
  propertyIds: string[],
): Promise<{ deleted: string[]; failed: { id: string; error: string }[] }> => {
  const deleted: string[] = [];
  const failed: { id: string; error: string }[] = [];

  // Process all deletions in parallel using Promise.allSettled
  const results = await Promise.allSettled(
    propertyIds.map((propertyId) =>
      safeDeleteById(
        IndividualProperties,
        propertyId,
        IndividualPropertiesResponseCodes.INDIVIDUAL_PROPERTY_IN_USE,
      ),
    ),
  );

  // Aggregate results
  results.forEach((result, index) => {
    const propertyId = propertyIds[index];
    if (result.status === 'fulfilled') {
      deleted.push(propertyId);
    } else {
      const errorMessage =
        result.reason instanceof ApiError
          ? result.reason.message
          : 'Failed to delete property';
      failed.push({ id: propertyId, error: errorMessage });
    }
  });

  return { deleted, failed };
};

/**
 * Bulk update visibility for multiple properties
 * @param propertyIds - Array of property ObjectIds
 * @param isPublicVisibility - Boolean flag for visibility (true = public, false = private)
 * @returns Object with success and failed updates
 */
export const bulkUpdatePropertyVisibility = async (
  propertyIds: string[],
  isPublicVisibility: boolean,
): Promise<{ updated: string[]; failed: { id: string; error: string }[] }> => {
  const updated: string[] = [];
  const failed: { id: string; error: string }[] = [];

  // eslint-disable-next-line no-restricted-syntax
  for (const propertyId of propertyIds) {
    try {
      const result = await IndividualProperties.findByIdAndUpdate(
        propertyId,
        { isPublicVisibility },
        { new: true, runValidators: true },
      );

      if (result) {
        updated.push(propertyId);
      } else {
        failed.push({ id: propertyId, error: 'Property not found' });
      }
    } catch (error) {
      const errorMessage =
        error instanceof ApiError
          ? error.message
          : 'Failed to update property visibility';
      failed.push({ id: propertyId, error: errorMessage });
    }
  }

  return { updated, failed };
};

export const bulkUpdate = async (
  propertyIds: string[],
  possessionStatus: string,
  updatedBy: string,
): Promise<{ modifiedCount: number; failedIds: string[] }> => {
  const failedIds: string[] = [];

  const results = await Promise.allSettled(
    propertyIds.map((propertyId) =>
      IndividualProperties.findByIdAndUpdate(
        propertyId,
        { possessionStatus, updatedBy },
        { new: true, runValidators: true },
      ),
    ),
  );

  results.forEach((result, index) => {
    if (result.status === 'rejected') {
      failedIds.push(propertyIds[index]);
    }
  });

  return {
    modifiedCount: propertyIds.length - failedIds.length,
    failedIds,
  };
};

export interface PropertyVisitor {
  activityId: Types.ObjectId;
  lead: {
    _id: Types.ObjectId;
    name: string;
    email?: string;
    phone?: Array<{ countryCode: string; number: string }>;
  } | null;
  visitDate: string | Date | null;
  visitStatus: string;
  assignedTo: {
    _id: Types.ObjectId;
    firstName: string;
    lastName: string;
    email: string;
  } | null;
  leadStage: {
    _id: Types.ObjectId;
    name: string;
  } | null;
  leadScore: number | null;
  comment: string;
}

/**
 * Get visitors (leads who did site-visit) for a property
 * @param propertyId - Property ObjectId
 * @param companyId - Company ObjectId
 * @param options - Query options (status, page, limit)
 * @returns Paginated list of visitors
 */
export const getPropertyVisitors = async (
  propertyId: Types.ObjectId,
  companyId: Types.ObjectId,
  options: {
    status?: 'pending' | 'completed' | 'overdue';
    page?: number;
    limit?: number;
  } = {},
): Promise<{
  total: number;
  page: number;
  limit: number;
  totalPages: number;
  results: PropertyVisitor[];
}> => {
  const { status, page = 1, limit = 10 } = options;

  // Import Activity model
  const { Activity } = await import('@/modules/activity/activity.model');

  // Build match criteria
  const matchCriteria: Record<string, unknown> = {
    type: { $in: ['schedule', 'siteVisit'] },
    property: propertyId,
    company: companyId,
  };

  // Add status filter if provided
  if (status) {
    matchCriteria.status = status;
  }

  // Count total documents
  const total = await Activity.countDocuments(matchCriteria);
  const totalPages = Math.ceil(total / limit);

  // Aggregation pipeline to get visitor details
  const pipeline: mongoose.PipelineStage[] = [
    // Match site-visit activities for the property
    { $match: matchCriteria },
    // Sort by createdAt descending (most recent first)
    { $sort: { createdAt: -1 as const } },
    // Lookup lead details
    {
      $lookup: {
        from: 'leads',
        localField: 'lead',
        foreignField: '_id',
        as: 'leadDoc',
      },
    },
    { $unwind: { path: '$leadDoc', preserveNullAndEmptyArrays: true } },
    // Lookup contact details
    {
      $lookup: {
        from: 'contacts',
        localField: 'leadDoc.contact',
        foreignField: '_id',
        as: 'contactDoc',
      },
    },
    { $unwind: { path: '$contactDoc', preserveNullAndEmptyArrays: true } },
    // Lookup assigned user
    {
      $lookup: {
        from: 'users',
        localField: 'assignedTo',
        foreignField: '_id',
        as: 'assignedToDoc',
      },
    },
    { $unwind: { path: '$assignedToDoc', preserveNullAndEmptyArrays: true } },
    // Lookup lead stage
    {
      $lookup: {
        from: 'leadstages',
        localField: 'leadDoc.leadStage',
        foreignField: '_id',
        as: 'leadStageDoc',
      },
    },
    { $unwind: { path: '$leadStageDoc', preserveNullAndEmptyArrays: true } },
    // Project final shape
    {
      $project: {
        activityId: '$_id',
        lead: {
          $ifNull: [
            {
              _id: '$leadDoc._id',
              name: {
                $trim: {
                  input: {
                    $concat: [
                      { $ifNull: ['$contactDoc.firstName', ''] },
                      ' ',
                      { $ifNull: ['$contactDoc.lastName', ''] },
                    ],
                  },
                },
              },
              email: '$contactDoc.email',
              phone: '$contactDoc.phone',
            },
            null,
          ],
        },
        visitDate: '$scheduleDateTime',
        visitStatus: '$status',
        assignedTo: {
          $ifNull: [
            {
              _id: '$assignedToDoc._id',
              firstName: '$assignedToDoc.firstName',
              lastName: '$assignedToDoc.lastName',
              email: '$assignedToDoc.email',
            },
            null,
          ],
        },
        leadStage: {
          $ifNull: [
            {
              _id: '$leadStageDoc._id',
              name: '$leadStageDoc.name',
            },
            null,
          ],
        },
        leadScore: { $ifNull: ['$leadDoc.leadScore', null] },
        comment: { $ifNull: ['$comment', ''] },
      },
    },
    // Pagination
    { $skip: (page - 1) * limit },
    { $limit: limit },
  ];

  const results = await Activity.aggregate(pipeline);

  return {
    total,
    page,
    limit,
    totalPages,
    results: results as PropertyVisitor[],
  };
};
