/**
 * Rule-based deal-health scoring. NO LLMs - every output traces back to a
 * dated/structured input the rep can see and contest. The chip displayed
 * on the interest header exposes the per-signal breakdown via tooltip so
 * an anti-AI stakeholder reading the screen never sees a black box.
 *
 * Inputs (all already available on getInterestById):
 *   - pipelineStage + per-doc sub-status (eoiDocStatus, etc.)
 *   - dateLastContact, dateFirstContact, dateEoiSent, dateEoiSigned,
 *     dateReservationSigned, dateContractSent, dateContractSigned,
 *     dateDepositReceived
 *   - depositExpectedAmount (numeric string)
 *
 * Scoring rubric (0–100, higher is healthier):
 *   Base 50.
 *   +5  if any activity log entry landed in the last 7 days (active engagement).
 *   +20 if the rep has logged contact in the last 7 days.
 *   +10 if contact within 14 days (and the 7d bonus didn't fire).
 *   -15 if no contact logged in 30+ days.
 *   -10 if the deal is older than 30d and still in 'enquiry' or 'qualified'.
 *   +10 for each stage past enquiry the deal has reached (capped at +30).
 *   -10 if EOI was sent more than 14d ago and isn't signed yet.
 *   -10 if reservation was signed but no deposit recorded in 21d.
 *   -10 if contract was sent more than 14d ago and isn't signed yet.
 *   +5 if outcome is 'won' (sanity bump, though won deals don't show this).
 *
 * Score buckets:
 *   ≥70 → 'hot'    (green; rep is on top of it)
 *   40-69 → 'warm' (amber; needs attention)
 *   <40 → 'cold'   (rose; at risk)
 *
 * The full signals[] array is surfaced to the UI so the tooltip can render
 * "+20 contacted 3 days ago", "-10 EOI awaiting signature 19d" etc.
 */

import type { PipelineStage } from '@/lib/constants';
import { PIPELINE_STAGES } from '@/lib/constants';

export interface DealHealthInput {
  pipelineStage: string;
  outcome?: string | null;
  archivedAt?: string | null;
  dateFirstContact?: string | Date | null;
  dateLastContact?: string | Date | null;
  dateEoiSent?: string | Date | null;
  dateEoiSigned?: string | Date | null;
  dateReservationSigned?: string | Date | null;
  dateContractSent?: string | Date | null;
  dateContractSigned?: string | Date | null;
  dateDepositReceived?: string | Date | null;
  eoiDocStatus?: string | null;
  reservationDocStatus?: string | null;
  contractDocStatus?: string | null;
  /** Optional: count of contact_log entries in the last 7 days. Drives the
   *  +5 "active engagement" signal. When omitted the signal is skipped - keep
   *  the scoring function pure / synchronous so the chip can render without a
   *  separate fetch on every interest list row. */
  recentActivityCount?: number | null;
  /** Phase 2 - risk signals captured in deal-pulse-trigger-audit.md.
   *  Any of these populated → strong negative signal pushed onto the
   *  chip so reps can triage cooling deals at a glance. All optional;
   *  callers populate from existing schema (document_events for
   *  declines, reservations.cancelled_at, interest_berths conflict). */
  dateDocumentDeclined?: string | Date | null;
  dateReservationCancelled?: string | Date | null;
  dateBerthSoldToOther?: string | Date | null;
  /** Optional per-port config that lets admins disable individual
   *  signals or rename their tier labels. When omitted, defaults
   *  apply - current callers stay byte-identical without changes. */
  config?: DealHealthConfig | null;
}

export interface DealHealthConfig {
  enabled?: boolean;
  signals?: Partial<Record<string, boolean>>;
  labels?: {
    hot?: string;
    warm?: string;
    cold?: string;
  };
}

export interface DealHealthSignal {
  /** Stable id useful for keying the tooltip rows. */
  id: string;
  /** +N or -N (signed integer for explicit math). */
  delta: number;
  /** Plain-English explanation surfaced in the tooltip. */
  detail: string;
}

