/**
 * Property Update Triggers Service
 *
 * Non-blocking notification triggers that fire when properties are created
 * or updated. Designed to be called from the property update API route
 * after a successful write.
 *
 * Trigger functions:
 *   - handlePriceChange   — price drop alerts to matched clients
 *   - handleStatusChange  — cascade viewings cancellation + status alerts
 *   - handleNewListing    — match against active client requirements
 *
 * All functions swallow errors and log them — they must never cause the
 * parent API request to fail.
 *
 * @module services/real-estate/property-update-triggers
 */

import { prisma } from '@/lib/db';
import { createId } from '@paralleldrive/cuid2';
import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errors';
/** JsonValue — replaces JsonValue after Convex migration */
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
import {
  sendPriceDropAlert,
  sendStatusChangeAlert,
  sendNewMatchNotification,
} from '@/lib/services/real-estate/notification.service';

const log = logger.scope('PropertyUpdateTriggers');

// ── Statuses that indicate a property is no longer on the market ─────────────

const OFF_MARKET_STATUSES = ['SOLD', 'RENTED', 'OFF_MARKET'] as const;

// ── Score weights (mirrors property-matching.service.ts) ─────────────────────

const WEIGHTS = {
  PRICE: 30,
  BEDROOMS: 20,
  AREA: 25,
  PROPERTY_TYPE: 15,
  AMENITIES: 10,
} as const;

const MAX_SCORE =
  WEIGHTS.PRICE + WEIGHTS.BEDROOMS + WEIGHTS.AREA + WEIGHTS.PROPERTY_TYPE + WEIGHTS.AMENITIES;

const MATCH_THRESHOLD = 60;

// ═════════════════════════════════════════════════════════════════════════════
// handlePriceChange
// ═════════════════════════════════════════════════════════════════════════════

/**
 * Called when a property's price changes. If new price < old price, send
 * price drop alerts to all matched clients.
 */
export async function handlePriceChange(
  propertyId: string,
  oldPrice: number,
  newPrice: number,
): Promise<void> {
  try {
    if (newPrice >= oldPrice) {
      log.info('Price did not drop, skipping alert', {
        propertyId,
        oldPrice,
        newPrice,
      });
      return;
    }

    log.info('Price drop detected, sending alerts', {
      propertyId,
      oldPrice,
      newPrice,
      dropPercent: Math.round(((oldPrice - newPrice) / oldPrice) * 100),
    });

    await sendPriceDropAlert(propertyId, oldPrice, newPrice);
  } catch (error) {
    log.error('Failed to handle price change trigger', {
      propertyId,
      oldPrice,
      newPrice,
      error: getErrorMessage(error),
    });
  }
}

// ═════════════════════════════════════════════════════════════════════════════
// handleStatusChange
// ═════════════════════════════════════════════════════════════════════════════

/**
 * Called when a property's status changes. Handles cascading effects:
 * - AVAILABLE -> SOLD/RENTED: Cancel all pending viewings, notify matched clients
 * - AVAILABLE -> OFF_MARKET: Cancel all pending viewings
 * - Any -> AVAILABLE (re-listed): Notify previously interested clients
 */
export async function handleStatusChange(
  propertyId: string,
  oldStatus: string,
  newStatus: string,
  userId: string,
): Promise<void> {
  try {
    log.info('Property status change detected', {
      propertyId,
      oldStatus,
      newStatus,
      userId,
    });

    const isNowOffMarket = (OFF_MARKET_STATUSES as readonly string[]).includes(newStatus);
    const isRelisted = newStatus === 'AVAILABLE' && oldStatus !== 'AVAILABLE';

    // ── Cancel pending viewings when property leaves the market ───────────
    if (isNowOffMarket) {
      const cancelledViewings = await prisma.propertyViewing.updateMany({
        where: {
          propertyId,
          status: { in: ['REQUESTED', 'CONFIRMED'] },
        },
        data: {
          status: 'CANCELLED',
        },
      });

      log.info('Cancelled pending viewings for off-market property', {
        propertyId,
        newStatus,
        cancelledCount: cancelledViewings.count,
      });

      // Notify matched clients about the status change (SOLD / RENTED)
      await sendStatusChangeAlert(propertyId, newStatus);
    }

    // ── Notify previously interested clients about re-listing ────────────
    if (isRelisted) {
      const interestedMatches = await prisma.propertyMatch.findMany({
        where: {
          propertyId,
          status: { in: ['interested', 'viewed', 'sent'] },
        },
        select: {
          requirementId: true,
        },
      });

      if (interestedMatches.length > 0) {
        // Group by requirement and send notifications
        const requirementIds = [
          ...new Set(interestedMatches.map((m) => m.requirementId)),
        ];

        for (const requirementId of requirementIds) {
          await sendNewMatchNotification(requirementId, [propertyId]);
        }

        log.info('Notified previously interested clients about re-listing', {
          propertyId,
          clientCount: requirementIds.length,
        });
      }
    }
  } catch (error) {
    log.error('Failed to handle status change trigger', {
      propertyId,
      oldStatus,
      newStatus,
      error: getErrorMessage(error),
    });
  }
}

