/* eslint-disable camelcase */
import mongoose, { Types } from 'mongoose';

import {
  IProjectDoc,
  IProjectFilter,
  IUnitDoc,
  NewCreatedProject,
  PopulatedProject,
  UpdateProjectBody,
} from '@/modules/project/project.interface';
import { ApiError } from '@/shared/utils/errors';
import { PaginateOptions } from '@/shared/utils/plugins/paginate/paginate';
import { defaultStatus } from '@/shared/utils/responseCode/httpStatusAlias';
import responseCodes from '@/shared/utils/responseCode/responseCode';
import {
  buildSearchPipeline,
  findSimilarProjects,
  normalizeSearchQuery,
  processFilter,
} from './project.constant';
import Project from './project.model';
import { parsePopulateString, removeBlankKeys } from '@/shared/utils';
import { safeDeleteById } from '@/shared/utils/guard/ref-guard';
import Unit from './unit/unit.model';
import { createNotification } from '../notification/notification.service';
import { getObjectId, safeFallbackValues } from '@/shared/utils/commonHelper';
import {
  NotificationStatus,
  UserType,
} from '../notification/notification.constant';
import { Team } from '../teams/teams.model';
import { Lead } from '../lead/lead.model';
import { LeadInterestType } from '@/shared/constants/enum.constant';

const { ProjectResponseCodes } = responseCodes;

const TERMINAL_STAGE_NAMES = [
  'Lead Won',
  'Lead Lost',
  'lead won',
  'lead lost',
] as const;

/** Active leads linked to a project via shortlistedProject or buy lead `project`. */
const getActiveLeadCountByProjectIds = async (
  projectIds: Types.ObjectId[],
): Promise<Map<string, number>> => {
  const map = new Map<string, number>();
  if (!projectIds.length) return map;

  const rows = await Lead.aggregate([
    {
      $match: {
        $or: [
          { shortlistedProject: { $in: projectIds } },
          {
            $and: [
              { interestType: LeadInterestType.BUY },
              { project: { $in: projectIds } },
            ],
          },
        ],
      },
    },
    {
      $lookup: {
        from: 'leadstages',
        localField: 'leadStage',
        foreignField: '_id',
        as: '_stage',
      },
    },
    { $unwind: { path: '$_stage', preserveNullAndEmptyArrays: true } },
    {
      $match: {
        $or: [
          { _stage: null },
          { '_stage.stageName': { $nin: [...TERMINAL_STAGE_NAMES] } },
        ],
      },
    },
    {
      $addFields: {
        buyProjectArray: {
          $cond: [
            {
              $and: [
                { $eq: ['$interestType', LeadInterestType.BUY] },
                { $ne: [{ $ifNull: ['$project', null] }, null] },
              ],
            },
            ['$project'],
            [],
          ],
        },
      },
    },
    {
      $addFields: {
        projectLinks: {
          $setUnion: [{ $ifNull: ['$shortlistedProject', []] }, '$buyProjectArray'],
        },
      },
    },
    { $unwind: '$projectLinks' },
    { $match: { projectLinks: { $in: projectIds } } },
    { $group: { _id: '$projectLinks', activeLeads: { $sum: 1 } } },
  ]);

  for (const row of rows) {
    if (row._id) map.set(String(row._id), row.activeLeads);
  }
  return map;
};

const attachActiveLeadsToProjectResults = async (results: unknown[]) => {
  if (!Array.isArray(results) || !results.length) return;

  const ids = results
    .map((r: any) => r?._id ?? r?.id)
    .filter(Boolean)
    .map((id) => getObjectId(id));

  if (!ids.length) return;

  const counts = await getActiveLeadCountByProjectIds(ids);
  for (const r of results as any[]) {
    const key = String(r._id ?? r.id);
    r.activeLeads = counts.get(key) ?? 0;
  }
};

/**
 * Builds a visibility filter query for projects
 * @param userId - Current user ObjectId
 * @param userTeams - Array of user's team ObjectIds
 * @returns MongoDB query object for visibility filtering
 */
