/**
 * Public-API berth shape + mapper.
 *
 * The public website's `/server/utils/berths.ts` previously read directly
 * from the legacy NocoDB Berths table; after Phase 3 it calls the CRM
 * `/api/public/berths` endpoint. The response shape must be a verbatim
 * match for the NocoDB output so the website's Vue templates and
 * `Berth` type don't need code changes (plan §7.3).
 *
 * Lives outside the route handler so vitest can exercise the mapping
 * + status-derivation logic without spinning up a Next.js request.
 */

import type { Berth, BerthMapData } from '@/lib/db/schema/berths';

export interface PublicMapData {
  path: string;
  x: string;
  y: string;
  transform: string;
  fontSize: string;
}

/** NocoDB display strings - the public website's `Berth.Status` enum. */
export type PublicStatus = 'Available' | 'Under Offer' | 'Sold';

/**
 * Verbatim NocoDB-style payload. Keys are quoted with capital-first
 * spacing because the upstream consumer (the public website) iterates
 * them by literal string key (e.g. `berth["Mooring Number"]`).
 */
export interface PublicBerth {
  Id: string;
  'Mooring Number': string;
  Length: number | null;
  Draft: number | null;
  'Side Pontoon': string | null;
  'Power Capacity': number | null;
  Voltage: number | null;
  Status: PublicStatus;
  Width: number | null;
  Area: string | null;
  'Mooring Type': string | null;
  'Bow Facing': string | null;
  'Cleat Type': string | null;
  'Cleat Capacity': string | null;
  'Bollard Type': string | null;
  'Bollard Capacity': string | null;
  'Nominal Boat Size': number | null;
  Access: string | null;
  'Berth Approved': boolean | null;
  'Water Depth': number | null;
  'Width Is Minimum': boolean | null;
  'Water Depth Is Minimum': boolean | null;
  'Length (Metric)': number | null;
  'Width (Metric)': number | null;
  'Draft (Metric)': number | null;
  'Water Depth (Metric)': number | null;
  'Nominal Boat Size (Metric)': number | null;
  CreatedAt: string;
  UpdatedAt: string;
  'Map Data'?: PublicMapData;
}

// ─── Helpers ────────────────────────────────────────────────────────────────

function toNumber(value: unknown): number | null {
  if (value === null || value === undefined || value === '') return null;
  const n = typeof value === 'number' ? value : parseFloat(String(value));
  return Number.isFinite(n) ? n : null;
}

function toString(value: unknown): string | null {
  if (value === null || value === undefined) return null;
  if (typeof value === 'string') return value;
  if (typeof value === 'number') return String(value);
  return null;
}

/**
 * Tenure types that are treated as "permanent-class" by the public-map
 * status resolver — an active tenancy with one of these tenure types
 * pushes a berth into "Sold" even when `berths.status` is something
 * weaker (`available`, `under_offer`). Per docs/tenancies-design.md §
 * "Public map status flip". Seasonal / fixed-term tenancies do NOT flip
 * the public status (they fall through to the existing precedence).
 */
const PERMANENT_TENURE_TYPES = new Set(['permanent', 'fee_simple', 'strata_lot']);

export function isPermanentTenureType(tenureType: string | null | undefined): boolean {
  return typeof tenureType === 'string' && PERMANENT_TENURE_TYPES.has(tenureType);
}

/**
 * Derive the public-facing status string. Precedence:
 *
 *   Sold > Under Offer > Available
 *
 * Sold can come from:
 *   1. berths.status = 'sold' (explicit admin set), OR
 *   2. an active tenancy with tenure_type in PERMANENT_TENURE_TYPES
 *      exists for this berth (only when the tenancies module is enabled).
 *
 * Under Offer fires when berth.status='under_offer' OR at least one
 * interest_berths row marked is_specific_interest=true exists on an
 * active (non-archived, non-closed) interest. Seasonal / fixed-term
 * active tenancies do not flip the public status — they fall through
 * to the existing under-offer / available precedence.
 *
 * Manual-override priority (soft pin): when `overrideMode === 'manual'` a
 * human has deliberately pinned this berth's status, so the pinned base
 * status wins over the interest-derived signal — a berth a rep pinned
 * `available` stays Available even with an active specific-interest link.
 * The pin is "soft": a real permanent tenancy (genuine occupancy) and an
 * explicit `sold` status still override it, because those represent facts
 * a stale pin must not hide. A real deposit-paid sale overrides at WRITE
 * time (the rules engine flips the berth to `sold` + `automated`), so by
 * the time we read here the manual pin is already gone in that case.
 * `overrideMode` defaults to null => pure automatic derivation (unchanged).
 */