// ═════════════════════════════════════════════════════════════════════════════
// handleNewListing
// ═════════════════════════════════════════════════════════════════════════════

/**
 * Called after a new property is created. Run matching against all active
 * client requirements for this organization and send alerts for good
 * matches (score > 60).
 */
export async function handleNewListing(
  propertyId: string,
  userId: string,
): Promise<void> {
  try {
    // ── Fetch the newly created property ─────────────────────────────────
    const property = await prisma.property.findUnique({
      where: { id: propertyId },
    });

    if (!property) {
      log.warn('Property not found for new listing trigger', { propertyId });
      return;
    }

    // ── Fetch all active client requirements for this organization ───────
    const activeRequirements = await prisma.clientRequirements.findMany({
      where: {
        userId,
        isActive: true,
      },
    });

    if (activeRequirements.length === 0) {
      log.info('No active client requirements to match against', {
        propertyId,
        userId,
      });
      return;
    }

    log.info('Scoring new listing against client requirements', {
      propertyId,
      requirementCount: activeRequirements.length,
    });

    // ── Score each requirement against the new property ──────────────────
    let matchCount = 0;

    for (const req of activeRequirements) {
      const score = scorePropertyAgainstRequirement(property, req);

      if (score <= MATCH_THRESHOLD) continue;

      // Build match reasons for traceability
      const matchReasons = buildMatchReasons(property, req, score);

      // Create the PropertyMatch record
      await prisma.propertyMatch.upsert({
        where: {
          propertyId_requirementId: {
            propertyId: property.id,
            requirementId: req.id,
          },
        },
        update: {
          matchScore: score,
          matchReasons: JSON.parse(JSON.stringify(matchReasons)),
        },
        create: {
          id: createId(),
          propertyId: property.id,
          requirementId: req.id,
          matchScore: score,
          matchReasons: JSON.parse(JSON.stringify(matchReasons)),
          status: 'new',
        },
      });

      matchCount++;

      // Send notification for this match
      await sendNewMatchNotification(req.id, [property.id]);
    }

    log.info('New listing matching complete', {
      propertyId,
      totalRequirements: activeRequirements.length,
      matchesCreated: matchCount,
    });
  } catch (error) {
    log.error('Failed to handle new listing trigger', {
      propertyId,
      userId,
      error: getErrorMessage(error),
    });
  }
}

// ═════════════════════════════════════════════════════════════════════════════
// Internal scoring helpers
// ═════════════════════════════════════════════════════════════════════════════

/**
 * Lightweight scoring of a property against a single client requirement.
 *
 * Categories:
 *   Price in range:         +30 pts
 *   Bedrooms match:         +20 pts
 *   Preferred area match:   +25 pts
 *   Property type match:    +15 pts
 *   Must-have amenities:    +10 pts
 *
 * @returns 0-100 integer score
 */
