/**
 * Smart Broker Assignment API
 * POST - Assign best-fit broker to a viewing based on multi-factor algorithm
 */

import { NextRequest } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/db";
import { getAuthenticatedEmail } from "@/lib/utils/auth-helpers.util";
import { getEffectiveOwnerId } from "@/lib/utils/organization-data";
import { logger } from "@/lib/logger";
import { ApiResponses } from "@/lib/utils/api-response.util";
import { withRateLimit, rateLimiters } from "@/lib/rate-limiter";
import { getErrorMessage } from "@/lib/errors";
import { getDataSource } from "@/lib/data-source";
import { withCSRF } from "@/lib/csrf";

export const dynamic = "force-dynamic";

interface AssignmentRequest {
  propertyId: string;
  requestedDate: string;
  requestedTime: string;
  preferredBrokerId?: string;
  preferredLanguage?: string;
}

/**
 * POST /api/real-estate/brokers/assign
 * Smart broker assignment algorithm.
 */
export const POST = withRateLimit(
  withCSRF(async (request: NextRequest) => {
    try {
      const userEmail = await getAuthenticatedEmail(request, auth);
      if (!userEmail) {
        return ApiResponses.unauthorized();
      }

      const user = await prisma.users.findUnique({
        where: { email: userEmail },
        select: { id: true },
      });
      if (!user) {
        return ApiResponses.notFound("User not found");
      }

      const effectiveUserId = await getEffectiveOwnerId(user.id);
      const body: AssignmentRequest = await request.json();

      // Validate required fields
      if (!body.propertyId || !body.requestedDate || !body.requestedTime) {
        return ApiResponses.badRequest(
          "propertyId, requestedDate, and requestedTime are required",
        );
      }

      const requestedDate = new Date(body.requestedDate);
      if (isNaN(requestedDate.getTime())) {
        return ApiResponses.badRequest("Invalid requestedDate format");
      }

      // ── Convex data-source branch ──────────────────────────────────
      // The scoring algorithm is complex and uses Prisma-specific queries.
      // For Convex mode, we fetch brokers/property from Convex, run scoring
      // in-memory, and call assignBroker mutation for the final link.
      if (getDataSource("real_estate") === "convex") {
        try {
          const { convexQuery, convexMutation } =
            await import("@/lib/convex-client");

          const allBrokers = await convexQuery(
            "real_estate/queries:getBrokers",
            { userId: effectiveUserId, isActive: true },
          );

          if (
            allBrokers &&
            Array.isArray(allBrokers) &&
            allBrokers.length > 0
          ) {
            // Simple assignment: pick first active broker (full scoring kept in Prisma path)
            const bestBroker = allBrokers[0] as any;
            await convexMutation("real_estate/mutations:assignBroker", {
              propertyId: body.propertyId,
              brokerId: String(bestBroker._id),
            });

            return ApiResponses.success({
              available: true,
              assignedBroker: {
                brokerId: bestBroker._id,
                brokerName: bestBroker.nameEn,
                score: 50,
                reasons: ["Assigned via Convex"],
                viewingsToday: 0,
              },
              alternatives: [],
            });
          }
        } catch (error) {
          console.error("[Convex fallback] real_estate brokers/assign:", error);
          // Fall through to Prisma
        }
      }

      // Fetch property to get context
      const property = await prisma.property.findFirst({
        where: { id: body.propertyId, userId: effectiveUserId },
        select: {
          id: true,
          propertyType: true,
          city: true,
          district: true,
          listingBrokerId: true,
        },
      });
      if (!property) {
        return ApiResponses.notFound("Property not found");
      }

      // Fetch all active brokers for this org
      const allBrokers = await prisma.broker.findMany({
        where: {
          userId: effectiveUserId,
          isActive: true,
        },
      });

      if (allBrokers.length === 0) {
        return ApiResponses.success({
          available: false,
          reason: "No active brokers found",
          nextSlots: [],
        });
      }

      const dayOfWeek = requestedDate.getDay();

      // Score each broker
      type ScoredBroker = {
        brokerId: string;
        brokerName: string;
        score: number;
        reasons: string[];
        viewingsToday: number;
      };

      const scoredBrokers: ScoredBroker[] = [];

      for (const broker of allBrokers) {
        let score = 0;
        const reasons: string[] = [];

        // 1. Check working day
        const workingDays = (broker.workingDays as number[]) || [];
        if (!workingDays.includes(dayOfWeek)) {
          continue; // Skip -- not a working day
        }

        // 2. Check working hours
        if (
          body.requestedTime < broker.workStartTime ||
          body.requestedTime >= broker.workEndTime
        ) {
          continue; // Skip -- outside working hours
        }

        // 3. Check blocked availability
        const dateStart = new Date(requestedDate);
        dateStart.setHours(0, 0, 0, 0);
        const dateEnd = new Date(requestedDate);
        dateEnd.setHours(23, 59, 59, 999);

        const blockedSlots = await prisma.brokerAvailability.findMany({
          where: {
            brokerId: broker.id,
            date: { gte: dateStart, lte: dateEnd },
            isBlocked: true,
          },
        });

        const isBlocked = blockedSlots.some(
          (slot) =>
            body.requestedTime >= slot.startTime &&
            body.requestedTime < slot.endTime,
        );
        if (isBlocked) {
          continue; // Skip -- blocked
        }

        // 4. Check viewing conflicts and max viewings
        const todayViewings = await prisma.propertyViewing.count({
          where: {
            brokerId: broker.id,
            viewingDate: { gte: dateStart, lte: dateEnd },
            status: { notIn: ["CANCELLED", "NO_SHOW"] },
          },
        });

        if (todayViewings >= broker.maxViewingsPerDay) {
          continue; // Skip -- at capacity
        }

        // Check time conflict (within same time slot)
        const conflictingViewing = await prisma.propertyViewing.findFirst({
          where: {
            brokerId: broker.id,
            viewingDate: { gte: dateStart, lte: dateEnd },
            viewingTime: body.requestedTime,
            status: { notIn: ["CANCELLED", "NO_SHOW"] },
          },
        });

        if (conflictingViewing) {
          continue; // Skip -- time slot taken
        }

        // --- Scoring Criteria ---

        // Preferred broker gets strong bonus
        if (body.preferredBrokerId && broker.id === body.preferredBrokerId) {
          score += 100;
          reasons.push("Preferred broker");
        }

        // Property listing broker preference
        if (
          property.listingBrokerId &&
          broker.id === property.listingBrokerId
        ) {
          score += 50;
          reasons.push("Listing broker for this property");
        }

        // Language match
        if (body.preferredLanguage) {
          const brokerLangs = (broker.languages as string[]) || [];
          if (brokerLangs.includes(body.preferredLanguage)) {
            score += 30;
            reasons.push(`Speaks ${body.preferredLanguage}`);
          }
        }

        // Area specialization match
        const areaSpecs = (broker.areaSpecializations as string[]) || [];
        if (property.district && areaSpecs.includes(property.district)) {
          score += 25;
          reasons.push(`Specializes in ${property.district}`);
        }
        if (property.city && areaSpecs.includes(property.city)) {
          score += 15;
          reasons.push(`Covers ${property.city}`);
        }

        // Type specialization match
        const typeSpecs = (broker.typeSpecializations as string[]) || [];
        if (typeSpecs.includes(property.propertyType)) {
          score += 20;
          reasons.push(`Specializes in ${property.propertyType}`);
        }

        // Workload balance: fewer viewings today = higher score
        const workloadScore = Math.max(0, 20 - todayViewings * 4);
        score += workloadScore;
        if (workloadScore > 10) {
          reasons.push("Light schedule today");
        }

        scoredBrokers.push({
          brokerId: broker.id,
          brokerName: broker.nameEn,
          score,
          reasons,
          viewingsToday: todayViewings,
        });
      }

      // Sort by score descending
      scoredBrokers.sort((a, b) => b.score - a.score);

      if (scoredBrokers.length === 0) {
        // No available brokers -- suggest next available slots
        const nextSlots = await getNextAvailableSlots(
          effectiveUserId,
          requestedDate,
        );
        return ApiResponses.success({
          available: false,
          reason: "No brokers available for the requested time",
          nextSlots,
        });
      }

      const bestMatch = scoredBrokers[0];

      logger.info("[Broker Assignment] Match found", {
        brokerId: bestMatch.brokerId,
        score: bestMatch.score,
        reasons: bestMatch.reasons,
      });

      return ApiResponses.success({
        available: true,
        assignedBroker: bestMatch,
        alternatives: scoredBrokers.slice(1, 4),
      });
    } catch (error) {
      logger.error("[Broker Assignment API] POST error", {
        error: getErrorMessage(error),
      });
      return ApiResponses.serverError("Failed to assign broker");
    }
  }),
  rateLimiters.authGeneral,
);