const buildVisibilityQuery = (userId?: Types.ObjectId, userTeams?: Types.ObjectId[]) => {
  return {
    $or: [
      // Default visibility: visibleToUsers and visibleToTeams are empty/null
      {
        $and: [
          {
            $or: [
              { visibleToUsers: { $exists: false } },
              { visibleToUsers: { $size: 0 } },
            ],
          },
          {
            $or: [
              { visibleToTeams: { $exists: false } },
              { visibleToTeams: { $size: 0 } },
            ],
          },
        ],
      },
      // Public visibility: visible to everyone
      { isPublicVisibility: true },
      // User is in visibleToUsers list
      { visibleToUsers: userId },
      // User's team is in visibleToTeams list
      { visibleToTeams: { $in: userTeams || [] } },
    ],
  };
};

export const createProject = async (
  data: NewCreatedProject,
): Promise<IProjectDoc | null> => {
  let project: IProjectDoc | null;

  try {
    if (data.team === undefined || (typeof data.team === 'string' && data.team === '')) {
      data.team = null as any;
    }

    if (data.projectName && !data.normalizedName)
      data.normalizedName = data.projectName.toLowerCase().trim();

    project = await Project.create(data);

    const populatedProject = await Project.findById(project._id)
      .populate({
        path: 'propertyType',
        select: 'name',
      })
      .lean<PopulatedProject>();

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

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

    if (
      teamLead &&
      teamLead?._id &&
      teamLead?.lead &&
      project?.isDraft === false
    )
      // send notification to Team manager and lead
      await createNotification(
        {
          title: 'New Project Added',
          description: `${safeFallbackValues(populatedProject?.projectName, 'Project')} is now live. Type: ${safeFallbackValues(
            populatedProject?.propertyType?.name,
          )}, Status: ${safeFallbackValues(populatedProject?.status)}`,
          userType: UserType.SELF,
          user: teamLead.lead as Types.ObjectId,
          status: NotificationStatus.SEND,
        },
        appRedirect,
      );
  } catch (error) {
    if (error.code === 11000)
      throw new ApiError(
        defaultStatus.FOUND,
        `Project with this name already exists in this location`,
        true,
        '',
        ProjectResponseCodes.PROJECT_DUPLICATE,
      );

    throw new ApiError(
      defaultStatus.OK,
      'Failed to create project',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }

  return project;
};

export const getProjectById = async (
  id: string,
  userId?: Types.ObjectId,
  userTeams?: Types.ObjectId[],
): Promise<IProjectDoc | null> => {
  const populateStr =
    'propertyType:name;subCategory:name;units;files;locality;paymentTerms;timelines;city:loc name;state:name;updatedBy:firstName email lastName createdAt updatedAt;amenities:name icon;createdBy:firstName email lastName createdAt updatedAt mediaUrls;companyId:name logo';
  const populateFields = parsePopulateString(populateStr);

  const query: any = { _id: id };

  // Add team-based access filter (backward compatibility)
  if (userTeams && userTeams.length > 0) {
    query.$or = [
      { team: { $in: userTeams } },
      { team: { $exists: false } },
      { team: null }
    ];
  } else {
    query.$or = [
      { team: { $exists: false } },
      { team: null }
    ];
  }

  // Add visibility filter (new feature)
  const visibilityQuery = buildVisibilityQuery(userId, userTeams);
  Object.assign(query, visibilityQuery);

  const project = await Project.findOne(query).populate(populateFields);

  if (!project)
    throw new ApiError(
      defaultStatus.OK,
      'Project not found or access denied',
      true,
      '',
      ProjectResponseCodes.PROJECT_NOT_FOUND,
    );

  return project;
};

export const updateProject = async (
  id: Types.ObjectId,
  updateData: UpdateProjectBody,
  userId?: Types.ObjectId,
  userTeams?: Types.ObjectId[],
): Promise<IProjectDoc | null> => {
  let project: IProjectDoc | null;

  try {
    const finalUpdateData = removeBlankKeys(updateData, { removeWhitespace: true }); 

    const query: any = { _id: id };

    // Add team-based access filter (backward compatibility)
    if (userTeams && userTeams.length > 0) {
      query.$or = [
        { team: { $in: userTeams } },
        { team: { $exists: false } },
        { team: null }
      ];
    } else {
      query.$or = [
        { team: { $exists: false } },
        { team: null }
      ];
    }

    // Add visibility filter (new feature)
    const visibilityQuery = buildVisibilityQuery(userId, userTeams);
    Object.assign(query, visibilityQuery);

    project = await Project.findOneAndUpdate(query, finalUpdateData, {
      new: true,
      runValidators: true,
    });
  } catch (_error) {
    throw new ApiError(
      defaultStatus.OK,
      'Failed to update project',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }

  if (!project)
    throw new ApiError(
      defaultStatus.OK,
      'Project not found or access denied',
      true,
      '',
      ProjectResponseCodes.PROJECT_NOT_FOUND,
    );

  return project;
};

export const updateProjectAmenities = async (
  id: Types.ObjectId,
  amenities: Types.ObjectId[],
): Promise<IProjectDoc | null> => {
  let project: IProjectDoc | null;

  try {
    project = await Project.findByIdAndUpdate(
      id,
      { amenities },
      {
        new: true,
        runValidators: false,
      },
    );
  } catch (_error) {
    console.log('🚀 ~ _error:', _error);
    throw new ApiError(
      defaultStatus.OK,
      'Failed to update project amenities',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }

  if (!project)
    throw new ApiError(
      defaultStatus.OK,
      'Project not found',
      true,
      '',
      ProjectResponseCodes.PROJECT_NOT_FOUND,
    );

  return project;
};

export const deleteProject = async (id: string, userId?: Types.ObjectId, userTeams?: Types.ObjectId[]): Promise<boolean> => {
  try {
    const query: any = { _id: id };

    // Add team-based access filter (backward compatibility)
    if (userTeams && userTeams.length > 0) {
      query.$or = [
        { team: { $in: userTeams } },
        { team: { $exists: false } },
        { team: null }
      ];
    } else {
      query.$or = [
        { team: { $exists: false } },
        { team: null }
      ];
    }

    // Add visibility filter (new feature)
    const visibilityQuery = buildVisibilityQuery(userId, userTeams);
    Object.assign(query, visibilityQuery);

    const project = await Project.findOne(query);
    if (!project) {
      throw new ApiError(
        defaultStatus.OK,
        'Project not found or access denied',
        true,
        '',
        ProjectResponseCodes.PROJECT_NOT_FOUND,
      );
    }

    await safeDeleteById(Project, id, ProjectResponseCodes.PROJECT_IN_USE);
    return true;
  } catch (_error) {
    throw new ApiError(
      defaultStatus.OK,
      'Failed to delete project',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }
};

export const queryProjects = async (
  filter: IProjectFilter = {},
  options: PaginateOptions = {},
) => {
  if (!options.fields)
    options.fields =
      'id projectName description propertyType pricePerSqYard locality city possessionStatus status createdBy createdAt units hasBlockLayout mediaUrls';
  const updatedFilter = processFilter(filter as Record<string, unknown>);
  if (filter.search) {
    const searchValue = filter.search as string;

    const pipeline = buildSearchPipeline(
      filter as Record<string, unknown>,
      searchValue,
      options.fields as string,
    );

    const response = await Project.paginate(
      {},
      {
        ...options,
        aggregation: pipeline,
      },
    );
    await attachActiveLeadsToProjectResults(response.results);
    await enrichProjectsWithUnitsAndPayments(response.results);
    return response;
  }
  const response = await Project.paginate(updatedFilter, options);
  await attachActiveLeadsToProjectResults(response.results);
  await enrichProjectsWithUnitsAndPayments(response.results);
  return response;
};

const enrichProjectsWithUnitsAndPayments = async (projects: any[]) => {
  if (!projects || projects.length === 0) return;
  
  // Get project IDs
  const projectIds = projects.map((p) => getObjectId(p.id || p._id));
  
  // Fetch units with payment data for all projects in one query
  const unitsWithPayments = await Unit.aggregate([
    { $match: { project: { $in: projectIds } } },
    
    // Lookup booking
    {
      $lookup: {
        from: 'unitbookingorholds',
        let: { unitId: '$_id' },
        pipeline: [
          { $match: { $expr: { $eq: ['$unit', '$$unitId'] } } },
          { $sort: { createdAt: -1 } },
          { $limit: 1 },
        ],
        as: 'bookingArray',
      },
    },
    { $addFields: { booking: { $arrayElemAt: ['$bookingArray', 0] } } },
    
    // Lookup customer (exclude cancelled)
    {
      $lookup: {
        from: 'customers',
        let: { bookingId: '$booking._id' },
        pipeline: [
          {
            $match: {
              $expr: {
                $and: [
                  { $eq: ['$unitBookingOrHold', '$$bookingId'] },
                  { $ne: ['$bookingStatus', 'cancelled'] },
                ],
              },
            },
          },
          { $limit: 1 },
        ],
        as: 'customerArray',
      },
    },
    { $addFields: { customer: { $arrayElemAt: ['$customerArray', 0] } } },
    
    // Lookup payments
    {
      $lookup: {
        from: 'customerpayments',
        let: { customerId: '$customer._id' },
        pipeline: [
          { $match: { $expr: { $eq: ['$customerId', '$$customerId'] } } },
        ],
        as: 'payments',
      },
    },
    
    // Add payment fields
    {
      $addFields: {
        paymentReceived: { $sum: '$payments.paidAmount' },
        paymentDue: {
          $subtract: [
            { $sum: '$payments.totalAmount' },
            { $sum: '$payments.paidAmount' },
          ],
        },
      },
    },
    
    // Clean up
    {
      $project: {
        bookingArray: 0,
        booking: 0,
        customerArray: 0,
        customer: 0,
        payments: 0,
      },
    },
  ]);
  
  // Group units by project ID
  const unitsByProjectId = new Map<string, any[]>();
  unitsWithPayments.forEach((unit: any) => {
    const pid = unit.project.toString();
    if (!unitsByProjectId.has(pid)) {
      unitsByProjectId.set(pid, []);
    }
    unitsByProjectId.get(pid)!.push(unit);
  });
  
  // Attach enriched units to each project
  projects.forEach((project) => {
    const pid = (project.id || project._id).toString();
    project.units = unitsByProjectId.get(pid) || [];
  });
};

export const searchProjectsWithAutocomplete = async (
  searchQuery: string,
  limit: number = 10,
  companyId?: Types.ObjectId,
  userId?: Types.ObjectId,
  userTeams?: Types.ObjectId[],
) => {
  try {
    const normalized = normalizeSearchQuery(searchQuery);

    const query: any = {
      companyId,
      $or: [
        { normalizedName: { $regex: normalized, $options: 'i' } },
        { projectName: { $regex: searchQuery, $options: 'i' } },
      ],
    };

    // Add team-based access filter (backward compatibility)
    if (userTeams && userTeams.length > 0) {
      query.$and = [
        {
          $or: [
            { team: { $in: userTeams } },
            { team: { $exists: false } },
            { team: null }
          ],
        },
      ];
    } else {
      query.$and = [
        {
          $or: [
            { team: { $exists: false } },
            { team: null }
          ],
        },
      ];
    }

    // Add visibility filter (new feature)
    const visibilityQuery = buildVisibilityQuery(userId, userTeams);
    Object.assign(query, visibilityQuery);

    // Search for projects
    const projects = await Project.find(query)
      .select('projectName _id type')
      .limit(limit)
      .sort({ searchCount: -1, createdAt: -1 });

    if (projects.length > 0)
      await Project.updateMany(
        { _id: { $in: projects.map((p) => p._id) } },
        { $inc: { searchCount: 1 } },
      );

    const formattedProjects = projects.map((project) => ({
      value: project._id.toString(),
      label: project.projectName,
      type: project.type,
    }));

    return {
      data: formattedProjects,
      total: projects.length,
      query: searchQuery,
    };
  } catch (_error) {
    throw new ApiError(
      defaultStatus.BAD_REQUEST,
      'Failed to search projects',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }
};

const calculateUnitStats = async (units: IUnitDoc[], companyId?: Types.ObjectId) => {
  const totalUnits = units.length;

  const availableUnits = units.filter((u) => u.status === 'available').length;

  const soldUnits = units.filter((u) => u.status === 'sold').length;

  // Get actual payment data from customers instead of unit prices
  const unitIds = units.map((u) => u._id);
  
  const paymentStats = await mongoose.model('Customer').aggregate([
    {
      $match: {
        ...(companyId && { company: companyId }),
        unitBookingOrHold: { $exists: true, $ne: null },
        bookingStatus: { $ne: 'cancelled' },
      },
    },
    {
      $lookup: {
        from: 'unitbookingorholds',
        localField: 'unitBookingOrHold',
        foreignField: '_id',
        as: 'booking',
      },
    },
    { $unwind: '$booking' },
    {
      $match: {
        'booking.unit': { $in: unitIds },
      },
    },
    {
      $lookup: {
        from: 'customerpayments',
        localField: '_id',
        foreignField: 'customerId',
        as: 'payments',
      },
    },
    {
      $lookup: {
        from: 'units',
        localField: 'booking.unit',
        foreignField: '_id',
        as: 'unit',
      },
    },
    { $unwind: { path: '$unit', preserveNullAndEmptyArrays: true } },
    {
      $group: {
        _id: '$unit.status',
        totalPaid: { $sum: { $sum: '$payments.paidAmount' } },
        totalDue: {
          $sum: {
            $subtract: [
              { $sum: '$payments.totalAmount' },
              { $sum: '$payments.paidAmount' },
            ],
          },
        },
      },
    },
  ]);
  
  const soldStats = paymentStats.find((s) => s._id === 'sold') || { totalPaid: 0, totalDue: 0 };
  const holdStats = paymentStats.find((s) => s._id === 'hold') || { totalPaid: 0, totalDue: 0 };

  const paymentReceived = soldStats.totalPaid  + holdStats.totalPaid;
  const paymentReceivable = soldStats.totalDue + holdStats.totalDue;

  return {
    totalUnits,
    availableUnits,
    soldUnits,
    paymentReceived,
    paymentReceivable,
  };
};

export const projectAnalytics = async (companyId?: Types.ObjectId) => {
  // Step 1: Find all projects for this company
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const projects: any[] = await Project.find(
    companyId ? { companyId } : {},
  ).lean();

  if (!projects.length)
    return {
      totalProjects: 0,
      totalUnits: 0,
      availableUnits: 0,
      soldUnits: 0,
      statusCounts: {
        under_construction: 0,
        possession_soon: 0,
        ready_possession: 0,
        launch: 0,
      },
      totalActiveLeads: 0,
      totalPaymentReceivable: 0,
      totalPaymentReceived: 0,
      totalProjectValue: 0,
    };

  // Step 2: Collect project IDs
  const projectIds = projects.map((p) => p._id);

  // Step 3: Fetch all units for these projects
  const units: IUnitDoc[] = await Unit.find({
    project: { $in: projectIds },
  }).lean();

  // Step 4: Calculate unit stats
  const {
    totalUnits,
    availableUnits,
    soldUnits,
    paymentReceived,
    paymentReceivable,
  } = await calculateUnitStats(units, companyId);

  // Step 5: Status counts
  const statusCounts = projects.reduce(
    (acc, project) => {
      acc[project.status] = (acc[project.status] || 0) + 1;
      return acc;
    },
    {
      under_construction: 0,
      possession_soon: 0,
      ready_possession: 0,
      launch: 0,
    } as Record<IProjectDoc['status'], number>,
  );

  const activeLeadsAgg = await Lead.aggregate([
    {
      $match: {
        $or: [
          { shortlistedProject: { $in: projectIds } },
          {
            $and: [
              { interestType: LeadInterestType.BUY },
              { project: { $in: projectIds } },
            ],
          },
        ],
      },
    },
    {
      $lookup: {
        from: 'leadstages',
        localField: 'leadStage',
        foreignField: '_id',
        as: '_stage',
      },
    },
    { $unwind: { path: '$_stage', preserveNullAndEmptyArrays: true } },
    {
      $match: {
        $or: [
          { _stage: null },
          { '_stage.stageName': { $nin: [...TERMINAL_STAGE_NAMES] } },
        ],
      },
    },
    { $group: { _id: '$_id' } },
    { $count: 'total' },
  ]);
  const totalActiveLeads = activeLeadsAgg[0]?.total ?? 0;

  return {
    totalProjects: projects.length,
    totalUnits,
    availableUnits,
    soldUnits,
    statusCounts,
    totalActiveLeads,
    totalPaymentReceivable: paymentReceivable,
    totalPaymentReceived: paymentReceived,
    totalProjectValue: paymentReceivable + paymentReceived,
  };
};

export const createProjectFromSearch = async (
  searchQuery: string,
  userId: Types.ObjectId,
  companyId: Types.ObjectId,
  location?: { area?: string; city?: string; state?: string },
) => {
  try {
    const { exact } = await findSimilarProjects(searchQuery, location);

    if (exact)
      return {
        data: {
          value: exact._id.toString(),
          label: exact.projectName,
          type: exact.type,
        },
        message: 'Existing project found',
      };

    const newProject = await Project.create({
      projectName: searchQuery,
      normalizedName: searchQuery.toLowerCase().trim(),
      createdBy: userId,
      companyId: companyId,
      isSystemGenerated: true,
      searchCount: 1,
      isDraft: true,
      locality: getObjectId(location?.area),
      city: getObjectId(location?.city),
      state: getObjectId(location?.state),
    });

    return {
      data: {
        value: newProject._id.toString(),
        label: newProject.projectName,
        type: newProject.type,
      },
      message: 'New project created from search',
    };
  } catch (_error) {
    throw new ApiError(
      defaultStatus.OK,
      'Failed to create project from search',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }
};

/**
 * Updates the visibility settings for a project
 * @param projectId - Project ObjectId
 * @param visibleToUsers - Array of user ObjectIds
 * @param visibleToTeams - Array of team ObjectIds
 * @returns Updated project
 */
export const updateProjectVisibility = async (
  projectId: Types.ObjectId,
  visibleToUsers: Types.ObjectId[],
  visibleToTeams: Types.ObjectId[],
): Promise<IProjectDoc | null> => {
  try {
    const project = await Project.findByIdAndUpdate(
      projectId,
      {
        visibleToUsers,
        visibleToTeams,
      },
      { new: true, runValidators: true },
    );

    if (!project)
      throw new ApiError(
        defaultStatus.OK,
        'Project not found',
        true,
        '',
        ProjectResponseCodes.PROJECT_NOT_FOUND,
      );

    return project;
  } catch (_error) {
    throw new ApiError(
      defaultStatus.OK,
      'Failed to update project visibility',
      true,
      '',
      ProjectResponseCodes.PROJECT_ERROR,
    );
  }
};

/**
 * Updates the public visibility setting for multiple projects
 * @param projectIds - Array of project ObjectIds
 * @param isPublicVisibility - Boolean flag for visibility (true = public, false = private)
 * @returns Object with success and failed updates
 */
export const updateProjectsPublicVisibility = async (
  projectIds: string[],
  isPublicVisibility: boolean,
): Promise<{ updated: string[]; failed: { id: string; error: string }[] }> => {
  const updated: string[] = [];
  const failed: { id: string; error: string }[] = [];

  for (const projectId of projectIds) {
    try {
      await Project.findByIdAndUpdate(
        projectId,
        { isPublicVisibility },
        { new: true, runValidators: true },
      );
      updated.push(projectId);
    } catch (_error) {
      failed.push({ id: projectId, error: 'Failed to update visibility' });
    }
  }

  return { updated, failed };
};