function scorePropertyAgainstRequirement(
  property: {
    price: number;
    bedrooms: number | null;
    district: string | null;
    city: string;
    amenities: JsonValue;
    propertyType: string;
  },
  requirement: {
    minBudget: number | null;
    maxBudget: number | null;
    minBedrooms: number | null;
    maxBedrooms: number | null;
    preferredAreas: JsonValue;
    mustHaves: JsonValue;
    propertyTypes: JsonValue;
  },
): number {
  let total = 0;

  // ── Price (30 pts) ──────────────────────────────────────────────────────
  if (requirement.maxBudget != null) {
    const minBudget = requirement.minBudget ?? 0;
    if (property.price >= minBudget && property.price <= requirement.maxBudget) {
      total += WEIGHTS.PRICE;
    }
  } else {
    // No budget constraint -- full credit
    total += WEIGHTS.PRICE;
  }

  // ── Bedrooms (20 pts) ──────────────────────────────────────────────────
  if (requirement.minBedrooms != null && property.bedrooms != null) {
    const maxBeds = requirement.maxBedrooms ?? requirement.minBedrooms + 1;
    if (property.bedrooms >= requirement.minBedrooms && property.bedrooms <= maxBeds) {
      total += WEIGHTS.BEDROOMS;
    }
  } else if (requirement.minBedrooms == null) {
    total += WEIGHTS.BEDROOMS;
  }

  // ── Preferred area (25 pts) ────────────────────────────────────────────
  const preferredAreas = Array.isArray(requirement.preferredAreas)
    ? (requirement.preferredAreas as string[])
    : [];

  if (preferredAreas.length > 0) {
    const district = property.district?.toLowerCase() ?? '';
    const city = property.city.toLowerCase();

    const matchesDistrict = preferredAreas.some(
      (a) => a.toLowerCase() === district,
    );
    const matchesCity = preferredAreas.some(
      (a) => a.toLowerCase() === city,
    );

    if (matchesDistrict) {
      total += WEIGHTS.AREA;
    } else if (matchesCity) {
      total += Math.round(WEIGHTS.AREA * 0.4);
    }
  } else {
    total += WEIGHTS.AREA;
  }

  // ── Property type (15 pts) ─────────────────────────────────────────────
  const propertyTypes = Array.isArray(requirement.propertyTypes)
    ? (requirement.propertyTypes as string[])
    : [];

  if (propertyTypes.length > 0) {
    if (propertyTypes.includes(property.propertyType)) {
      total += WEIGHTS.PROPERTY_TYPE;
    }
  } else {
    total += WEIGHTS.PROPERTY_TYPE;
  }

  // ── Must-have amenities (10 pts) ──────────────────────────────────────
  const mustHaves = Array.isArray(requirement.mustHaves)
    ? (requirement.mustHaves as string[])
    : [];
  const propertyAmenities = Array.isArray(property.amenities)
    ? (property.amenities as string[])
    : [];

  if (mustHaves.length > 0) {
    const matched = mustHaves.filter((mh) =>
      propertyAmenities.some((a) => a.toLowerCase() === mh.toLowerCase()),
    );
    const ratio = matched.length / mustHaves.length;
    total += Math.round(WEIGHTS.AMENITIES * ratio);
  } else {
    total += WEIGHTS.AMENITIES;
  }

  // Normalize to 0-100 scale
  return Math.round((total / MAX_SCORE) * 100);
}

/**
 * Build a human-readable array of match reason strings for a given
 * property-requirement pair.
 */
function buildMatchReasons(
  property: {
    price: number;
    bedrooms: number | null;
    district: string | null;
    city: string;
    propertyType: string;
  },
  requirement: {
    minBudget: number | null;
    maxBudget: number | null;
    minBedrooms: number | null;
    maxBedrooms: number | null;
    preferredAreas: JsonValue;
    propertyTypes: JsonValue;
  },
  score: number,
): string[] {
  const reasons: string[] = [];

  // Price
  if (requirement.maxBudget != null) {
    const minBudget = requirement.minBudget ?? 0;
    if (property.price >= minBudget && property.price <= requirement.maxBudget) {
      reasons.push('Price within budget range');
    }
  }

  // Bedrooms
  if (requirement.minBedrooms != null && property.bedrooms != null) {
    const maxBeds = requirement.maxBedrooms ?? requirement.minBedrooms + 1;
    if (property.bedrooms >= requirement.minBedrooms && property.bedrooms <= maxBeds) {
      reasons.push(`${property.bedrooms} bedrooms matches requirement`);
    }
  }

  // Area
  const preferredAreas = Array.isArray(requirement.preferredAreas)
    ? (requirement.preferredAreas as string[])
    : [];
  if (preferredAreas.length > 0 && property.district) {
    if (preferredAreas.some((a) => a.toLowerCase() === property.district!.toLowerCase())) {
      reasons.push(`Located in preferred area: ${property.district}`);
    }
  }

  // Property type
  const propertyTypes = Array.isArray(requirement.propertyTypes)
    ? (requirement.propertyTypes as string[])
    : [];
  if (propertyTypes.length > 0 && propertyTypes.includes(property.propertyType)) {
    reasons.push(`Property type ${property.propertyType} matches preference`);
  }

  reasons.push(`Overall match score: ${score}%`);

  return reasons;
}