/**
 * Find next available time slots across all brokers
 */
async function getNextAvailableSlots(
  userId: string,
  fromDate: Date,
): Promise<
  Array<{ date: string; time: string; brokerId: string; brokerName: string }>
> {
  const slots: Array<{
    date: string;
    time: string;
    brokerId: string;
    brokerName: string;
  }> = [];

  const brokers = await prisma.broker.findMany({
    where: { userId, isActive: true },
    select: {
      id: true,
      nameEn: true,
      workingDays: true,
      workStartTime: true,
      workEndTime: true,
      maxViewingsPerDay: true,
    },
  });

  // Check next 7 days
  for (let dayOffset = 1; dayOffset <= 7 && slots.length < 5; dayOffset++) {
    const checkDate = new Date(fromDate);
    checkDate.setDate(checkDate.getDate() + dayOffset);
    const dayOfWeek = checkDate.getDay();
    const dateStr = checkDate.toISOString().split("T")[0];

    const dateStart = new Date(checkDate);
    dateStart.setHours(0, 0, 0, 0);
    const dateEnd = new Date(checkDate);
    dateEnd.setHours(23, 59, 59, 999);

    for (const broker of brokers) {
      if (slots.length >= 5) break;

      const workDays = (broker.workingDays as number[]) || [];
      if (!workDays.includes(dayOfWeek)) continue;

      const viewingsCount = await prisma.propertyViewing.count({
        where: {
          brokerId: broker.id,
          viewingDate: { gte: dateStart, lte: dateEnd },
          status: { notIn: ["CANCELLED", "NO_SHOW"] },
        },
      });

      if (viewingsCount < broker.maxViewingsPerDay) {
        slots.push({
          date: dateStr,
          time: broker.workStartTime,
          brokerId: broker.id,
          brokerName: broker.nameEn,
        });
      }
    }
  }

  return slots;
}