export function derivePublicStatus(
  internalStatus: string,
  hasSpecificInterest: boolean,
  hasActivePermanentTenancy = false,
  overrideMode: string | null = null,
): PublicStatus {
  // Real permanent occupancy / explicit sold always win, even over a pin.
  if (internalStatus === 'sold' || hasActivePermanentTenancy) return 'Sold';
  // Soft pin: a manual override wins over the interest-derived signal.
  if (overrideMode === 'manual') {
    return internalStatus === 'under_offer' ? 'Under Offer' : 'Available';
  }
  // Automatic derivation.
  if (internalStatus === 'under_offer' || hasSpecificInterest) return 'Under Offer';
  return 'Available';
}

function mapMapData(row: BerthMapData | null | undefined): PublicMapData | undefined {
  if (!row) return undefined;
  return {
    path: row.svgPath ?? '',
    x: row.x === null || row.x === undefined ? '' : String(row.x),
    y: row.y === null || row.y === undefined ? '' : String(row.y),
    transform: row.transform ?? '',
    fontSize: row.fontSize === null || row.fontSize === undefined ? '' : String(row.fontSize),
  };
}

/**
 * Map a single CRM berth row + its (optional) map_data + a flag about
 * whether the berth has an active specific-interest link, into the
 * public response shape. The flag is computed once for a batch by the
 * route handler; this function is pure.
 */
export function toPublicBerth(
  berth: Berth,
  mapData: BerthMapData | null | undefined,
  hasSpecificInterest: boolean,
  hasActivePermanentTenancy = false,
): PublicBerth {
  return {
    Id: berth.id,
    'Mooring Number': berth.mooringNumber,
    Length: toNumber(berth.lengthFt),
    Draft: toNumber(berth.draftFt),
    Width: toNumber(berth.widthFt),
    'Side Pontoon': toString(berth.sidePontoon),
    'Power Capacity': toNumber(berth.powerCapacity),
    Voltage: toNumber(berth.voltage),
    Status: derivePublicStatus(
      berth.status,
      hasSpecificInterest,
      hasActivePermanentTenancy,
      berth.statusOverrideMode,
    ),
    Area: toString(berth.area),
    'Mooring Type': toString(berth.mooringType),
    'Bow Facing': toString(berth.bowFacing),
    'Cleat Type': toString(berth.cleatType),
    'Cleat Capacity': toString(berth.cleatCapacity),
    'Bollard Type': toString(berth.bollardType),
    'Bollard Capacity': toString(berth.bollardCapacity),
    'Nominal Boat Size': toNumber(berth.nominalBoatSize),
    Access: toString(berth.access),
    'Berth Approved': berth.berthApproved,
    'Water Depth': toNumber(berth.waterDepth),
    'Width Is Minimum': berth.widthIsMinimum,
    'Water Depth Is Minimum': berth.waterDepthIsMinimum,
    'Length (Metric)': toNumber(berth.lengthM),
    'Width (Metric)': toNumber(berth.widthM),
    'Draft (Metric)': toNumber(berth.draftM),
    'Water Depth (Metric)': toNumber(berth.waterDepthM),
    'Nominal Boat Size (Metric)': toNumber(berth.nominalBoatSizeM),
    CreatedAt: berth.createdAt.toISOString(),
    UpdatedAt: berth.updatedAt.toISOString(),
    ...(mapMapData(mapData) ? { 'Map Data': mapMapData(mapData)! } : {}),
  };
}