export interface DealHealth {
  score: number;
  pulse: 'cold' | 'warm' | 'hot';
  signals: DealHealthSignal[];
}

function daysSince(iso: string | Date | null | undefined): number | null {
  if (!iso) return null;
  const t = iso instanceof Date ? iso.getTime() : new Date(iso).getTime();
  if (Number.isNaN(t)) return null;
  return Math.floor((Date.now() - t) / 86_400_000);
}

function signalEnabled(input: DealHealthInput, signalId: string): boolean {
  const flag = input.config?.signals?.[signalId];
  return flag !== false;
}

export function computeDealHealth(input: DealHealthInput): DealHealth {
  let score = 50;
  const signals: DealHealthSignal[] = [];

  // Master toggle - admin can hide the chip entirely per-port.
  // Returning the neutral shape keeps callers happy; the chip uses
  // a separate "visible" prop derived from config.enabled before
  // calling compute. We still return real data so reports can read it.

  // Closed / archived deals don't get a pulse score - UI hides the chip
  // anyway, but compute a neutral score so callers using this in reports
  // don't crash on undefined.
  if (input.archivedAt || input.outcome) {
    return { score: 50, pulse: 'warm', signals: [] };
  }

  // Active engagement: counts every distinct activity-log entry in the last
  // 7 days. Surfaces "the rep is actively working this deal" separately from
  // the coarse dateLastContact bump (which only moves on the most-recent
  // entry's date). A 5-call week scores +5 once; we don't double-count.
  if (input.recentActivityCount !== null && input.recentActivityCount !== undefined) {
    if (input.recentActivityCount >= 1) {
      score += 5;
      signals.push({
        id: 'active_engagement',
        delta: +5,
        detail: `${input.recentActivityCount} activity log entr${
          input.recentActivityCount === 1 ? 'y' : 'ies'
        } in the last 7d - rep is engaged.`,
      });
    }
  }

  const contactDays = daysSince(input.dateLastContact);
  if (contactDays !== null) {
    if (contactDays <= 7) {
      score += 20;
      signals.push({
        id: 'contact_recent',
        delta: +20,
        detail: `Contact logged ${contactDays}d ago - fresh.`,
      });
    } else if (contactDays <= 14) {
      score += 10;
      signals.push({
        id: 'contact_warm',
        delta: +10,
        detail: `Contact logged ${contactDays}d ago - still warm.`,
      });
    } else if (contactDays >= 30) {
      score -= 15;
      signals.push({
        id: 'contact_stale',
        delta: -15,
        detail: `No contact logged in ${contactDays}d - going cold.`,
      });
    }
  }

  // Stage progress: every step past enquiry signals momentum.
  const stageIdx = PIPELINE_STAGES.indexOf(input.pipelineStage as PipelineStage);
  if (stageIdx > 0) {
    const bonus = Math.min(30, stageIdx * 10);
    score += bonus;
    signals.push({
      id: 'stage_progress',
      delta: +bonus,
      detail: `Reached ${input.pipelineStage.replace(/_/g, ' ')} stage.`,
    });
  }

  // Age penalty for stuck top-of-funnel leads.
  const firstDays = daysSince(input.dateFirstContact);
  if (
    firstDays !== null &&
    firstDays >= 30 &&
    (input.pipelineStage === 'enquiry' || input.pipelineStage === 'qualified')
  ) {
    score -= 10;
    signals.push({
      id: 'stuck_top_funnel',
      delta: -10,
      detail: `Deal opened ${firstDays}d ago and still pre-EOI.`,
    });
  }

  // EOI in-flight too long.
  const eoiSentDays = daysSince(input.dateEoiSent);
  if (
    eoiSentDays !== null &&
    eoiSentDays >= 14 &&
    input.eoiDocStatus !== 'signed' &&
    !input.dateEoiSigned
  ) {
    score -= 10;
    signals.push({
      id: 'eoi_awaiting',
      delta: -10,
      detail: `EOI awaiting signature for ${eoiSentDays}d.`,
    });
  }

  // Reservation signed but deposit not received.
  const reservationDays = daysSince(input.dateReservationSigned);
  if (reservationDays !== null && reservationDays >= 21 && !input.dateDepositReceived) {
    score -= 10;
    signals.push({
      id: 'deposit_pending',
      delta: -10,
      detail: `Reservation signed ${reservationDays}d ago but deposit not recorded.`,
    });
  }

  // Contract awaiting signature.
  const contractSentDays = daysSince(input.dateContractSent);
  if (
    contractSentDays !== null &&
    contractSentDays >= 14 &&
    input.contractDocStatus !== 'signed' &&
    !input.dateContractSigned
  ) {
    score -= 10;
    signals.push({
      id: 'contract_awaiting',
      delta: -10,
      detail: `Contract awaiting signature for ${contractSentDays}d.`,
    });
  }

  // Phase 2 - positive momentum signals.
  // EOI sent recently: forward motion that the original score didn't
  // surface (the awaiting penalty only fires after 14d). Brightens the
  // chip for fresh-EOI deals so reps see progress.
  const eoiSentDaysPos = daysSince(input.dateEoiSent);
  if (eoiSentDaysPos !== null && eoiSentDaysPos <= 14 && signalEnabled(input, 'eoi_sent_recent')) {
    score += 5;
    signals.push({
      id: 'eoi_sent_recent',
      delta: +5,
      detail: `EOI sent ${eoiSentDaysPos}d ago - awaiting signature.`,
    });
  }

  // Deposit received: near-commit signal. Was previously invisible on
  // the chip even though it's one of the strongest forward signals.
  const depositDays = daysSince(input.dateDepositReceived);
  if (depositDays !== null && signalEnabled(input, 'deposit_received')) {
    score += 15;
    signals.push({
      id: 'deposit_received',
      delta: +15,
      detail: `Deposit received ${depositDays}d ago.`,
    });
  }

  // Contract signed: closed-loop reinforcement. The interest's outcome
  // flips to 'won' shortly after, but until that happens the contract
  // signature is a strong positive signal.
  const contractSignedDays = daysSince(input.dateContractSigned);
  if (contractSignedDays !== null && signalEnabled(input, 'contract_signed')) {
    score += 10;
    signals.push({
      id: 'contract_signed',
      delta: +10,
      detail: `Contract signed ${contractSignedDays}d ago.`,
    });
  }

  // Phase 2 - risk signals. These are the strongest cooling indicators
  // and previously didn't move the chip at all, leaving reps to discover
  // them by clicking into the detail page.

  // Document declined (EOI/contract/reservation rejected by the client).
  const declinedDays = daysSince(input.dateDocumentDeclined);
  if (declinedDays !== null && signalEnabled(input, 'document_declined')) {
    score -= 25;
    signals.push({
      id: 'document_declined',
      delta: -25,
      detail: `Client declined a document ${declinedDays}d ago - intervene.`,
    });
  }

  // Reservation cancelled: booked-then-cancelled is a high-value warning.
  const reservationCancelledDays = daysSince(input.dateReservationCancelled);
  if (reservationCancelledDays !== null && signalEnabled(input, 'reservation_cancelled')) {
    score -= 20;
    signals.push({
      id: 'reservation_cancelled',
      delta: -20,
      detail: `Reservation cancelled ${reservationCancelledDays}d ago.`,
    });
  }

  // Berth resold to a different deal - this interest is effectively dead
  // (the asset they wanted is gone). Sharp drop so the chip turns cold.
  const berthSoldDays = daysSince(input.dateBerthSoldToOther);
  if (berthSoldDays !== null && signalEnabled(input, 'berth_sold_to_other')) {
    score -= 30;
    signals.push({
      id: 'berth_sold_to_other',
      delta: -30,
      detail: `Primary berth was sold to a different deal ${berthSoldDays}d ago.`,
    });
  }

  // Clamp to [0, 100].
  score = Math.max(0, Math.min(100, score));

  const pulse: DealHealth['pulse'] = score >= 70 ? 'hot' : score >= 40 ? 'warm' : 'cold';
  return { score, pulse, signals };
}
